外观
单元测试隔离地测试智能体中较小、确定性的组成部分。通过用内存假对象(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_calls 的 AIMessage 来生成工具调用:
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() 还接受一个根据输入消息计算响应的函数。该函数接收完整的消息数组,并返回 BaseMessage 或 Error:
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 测试您的智能体。