测试 Agent 为什么漏掉 Bug?从读 trace 到失败分类

先读 trace,再定指标。一条条读,记下第一个出错的地方,归成几类,数一数,再决定先修什么、给什么写评测。

召回率 45% 只告诉你"不够好"。错误分析回答"怎么不好":是没进到页面、看到了没认出来,还是把环境故障当成了 Bug。这几种的修法完全不同。

1为什么先做错误分析,而不是先定指标

场景:BugHunt 的测试 Agent 在 8 个注入了 Bug 的功能区上跑,召回率 45%。常见的反应有三种:换一个更强的模型;在 prompt 里加一句"请仔细检查";接一个现成的"有用性 1~5 分"Judge 天天打分。三种都没回答一个问题:它到底是怎么漏的。

Hamel Husain 和 Shreya Shankar 的 FAQ《AI Evals: Everything You Need to Know》开门见山:"Error analysis is the most important activity in evals.",理由是它"helps you decide what evals to write in the first place"。对现成的通用指标,他们的态度是:拿来当质量度量会浪费时间、制造虚假的信心,但可以用来挑出值得看的 trace。(这份 FAQ 页面标注发布于 2026-09-18,作者自己说明这些是"sharp opinions",不是普遍真理。)

Who Validates the Validators?(Shankar, Zamfirescu-Pereira, Hartmann, Parameswaran, Arawjo,UIST 2024)给了一个更根本的理由。作者让 9 位业界从业者用他们的工具 EvalGen 给 LLM 输出写评估标准,观察到一个现象,起名 criteria drift:要打分得先有标准,可标准又是在打分的过程中才想清楚的(摘要原文:"users need criteria to grade outputs, but grading outputs helps users define criteria")。参与者看到新类型的坏输出就想加新标准,打得越多,对已有标准的理解也在变。论文据此认为,评估标准不可能在人看输出之前就完全定下来。

所以顺序是:先读 trace,让失败分类从数据里长出来,再给每一类决定怎么修、要不要写评测。trace 怎么记,见第 2 周「Agent 做错了,你怎么知道它错在哪一步?」。

2四步:读 trace → 开放编码 → 轴向编码 → 计数排序

步骤做什么FAQ 里的建议
1. 收集 trace攒一批有代表性的 trace没有真实数据就先用合成数据;先随机抽样,再用异常值、按指标排序、分层抽样去找有意思的
2. 开放编码一条条读,用自由文本写下哪里不对,像写日记刚开始只记 trace 里第一个失败,因为上游的错会引出下游的错(熟练后可以再标相互独立的失败);至少先自己标 30 条,再看 agent 的建议
3. 轴向编码把笔记归成一张失败分类表(failure taxonomy)"Axial coding is the most important step.";可以让 LLM 先帮忙分组,但必须自己审
4. 计数、迭代数每一类有几条,排序;继续读,直到新的 trace 不再带来新类别这叫理论饱和(theoretical saturation);建议至少读 100 条

开放编码、轴向编码这两个词来自质性研究。谁来做:FAQ 建议中小公司指定一位懂业务的领域专家(他们叫 "benevolent dictator")说了算,而不是把标注外包出去。对 BugHunt 来说,这个人就是最懂被测应用的测开。

3开放编码:一条笔记该怎么写

不好的笔记问题好的笔记(合成样例)
"漏报了"这是结局,不是哪里出了错"跳到优惠券叠加后又被重定向回登录页,后面都在登录页里转"
"模型能力不够"是猜原因,读不出发生了什么"购物车合计页面上的数字明显不对,Agent 截了图但只说页面正常显示"
"一直重复点按钮,然后漏报"记的是下游症状,第一个错在更前面"订单取消:点了按钮但页面没变化,选择器指向了一个隐藏元素"
  1. 写看到的现象,先不追原因。FAQ 的说法是先别管为什么出错,等决定修哪些之后再查根因;这样标注质量更高,也能多看一些数据。
  2. 刚开始只记第一个上游失败。登录态丢了,后面的"在登录页里反复点"、"判定没 Bug"都是连带的。FAQ 也说,熟练后可以再标同一条 trace 里相互独立的失败,但下游连带症状不算独立失败。
  3. 不是模型的问题也记。登录态丢失很可能是 harness 没处理会话过期,这同样让产品变差。
  4. 通过的 trace 也要读。没 Bug 的功能区上说"没 Bug",可能是根本没进到页面,结论碰巧对。

4轴向编码:分类表要满足什么

  1. 按原因分,不按结局分。"漏报 / 误报"是另一个维度(结局),把它放进分类表,它会因为每条失败都带一个而排到第一,却什么也没告诉你。
  2. 互斥:每条失败恰好归一类。放不进去就新开一类,不要硬塞进"其他"。
  3. 能判定:拿一条新 trace,另一个人按类名和说明也能归到同一类。标注一致性怎么量,留到本周后面讲 Judge 校准时再说。
  4. 会变:读得越多,类会拆、会并、会重新定义。这就是 criteria drift,正常现象。
