Agent 评测体系搭建自动化场景、人工抽检与回归测试一、Agent 改了一行 Prompt线上准确率从 92% 掉到 78%这事发生在 Agent 上线第二个月。一个优化——在系统 Prompt 里加了请更详细地回答——导致 Agent 在数学计算类问题上开始脑补推理步骤幻觉率从 5% 跳到 23%。没有任何评测体系拦着直接上线用户投诉。Agent 的评测比传统软件的自动化测试复杂一个数量级。传统软件输出是确定性的112Agent 输出是概率性的同一个问题问两次可能得到不同答案。这意味着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[阻塞 PRbr/禁止合并] 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[触发深度 reviewbr/调整 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、空输入、超长输入、多语言混杂输入这些边界条件在概率性输出下更容易出问题。传统软件的空输入最多返回 400Agent 的空输入可能会脑补一个回答。Layer 2场景评测运行慢10-30 分钟有 API 调用成本。每日跑一次 发版前全量跑。标准题库的维护是最容易被低估的成本——Agent 的能力边界在扩展三个月前的 200 题可能已经不再覆盖新功能。需要建立题库更新机制每次发现线上 Bad Case 后第一时间转化为评测用例保证题库随业务演进。题目的分类策略同样关键按领域数学/代码/推理/闲聊和按难度easy/medium/hard交叉分组才能精准定位劣化在哪个维度。Layer 3人工抽检最贵最慢但最能发现自动化测不出的问题如语气不恰当、回答啰嗦。每周一次每次随机抽取 50 条真实会话。人工评分的维度需要结构化——有用性1-5、安全性pass/fail、流畅度1-5——避免评分者主观波动。关键实践同一组会话由两人交叉评分计算 Cohens 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), timeout30.0 ) tool_calls calls except asyncio.TimeoutError: return EvalResult( test_case_idtest_case.id, agent_response, tool_calls_made[], latency_ms30000, scores{}, passedFalse, errorTimeout, ) except Exception as e: return EvalResult( test_case_idtest_case.id, agent_response, tool_calls_made[], latency_ms0, scores{}, passedFalse, errorstr(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_idtest_case.id, agent_responseresponse, tool_calls_madetool_calls, latency_mslatency_ms, scoresscores, passedpassed, ) 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( modelgpt-4, messages[{role: user, content: eval_prompt}], temperature0, # 评测必须确定性temperature0 max_tokens500, 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_exceptionsTrue) # 汇总统计 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/checkoutv4 - 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 条日活面前统计意义有限。人工层的真正价值是发现自动化覆盖不到的问题类型然后把这些类型转化为新的自动化评测维度。