作者因生产AI agent出现幻觉折扣导致信任事故,开发了包含131个测试、4层覆盖、每次运行仅$0.03的评估体系,强调AI agent必须单独做eval。
单元测试验证的是我的代码。它们断言 my_function(input) 返回 expected_output。对于传统软件,这就够了。但对于 AI Agent,这远远不够,而且很危险。AI Agent 的核心逻辑并不是我写的确定性代码,而是 LLM 与工具和外部系统交互后涌现出的行为。
想象一个用于处理客户咨询的多 Agent 系统:
单元测试可能验证的是:router_agent.classify("I need a new laptop") 返回 "sales"。但如果 Router Agent 背后的 LLM 在一次模型更新或训练数据的细微变化后,开始将 "I need help configuring my new laptop" 分类为 "sales" 而不是 "support" 呢?单元测试依然通过,因为 classify 函数执行没有报错。语义意图已经改变了。这就是 AI Agent 评估测试床 131 tests production 变得不可或缺的地方。
我的测试床运行在 Oracle Cloud Infrastructure(OCI)Functions 上,由新代码提交触发。每一层针对 Agent 可靠性的不同方面。
这些测试最接近传统单元测试,但它们在 Agent 的公开 API 层面运作。它们验证 Agent 能否执行其主要任务。
示例:对于潜在客户资格审核 Agent,测试包括:
agent.process_message("I need 100 widgets") -> asserts response.includes("quote") and response.includes("delivery time")
agent.process_message("Tell me about product X") -> asserts response.includes("features of X")
agent.process_message("I want to speak to a human") -> asserts response.includes("transferring to human")
这些测试使用特定的、确定性的输入,并检查是否包含预期的关键词或结构化输出。它们能捕捉工具调用、API 集成或基本 prompt 遵循方面的回归。
这是测试床开始与单元测试显著分歧的地方。这些测试关注 Agent 是否理解和根据用户意图采取适当行动,即使表达方式多种多样。这一层对于捕捉我曾经历过的"静默故障"至关重要。
方法论:每个测试用例包含:
user_input_variations:3-5 个语义相似的短语数组(如 "I want a quote", "How much does it cost?", "Pricing for X")expected_intent:一个分类标签(如 "request_quote")expected_action:Agent 应调用的工具或内部函数(如 call_pricing_api)expected_output_keywords:必须出现在最终响应中的关键词unexpected_output_keywords:不能出现的关键词示例测试用例:
{
"name": "Pricing Inquiry - Product A",
"user_input_variations": [
"How much is Product A?",
"Cost of Product A please.",
"Give me a quote for Product A.",
"What's the price tag on Product A?"
],
"expected_intent": "request_pricing",
"expected_action": "call_product_pricing_tool",
"expected_output_keywords": ["price", "Product A", "USD"],
"unexpected_output_keywords": ["discount", "shipping cost"]
}
测试床将每个变体通过 Agent 运行。然后使用一个小的微调分类模型(或带有严格 prompt 的独立 LLM 调用)来验证 Agent 内部日志中的 expected_intent 和工具调用日志中的 expected_action。最后,它根据 expected_output_keywords 和 unexpected_output_keywords 检查最终输出。这能捕捉到折扣的幻觉或错误的路由。
这一层用畸形输入、歧义请求和高负载场景来考验 Agent。
"I need help."(应触发澄清或转人工)"Quote for widgets."(应询问数量)"Tell me a joke."(应礼貌拒绝或重定向)对于每一种情况,测试床都会断言特定的降级行为或错误消息。
这些测试专注于防止有害或不适当的响应。
测试床期望得到拒绝、重定向或预置的安全响应。我使用 Groq 进行这些检查,因为它速度快——因为实时审核的响应时间至关重要。
运行跨越四层的 131 个测试并非免费,但足够便宜,可以每次提交都运行。
LLM 调用:主要成本驱动因素。我在 Groq(用于速度敏感型的简单分类/拒绝检查)和 Claude 3 Haiku(用于更复杂的推理和输出生成)之间动态路由。
OCI Functions:测试床逻辑的无服务器执行。按调用次数和 GB-秒计费。在我这个规模下可以忽略不计。
OCI Object Storage:存储测试用例、结果和日志。同样可以忽略不计。
一次典型的完整运行包括:
总计:约 $0.0175 + 可忽略不计。我向上取整到 $0.03,以应对偶尔更长的响应或额外的内部日志。这个成本只是可能因一次静默故障导致的潜在收入损失或声誉损害的一小部分。
上个月,我正在为客服 Agent 开发一个新功能:基于用户查询的动态 FAQ 生成。想法是使用 RAG 系统来拉取相关知识库文章并进行总结。
单元测试通过了。RAG 系统检索到了正确的文章。摘要 prompt 在孤立示例上运行良好。我推送了代码。
评估测试床运行了。第二层"语义意图与推理"有一个测试失败:"Query about refund policy."。预期输出是退款政策的摘要。实际输出包含一句话:"Please note, all refunds are subject to a 10% processing fee."
这是一个幻觉。我们的退款政策没有处理费。RAG 系统正确检索了政策。摘要 LLM(当时是 Claude 3 Haiku)添加了这个细节,可能是从其关于退款的通用训练数据中来的,尽管 prompt 中明确指示只使用提供的上下文。
没有测试床,这就会发货。一个询问退款的客户会被告知一个不存在的费用,导致困惑、沮丧,并产生一个直接的support工单。那次评估运行花费的 $0.03 为我节省了一次客户互动,并维护了信任。
Building an AI agent evaluation harness 131 tests production 对于认真的 AI 开发来说不是可选的。它是一个关键的基础设施组件,能捕捉单元测试无法捕捉的涌现性故障。它是发布稳健、可靠的 Agent 与不断扑灭静默的、侵蚀信任的 bug 之间的区别。我每次运行 $0.03 是我买过的最便宜的保险单。
Q: 你如何管理 131 个测试的测试数据?都是硬编码的 JSON 吗?
A: 核心测试用例是存储在 OCI Object Storage 中的 JSON 文件。对于变体和负面测试,我使用一个小 Python 脚本,根据模板编程生成额外输入,确保覆盖率,而不需要手动编写每种排列组合。
Q: 如果 LLM 本身改变了行为,导致现有测试失败,即使我的代码是正确的怎么办?
A: 这正是测试床设计用来检测的。如果 LLM 更新导致测试失败,这是一个信号,要么调整 prompt(引导 LLM 回到期望行为),要么接受新行为并更新测试的预期输出。这是一个持续的校准过程。
Q: 你如何在断言中处理 LLM 输出的非确定性?
A: 我避免严格的字符串相等。相反,我使用关键词存在/不存在检查、正则表达式模式,有时还使用一个次要的、更小的 LLM(如 Groq)来将 Agent 的输出分类为预期意图或情感。这允许变化,同时确保核心需求得到满足。
Q: 每次运行 $0.03 对更大的团队或更复杂的 Agent 可持续吗?
A: 可持续。成本随 LLM 使用量而扩展,而不是随测试数量线性增长——如果测试设计高效的话。对于更大的团队,每次运行的成本可能会增加,但尽早发现关键错误的价值远远超过它。我当前的设置针对每个测试的最小 token 使用量进行了优化。
Q: 你如何与 CI/CD 集成?
A: 我的 OCI Functions 由 Git 仓库的新提交触发。如果任何测试失败,CI 流水线就会中断,防止新代码部署到生产环境。这确保了没有任何未经测试或存在故障的 Agent 代码到达用户。
— Elena Revicheva · AIdeazz · Portfolio