Skip to main content

Documentation Index

Fetch the complete documentation index at: https://nvd-54.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

集成测试验证你的智能体与模型 API 和外部服务正确协同工作。与使用模拟和伪造的单元测试不同,集成测试进行实际的网络调用,以确认各组件协同工作、凭证有效、以及延迟可接受。 由于 LLM 响应是非确定性的,集成测试需要与传统软件测试不同的策略。本指南涵盖如何组织、编写和运行智能体的集成测试。如需为 LangChain 本身贡献代码的通用测试基础设施,请参阅贡献代码

分离单元测试和集成测试

集成测试较慢且需要 API 凭证,所以要将它们与单元测试分开。这让你可以在每次更改时运行快速的单元测试,而将集成测试保留给 CI 或部署前检查。 使用文件命名约定来分离集成测试。将集成测试文件命名为 *.int.test.ts,并配置 vitest 在默认运行中排除它们:
vitest.config.ts
import { configDefaults, defineConfig } from "vitest/config";

export default defineConfig((env) => {
  if (env.mode === "int") {
    return {
      test: {
        testTimeout: 100_000,
        include: ["**/*.int.test.ts"],
        setupFiles: ["dotenv/config"],
      },
    };
  }

  return {
    test: {
      testTimeout: 30_000,
      exclude: ["**/*.int.test.ts", ...configDefaults.exclude],
    },
  };
});
package.json 中添加脚本:
{
  "scripts": {
    "test": "vitest",
    "test:integration": "vitest --mode int"
  }
}
显式运行集成测试:
npm run test:integration

管理 API 密钥

集成测试需要真实的 API 凭证。从环境变量中加载它们,以使密钥不进入源代码控制。 添加 dotenv/config 作为 vitest 的 setup 文件,以便从 .env 自动加载环境变量:
vitest.config.ts
export default defineConfig({
  test: {
    setupFiles: ["dotenv/config"],
  },
});
.env
OPENAI_API_KEY=sk-...
当密钥缺失时跳过测试:
import { test } from "vitest";

test.skipIf(!process.env.OPENAI_API_KEY)(
  "agent responds with tool call",
  async () => {
    // ...
  }
);
.env 添加到你的 .gitignore 以避免提交凭证。在 CI 中,通过你的提供商的密钥管理注入密钥(例如 GitHub Actions secrets)。

断言结构而非内容

LLM 响应在每次运行之间有所不同。不要断言确切的输出字符串,而是验证响应的结构属性:消息类型、工具调用名称、参数形状和消息数量。
test("agent calls weather tool", async () => {
  const agent = createAgent({ model: "claude-sonnet-4-6", tools: [getWeather] });
  const result = await agent.invoke({
    messages: [new HumanMessage("What's the weather in SF?")]
  });

  const aiMsg = result.messages.find(
    (m) => AIMessage.isInstance(m) && m.tool_calls?.length
  );
  expect(aiMsg).toContainToolCall({ name: "get_weather" });
  expect(result.messages.at(-1)).toBeAIMessage();
});
此示例使用了自定义测试匹配器。请参阅下面的章节了解设置和完整的匹配器参考。
如需更严格的轨迹断言,使用 AgentEvals 评估器,它支持 unorderedsuperset 等模糊匹配模式。

使用自定义测试匹配器

langchain 提供了自定义 vitest 匹配器,使结构断言更可读,并在失败时产生清晰的错误消息。在 setup 文件中注册一次,它们就可以在每个 expect() 调用中使用。

设置

添加一个 vitest setup 文件,用 LangChain 匹配器扩展 expect
vitest.setup.ts
import { langchainMatchers } from "@langchain/core/testing";

expect.extend(langchainMatchers);
在你的 vitest 配置中引用它:
vitest.config.ts
export default defineConfig({
  test: {
    setupFiles: ["vitest.setup.ts"],
  },
});
TypeScript 类型自动包含,因此不需要额外配置即可获得自动补全。

检查消息类型

每个消息类都有对应的匹配器:toBeHumanMessage()toBeAIMessage()toBeSystemMessage()toBeToolMessage()。不带参数调用只检查类型,或传入字符串来同时匹配内容:
const response = await agent.invoke({
  messages: [new HumanMessage("What's the weather?")]
});
const lastMessage = response.messages.at(-1);

