Skip to content

单元测试隔离地测试智能体中较小、确定性的组成部分。通过用内存假对象(in-memory fake,也称 fixture)替换真实的 LLM,您可以预编排精确的响应(文本、工具调用和错误),从而使测试无需 API 密钥即可快速、免费且可重复地运行。

模拟对话模型

LangChain 提供 GenericFakeChatModel 用于模拟文本响应。它接收一个响应迭代器(AIMessage 对象或字符串),每次调用返回一个响应。它同时支持常规用法和流式用法。

python
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel

model = GenericFakeChatModel(messages=iter([
    AIMessage(content="", tool_calls=[ToolCall(name="foo", args={"bar": "baz"}, id="call_1")]),
    "bar"
]))

model.invoke("hello")
# AIMessage(content='', ..., tool_calls=[{'name': 'foo', 'args': {'bar': 'baz'}, 'id': 'call_1', 'type': 'tool_call'}])

如果再次调用模型,它将返回迭代器中的下一个元素:

python
model.invoke("hello, again!")
# AIMessage(content='bar', ...)

InMemorySaver 检查点器

要在测试期间启用持久化,您可以使用 InMemorySaver 检查点器。这允许您模拟多轮对话,以测试依赖状态的行为:

python
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model,
    tools=[],
    checkpointer=InMemorySaver()
)

# 第一次调用
agent.invoke(
    {"messages": [HumanMessage(content="I live in Sydney, Australia")]},
    config={"configurable": {"thread_id": "session-1"}}
)

# 第二次调用:第一条消息已被持久化(悉尼位置),因此模型返回 GMT+10 时区的时间
agent.invoke(
    {"messages": [HumanMessage(content="What's my local time?")]},
    config={"configurable": {"thread_id": "session-1"}}
)

使用 fakeModel 模拟对话模型

fakeModel 是一个构建器风格的模拟对话模型,它让您可以预编排精确的响应(文本、工具调用、错误),并断言模型收到的内容。它继承自 BaseChatModel,因此可以在任何需要真实模型的地方使用。

typescript
import { fakeModel } from "langchain";

快速上手

创建一个模型,使用 .respond() 将响应排入队列,然后调用。每次 invoke() 都会按顺序消费下一个排队的响应:

typescript
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";

const model = fakeModel()
  .respond(new AIMessage("I can help with that."))
  .respond(new AIMessage("Here's what I found."))
  .respond(new AIMessage("You're welcome!"));

const r1 = await model.invoke([new HumanMessage("Can you help?")]);
// r1.content === "I can help with that."

const r2 = await model.invoke([new HumanMessage("What did you find?")]);
// r2.content === "Here's what I found."

const r3 = await model.invoke([new HumanMessage("Thanks!")]);
// r3.content === "You're welcome!"

如果模型的调用次数超过排队的响应数量,它会抛出一个描述性错误:

typescript
const model = fakeModel()
  .respond(new AIMessage("only one"));

await model.invoke([new HumanMessage("first")]);  // 正常工作
await model.invoke([new HumanMessage("second")]); // 抛出错误:"no response queued for invocation 1"

工具调用响应

.respond() 支持通过传入带 tool_callsAIMessage 来生成工具调用:

typescript
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";

const model = fakeModel()
  .respond(new AIMessage({
    content: "",
    tool_calls: [
      { name: "get_weather", args: { city: "San Francisco" }, id: "call_1", type: "tool_call" },
    ],
  }))
  .respond(new AIMessage("It's 72°F and sunny in San Francisco."));

const r1 = await model.invoke([new HumanMessage("What's the weather in SF?")]);
console.log(r1.tool_calls[0].name); // "get_weather"

const r2 = await model.invoke([new HumanMessage("Thanks")]);
console.log(r2.content); // "It's 72°F and sunny in San Francisco."

.respondWithTools() 是同一功能的简写形式。您无需构造完整的 AIMessage,只需提供工具名称和参数:

typescript
// 这两个队列条目会产生完全相同的响应:

model.respond(new AIMessage({
  content: "",
  tool_calls: [
    { name: "get_weather", args: { city: "SF" }, id: "call_1", type: "tool_call" },
  ],
}));

// 等效简写:
model.respondWithTools([  
  { name: "get_weather", args: { city: "SF" }, id: "call_1" },  
]);  

id 字段是可选的。如果省略,会自动生成一个唯一 ID。

TIP

.respond().respondWithTools() 可以按任意顺序自由混用。这在测试智能体循环时尤为有用,因为模型中工具调用与文本响应会交替出现。

模拟错误

在特定轮次抛出错误

.respond() 传入 Error 会使模型在对应的那次调用中抛出错误。错误可以出现在序列中的任意位置:

typescript
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";

const model = fakeModel()
  .respond(new Error("rate limit exceeded"))  // 第 1 轮:抛出错误
  .respond(new AIMessage("Recovered!"));      // 第 2 轮:成功

try {
  await model.invoke([new HumanMessage("first")]);
} catch (e) {
  console.log(e.message); // "rate limit exceeded"
}

const result = await model.invoke([new HumanMessage("retry")]);
console.log(result.content); // "Recovered!"

在每次调用时抛出错误

.alwaysThrow() 会使每次调用都抛出错误,无论队列中有何内容。这对于测试错误处理和重试逻辑非常有用:

typescript
import { fakeModel } from "langchain";
import { HumanMessage } from "@langchain/core/messages";

const model = fakeModel().alwaysThrow(new Error("service unavailable"));

await model.invoke([new HumanMessage("a")]); // 抛出 "service unavailable" 错误
await model.invoke([new HumanMessage("b")]); // 抛出 "service unavailable" 错误

