AI Agent 测试 — 部署前怎么做评估

AI Agent 测试 — 部署前怎么做评估
开场
去年我上线了一个 Agent,测试环境一切正常,生产环境第三天就出了事故——Agent 在特定时区的客户对话中把 "AM" 和 "PM" 搞混了,导致批量发出了错误的会议邀请。根本原因:我的测试用例全部用的 UTC 时间,没有覆盖时区转换场景。这件事让我彻底重新设计了 Agent 的测试体系。这篇文章分享我现在用的完整测试框架。
问题背景
Agent 测试和传统软件测试有本质区别:
输出不确定:同样的输入,Agent 可能给出不同的输出。你不能用 assertEqual(output, expected) 来测试。
行为链路长:一个 Agent 可能经过 "理解问题 → 选择工具 → 调用 API → 解析结果 → 生成回复" 五个步骤,每一步都可能出错。
外部依赖多:Agent 依赖 LLM API、数据库、外部工具。这些依赖的行为也不确定。
边界模糊:什么算 "正确" 的回答?客户问 "你们最便宜的方案多少钱",Agent 回答 "$29/月" 和 "我们的入门方案是每月 29 美元" 都算对,但 "大约 30 美元" 就有争议。
传统的单元测试和集成测试仍然需要,但不够。Agent 需要额外的评估层。
核心框架
测试金字塔(Agent 版)
╱ ╲
╱ E2E╲ ← 端到端场景测试(少量,耗时长)
╱ Tests ╲
╱─────────╲
╱ Agent ╲ ← Agent 行为评估(LLM-as-judge)
╱ Evaluation ╲
╱───────────────╲
╱ Integration ╲ ← 工具调用 + API 集成测试
╱ Tests ╲
╱─────────────────────╲
╱ Unit Tests ╲ ← 纯函数测试(工具函数、解析逻辑)
╱─────────────────────────╲
Unit Tests:测试不依赖 LLM 的部分——工具函数、数据解析、格式验证。快、便宜、确定性。
Integration Tests:测试工具调用链路——能不能正确调用 API、解析返回值、处理错误。
Agent Evaluation:用 LLM 评估 Agent 输出的质量。这是 Agent 测试的核心,也是最有挑战的部分。
E2E Tests:完整的用户场景模拟,从输入到最终输出,覆盖多轮对话。
实现细节
Step 1: Unit Tests——测试确定性部分
把 Agent 系统中所有不依赖 LLM 的逻辑抽出来做单元测试:
import pytest
from agent.tools import parse_order_id, format_price, validate_email
# 确定性函数的测试:传统 assert 即可
class TestToolFunctions:
def test_parse_order_id_valid(self):
assert parse_order_id("ORD-2026-0001") == "ORD-2026-0001"
def test_parse_order_id_from_text(self):
text = "我的订单号是ORD-2026-0001,请帮我查一下"
assert parse_order_id(text) == "ORD-2026-0001"
def test_parse_order_id_invalid(self):
assert parse_order_id("没有订单号的文本") is None
def test_format_price_cny(self):
assert format_price(2999, "CNY") == "¥2,999.00"
def test_format_price_usd(self):
assert format_price(29.99, "USD") == "$29.99"
def test_validate_email(self):
assert validate_email("user@example.com") is True
assert validate_email("not-an-email") is False
这些测试跑得快(毫秒级),不花 API 钱,可以在每次 commit 时运行。
Step 2: Integration Tests——Mock LLM,测试工具链路
import pytest
from unittest.mock import AsyncMock, patch
from agent.core import CustomerServiceAgent
class TestToolIntegration:
"""测试 Agent 的工具调用是否正确"""
@pytest.fixture
def agent(self):
return CustomerServiceAgent(model="claude-sonnet-4-5-20250514")
@pytest.mark.asyncio
async def test_order_query_calls_correct_tool(self, agent):
"""验证订单查询消息触发正确的工具调用"""
# Mock LLM 返回一个 tool_use 响应
mock_response = create_mock_tool_use_response(
tool_name="get_order_status",
tool_input={"order_id": "ORD-2026-0001"}
)
with patch.object(agent.client.messages, "create", return_value=mock_response):
# Mock 工具执行
with patch.object(agent, "execute_tool") as mock_tool:
mock_tool.return_value = '{"status": "shipped", "tracking": "SF1234"}'
await agent.handle_message(
"我的订单 ORD-2026-0001 现在什么状态?",
customer_id="CUST001"
)
# 验证调用了正确的工具和参数
mock_tool.assert_called_once_with(
"get_order_status",
{"order_id": "ORD-2026-0001"}
)
@pytest.mark.asyncio
async def test_tool_error_handling(self, agent):
"""验证工具调用失败时的错误处理"""
with patch.object(agent, "execute_tool", side_effect=TimeoutError("API timeout")):
result = await agent.handle_message(
"查一下订单 ORD-2026-0001",
customer_id="CUST001"
)
# Agent 应该优雅降级,而不是抛出异常
assert "稍后" in result or "重试" in result or "人工" in result
Step 3: LLM-as-Judge——Agent 输出质量评估
这是 Agent 测试的核心。用一个 "评判模型" 来评估 Agent 的输出质量:
import anthropic
import json
from dataclasses import dataclass
@dataclass
class EvalCase:
"""评估用例"""
input_message: str # 用户输入
context: str # 场景上下文
expected_behavior: str # 期望行为(自然语言描述)
criteria: list[str] # 评估维度
@dataclass
class EvalResult:
"""评估结果"""
score: float # 0-1
passed: bool
reasoning: str
dimension_scores: dict[str, float]
class LLMJudge:
"""用 LLM 评估 Agent 输出的质量"""
def __init__(self):
self.client = anthropic.Anthropic()
async def evaluate(
self,
eval_case: EvalCase,
agent_response: str
) -> EvalResult:
"""对 Agent 的回复打分"""
judge_prompt = f"""你是一个 AI Agent 输出质量的评估专家。
## 评估场景
用户输入: {eval_case.input_message}
场景上下文: {eval_case.context}
期望行为: {eval_case.expected_behavior}
## Agent 的实际回复
{agent_response}
## 评估维度
{json.dumps(eval_case.criteria, ensure_ascii=False)}
## 评估规则
- 对每个评估维度打分 (0.0-1.0)
- 关注事实准确性(最重要)、信息完整性、语气适当性
- 如果 Agent 编造了信息,该维度直接给 0
- 如果 Agent 正确地表示不确定并建议转人工,即使没有直接回答问题也应给高分
输出 JSON:
{{
"overall_score": 0.0-1.0,
"dimensions": {{
"维度名": {{"score": 0.0-1.0, "reason": "原因"}}
}},
"critical_issues": ["严重问题列表"],
"reasoning": "整体评估理由"
}}"""
response = self.client.messages.create(
model="claude-sonnet-4-5-20250514", # 评判模型用 Sonnet
max_tokens=1024,
messages=[{"role": "user", "content": judge_prompt}]
)
result = json.loads(response.content[0].text)
return EvalResult(
score=result["overall_score"],
passed=result["overall_score"] >= 0.75,
reasoning=result["reasoning"],
dimension_scores={
k: v["score"] for k, v in result["dimensions"].items()
}
)
# 评估用例集
EVAL_SUITE = [
EvalCase(
input_message="你们最便宜的方案多少钱?",
context="产品有三个方案: Starter $29/月, Pro $79/月, Enterprise 定制",
expected_behavior="准确报出 Starter 方案的价格 $29/月",
criteria=["价格准确性", "信息完整性", "语气适当性"]
),
EvalCase(
input_message="我要退款",
context="退款政策: 购买后 30 天内可全额退款,需人工处理",
expected_behavior="告知退款政策,然后转接人工客服处理",
criteria=["政策准确性", "是否正确转人工", "语气适当性"]
),
EvalCase(
input_message="你们支持 SAML SSO 吗?",
context="知识库中没有关于 SAML SSO 的信息",
expected_behavior="承认不确定,建议转人工或查看文档,不要编造答案",
criteria=["诚实性", "是否编造信息", "引导行为"]
),
]
Step 4: A/B 测试框架
在生产环境中对比两个版本的 Agent:
import random
import time
from dataclasses import dataclass, field
@dataclass
class ABTestConfig:
"""A/B 测试配置"""
test_name: str
variant_a: dict # Agent A 配置
variant_b: dict # Agent B 配置
traffic_split: float # A 的流量比例 (0.0-1.0)
min_sample_size: int # 最小样本量
@dataclass
class ABTestMetrics:
"""A/B 测试指标"""
variant: str
response_time_ms: float
token_cost: float
customer_satisfaction: float | None = None
escalation_rate: float = 0.0
class ABTestRunner:
def __init__(self, config: ABTestConfig):
self.config = config
self.metrics: dict[str, list[ABTestMetrics]] = {"A": [], "B": []}
def assign_variant(self, customer_id: str) -> str:
"""基于客户 ID 确定性分组(同一客户始终在同一组)"""
hash_val = hash(customer_id) % 100
return "A" if hash_val < self.config.traffic_split * 100 else "B"
async def run_and_record(
self, customer_id: str, message: str
) -> tuple[str, str]:
"""执行 A/B 测试并记录指标"""
variant = self.assign_variant(customer_id)
agent_config = (
self.config.variant_a if variant == "A"
else self.config.variant_b
)
start = time.time()
response = await run_agent(message, customer_id, agent_config)
elapsed = (time.time() - start) * 1000
self.metrics[variant].append(ABTestMetrics(
variant=variant,
response_time_ms=elapsed,
token_cost=response["token_cost"],
))
return variant, response["text"]
def get_summary(self) -> dict:
"""汇总 A/B 测试结果"""
summary = {}
for variant in ("A", "B"):
data = self.metrics[variant]
if not data:
continue
summary[variant] = {
"sample_size": len(data),
"avg_response_ms": sum(m.response_time_ms for m in data) / len(data),
"avg_cost": sum(m.token_cost for m in data) / len(data),
"p95_response_ms": sorted(
[m.response_time_ms for m in data]
)[int(len(data) * 0.95)],
}
return summary
Step 5: 回归测试——每次改 prompt 都要跑
# 回归测试套件
class AgentRegressionTest:
"""每次修改 prompt 或模型后必须运行的测试"""
def __init__(self):
self.judge = LLMJudge()
self.test_cases = self._load_test_cases()
def _load_test_cases(self) -> list[EvalCase]:
"""从 JSON 文件加载测试用例"""
import json
with open("tests/regression_cases.json") as f:
cases = json.load(f)
return [EvalCase(**case) for case in cases]
async def run_full_suite(self, agent) -> dict:
"""运行完整回归测试"""
results = []
for case in self.test_cases:
# 运行 Agent
response = await agent.handle_message(
case.input_message,
customer_id="TEST_USER"
)
# LLM 评估
eval_result = await self.judge.evaluate(case, response)
results.append({
"case": case.input_message[:50],
"score": eval_result.score,
"passed": eval_result.passed,
"reasoning": eval_result.reasoning
})
# 汇总
total = len(results)
passed = sum(1 for r in results if r["passed"])
avg_score = sum(r["score"] for r in results) / total
return {
"total_cases": total,
"passed": passed,
"failed": total - passed,
"pass_rate": passed / total,
"avg_score": avg_score,
"failed_cases": [r for r in results if not r["passed"]]
}
实战经验
生产数据
我的客服 Agent 测试体系:
| 测试层级 | 用例数 | 运行时间 | 运行频率 | 成本/次 |
|---|---|---|---|---|
| Unit Tests | 127 | 3 秒 | 每次 commit | $0 |
| Integration Tests | 43 | 25 秒 | 每次 PR | $0 |
| LLM-as-Judge 评估 | 85 | 12 分钟 | 每次 prompt 变更 | $2.50 |
| 回归测试全套 | 85 | 15 分钟 | 每周 | $3.20 |
| A/B 测试 | 持续 | 持续 | 上新版本时 | ~$50/周 |
踩过的坑
坑 1:LLM-as-Judge 的偏见。 LLM 倾向于给更长、更正式的回答更高分。解决方案:在 judge prompt 中明确强调 "简洁精确的回答优于冗长但准确的回答",并且加入长度惩罚项。
坑 2:测试用例的维护成本。 每次产品功能更新,测试用例也要更新。我们建立了一个规则:每个新功能的 PRD 里必须包含 3-5 个 Agent 测试用例。
坑 3:回归测试的结果波动。 同一套测试用例,两次运行的 LLM-as-Judge 评分可能有 5-10% 的波动。解决方案:每个用例跑 3 次取中位数,减少随机性。
坑 4:A/B 测试的样本量问题。 日活 2 万的产品,客服请求只有几百次。要积累到统计显著的样本量需要 2-3 周。加速方案:同时做 synthetic evaluation(用 LLM 模拟客户发送测试消息)。
总结
三条核心 takeaway:
-
Agent 测试是分层的,不是一种方法打天下——确定性部分用传统单元测试,工具链路用集成测试,输出质量用 LLM-as-Judge,生产效果用 A/B 测试。每一层解决不同的问题。
-
LLM-as-Judge 是 Agent 评估的核心手段,但要注意它的局限性——评判模型自身有偏见(偏好长回答、正式语气),需要在 prompt 中显式校正。关键质量指标(价格、政策)仍然需要规则检查。
-
回归测试是安全网——每次改 prompt、换模型、更新知识库,都必须跑回归测试。没有这个安全网,你不敢改任何东西,Agent 就会停在第一版不再迭代。
如果你还没有给 Agent 建测试体系,从最简单的开始:写 10 个核心场景的 LLM-as-Judge 评估用例,配合 20 个确定性的单元测试。这个组合足以覆盖最常见的问题。
你是怎么测试 Agent 的?有没有踩过类似的坑?欢迎分享。