1. 评测这件事,为什么在 Agent 场景下变得格外棘手
做 Agent 开发的人大多有过类似的体验:模型换了一版,工具调用链路调了一轮,Prompt 改了几行,然后呢?然后就没有然后了。你问自己"这次改动到底让 Agent 变好了还是变差了",答案往往是"感觉好像好一点"。这种"凭感觉"的状态,在传统软件工程里是不可接受的,但在 Agent 领域却长期存在。
原因不复杂。传统软件的输入输出是确定的,一个函数给定输入必然返回可预期的结果,写个单元测试就能锁住行为。Agent 不一样,它的输出是自然语言、是工具调用序列、是多轮对话中的决策路径,同一个任务跑两次可能得到两条完全不同的轨迹,但两条轨迹可能都对。这就让"评测"从一个工程问题,变成了一个既要工程手段、又要语义判断的复合问题。
我最初做 Agent 评测的时候,走的是最朴素的路子:准备一批测试用例,跑一遍,人工看结果,打分。前二十条还行,到第五十条就开始走神,到第一百条基本就是扫一眼"看起来没问题"就过了。这种评测的置信度极低,而且完全不可复现——换个人来评,分数可能差出一大截。
后来我意识到,Agent 评测体系要解决的核心矛盾是:如何在输出不确定的前提下,建立一套可复现、可量化、可对比的评估机制。这套机制需要三个支柱:一是标准化的执行环境(也就是 harness),二是明确的评分标准(也就是 rubric),三是可规模化的自动评判(也就是 LLM-judge)。这三个词不是随便凑的,它们分别对应评测流程中的"怎么跑""怎么评""谁来评"。
这篇内容适合谁看?如果你正在做 Agent 项目,不管是工具调用型、多轮对话型还是任务编排型,只要你需要回答"我的 Agent 到底行不行"这个问题,这套体系就能用上。如果你还没开始做 Agent,但想提前了解评测该怎么搭,也可以先建立框架认知。我会尽量把每个环节的"为什么"讲清楚,而不是只丢一堆配置让你抄。
2. Harness:把 Agent 关进一个可复现的笼子里
2.1 Harness 到底解决的是什么问题
Harness 这个词在 Agent 语境下,指的是一套标准化的执行框架,它负责把 Agent 跑起来、记录过程、收集结果。你可以把它理解成"Agent 的测试跑道"——没有跑道,你每次测试都在不同的地形上跑,成绩没有可比性。
我见过很多团队的评测方式是:打开对话框,手动输入问题,看 Agent 回复,截图存档。这种方式的问题在于,每次执行的温度参数可能不同、上下文可能被污染、工具返回可能因为网络波动而变化。你根本不知道这次跑出来的结果,是 Agent 能力的变化,还是环境噪声。
一个合格的 harness 需要做到几件事:
- 输入标准化:每个测试用例的输入格式统一,包括用户消息、系统提示、可用工具列表、初始状态。
- 执行隔离:每个用例在独立的环境中执行,前一个用例的副作用不能影响后一个。
- 过程记录:完整记录 Agent 的每一步决策,包括思考过程、工具调用参数、工具返回结果、最终输出。
- 结果结构化:把执行结果整理成可分析的格式,方便后续评分和对比。
提示:如果你的 Agent 涉及文件操作、数据库写入等有副作用的工具,执行隔离尤其重要。我踩过的坑是,前一个用例写了一个文件,后一个用例读到了这个文件,导致结果完全不可复现。
2.2 自建 Harness 的最小可行结构
不一定非要上重型框架,一个最小可行的 harness 用几百行代码就能搭起来。核心结构大概是这样的:
class AgentHarness: def __init__(self, agent, tools, sandbox): self.agent = agent self.tools = tools self.sandbox = sandbox def run_case(self, case): # 1. 重置沙箱环境 self.sandbox.reset() # 2. 注入工具和初始状态 env = self.sandbox.setup(case.initial_state) # 3. 执行 Agent,记录完整轨迹 trace = [] for step in range(case.max_steps): action = self.agent.step(case.input, env, trace) trace.append(action) if action.is_terminal: break observation = env.execute(action) trace.append(observation) # 4. 收集结果 return { "case_id": case.id, "trace": trace, "final_output": trace[-1], "steps": len(trace), "status": "completed" if action.is_terminal else "timeout" }这个结构的关键点在于trace的完整性。很多人只记录最终输出,但 Agent 评测中,过程往往比结果更重要。一个 Agent 可能碰巧给出了正确答案,但中间调用了错误的工具、传了错误的参数,这种"蒙对"的情况如果不看轨迹是发现不了的。
2.3 执行环境隔离的几种做法
隔离级别从轻到重大致有三种:
| 隔离方式 | 隔离程度 | 启动速度 | 适用场景 |
|---|---|---|---|
| 进程内重置 | 低 | 极快 | 纯对话型 Agent,无副作用工具 |
| 子进程隔离 | 中 | 快 | 有文件操作,但不需要复杂依赖 |
| 容器隔离 | 高 | 中等 | 需要完整环境、有网络调用、有系统级操作 |
我自己的选择是:日常快速迭代用子进程隔离,正式评测跑容器隔离。容器隔离虽然启动慢一点,但能保证每次执行的环境完全一致,包括依赖版本、环境变量、文件系统状态。如果你的 Agent 需要调用外部 API,容器里还可以做网络层的 mock,避免因为外部服务不稳定导致评测结果波动。
注意:容器隔离时,镜像的构建要固定版本。我遇到过因为基础镜像更新导致 Python 版本变化,进而影响 Agent 行为的情况。所有依赖都要 pin 到具体版本。
2.4 轨迹记录里最容易被忽略的字段
大部分人记录轨迹时只记了"调用了什么工具、参数是什么、返回是什么"。但有几个字段对后续分析极其重要,却经常被漏掉:
- 时间戳:每一步的耗时,能帮你发现性能瓶颈。有些 Agent 在某个工具调用上反复重试,耗时飙升,但只看结果看不出来。
- Token 消耗:每一步的输入输出 token 数,直接关系到成本。评测不只看效果,也要看效率。
- 重试次数:Agent 是否在某个步骤上反复尝试。高重试次数往往意味着 Prompt 或工具描述有问题。
- 中间思考:如果 Agent 有显式的思考过程(比如 ReAct 模式),一定要记录下来。这是分析 Agent 决策逻辑的核心素材。
我后来在轨迹里加了一个retry_count字段,结果发现某个工具的描述有歧义,导致 Agent 在 30% 的用例里都要重试两三次才调对。这个问题光看最终输出是发现不了的。
3. Rubric:把"好不好"翻译成可打分的标准
3.1 为什么不能只用"对/错"来评分
Agent 的任务大多不是选择题,没有唯一正确答案。比如你让 Agent 帮忙订机票,它可能选了不同的航班、不同的舱位、不同的时间,但只要满足用户约束(预算、时间范围、出发地目的地),就都是对的。这种情况下,二元评分完全不够用。
Rubric 就是一套评分量表,它把"好不好"拆解成多个维度,每个维度有明确的评分标准和分值。一个好的 rubric 应该做到:不同的人拿着同一份 rubric 去评同一个结果,能得出接近的分数。
我见过的最糟糕的 rubric 是"回答质量:1-5 分"。这种 rubric 等于没有,因为每个人对"质量"的理解都不一样。好的 rubric 应该是"是否准确回答了用户问题:完全准确(5分)、基本准确但有细节遗漏(3分)、部分错误(1分)、完全错误(0分)"。
3.2 构建 Rubric 的实操方法
构建 rubric 的流程,我总结为四步:
第一步:收集失败案例。先别急着定标准,先跑一批用例,把 Agent 表现不好的案例收集起来。看看它都在哪些地方翻车,这些翻车点就是 rubric 需要覆盖的维度。
第二步:归类失败模式。把收集到的失败案例归类。比如"工具调用参数错误""多轮对话中丢失上下文""最终输出格式不符合要求""推理过程有逻辑跳跃"等等。每一类对应 rubric 中的一个维度。
第三步:定义评分等级。每个维度定义 3-5 个等级,等级描述要具体、可观察。避免使用"较好""一般"这种模糊词汇,用"包含所有必要信息""缺少一个关键信息""缺少两个以上关键信息"这种可数的描述。
第四步:确定权重。不同维度的重要性不同。比如对于工具调用型 Agent,"工具选择是否正确"可能比"输出语言是否流畅"重要得多。权重分配要反映业务的实际优先级。
一个实际用过的 rubric 示例:
| 维度 | 权重 | 评分标准 |
|---|---|---|
| 任务完成度 | 40% | 5=完全完成;3=部分完成;0=未完成 |
| 工具调用准确性 | 25% | 5=工具和参数均正确;3=工具正确参数有误;0=工具错误 |
| 推理逻辑性 | 20% | 5=逻辑连贯无跳跃;3=有小跳跃但不影响结论;0=逻辑混乱 |
| 输出规范性 | 15% | 5=完全符合格式要求;3=格式有小问题;0=格式错误 |
3.3 Rubric 设计中的常见陷阱
陷阱一:维度太多。我一开始设计了十几个维度,结果评分时根本顾不过来,而且维度之间高度相关,评了等于白评。后来精简到 4-6 个核心维度,评分效率和一致性都大幅提升。
陷阱二:等级描述不可观察。"推理过程合理"这种描述就是不可观察的,什么叫合理?应该改成"每一步推理都有前一步的依据,没有引入未提及的假设"。
陷阱三:忽略负面案例。Rubric 不仅要定义"做对了是什么样",也要定义"做错了是什么样"。特别是要区分"做错了"和"没做"——前者可能是能力问题,后者可能是理解问题,修复方向完全不同。
提示:Rubric 不是定完就锁死的。我通常会在评测进行到一半时,回头看看有没有大量用例落在两个等级之间难以判断,如果有,说明这个维度的等级划分需要细化。
3.4 让 Rubric 可执行:从文档到代码
Rubric 写在文档里只是第一步,真正要让它可执行,需要把它翻译成评分逻辑。对于可以自动判断的维度(比如格式是否符合、工具调用是否正确),直接写代码判断。对于需要语义理解的维度(比如推理逻辑性),则交给 LLM-judge。
我的做法是混合评分:能自动判的自动判,不能自动判的走 LLM-judge,最后加权汇总。这样既保证了效率,又保证了语义层面的评估质量。
def score_case(case, trace, rubric): scores = {} # 自动评分维度 scores["format"] = check_format(trace.final_output, case.expected_format) scores["tool_accuracy"] = check_tool_calls(trace, case.expected_tools) # LLM-judge 评分维度 scores["reasoning"] = llm_judge_reasoning(trace, rubric.reasoning_criteria) scores["completeness"] = llm_judge_completeness(trace, case.task, rubric.completeness_criteria) # 加权汇总 total = sum(scores[k] * rubric.weights[k] for k in scores) return total, scores4. LLM-judge:让模型来当裁判,但别让它随便判
4.1 LLM-judge 的能力边界
用 LLM 来评判 LLM 的输出,听起来有点"自己判自己"的味道,但实际用下来,在明确定义的评分标准下,LLM-judge 和人工评分的一致性可以做到相当高。关键是你要清楚它能做什么、不能做什么。
LLM-judge 擅长的:语义层面的判断,比如"回答是否准确""推理是否连贯""是否遗漏了关键信息"。这些任务人类评起来也费劲,LLM 反而因为见过大量文本,判断得又快又稳。
LLM-judge 不擅长的:需要精确计算或外部验证的判断,比如"这个数字算得对不对""这个 API 调用是否真的成功了"。这些应该交给代码去判,不要让 LLM 去猜。
我踩过的一个坑是:让 LLM-judge 判断"工具调用参数是否正确",结果它经常把参数顺序不同但语义相同的调用判为错误。后来我把这个维度改成代码判断,只比较参数的实际语义值,问题就解决了。
4.2 写好 Judge Prompt 的几个关键点
Judge Prompt 的质量直接决定评分质量。我总结的几个要点:
第一,给出明确的评分标准。不要只说"请评分",要把 rubric 完整地放进 Prompt 里,包括每个等级的具体描述。
第二,提供参考示例。在 Prompt 里放一两个已评分的示例,让 Judge 知道什么样的回答对应什么分数。这能显著提升评分一致性。
第三,要求给出评分理由。让 Judge 在给出分数之前,先写出判断理由。这不仅能让评分更可解释,还能让 Judge 的推理更谨慎。
第四,限制输出格式。要求 Judge 以固定格式输出,比如 JSON,方便后续解析。不要让它自由发挥,否则解析起来很痛苦。
一个 Judge Prompt 的骨架:
你是一个 Agent 输出评分员。请根据以下评分标准,对给定的 Agent 执行轨迹进行评分。 评分标准: - 任务完成度(40%):5=完全完成用户请求的所有子任务;3=完成了主要任务但有遗漏;0=未完成核心任务 - 推理逻辑性(30%):5=每步推理都有依据,逻辑连贯;3=有轻微跳跃但不影响结论;0=逻辑混乱或存在明显矛盾 - 输出规范性(30%):5=完全符合指定格式;3=格式有轻微偏差;0=格式错误 请先分析轨迹,然后给出每个维度的分数和理由,最后以 JSON 格式输出: {"completeness": {"score": 5, "reason": "..."}, "reasoning": {...}, "format": {...}}4.3 校准 LLM-judge:让它和人类评委对齐
LLM-judge 刚上线时,评分和人工评分往往有偏差。校准的方法是:先人工评一批用例(至少 30-50 条),然后让 LLM-judge 评同一批,对比两者的差异。
如果发现系统性偏差(比如 LLM-judge 普遍给分偏高),就调整 Prompt 里的标准描述。如果发现个别维度的偏差大,就针对那个维度补充示例或细化标准。
我做过一次校准,发现 LLM-judge 在"推理逻辑性"这个维度上普遍比人工评分高 1 分左右。分析后发现,是因为 Judge 倾向于认为"只要最终结论对,推理过程就是合理的"。后来我在 Prompt 里加了一句"请独立评估推理过程,不要因为最终结论正确就默认推理过程正确",偏差就明显缩小了。
注意:校准不是一次性的。每次更换 Judge 模型、修改评分标准、或者 Agent 行为发生较大变化时,都需要重新校准。我通常每隔一段时间就抽一批用例做人工复核,确保 Judge 没有漂移。
4.4 多 Judge 投票:降低单次评判的随机性
LLM 的输出有随机性,同一个用例跑两次可能得到不同的评分。对于关键评测,我会用多个 Judge 分别评分,然后取中位数或多数投票。
具体做法是:用同一个 Judge Prompt,但设置不同的随机种子(或者用不同的模型),跑 3-5 次,然后对每个维度的分数取中位数。如果某个维度的分数方差很大,说明这个用例本身处于评分标准的模糊地带,需要人工介入判断。
这种方法成本会高一些,但对于最终决策级别的评测(比如"这个版本能不能上线"),多花这点成本是值得的。
5. 把三个组件串起来:评测流水线的搭建与运行
5.1 流水线的整体架构
Harness、Rubric、LLM-judge 三个组件不是孤立的,它们需要串成一条流水线:
- 用例加载:从用例库中读取测试用例,每个用例包含输入、期望行为、适用的 rubric。
- Harness 执行:逐个执行用例,收集完整轨迹。
- 自动评分:对可自动判断的维度进行代码评分。
- LLM-judge 评分:对需要语义判断的维度进行模型评分。
- 结果汇总:合并所有评分,生成评测报告。
- 差异分析:与基线版本对比,找出退化和改进的用例。
这条流水线可以手动触发,也可以集成到 CI 中,每次 Agent 代码变更时自动跑一遍。
5.2 用例库的维护策略
用例库不是越多越好,关键是覆盖度。我的用例库分为几个层次:
- 冒烟用例(10-20 条):覆盖核心功能,每次变更都跑,快速发现问题。
- 回归用例(100-200 条):覆盖已知的边界情况和历史 bug,每周跑一次。
- 探索用例(持续增加):从线上失败案例中提取,不断补充。
用例的格式要统一,每个用例至少包含:用例 ID、输入、期望行为描述、适用 rubric、优先级。期望行为描述不要写得太死,要给 Agent 留出合理的发挥空间。
5.3 评测报告的读法
评测报告不是只看总分。总分从 3.2 涨到 3.5,可能意味着整体提升,也可能意味着某些用例大幅提升掩盖了另一些用例的退化。我读报告的习惯是:
先看总分变化,再看各维度变化,然后看用例级别的差异。重点关注三类用例:从通过变为不通过的(退化)、从不通过变为通过的(改进)、以及评分方差大的(不稳定)。
对于退化的用例,要逐条分析轨迹,找出退化的原因。是 Prompt 改动导致的?是工具描述变化导致的?还是模型版本更新导致的?只有定位到原因,才能决定是修复还是接受。
5.4 评测频率与成本控制
全量评测的成本不低,特别是涉及 LLM-judge 的时候。我的策略是分层:
- 每次代码提交:跑冒烟用例,只做自动评分,不跑 LLM-judge。
- 每日构建:跑回归用例,自动评分 + 单 Judge 评分。
- 版本发布前:跑全量用例,自动评分 + 多 Judge 投票。
这样既保证了日常迭代的快速反馈,又保证了发布前的评测质量。
6. 那些只有踩过才知道的评测坑
6.1 用例污染:Agent 记住了答案
这是最隐蔽的坑。如果你的用例库在迭代过程中被 Agent 的训练数据或上下文"见过",评测结果就会虚高。我遇到过一种情况:某个用例在多次评测中被反复使用,Agent 虽然没有跨会话记忆,但 Prompt 里的 few-shot 示例恰好包含了类似场景,导致它在这个用例上表现异常好。
避免方法是:保留一个"留出集",这部分用例不参与日常调试,只在最终评测时使用。另外,定期更新用例库,避免长期使用同一批用例。
6.2 评分标准漂移:rubric 悄悄变了
Rubric 在迭代过程中被修改是正常的,但如果没有版本管理,就会出现"这次评测用的是新标准,上次用的是旧标准,分数没法比"的情况。我的做法是给 rubric 打版本号,每次评测报告里标注使用的 rubric 版本。跨版本对比时,要么用同一版本重新评历史结果,要么明确说明标准变化。
6.3 LLM-judge 的"老好人"倾向
LLM-judge 普遍倾向于给高分,这是训练数据导致的。如果你的评分结果普遍偏高,区分度就会很差。解决方法除了前面说的校准之外,还可以在 Prompt 里明确要求"请严格评分,不要因为回答看起来流畅就给高分"。另外,可以在评分等级里增加"0分"的使用场景,让 Judge 知道什么时候该给零分。
6.4 执行环境的隐性依赖
Harness 执行时,Agent 可能依赖一些环境变量、系统时间、网络状态等。这些隐性依赖如果不固定,评测结果就会波动。我遇到过一次:Agent 在某个用例中需要获取当前日期,而评测在午夜前后跑,日期不同导致结果不同。后来我在 harness 里固定了系统时间,问题才解决。
提示:把所有隐性依赖都显式化。时间、随机种子、环境变量、外部服务状态,能固定的都固定,不能固定的就 mock。
6.5 评测通过但线上失败
这是最让人头疼的情况。评测全绿,上线后用户反馈一堆问题。原因通常是评测用例和真实场景有差距。真实用户的输入更随意、更模糊、更出人意料。解决方法是持续从线上收集失败案例,补充到用例库中。我现在的习惯是每周花半小时看线上日志,把有意思的失败案例整理成新用例。
7. 从零搭建评测体系的实际推进节奏
如果你现在还没有评测体系,不要想着一步到位。我建议的推进节奏是:
第一周:搭一个最简 harness,能跑用例、能记录轨迹就行。用例先准备 10 条,覆盖核心功能。评分先全部人工,目的是建立对 Agent 行为的直观感受。
第二周:把人工评分中反复出现的判断标准整理成 rubric。开始对可自动判断的维度写代码评分。
第三周:引入 LLM-judge,先在一个维度上试点,和人工评分对比校准。
第四周:把流水线串起来,集成到日常开发流程中。开始积累回归用例。
之后就是持续迭代:补充用例、细化 rubric、校准 Judge、优化 harness。评测体系不是建完就完事的项目,而是一个需要持续维护的基础设施。
我个人在实际操作中的体会是,评测体系的价值不在于给出一个分数,而在于它强迫你把"Agent 好不好"这个模糊的问题,拆解成一系列具体的、可回答的子问题。这个拆解过程本身,往往比评测结果更有价值——它会让你发现很多之前没意识到的 Agent 行为模式,也会让你对 Prompt 和工具设计的理解更深一层。
最后分享一个小技巧:每次评测完,不要只看报告,挑几条轨迹从头到尾读一遍。读轨迹的时候,你会以 Agent 的视角重新经历一遍任务,经常能发现报告里看不出来的问题——比如 Agent 在某个步骤犹豫了很久、或者用了一个很绕的方式完成了本可以简单完成的任务。这些细节,才是评测体系真正能给你的东西。