使用工厂函数生成动态响应

.respond() 还接受一个根据输入消息计算响应的函数。该函数接收完整的消息数组,并返回 BaseMessageError

typescript
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";

const model = fakeModel()
  .respond((messages) => {  
    const last = messages[messages.length - 1].text;  
    return new AIMessage(`You said: ${last}`);  
  });  

const result = await model.invoke([new HumanMessage("hello")]);
console.log(result.content); // "You said: hello"

工厂函数也可以返回错误:

typescript
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";

const model = fakeModel()
  .respond((messages) => {
    const content = messages[messages.length - 1].text;
    if (content.includes("forbidden")) {
      return new Error("Content policy violation");
    }
    return new AIMessage("OK");
  });

await model.invoke([new HumanMessage("forbidden topic")]); // 抛出 "Content policy violation" 错误

INFO

每个函数都是单个队列条目,仅被消费一次。要在多轮对话中复用相同的动态逻辑,请将多个 respond 函数调用排入队列。

结构化输出

对于使用 .withStructuredOutput() 的代码,请使用 .structuredResponse() 配置假返回的值:

typescript
import { fakeModel } from "langchain";
import { HumanMessage } from "@langchain/core/messages";
import { z } from "zod";

const model = fakeModel()
  .structuredResponse({ temperature: 72, unit: "fahrenheit" });  

const structured = model.withStructuredOutput(
  z.object({
    temperature: z.number(),
    unit: z.string(),
  })
);

const result = await structured.invoke([new HumanMessage("Weather?")]);
console.log(result);
// { temperature: 72, unit: "fahrenheit" }

传给 .withStructuredOutput() 的模式会被忽略。模型始终返回通过 .structuredResponse() 配置的值。这使测试聚焦于应用逻辑,而不是解析逻辑。

断言模型收到的内容

fakeModel 会记录每次调用,包括传给模型的消息和选项。它的作用类似于传统测试框架中的间谍(spy)或模拟对象(mock):

typescript
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";

const model = fakeModel()
  .respond(new AIMessage("first"))
  .respond(new AIMessage("second"));

await model.invoke([new HumanMessage("question 1")]);
await model.invoke([new HumanMessage("question 2")]);

console.log(model.callCount); // 2

console.log(model.calls[0].messages[0].content); // "question 1"
console.log(model.calls[1].messages[0].content); // "question 2"

即使模型抛出错误,调用也会被记录:

typescript
import { fakeModel } from "langchain";
import { HumanMessage } from "@langchain/core/messages";

const model = fakeModel().respond(new Error("boom"));

try {
  await model.invoke([new HumanMessage("will fail")]);
} catch {
  // 错误已处理
}

console.log(model.callCount); // 1
console.log(model.calls[0].messages[0].content); // "will fail"

bindTools 一起使用

LangChain 智能体和 LangGraph 等智能体框架会在内部调用 model.bindTools(tools)fakeModel 会自动处理这一点。绑定后的模型与原模型共享相同的响应队列和调用记录,因此无需特殊设置:

typescript
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const searchTool = tool(async ({ query }) => `Results for: ${query}`, {
  name: "search",
  description: "Search the web",
  schema: z.object({ query: z.string() }),
});

const model = fakeModel()
  .respondWithTools([{ name: "search", args: { query: "weather" }, id: "1" }])
  .respond(new AIMessage("The weather is sunny."));

const bound = model.bindTools([searchTool]);

const r1 = await bound.invoke([new HumanMessage("weather?")]);
console.log(r1.tool_calls[0].name); // "search"

const r2 = await bound.invoke([new HumanMessage("thanks")]);
console.log(r2.content); // "The weather is sunny."

// 调用记录是共享的。请通过原始模型查看。
console.log(model.callCount); // 2

完整示例:使用 vitest 测试工具调用智能体

typescript
import { describe, test, expect } from "vitest";
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage, ToolMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const getWeather = tool(
  async ({ city }) => `72°F and sunny in ${city}`,
  {
    name: "get_weather",
    description: "Get weather for a city",
    schema: z.object({ city: z.string() }),
  }
);

async function runAgent(
  model: ReturnType<typeof fakeModel>,
  input: string
) {
  const messages: any[] = [new HumanMessage(input)];
  const bound = model.bindTools([getWeather]);

  while (true) {
    const response = await bound.invoke(messages);
    messages.push(response);

    if (!response.tool_calls?.length) {
      return { messages, finalResponse: response };
    }

    for (const tc of response.tool_calls) {
      const result = await getWeather.invoke(tc.args);
      messages.push(new ToolMessage({
        content: result as string,
        tool_call_id: tc.id!,
      }));
    }
  }
}

describe("weather agent", () => {
  test("calls get_weather and returns a final answer", async () => {
    const model = fakeModel()
      .respondWithTools([
        { name: "get_weather", args: { city: "SF" }, id: "call_1" },
      ])
      .respond(new AIMessage("It's 72°F and sunny in SF!"));

    const { finalResponse } = await runAgent(model, "Weather in SF?");

    expect(finalResponse.content).toBe("It's 72°F and sunny in SF!");
    expect(model.callCount).toBe(2);

    const secondCall = model.calls[1].messages;
    const toolMsg = secondCall.find((m: any) => m._getType() === "tool");
    expect(toolMsg?.content).toContain("72°F and sunny in SF");
  });

  test("handles model errors gracefully", async () => {
    const model = fakeModel()
      .respond(new Error("rate limit"));

    await expect(
      runAgent(model, "Weather?")
    ).rejects.toThrow("rate limit");

    expect(model.callCount).toBe(1);
  });
});

后续步骤

集成测试中了解如何使用真实的模型提供商 API 测试您的智能体。