告别“追句子”式测试:Google ADK TypeScript Agent 测试实战指南
针对LLM输出非确定性导致的测试不稳定问题,本文详细讲解如何利用Google ADK构建分层测试体系。通过测试工具决策而非输出文本,结合Zod Schema和Trajectory Summary,实现Agent的高稳定性测试。
告别“追句子”式测试:Google ADK TypeScript Agent 测试实战指南
在开发 AI Agent 时,开发者常陷入一个误区:试图通过断言模型输出的最终句子来验证功能。例如,期望 Agent 回复“我会帮您找巴黎的酒店”,而它实际输出了“当然,我可以为您查找巴黎的酒店选项”。虽然行为正确,但测试却因为措辞不同而失败。这种测试方式不仅脆弱,而且毫无工程价值。
Google 的 Agent Development Kit (ADK) 的核心价值之一,就是将 Agent 开发带向传统软件工程的范式。它通过代码和运行时原语(如 Orchestration、Sessions、Events)来管理 Agent,这让我们得以在非确定性的模型之上建立确定性的契约。
核心原则:测试决策,而非个性
不要测试 Agent 的“个性”或修辞风格,而应测试它的决策逻辑和边界条件。一个实用的测试套件应当包含四个层级,从底向上构建稳定性:
- 确定性单元测试:针对工具和适配器。
- 运行时契约:策略、状态和 Schema。
- 端到端轨迹场景:模拟完整交互流程。
- 人工审查评估:少量的、经过验证的评估。
绝大多数测试应位于底部,因为它们快速、廉价且确定。
实战一:工具层测试(Mock 逻辑)
ADK 的 TypeScript 工具通过 FunctionTool 和 Zod 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
阅读原文