智能工具库

告别“追句子”式测试:Google ADK TypeScript Agent 测试实战指南

针对LLM输出非确定性导致的测试不稳定问题,本文详细讲解如何利用Google ADK构建分层测试体系。通过测试工具决策而非输出文本,结合Zod Schema和Trajectory Summary,实现Agent的高稳定性测试。

2026-09-01 0来源:dev.to AI

告别“追句子”式测试:Google ADK TypeScript Agent 测试实战指南

在开发 AI Agent 时,开发者常陷入一个误区:试图通过断言模型输出的最终句子来验证功能。例如,期望 Agent 回复“我会帮您找巴黎的酒店”,而它实际输出了“当然,我可以为您查找巴黎的酒店选项”。虽然行为正确,但测试却因为措辞不同而失败。这种测试方式不仅脆弱,而且毫无工程价值。

Google 的 Agent Development Kit (ADK) 的核心价值之一,就是将 Agent 开发带向传统软件工程的范式。它通过代码和运行时原语(如 Orchestration、Sessions、Events)来管理 Agent,这让我们得以在非确定性的模型之上建立确定性的契约

核心原则:测试决策,而非个性

不要测试 Agent 的“个性”或修辞风格,而应测试它的决策逻辑边界条件。一个实用的测试套件应当包含四个层级,从底向上构建稳定性:

  1. 确定性单元测试:针对工具和适配器。
  2. 运行时契约:策略、状态和 Schema。
  3. 端到端轨迹场景:模拟完整交互流程。
  4. 人工审查评估:少量的、经过验证的评估。

绝大多数测试应位于底部,因为它们快速、廉价且确定

实战一:工具层测试(Mock 逻辑)

ADK 的 TypeScript 工具通过 FunctionToolZod Schema 定义。值得注意的是,工具底层的业务逻辑依然是普通的 TypeScript 代码。因此,工具测试不应调用 LLM

我们可以使用 Vitest 直接测试业务逻辑,并 Mock 外部依赖(如数据库网关):

import { FunctionTool } from "@google/adk";
import { z } from "zod";

// 业务逻辑函数
export const searchHotels = async ({
  city,
  maxNightlyPriceUsd,
}: {
  city: string;
  maxNightlyPriceUsd?: number;
}) => {
  return hotelGateway.search({ city, maxNightlyPriceUsd });
};

// 定义工具
export const searchHotelsTool = new FunctionTool({
  name: "search_hotels",
  description: "Search available hotels. This tool never creates a booking.",
  parameters: z.object({
    city: z.string().min(2),
    maxNightlyPriceUsd: z.number().positive().optional(),
  }),
  execute: searchHotels,
});

测试代码

it("passes normalized filters to the hotel gateway", async () => {
  vi.spyOn(hotelGateway, "search").mockResolvedValue([]);

  await searchHotels({ city: "Paris", maxNightlyPriceUsd: 250 });

  expect(hotelGateway.search).toHaveBeenCalledWith({
    city: "Paris",
    maxNightlyPriceUsd: 250,
  });
});

这种测试方式保证了数据映射、错误归一化和幂等性是确定的。

实战二:集成层测试(Trajectory Summary)

当需要测试 Agent 的决策(例如:它是否调用了工具,或者是否拒绝了某个请求)时,我们需要运行 ADK 的运行时。这里的关键技巧是解耦测试逻辑与内部事件

我们可以使用 InMemoryRunner 在内存中运行 Agent,收集事件,但将其转换为简单的应用级摘要。这样,测试就不依赖于 LLM 生成的每一个内部 token,而是关注于行为结果。

测试代码

import { InMemoryRunner, LlmAgent } from "@google/adk";

// 定义一个稳定的契约类型
// TrajectorySummary 是你的适配层,将框架事件转化为应用关心的数据
const agent = new LlmAgent({
  name: "travel_assistant",
  model: "gemini-2.5-flash",
  instruction: "Use search_hotels for availability questions...",
  tools: [searchHotelsTool, bookHotelTool],
});

async function runScenario(input: string) {
  const runner = new InMemoryRunner({ agent });
  const session = await runner.sessionService.createSession({
    appName: runner.appName,
    userId: "test-user",
  });

  const events = [];
  for await (const event of runner.runAsync({
    userId: session.userId,
    sessionId: session.id,
    newMessage: { role: "user", parts: [{ text: input }] },
  })) {
    events.push(event);
  }
  return summarizeTrajectory(events);
}

// 断言行为而非文本
it("searches but never books for an availability question", async () => {
  const run = await runScenario(
    "What hotels are available in London next weekend?"
  );
  expect(run.toolCalls.map((call) => call.name)).toContain("search_hotels");
  // 这里可以断言没有调用 book_hotel
});

总结

通过将 Agent 测试分层,我们成功将“不可控”的 LLM 输出转化为“可控”的代码逻辑。对于开发者而言,这意味着 CI/CD 流水线不再因为模型的随机波动而频繁失败。记住:工具是确定的,决策是可控的,只有语言是随机的。

本文基于 dev.to AI 的公开内容,由 AI 辅助整理改写后发布。

原标题:Testing Google ADK TypeScript Agents Without Chasing Sentences

阅读原文