两个智能体总分接近,说明总分这个指标已经把差别抹平了,再用总分去选,等于在噪声里挑。更可操作的做法是把 LoHoSearch 的评测日志捞出来,按任务类型把失败样本分开记数:看失败是不是集中在少数几类任务上,再判断这几类任务在你业务里占多大份量。如果 A 的失败集中在你不做的任务上,B 的失败却落在你的主场景里,那么总分谁高一点已经没有意义。下面的做法只依赖评测日志、任务清单标注和可重复的固定输入,能通过配置、日志和命令复现,不依赖任何外部结论。
总分接近的智能体,选型依据应该换成失败样本的任务类型分布:先按任务类型记失败数,再按“没找到、没说清、中途断掉”分形态,然后在你业务最在意的任务类型上逐条对照两边输出,最后用一份不可接受的失败清单做否决。适用前提是有一份带任务标签的固定评测集;某一类样本条数太少时只作观察,不作为选型依据。
把失败样本按任务类型分开记数
第一步不是看谁分高,而是让每条评测记录都能被任务类型切开。类型标签通常有两个来源:一是任务清单里本来就带标注(例如 label 文件里的 task_type 或场景字段),二是没有标注时按任务指令人工归类,归类规则要固定成一份小字典并写下来,例如检索定位、多跳推理、字段抽取、格式约束输出、时效性问答。两边智能体必须用同一份字典,否则统计不可比。把 task_type 和 agent_id 一起写进日志,是后面所有对照的前提。
计数表建议就四列,不要加太多指标:任务类型、该类型总条数、失败条数、失败样本编号列表。最后一列很关键,它把数字变成可回查的证据。留一列“失败形态”可以稍后再填,见下一节。
{"request_id":"r-0187","agent_id":"A","task_id":"t-22","task_type":"字段抽取","pass":false,"error_type":"schema_mismatch","latency_ms":1830}
{"request_id":"r-0188","agent_id":"B","task_id":"t-22","task_type":"字段抽取","pass":true,"error_type":null,"latency_ms":2410}
有了上面这种逐行记录,统计可以直接用命令跑,不必手工数:
jq -r '[.agent_id, .task_type, (.pass|tostring)] | @tsv' runs.jsonl \
| awk -F'\t' '{k=$1"\t"$2; n[k]++; if($3=="false") f[k]++} END{for(k in n) printf "%s\t%d\t%d\n", k, n[k], f[k]+0}' \
| sort
跑完要看的不是“失败最多的是哪类”,而是“失败是不是集中在少数几类”。如果失败散落在所有类型里,说明问题更可能是链路、提示词或解析器层面的共性缺陷,先修共性再谈选型;如果失败集中在一两类,继续往下做形态拆分和逐条对照。另外要标不确定性:某类型总条数只有个位数时,在表里注明“样本不足,仅作观察”,不要拿这类小样本的失败比例去支撑选型结论,也不要因为它好看就忽略。
把失败形态分成没找到、找到了没说清、中途断掉三类
把失败都当成同一类问题,会导致修错地方。同一份失败集合,按判定依据拆成三类,成本不高但判断质量差别很大。
- 没找到:正确依据根本没进候选集。判定看检索阶段返回的 doc_id / chunk_id 与标注答案来源有没有交集;交集为空即归此类。这类失败改检索策略、改写查询、调召回范围,改生成提示词基本无效。
- 找到了没说清:依据在候选里,但最终回答缺关键字段、与原文表述冲突、没有落到具体位置,或者答非所问却语气肯定。判定看最终输出与标注要点逐项对齐的结果,同时确认依据 id 出现在候选集中。
- 中途断掉:执行链路异常终止,典型表现是工具调用报错、超时、步数耗尽、输出被截断、结构化结果解析失败。判定看日志里的 error_type、状态码和最后一步工具返回。
每类至少抓一条原始样本存成判例,判例里放 request_id、原始输入、检索候选列表、最终输出和错误栈。判例的作用是让后面所有讨论都指向同一份事实,而不是各自回忆印象。判例文件建议按 agent_id 和 task_type 命名,方便对照时直接打开。
在同一类型任务上对两个智能体做逐条对照
总分差异要还原成具体任务上的差异,做法是固定任务、并排看输出。对照表的行用 task_id(只取你在意的任务类型),列建议为:task_id、任务类型、A 是否通过、B 是否通过、A 的失败形态、B 的失败形态、差异是否影响业务判断。行数不必多,一个类型挑十来条有代表性的就够用,关键是每条都能打开原始输入复看。
并排展示时不要把两段长文本整段贴在一起,容易看花。把结构化字段抽出来按字段对比,非结构化输出只保留结论句和引用位置,两边放在同一段里对照:
task_id: t-22 类型: 字段抽取
输入: (固定样例,附 task 文件路径)
A 输出: {"amount": null, "currency": "CNY", "source": "p.3"}
B 输出: {"amount": "1280.00", "currency": "CNY", "source": "p.3"}
判定: A 为“找到了没说清”(字段缺失),B 通过
这样对照下来,通常会看到差异只出现在某一两类任务上,而不是整体水平差一截。对照时也要记录相反方向的例子——B 在哪类任务上不如 A,避免只找支持自己偏好的样本。
写下业务上不能接受的失败类型
选型结论最后要落成边界,而不是“感觉 A 更稳”。建议先把业务场景列出来,对每个场景写清哪类失败是硬性不可接受、哪类可以容忍,再回填对应智能体的失败样本证据。下面是一份可以直接改用的清单结构,场景名和判定依据按你的实际情况替换。
| 业务场景 | 不可接受的失败 | 可容忍的失败 | 复核方式 |
|---|---|---|---|
| 对外发布的内容生成 | 编造依据、结论与原文冲突 | 表述冗长、缺少修饰 | 从该类型失败样本里抽若干条,固定输入人工复跑,确认是否可复现 |
| 辅助人工决策的检索问答 | 没找到(漏掉关键依据)且无提示 | 说明不完整,人工可补 | 核对候选集与标注来源的交集,确认漏检是否稳定出现 |
| 结构化字段填充 | 字段缺失、格式不合法、解析失败 | 附加说明文字偏多 | 用同一批输入跑 schema 校验脚本,统计不合法条目的具体字段 |
| 长流程自动执行 | 中途断掉且无中间状态、无重试 | 单步耗时偏长 | 查看日志中的失败步骤与 error_type,确认断点位置是否集中 |
清单填完后,用失败样本编号把每条“不可接受”对应到具体证据上;对不上的条目,说明是凭印象写的,需要先补样本再看。每次更换模型、改检索配置或改提示词,都按同一份清单复跑一遍对照,结论才有连续性。如果两个智能体在某类硬性不可接受失败上的表现都一样差,那么在这类任务上不要二选一,先接受“当前都不可用”这个判断,再决定是加人工复核环节还是缩小自动化的范围。