expect(lastMessage).toBeAIMessage();
expect(lastMessage).toBeAIMessage("It's 72°F and sunny.");
传入对象来匹配特定字段:
expect(lastMessage).toBeAIMessage({ name: "weather-bot" });
expect(toolMsg).toBeToolMessage({ tool_call_id: "call_1" });

断言工具调用

三个匹配器覆盖了对 AIMessage 的工具调用断言:
const response = await agent.invoke({
  messages: [new HumanMessage("Weather in SF and NYC?")]
});
const aiMsg = response.messages.find(
  (m) => AIMessage.isInstance(m) && m.tool_calls?.length
);

// 检查是否存在特定的工具调用(顺序无关)
expect(aiMsg).toHaveToolCalls([
  { name: "get_weather", args: { city: "San Francisco" } },
  { name: "get_weather", args: { city: "New York" } },
]);

// 只检查数量
expect(aiMsg).toHaveToolCallCount(2);

// 检查至少有一个工具调用匹配(支持 .not)
expect(aiMsg).toContainToolCall({ name: "get_weather" });
expect(aiMsg).not.toContainToolCall({ name: "send_email" });

断言工具消息

toHaveToolMessages() 接受完整的消息数组,并按顺序检查其中的 ToolMessage 实例:
expect(response.messages).toHaveToolMessages([
  { content: "72°F and sunny in San Francisco" },
  { content: "68°F and cloudy in New York" },
]);

断言中断和结构化响应

toHaveBeenInterrupted() 检查 LangGraph 中断结果中的 __interrupt__ 字段。传入值来匹配中断负载:
const result = await graph.invoke(input);

expect(result).toHaveBeenInterrupted();
expect(result).toHaveBeenInterrupted("confirm_action");
toHaveStructuredResponse() 检查结果上的 structuredResponse 字段。传入对象来匹配特定字段:
expect(result).toHaveStructuredResponse();
expect(result).toHaveStructuredResponse({ name: "Alice", age: 30 });

匹配器参考

匹配器描述
toBeHumanMessage(expected?)检查值是否为 HumanMessage。可选匹配内容(字符串)或字段(对象)。
toBeAIMessage(expected?)检查值是否为 AIMessage。可选匹配内容或字段。
toBeSystemMessage(expected?)检查值是否为 SystemMessage。可选匹配内容或字段。
toBeToolMessage(expected?)检查值是否为 ToolMessage。可选匹配内容或 tool_call_id 等字段。
toHaveToolCalls(expected)检查 AIMessage 是否包含恰好给定的工具调用(顺序无关)。
toHaveToolCallCount(n)检查 AIMessage 是否恰好有 n 个工具调用。
toContainToolCall(expected)检查 AIMessage 是否包含至少一个匹配的工具调用。支持 .not
toHaveToolMessages(expected)检查消息数组是否按顺序包含给定的 ToolMessage 实例。
toHaveBeenInterrupted(value?)检查结果是否有 __interrupt__。可选匹配中断值。
toHaveStructuredResponse(expected?)检查结果是否有 structuredResponse。可选匹配特定字段。

降低成本和延迟

调用 LLM API 的集成测试会产生实际成本。一些实践有助于保持测试套件快速且经济:
  • 使用较小的模型gemini-3.1-flash-lite-preview 或同等模型,用于只需验证工具调用和响应结构的测试。
  • 设置 maxTokens:限制响应长度以避免冗长、昂贵的补全。
  • 限制测试范围:每个测试测试一个行为。当单轮测试足够时,避免链接多个 LLM 调用的端到端场景。
  • 选择性运行:使用上面的测试分离,只在 CI 或部署前运行集成测试,而不是在每次文件保存时运行。
const agent = createAgent({
  model: "gemini-3.1-flash-lite-preview",
  tools: [getWeather],
  modelArgs: { maxTokens: 256 },
});

下一步

了解如何使用确定性匹配或 LLM 评委评估器来评估智能体轨迹,请参阅评估