Agent 评测体系搭建:自动化场景、人工抽检与回归测试
一、Agent 改了一行 Prompt,线上准确率从 92% 掉到 78%
这事发生在 Agent 上线第二个月。一个"优化"——在系统 Prompt 里加了"请更详细地回答"——导致 Agent 在数学计算类问题上开始"脑补"推理步骤,幻觉率从 5% 跳到 23%。没有任何评测体系拦着,直接上线,用户投诉。
Agent 的评测比传统软件的自动化测试复杂一个数量级。传统软件输出是确定性的(1+1=2),Agent 输出是概率性的(同一个问题问两次可能得到不同答案)。这意味着"pass/fail"的二元判定不够用,需要构建多维度的评分体系。
更深一层看,Agent 的评测要同时面对三种不确定性:模型本身的不确定性(同样的 Prompt 生成不同回答)、工具调用的不确定性(可能调用错误的工具、调用次数异常)、以及上下文依赖的不确定性(同一个问题在不同对话历史中的回答可能不同)。传统单元测试靠 assert 搞定,Agent 评测得靠"分层打分 + 回归基线 + 人工兜底"三层机制才能兜住质量。另一个真实案例:一个 Agent 在处理"帮我分析这组数据"时,偶尔会调用数据库工具返回全表数据而非聚合查询,导致 Token 消耗从 2K 飙到 50K。这个 Bug 在 100 次测试中只出现 3 次——概率性缺陷必须靠大量重复评测才能捕获。
二、三层评测金字塔:自动化→场景→人工
graph TD A["代码变更 (PR)"] --> B["Layer 1: 自动化回归<br/>每次 PR 触发"] B --> B1["断言测试<br/>- 格式完整性<br/>- 工具调用次数<br/>- 响应时间"] B --> B2["对抗测试<br/>- 注入攻击<br/>- 边界输入<br/>- 空输入"] B --> C{"Layer 1 通过?"} C -->|"否"| D["阻塞 PR<br/>禁止合并"] C -->|"是"| E["Layer 2: 场景评测<br/>每日定时 + 发版前"] E --> E1["标准题库 (200题)<br/>- 覆盖 10 个领域<br/>- 自动评分"] E --> E2["评分维度<br/>- 准确率<br/>- 幻觉率<br/>- 工具使用效率<br/>- 回复长度分布"] E --> F{"指标劣化?"} F -->|"是 (>5%)"| G["阻塞发版<br/>需人工 review"] F -->|"否"| H["Layer 3: 人工抽检<br/>每周一次"] H --> H1["随机抽样 50 条<br/>会话记录"] H --> H2["评分维度<br/>- 有用性 (1-5)<br/>- 安全性 (pass/fail)<br/>- 流畅度 (1-5)"] H --> I{"评分趋势"} I -->|"下降"| J["触发深度 review<br/>调整 Prompt / 工具"] I -->|"稳定/上升"| K["通过"] style D fill:#FF6B6B,color:#fff style G fill:#FF6B6B,color:#fff style K fill:#50B86C,color:#fff评测金字塔的三层设计:
- Layer 1(自动化回归):运行快(< 5 分钟),成本低,覆盖基础断言。每次 PR 必须通过。核心是断言测试——验证 Agent 的输出格式是否正确、工具调用次数是否合理、响应时间是否在阈值内。对抗测试同样必不可少:注入攻击 Prompt、空输入、超长输入、多语言混杂输入,这些边界条件在概率性输出下更容易出问题。传统软件的空输入最多返回 400,Agent 的空输入可能会"脑补"一个回答。
- Layer 2(场景评测):运行慢(10-30 分钟),有 API 调用成本。每日跑一次 + 发版前全量跑。标准题库的维护是最容易被低估的成本——Agent 的能力边界在扩展,三个月前的 200 题可能已经不再覆盖新功能。需要建立题库更新机制:每次发现线上 Bad Case 后第一时间转化为评测用例,保证题库随业务演进。题目的分类策略同样关键:按领域(数学/代码/推理/闲聊)和按难度(easy/medium/hard)交叉分组,才能精准定位劣化在哪个维度。
- Layer 3(人工抽检):最贵,最慢,但最能发现自动化测不出的问题(如语气不恰当、回答啰嗦)。每周一次,每次随机抽取 50 条真实会话。人工评分的维度需要结构化——有用性(1-5)、安全性(pass/fail)、流畅度(1-5)——避免评分者主观波动。关键实践:同一组会话由两人交叉评分,计算 Cohen's Kappa 一致性系数,低于 0.6 则需要重新校准评分标准。人工层不是兜底,是质量体系的质量体系。
四、生产级评测平台实现
场景评测引擎
""" Agent 评测引擎 支持多维度评分、回归基线和趋势分析 """ import asyncio import json import time from typing import List, Dict, Any, Optional from dataclasses import dataclass, field from enum import Enum import numpy as np from openai import AsyncOpenAI class ScoreDimension(str, Enum): """评测维度""" ACCURACY = "accuracy" # 准确率:回答是否事实正确 HALLUCINATION = "hallucination" # 幻觉率:是否捏造了信息 TOOL_EFFICIENCY = "tool_efficiency" # 工具使用效率:是否调用了不必要的工具 LATENCY = "latency" # 响应延迟 COMPLETENESS = "completeness" # 完整性:是否回答了问题的所有部分 @dataclass class TestCase: """通用评测用例""" id: str category: str # 题目分类:math/code/reasoning/... user_query: str expected_tools: List[str] # 期望调用的工具列表 forbidden_tools: List[str] # 不应调用的工具 expected_facts: List[str] # 期望回答中包含的事实点 difficulty: str = "medium" # easy/medium/hard @dataclass class EvalResult: """单题评测结果""" test_case_id: str agent_response: str tool_calls_made: List[str] latency_ms: float scores: Dict[str, float] passed: bool error: Optional[str] = None class AgentEvaluator: """Agent 评测器""" def __init__(self, llm_client: AsyncOpenAI, config: dict): self.llm = llm_client self.config = config # 评测基线的历史数据,用于回归检测 self.baseline_metrics: Dict[str, float] = {} # 评分阈值 self.thresholds = { ScoreDimension.ACCURACY: 0.85, ScoreDimension.HALLUCINATION: 0.10, # 允许最大 10% 幻觉率 ScoreDimension.TOOL_EFFICIENCY: 0.70, } async def evaluate_test_case( self, test_case: TestCase, agent_call_fn, # Agent 调用函数 ) -> EvalResult: """ 评测单个测试用例 agent_call_fn 是抽象的回调: 为什么用回调而非硬编码 Agent 调用: 不同 Agent 版本可能有不同的调用方式(HTTP/SDK/Local), 评测引擎不应该耦合到具体调用实现 """ t0 = time.time() tool_calls = [] try: # 设置 30s 超时,避免模型响应过慢卡住评测流水线 response, calls = await asyncio.wait_for( agent_call_fn(test_case.user_query), timeout=30.0 ) tool_calls = calls except asyncio.TimeoutError: return EvalResult( test_case_id=test_case.id, agent_response="", tool_calls_made=[], latency_ms=30000, scores={}, passed=False, error="Timeout", ) except Exception as e: return EvalResult( test_case_id=test_case.id, agent_response="", tool_calls_made=[], latency_ms=0, scores={}, passed=False, error=str(e), ) t1 = time.time() latency_ms = (t1 - t0) * 1000 # —— 自动化评分 —— scores = {} # 评分 1: 工具调用正确性(断言检查) expected_set = set(test_case.expected_tools) actual_set = set(tool_calls) # 必须调用的工具都调了 tool_precision = len(expected_set & actual_set) / max(len(expected_set), 1) # 禁止调用的工具没调 forbidden_set = set(test_case.forbidden_tools) tool_violation = len(forbidden_set & actual_set) tool_score = tool_precision * (1.0 if tool_violation == 0 else 0.5) scores["tool_correctness"] = tool_score # 评分 2: 用 LLM 评测事实准确性和幻觉 accuracy, hallucination = await self._llm_evaluate( test_case, response ) scores[ScoreDimension.ACCURACY] = accuracy scores[ScoreDimension.HALLUCINATION] = hallucination # 评分 3: 工具使用效率 # 超过期望工具数 3 个以上 → 效率低 efficiency = min(1.0, len(test_case.expected_tools) / max(len(tool_calls), 1)) scores[ScoreDimension.TOOL_EFFICIENCY] = efficiency # 综合判定 passed = all([ scores.get(ScoreDimension.ACCURACY, 0) >= self.thresholds[ScoreDimension.ACCURACY], scores.get(ScoreDimension.HALLUCINATION, 1) <= self.thresholds[ScoreDimension.HALLUCINATION], scores.get(ScoreDimension.TOOL_EFFICIENCY, 0) >= self.thresholds[ScoreDimension.TOOL_EFFICIENCY], ]) return EvalResult( test_case_id=test_case.id, agent_response=response, tool_calls_made=tool_calls, latency_ms=latency_ms, scores=scores, passed=passed, ) async def _llm_evaluate( self, test_case: TestCase, response: str ) -> tuple[float, float]: """ 使用 LLM 评估回答质量 为什么用 LLM 评测 LLM: 事实准确性和幻觉判断需要语义理解,用规则无法覆盖。 这是当前业界共识的"LLM-as-Judge"模式。 为什么用单独的 prompt 而非 Agent 自身的系统 prompt: 评测的评分标准需要固定,不能受 Agent 系统 prompt 变化影响 """ eval_prompt = f"""你是一个严格的评测员。请根据以下标准评估回答: 问题:{test_case.user_query} 期望包含的事实点:{', '.join(test_case.expected_facts)} Agent 的回答:{response} 请以 JSON 格式输出: {{ "accuracy": 0.0-1.0, // 事实正确性分数 "hallucination_rate": 0.0-1.0, // 幻觉率(捏造事实的比例) "explanation": "简短解释" }} 评分细则: - accuracy: 期望事实点中正确提及的比例 - hallucination_rate: 回答中非问题事实且无法验证的内容占比 """ try: resp = await self.llm.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": eval_prompt}], temperature=0, # 评测必须确定性,temperature=0 max_tokens=500, response_format={"type": "json_object"}, ) result = json.loads(resp.choices[0].message.content) return float(result.get("accuracy", 0)), float(result.get("hallucination_rate", 0)) except Exception: # LLM 评测本身也可能失败,返回默认值并标记需要人工复核 return 0.0, 0.0 async def run_full_evaluation( self, test_cases: List[TestCase], agent_call_fn, max_concurrency: int = 5, ) -> Dict[str, Any]: """ 运行全量评测 max_concurrency: 并发限制 为什么限制并发:LLM API 有 rate limit, 无限制并发会触发 429 限流 """ semaphore = asyncio.Semaphore(max_concurrency) async def limited_evaluate(tc: TestCase): async with semaphore: return await self.evaluate_test_case(tc, agent_call_fn) tasks = [limited_evaluate(tc) for tc in test_cases] results = await asyncio.gather(*tasks, return_exceptions=True) # 汇总统计 valid_results = [r for r in results if isinstance(r, EvalResult)] passed_count = sum(1 for r in valid_results if r.passed) summary = { "total": len(test_cases), "passed": passed_count, "failed": len(test_cases) - passed_count, "errors": sum(1 for r in results if isinstance(r, Exception)), "mean_accuracy": np.mean([r.scores.get("accuracy", 0) for r in valid_results]), "mean_hallucination": np.mean([r.scores.get("hallucination", 0) for r in valid_results]), "mean_latency_ms": np.mean([r.latency_ms for r in valid_results]), "per_category": self._per_category_stats(valid_results, test_cases), "details": [r for r in valid_results if not r.passed], } # 回归检测:与基线对比 if self.baseline_metrics: regression = self._detect_regression(summary) if regression: summary["regression_warning"] = regression return summary def _per_category_stats(self, results, test_cases): """按类别统计通过率""" tc_map = {tc.id: tc for tc in test_cases} category_stats = {} for r in results: tc = tc_map.get(r.test_case_id) if tc and tc.category not in category_stats: category_stats[tc.category] = {"total": 0, "passed": 0} if tc: category_stats[tc.category]["total"] += 1 if r.passed: category_stats[tc.category]["passed"] += 1 return category_stats def _detect_regression(self, current: dict) -> Optional[str]: """检测关键指标是否劣化超过阈值""" warnings = [] if current["mean_accuracy"] < self.baseline_metrics.get("mean_accuracy", 0) - 0.05: warnings.append(f"准确率下降: {current['mean_accuracy']:.2%}") if current["mean_hallucination"] > self.baseline_metrics.get("mean_hallucination", 0) + 0.05: warnings.append(f"幻觉率上升: {current['mean_hallucination']:.2%}") return "; ".join(warnings) if warnings else NoneCI 集成
# .github/workflows/agent-eval.yml name: Agent Evaluation on: pull_request: paths: - 'agent/**' # Agent 相关代码变更 - 'prompts/**' # Prompt 变更 jobs: eval: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Agent Evaluation run: | python scripts/eval.py \ --test-suite standard_200 \ --output eval_report.json \ --baseline eval_baselines/main.json - name: Check Results run: | python scripts/check_eval.py eval_report.json # 如果准确率下降 > 5% 或幻觉率上升 > 5%,exit 1 阻塞 PR四、评测体系的边界与成本
核心矛盾:评测精度 vs 评测成本。Layer 1 几乎免费但只能测格式;Layer 2 每次跑 200 题约 $5-10 的 API 费用;Layer 3 的人力成本在规模化后是指数级增长。你需要根据业务阶段选择评测深度:MVP 阶段靠人工兜底,快速迭代阶段靠自动回归防退化,规模化阶段三管齐下。
缺点:
- LLM-as-Judge 的自身偏见:用 GPT-4 评测 GPT-4 的生成结果,可能高估实际质量——模型倾向于给自己的输出打高分。应使用不同类型的模型(如 Claude)做交叉验证,或引入结构化评分标准(如 G-Eval 的 Chain-of-Thought 评分法)降低主观偏差。另一种缓解策略是"反向评测":让评测模型从最严格的角度挑错,而非从最宽松的角度打分。
- 评测用例的时效性:Agent 的能力边界在扩展,评测用例需要持续更新。标准题库的维护成本不可忽视——200 题看起来不多,但每个题目需要标注 expected_tools、expected_facts、forbidden_tools 等元信息,单个题目的标注成本约 2-5 分钟。题库扩大到 500 题时,单次全量评测的 API 费用也随之翻倍。
- 自动化评分的覆盖盲区:工具使用效率可以用规则判断,但回答的"语气是否恰当""解释是否清晰""是否过度自信"等维度仍需人工。一个 Agent 可能准确率 95% 但语气傲慢——自动化评测发现不了这个问题,直到用户流失。
禁用场景:
- 预算极有限的个人项目:LLM 评测每次跑 200 题的 API 费用约 $5-10,不适合频繁执行。最小可用方案是 Layer 1 断言测试 + 手动抽检 5 条/周。
- 对话内容涉及强个性化的 Agent(如心理辅导、情感陪伴):自动化评测无法替代专业评审,且 LLM-as-Judge 对情感类内容的判断准确度不足。
五、总结
Agent 评测体系的三层金字塔(自动化回归→场景评测→人工抽检),分别对应 CI 检查、发版门禁和定期质量审计。自动化层用断言+LLM-as-Judge 做多维评分,场景层用标准题库做回归检测,人工层用抽样做最终兜底。
关键设计决策:
- 多维度评分取代二元判定:准确率、幻觉率、工具效率、响应延迟——任何一个维度的劣化都应该触发警报,而非等整体 pass/fail 不可靠时才发现。
- 回归基线机制:每次评测结果与基线对比,任何质量劣化超过 5% 就阻塞发版。这是防退化的最后防线。
- LLM-as-Judge 的交叉验证:用不同模型的评测结果做交叉比对,降低单一模型的评分偏见。
踩过的坑:别指望人工抽检能覆盖全量——50 条的样本量在 5000 条日活面前统计意义有限。人工层的真正价值是发现自动化覆盖不到的问题类型,然后把这些类型转化为新的自动化评测维度。