软件工程有一条基本前提:缺陷可复现,才能被修复。给一个工程师稳定的复现步骤,剩下的只是时间问题;如果他拿不到复现路径,再强的调试能力也无从下手。
Agent 把这条前提打破了。
论文把原因讲得很清楚,失败依赖三件事:一次不比特级可重现的推理、读取变化状态的工具、以及几乎不会重走一遍的多步轨迹。三者叠加之后,同一个 bug 你昨天遇到、今天再跑可能就消失了,而修复必须在能稳定复现的前提下才谈得上。
一、记录不是测试
已经有一类工具能解决「复现」问题:record-and-replay。把一次 agent 运行完整录下来,之后可以按记录重放,不调用真实模型。
但它们和回归测试之间差着一步。论文指出,现有的 agent 工具记录运行只是为了追踪链路或者给轨迹打分——记录是观测手段,不是测试手段。
差别在哪?当你在修完一个 bug 之后想验证「这类事故不会再发生了」,重放给你看的是当时那段旧代码的行为。它能帮你理解事故怎么发生的,却完全测不出你的修复是否生效。要测新代码,就必须让新代码真的跑起来。
Chronicle 补的正是这一环。
二、在非确定性边界上打点
Chronicle 的设计起点是一个观察:一次 agent 运行里,真正无法由当前代码控制的只有少数位置。
这些位置被称作非确定性边界,最典型的两类:
- 模型调用:可能带随机采样,且即使固定种子也未必比特级可重复;
- 读取外部状态的工具调用:数据库内容变了、接口返回值变了。
除了这些边界,轨迹的推进逻辑、工具的选择与编排、参数的构造,全部由你自己的代码决定。
于是 Chronicle 在每次跨越边界时,把返回结果封成一个不可变信封(immutable envelope)。运行过程由此变成一串可精确重建的记录序列。论文用「不可变」这个限定词是有讲究的:信封一旦写下就不能被后续步骤改写,否则记录本身就引入了新的不确定性。
💡 提示:信封的粒度值得细想。记太细会把确定性逻辑也录进去,测试就失去意义;记太粗则场景无法重建。只录真正的边界,是这套方案能成立的关键前提。
三、核心操作:切点重放
有了记录,Chronicle 的核心操作 cut-point replay 做的是给边界集合划一条分割线:
- 一部分边界由记录提供服务:返回当时录下的结果,保证场景可重建;
- 互补的那部分用当前代码实时执行:让修改过的逻辑真正跑起来。
这个分割线就是「切点」这个名字的由来。它把一条录下来的事故转化成一个可以在 CI 里反复运行的回归测试。
用伪代码表达这个过程:
def cut_point_replay(record, cut_point, new_code):
"""record: 录制的边界序列;cut_point: 由记录服务的边界集合"""
replayed = set(cut_point)
for crossing in record.crossings:
if crossing.id in replayed:
value = crossing.envelope # 从记录取,保证场景可重建
else:
value = new_code.execute(crossing) # 跑新代码,被测对象
record.advance(value)
return record.assertions_result()
关键洞察是:你显式声明了哪些因素是噪声、哪些是被测对象。只要被回放的部分覆盖了环境的不确定性,被实时执行的部分就能被稳定检验。这就是同一条记录可以反复用于验证不同代码改动的原理。
四、实验数字
论文在一组 6 个记录失败的基准上做了评测(模型边界用模拟实现):
| 指标 | 结果 |
|---|---|
| 记录开销 | 23 微秒 / 每次边界跨越 |
| 相对模型调用耗时 | 0.008%(按假设 300 毫秒一次模型调用计) |
| 完整重放模型调用次数 | 0 |
| 重放稳定性 | 20 次重复比特稳定 |
| 切点测试行为 | 有缺陷代码失败,受保护与良性改动通过(6/6) |
23 微秒这个数字值得单独说。它意味着记录的成本相对模型调用可以忽略不计——300 毫秒和 23 微秒差了四个数量级,也就是说在生产环境常开记录几乎不带来额外延迟。这一条对落地很关键:如果记录本身很贵,就只能在预发环境开,那线上事故就录不到。
「完全重放零模型调用」是另一条硬指标。它让这些测试可以在 CI 里免费反复跑,不会因为模型费用而被迫减少测试频率。而 20 次重复比特稳定说明重放路径确实消除了推理的随机性。
真正说明问题的是变异研究的对比。研究者对受保护的工具做变异,然后比较两种测试方式:
- 切点测试:抓住了每一个让不安全动作得以通过的变异体;
- 全打桩基线:把每个边界都打桩,用同样的断言,一个都没抓住。
这个对比的结论有点反直觉。全打桩看起来更彻底——所有不确定性都被消除了,测试应该更稳定才对。但正因为所有边界都被桩替代,被测逻辑失去了真实的执行路径,断言变成了在检查桩的返回值,而不是在检查代码的行为。测试的有效性不只取决于断言写得好不好,还取决于被测代码是否真的被执行了。
五、这套方法适合什么场景
Chronicle 的定位很明确:把已经发生过的线上事故固化下来,防止同类问题回归。它最适合的场景是:
- Agent 有明确的安全边界或不变量约束(比如「不得执行某类操作」),可以用断言表达;
- 事故虽然偶发但确实发生过,有轨迹可录;
- 团队有 CI 流水线,能让这些测试持续运行。
不适合的场景也同样清楚:如果问题不是「某类行为不应发生」而是「质量不够好」,那就没有可断言的硬约束,回归测试无从写起。
⚠️ 注意:论文的评测基准只有 6 个记录失败,且模型边界是模拟的。真实环境里模型调用的不确定性来源更多,工具返回的内容也更复杂。迁移到自己系统时,先挑一两个已经复现过的真实事故做原型,验证记录粒度是否够用。
Chronicle 已在 GitHub 开源(仓库名 chronicle,组织为 theagentplane),论文同时放出了基准数据。对正在给 Agent 建质量体系的团队来说,这套「录下来、切开、只跑该跑的部分」的思路,比任何一个具体实现都更值得借鉴——它把不可复现的问题,转化成了测试工程量的问题。