计数的单位是 trace。每条失败 trace 只算一次(第一个上游失败),各类的条数加起来正好等于失败条数,占比才有意义。熟练后补标的、相互独立的第二个失败,要和第一个上游失败分开统计。演示里的"看到什么记什么"把下游症状和结局也记了进去,FAQ 并不推荐,这里只当反例,看排行怎样失真。

5读多少条才够:饱和与 rule of three

某类失败在线上流量里占比 \(p\),独立地读 \(n\) 条,至少碰到它一次的概率和需要的条数:

$$P(\text{至少一次}) = 1-(1-p)^n,\qquad n_{95} = \left\lceil \frac{\ln 0.05}{\ln(1-p)} \right\rceil$$
占比 p读 100 条至少见到一次要 95% 把握需读
10%>99.9%29
5%99.4%59
3%95.2%99
2%86.7%149
1%63.4%299

反过来读:100 条里一次也没见到某类,按第 1 周「30 次全过,能说明什么?」的反证法,它的占比 95% 上界是 \(1-0.05^{1/100} \approx 2.95\%\),rule of three 近似给 3%。"读 100 条"的意思是:占比 3% 以上的失败类型,大概率至少露一次面;更少见的可能一次都碰不到。

前提是各条 trace 独立。同一个功能区重复跑的 trace 会扎堆(第 1 周「跑了 300 次,就是 300 个样本吗?」),有效样本更少,要读得更多,或者按功能区分层抽。

6自动信号不等于失败:本机 Codex 会话的只读统计

能不能跳过人读,直接用程序算出的信号当失败率?拿本机 ~/.codex/sessions 试一下。本机会话由多个 Codex 版本写成,命令执行有两种记录方式:

  1. A. 直接调用 exec_command(2026-03~07):输出头里有一行 Process exited with code N,进程还在跑时没有这一行(core/src/tools/context.rs:534-540)。
  2. B. 代码模式(2026-07 以后):模型调用 exec 工具,在 JS 里再调 tools.exec_command,退出码以结构化字段返回给 JS(context.rs:448-479),没有上面那行文字;但每条命令结束时会写一个 CommandExecution 条目,带 exit_code 和 status,而且命令跑完退出码非 0 就记成 Failed(工具调用被拒的另记 Declined)(core/src/tools/events.rs:532-548)。

脚本只读、只输出聚合数字,截止 2026-10-01 18:00(北京时间):

来源文件带退出码非零
A. 直接调用 exec_command(2026-03~07)454,685178(3.8%)
B. CommandExecution 条目(2026-09~10)17610,591860(8.1%)
A + B 合计22115,2761,038(6.8%)
未纳入:代码模式但没有条目(2026-07~09,退出码只在 JS 打印的文本里)41exec 调用 1,513 次
程序大类(A + B)带退出码非零退出码 1127128
查看 / 搜索(grep、sed、cat、test、diff 等)7,33225221902
版本控制(git 等)1,5278450027
运行 / 构建 / 测试1,44325820850
其他(本机工具、shell 控制结构)4,974444346164

同一个数字,含义因程序而异(本机实测):BSD grep 2.6.0 没匹配到返回 1;git 2.50.1 在非仓库目录执行 git status 返回 128,git status、git diff 这类命令参数写错返回 129(git log 参数写错是 128);bash 3.2 找不到命令返回 127,被 SIGINT 中断返回 130(128 + 2),被 kill -9 返回 137(128 + 9)。查看 / 搜索类的 252 次非零里 219 次是退出码 1,多半只是没搜到、条件不成立或有差异。

反过来,失败也不一定表现为非零退出码。A 里有 176 次调用的输出没有退出码:146 次是进程还在跑,24 次是 apply_patch 被拦截执行成功,剩下 6 次是真的失败(3 次 exec_command failed,命令没能执行;3 次 apply_patch 校验失败)。只数非零退出码,这 6 次一次也数不到。跑错了测试文件、测试全过,退出码也照样是 0。

所以 6.8% 不是失败率,Codex 自己记的 failed 也不是。自动信号的用处是决定先读哪些(FAQ 里说的按指标排序、找异常值),读完再归类。

▶互动演示:错误分析工作台

120 条合成的 BugHunt trace(12 个功能区各跑 10 次,其中 8 个功能区注入了 Bug;种子固定,和本地 analysis.py 的输出一致)。一条条读:左边是 trace 的每一步和评审笔记,右边是到目前为止的计数。可以切换"只记第一个上游失败"和"看到什么记什么",看排行怎么变;下面的曲线是"读到第 n 条时见过几种失败模式"。

