跳到主要内容

修复验证是抽查,不是证据:cut-point replay 值得抄一遍作业

0x7F
0x7F

· 阅读约 7 分钟

Part 1 里那个 9:04 删除错误记录的事故,比事故本身更让人牙痒的是那句"无法复现"。几个工程师轮流在本地跑,日志全开,agent 就是不改了。我认识这种时刻——不是灵异事件,是模型换了一批采样路径,删对了的概率在这一次碰巧跌到零。你盯着屏幕,它给你一个"没有权限"的错误或者干脆跳过,你会怀疑刚才是不是做梦。

于是谁都会说:那你再多跑几次嘛。

多跑几次确实有用——不是证明修复有效,而是给你一种修复有效的错觉。这是这篇文章真正的开头:重新运行智能体来验证修复,本身就是一个统计陷阱。 模型不是数据库,同一段代码不会在同一个输入上返回同一个输出。它是撒骰子的,只是骰子灌了铅。你修完 bug 重跑一次,得到的是铅骰子在新条件下一次新的测量。通过,不说明修复盖住了故障;不通过,也未必是修复失效,可能只是这轮采样运气差。这就是文章说的"重新采样"问题:你验证的是一段概率分布里的一次抽样,不是一条因果链。

Cut-point replay 把这个问题换了个写法。 不重跑模型,重放记录。跑一次失败的完整过程,把精确提示词、采样补全内容、工具调用、检索块、模型版本全部录下来。然后验证的时候,冻结一切重采样点——模型调用全部用录制时的输入顶回去——只有你改过的那条工具边界是活的,实时跑,看它放出来的下游行为是不是对的。换句话说:不是"重演一遍看看运气好不好",是"除了我改的那个螺丝,整台机器保持故障时的原样,单独看螺丝拧上以后咬合得怎么样"。

这套思路不新。LangGraph 的 checkpoint replay 干的就是上半截,把过往执行记录存下来重新走。评论区有人提这个,作者也承认相似。但 cut-point replay 的落点不一样:checkpoint replay 是调试工具,给你看当时发生了什么;cut-point replay 是回归测试工具,给"事故能不能拆掉引信"一个可验证的答案。它刻意只让 cut point 处 live,其余全部 stub——包括工具调用。很多人第一反应是"工具调用为什么不 live,都 stub 了还验什么"。作者的回应很直接:你让工具重新执行,工具会去读当前的真实数据,你以为在验证修复,其实在验证今天的数据库长什么样。修复归因就被污染了。

——对。这个坑我踩过。当年调一个 MCP server 的越权问题,改完权限跑单测,过了,屁颠屁颠上线。第二天出更大的事,一查发现测试跑的时候外部 API 的限流策略变了,请求被 429 顶回来,测试"通过"的一部分原因压根不是我的修复。工具 live 会读到新状态,新状态不是你这次改动造成的,却会混进你这次改动的结论里。数据没变,修复才谈得上被验证。

这方法给出的验证结构是两层:第一层,确定性校验——工具调用的名字对不对、参数类型对不对、该调的时候调了没有。这一层是机器干的,cold hard facts。第二层,语义校验——拿 LLM-as-judge,把新输出和录下来的金标准放在一起判语义是否一致。层一抓"没按协议来",层二抓"按协议来了但话是胡说的"。这两层都得有,缺一层都不构成证据:只靠结构验证,模型把该调的工具都调了但参数是瞎编的,你看不出问题;只靠语义判断,LLM-as-judge 自己也是个概率模型,每次跑出来的分数同样是抽样。

还有个细节值得停下来看一眼:作者建议把录制痕迹当回归测试直接提交进 git。故障录制→脱敏→入库→CI 每次跑一遍。这是把不可复现的故障变成可复现的断言,方向完全正确。但这里我必须从安全角度横插一刀:录制痕迹进 git 之前不脱敏,等于把攻击面地图主动提交进仓库。 客户姓名、邮件、API key、内部 URL,一次运行录个全。你想想你平时让 agent 跑的都是什么东西——数据库连接串、生产环境变量、内部服务的端点地址。这要原样提交上去,GitHub 的 secret scanning 可能先替你报警,也可能不报。更阴的地方在这里:agent 的完整运行记录天然包含"哪一步读了 .env、哪一步带了哪个 key 去请求了哪个内网服务",这本身就是一份精确到步骤的攻击路径指南。所以文章里那句"提交前必须脱敏",在安全语境里不是一条流程建议,是一条合规底线。录制完先跑一遍扫描器,再人眼过一遍,再考虑进 git。

不过这套方法自己也埋着雷。 作者在评论区自己承认了一个缺陷:stub 边界上如果出现了录制时没有的额外调用——比如你改了 cut point 的那个工具,新代码逻辑里多发出了一次模型调用——回放机制可能静默地把这次新调用匹配到相邻的记录上复用,而不是报错。它不告诉你"这里没有记录可对应,我不确定怎么处理",它会找个最像的记录顶上去,看起来一切正常。这不就是回放版的幻视吗。 模型撒谎的时候至少有时候会语无伦次,这个缺陷撒谎的时候脸不红心不跳。

作者说补偿方案是加调用计数检查,多调一次、少调一次都归零。这听起来对,但你细想一层:调用计数检查本身也是"边界"的一部分,你改了工具边界,计数检查是挂在边界上的哨兵,它自己也会被新代码改坏。 这不是抬杠,是这套方法的结构性弱点——你要 cut point 以 live 模式运行,就要信任 live 模式内部不会出现你没预料到的新动作;而新动作恰恰是修复代码时最容易引入的东西。递归问题:验证方法本身建立在"变化只在边界发生"的假设上,但验证边界的那层代码,也会被同一次修改波及。

——离题了,收。

这篇和 Part 1 是同一场 AI Engineer World's Fair 上的展示。评论区有个非常实践的追问:cut point 划在哪,谁说了算。这问题问到了命门上,因为整个方法的前提就是"你知道故障发生在哪条边界上",但实际排障时,9:04 删了错误记录,故障的"边界"可能是 delete 工具的参数校验,可能是上游检索召回顺序错了,也可能是模型这一步的采样本身就歪了。你能划对,回放才有意义;划错了,你验证的不是故障,是你以为的故障。

所以拆弹清单是:

  1. 别再用"重跑一遍过了"来验证 agent 修复——那是抽样,不是证据;
  2. 要录就录全:精确提示词、补全、工具调用、检索块、模型版本,缺一项回放就没法冻结重采样点;
  3. 录制痕迹进 git 之前必须脱敏,否则你是在往仓库里放攻击者最想要的赏金;
  4. 别把 cut-point replay 当成万能——模型本身的劣质生成它管不了,提示词一改痕迹就得重录,还有那个静默复用相邻记录的坑,计数器该加就加。

别慌,这不是让你放弃人工 code review 转身拥抱录制回放。正好相反:这套方法能把"这个修复到底起作用没有"从玄学变成可验证的断言,给人工 review 留出真正该看的注意力。但要记住,验证工具只能验证它假设的故障。它验证不了你没料到的那个。

排雷记录:重跑是抽查运气,回放才是验证因果。