使用 fakeModel 模拟聊天模型
fakeModel 是一个构建器风格的假聊天模型,它允许您编写确切的响应(文本、工具调用、错误)并断言模型接收到的内容。它扩展自 BaseChatModel,因此可以在任何需要真实模型的地方使用。
import { fakeModel } from "langchain";
快速开始
创建一个模型,使用.respond() 队列化响应,然后调用。每次 invoke() 按顺序消耗下一个队列中的响应:
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!"
const model = fakeModel()
.respond(new AIMessage("only one"));
await model.invoke([new HumanMessage("first")]); // works
await model.invoke([new HumanMessage("second")]); // throws: "no response queued for invocation 1"
工具调用响应
.respond() 通过传递带有 tool_calls 的 AIMessage 来支持工具调用:
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,只需提供工具名称和参数:
// 这两个队列条目产生相同的响应:
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。
.respond() 和 .respondWithTools() 可以按任意顺序自由混合使用。这对于测试智能体循环特别有用,其中模型在工具调用和文本响应之间交替。模拟错误
特定回合的错误
向.respond() 传递一个 Error 会使模型在该特定调用时抛出错误。错误可以出现在序列中的任何位置:
import { fakeModel } from "langchain";
import { AIMessage, HumanMessage } from "@langchain/core/messages";
const model = fakeModel()
.respond(new Error("rate limit exceeded")) // Turn 1: throws
.respond(new AIMessage("Recovered!")); // Turn 2: succeeds
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() 使每次调用都抛出错误,无论队列如何。这对于测试错误处理和重试逻辑很有用:
import { fakeModel } from "langchain";
import { HumanMessage } from "@langchain/core/messages";
const model = fakeModel().alwaysThrow(new Error("service unavailable"));
await model.invoke([new HumanMessage("a")]); // throws "service unavailable"
await model.invoke([new HumanMessage("b")]); // throws "service unavailable"
使用工厂函数的动态响应
.respond() 也接受一个函数,该函数根据输入消息计算响应。该函数接收完整的消息数组,并返回 BaseMessage 或 Error:
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"
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")]); // throws "Content policy violation"
每个函数都是一个单独的队列条目,只消耗一次。要为多个回合重用相同的动态逻辑,请队列化多个
respond 函数调用。结构化输出
对于使用.withStructuredOutput() 的代码,使用 .structuredResponse() 配置假返回值:
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 记录每次调用,包括传递给模型的消息和选项。这类似于传统测试框架中的间谍或模拟:
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"
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 {
// error handled
}
console.log(model.callCount); // 1
console.log(model.calls[0].messages[0].content); // "will fail"
与 bindTools 一起使用
LangChain 智能体和 LangGraph 等智能体框架在内部调用 model.bindTools(tools)。fakeModel 会自动处理此问题。绑定的模型共享与原始模型相同的响应队列和调用记录,因此无需特殊设置:
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 测试工具调用智能体
完整示例:使用 vitest 测试工具调用智能体
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);
});
});