阅读顺序
计数方式
已读 trace
其中失败
见过几种失败模式
召回率(已读里有 Bug 的)
结论碰巧对(没测到)
第一版分类放不进去的

▶互动演示:读多少条才够

某类失败在线上流量里占比 p,独立地读 n 条。拖动看至少碰到一次的概率,以及"读了 n 条一次没见到"时,它的占比最多是多少。

至少见到一次的概率
要 95% 把握需读
期望见到几次
n 条零发生:95% 上界(精确)
rule of three:3 / n
一次都没见到的概率

✎练习

题 1(分类):把"看到什么记什么"的标签排个序,第一名是"漏报"(39 条),第二名是"重复调用同一工具"(19 条)。下一步该怎么做?
"漏报"是结局,每条漏报的 trace 都会带一个,排第一不提供任何信息。"重复调用"在这批数据里只跟在"登录态丢失"和"操作没生效"后面,是连带症状;加重复检测只能让它停得早一点,Bug 照样漏。按第一个上游失败计数,排前三的是看到了没比对预期(17)、登录态丢失(14)、操作没生效(11)。
题 2(算):某类失败在线上占 2%,各条 trace 独立。读 100 条,至少碰到它一次的概率是多少?
%
1 − 0.98100 ≈ 1 − 0.1326 = 86.7%。也就是说,读完 100 条,仍有约 13% 的概率一次都没见过它。
题 3(算):想有 95% 的把握至少见到一次占比 3% 的失败类型,最少要读几条?
条
n ≥ ln 0.05 / ln 0.97 ≈ 98.4,向上取整 99。验证:1 − 0.9799 ≈ 95.1%,1 − 0.9798 ≈ 94.9%。
题 4(criteria drift):团队想省时间,先开会把评分 rubric 定死,再让大家照着 rubric 标 trace。按 Shankar et al. 2024 的发现,主要风险是什么?
论文把这个现象叫 criteria drift:要打分得先有标准,标准又是在打分中才定义出来的;参与者看到新类型的坏输出会想加新标准,也会重新解释已有标准。FAQ 也建议先做错误分析,再从笔记里整理 rubric,并定期回头更新。C 说的是标注一致性,是另一个问题。
题 5(自动信号):本机 Codex 会话里,带退出码的命令执行有 6.8% 退出码非零。这个数能怎么用?
非零不一定是失败(grep 没匹配到返回 1,查看 / 搜索类的 252 次非零里 219 次是 1),失败也不一定非零(命令没能执行、apply_patch 校验失败时根本没有退出码)。它适合当筛选信号:先读非零的、耗时异常的、重试多的,读完再归类计数。

7面试要点

一句话讲清楚

错误分析是评测的第一步:读一百条左右的 trace,每条记下第一个上游失败,归成互斥的失败分类,数每一类的条数,再按频率、严重程度和修复成本排优先级;能用代码判的写断言,需要人判断的才上 LLM Judge,并且先用人工标注校准。

追问准备

  1. 为什么不直接上一个通用的打分 Judge?通用分数说不清怎么坏、该修哪里;而且标准要看过输出才定得下来(criteria drift)。先错误分析,Judge 只给分类表里需要人判断的那几类写。
  2. 读多少条够?读到新 trace 不再带来新类别为止。粗算:占比 3% 的类,读 99 条有 95% 把握至少见一次;读 100 条没见到某类,只能说它的占比上界约 3%。
  3. 两类条数接近(17 对 14),谁先修?这批数据分不出谁更常见(条件二项检验双侧 p = 0.72)。再看严重程度和修复成本:如果登录态丢失是 harness 没处理会话过期,改一处就能修好,可以排在前面。
  4. LLM 能不能替你做?开放编码自己做,至少先标 30 条;轴向编码可以让 LLM 先出分组,再逐条审。

常见错误说法

❌ "先把评分标准定好,再去看数据":标准是看数据的过程中才想清楚的(criteria drift)。
❌ "漏报最多,先修漏报":漏报是结局,不是原因。
❌ "每个症状都记一笔,越全越好":下游症状会挤到根因前面,排行失真;熟练后可以补标相互独立的失败,但下游症状不算。
❌ "非零退出码 6.8%,命令失败率就是 6.8%":退出码的含义因程序而异;命令没能执行时根本没有退出码,0 也不代表做对了。
❌ "读了 30 条没见过,就说明没有这类问题":只能说占比上界约 10%(rule of three)。

代码

本地 week06_Judge与错误分析/code/error_analysis/:traces.py 生成合成 trace,analysis.py 计数、区间和饱和,codex_signals.py 只读统计 Codex 退出码,test_error_analysis.py 22 个测试。