跳到主要内容

没有复现步骤的故障工单:可复现性是工程化的结果

· 阅读需 11 分钟
Tian Pan
Software Engineer

这张故障工单具有只有真实事故才具备的典型特征。在 02:14,支持代理关闭了一个本应进入 30 天宽限期的客户账户。客户发现了。工单落到你的桌面上,“复现步骤”一栏下面只有一行字:未知

你打开追踪记录。你看到代理调用了 close_account 而不是 set_grace_period。你看到工具执行成功了。你看不出的是模型为什么选择了那个分支 —— 而且当你通过同一个代理重新运行同一条客户消息时,它做出了正确的选择。做了两次。现在的事故复盘报告中,原本该写根本原因的地方出现了一个段落大小的空洞,而你唯一能诚实写下的只有“无法复现”。

这就是生产环境中智能体系统的一种隐蔽失败模式。不是宕机 —— 宕机是可见的。这种事故只发生一次,留下一名受害者,并在离开时抹去了自己的复现步骤。传统软件在每个调试工作流中都植入了一个舒适的假设:相同的输入产生相同的输出,因此只要你能描述 Bug,你就能重新触发它。智能体打破了这个假设,大多数团队在第一次尝试写复盘报告时才发现这一点,他们意识到自己只有一份发生了什么的日志,却没有办法让它再次发生。

“我试的时候是好的”是常态,而非例外

当你重新运行客户消息而智能体表现正常时,直觉上是感到宽慰。但那应该是警报。一个无法复现的故障并不是消失了的故障 —— 而是你失去了研究能力的故障。

在事故发生瞬间和你调查瞬间之间,有四个因素在独立变化,其中任何一个都足以让“我试过了,没问题”变得毫无意义。

模型即使在你认为已经固定了它的情况下,也是非确定性的。设置 temperature=0 并不意味着获得了确定性:生产环境中的 LLM 推理不是批次无关(batch-invariant)的,因此相同的提示词会产生不同的数值结果,这取决于还有什么请求共享了 GPU 批次。一项广受讨论的分析发现,一个流行的开源模型在 temperature=0 时,在 1,000 个相同的请求中产生了 80 个不同的补全结果 —— 这种变化完全来自于归一化、矩阵乘法和注意力内核中与批次相关的浮点归约顺序。确定性是可以实现的,但它要多付出大约 60% 的延迟成本,而且你必须通过工程手段实现它。默认情况下,“相同输入”并不意味着“相同输出”。

检索到的上下文已经变了。RAG 步骤在 02:14 提取的文档不再是现在提取的文档。知识库文章被编辑过,向量索引被重建过,客户记录改变了状态。事故发生时的智能体看到了一个已经不存在的世界。

**提示词(Prompt)**此后已经发布了两个版本。有人在周二修复了一个无关紧要的措辞问题,并在周三收紧了一个工具描述。做出错误决策的那个智能体已经不再运行了。你正在调试的是它的继任者。

还有对话状态 —— 记忆、先前的轮次、作为决策前提的工具输出 —— 都是你没有保存的特定排列。你保存了最终答案。你保存了一行日志。你没有保存当时的情境。

把这些放在一起,结论虽然令人不安,却也让人释然:可复现性不是你的技术栈自带的属性。它是你构建出来的属性,如果你不是有目的地去构建它,你就不会拥有它。

日志告诉你发生了什么。记录让你能重做一遍。

这种区别就是核心所在,值得对其进行精确定义。

日志是证词。它说“智能体在 02:14 调用了 close_account”。它是过去式、有损的,且设计上是为了让人类阅读 —— 这意味着它在被记录下来之前就已经被总结过了。你可以阅读日志并形成一个理论。但你不能执行日志。

记录是重新执行套件。它包含智能体消耗的每一个非确定性输入,在消耗的瞬间捕获,且精度足以让你将这些确切的输入反馈给智能体,并观察同一个决策的展开。你不是阅读记录。你是运行记录。

系统社区几十年前就知道了这种区别。确定性的记录与重放(Record-and-replay)—— 捕获进程执行并随后逐位重新运行的技术 —— 之所以有效,正是因为给定输入后,程序的大部分行为是确定性的。你不需要存储每一个中间状态;你只需要存储非确定性的输入(来自网络的字节、时序、随机抽取),然后重新执行。基于这一理念构建的重放调试器专门设计用于捕捉“海森堡 Bug”(heisenbugs):那些在你尝试观察它们的瞬间就消失的稀有生产环境故障。智能体的错误工具调用就是一个海森堡 Bug。它理应受到同样的对待。

团队犯的错误是假设他们的可观测性供应商已经做了这件事。追踪仪表板在日志工作方面非常出色 —— 时间线、Span、延迟、Token 计数。但很少有能捕获记录的。一个追踪记录如果只显示渲染后的提示词,而不显示提示词模板版本加上填充它的变量,那它就是日志。一个追踪记录如果只显示检索到的文本块,而不显示它们来源的索引快照,那它就是日志。虽然有用,但你仍然无法运行它。

可重现数据包中包含什么

如果可重现性是设计出来的,那么其工程单位就是:附加在每次智能体(Agent)运行中的决策上下文数据包(decision context bundle)。注意是每次运行,而不仅仅是失败的运行,因为在故障发生前,你无法预知哪次运行会演变成事故。

该数据包锁定了智能体看到的每一个输入:

  • 模型标识(Model identity) — 确切的模型版本和提供商、解码参数,以及你设置的种子(seed)。“GPT 级模型,temperature 0” 不是标识,特定版本的端点(endpoint)才是。
  • 提示词溯源(Prompt provenance) — 不是渲染后的提示词字符串,而是模板版本(内容哈希或版本标签)以及插入其中的全量变量。渲染后的字符串是衍生产物;模板加变量才是源头。
  • 检索到的上下文(已冻结) — 实际返回的代码块,以及它们所属的索引或快照标识,还有检索它们的查询(query)。如果你的知识库支持时间点读取(point-in-time reads),请存储时间戳;如果不支持,请直接存储代码块内容。
  • 工具输入输出(Tool I/O) — 每次工具调用的参数,以及至关重要的、工具返回的确切响应。在重现时,你不会重新调用支付 API,而是重放记录的响应,因为外部世界已经发生了变化,重新调用既不真实又危险。
  • 对话和记忆状态 — 完整的消息历史以及决策时处于作用域内的任何记忆条目,捕捉的是它们当时的状态,而非现在的状态。

数据包应该是内容寻址(content-addressed)且不可变的。当提示词发布两次时,旧的模板版本仍然可以通过哈希解析。当索引重建时,旧的代码块仍然锁定在数据包中。凌晨 02:14 的故障变成了一个你可以握在手中的实体,而不是一个你必须凭记忆和运气去重建的虚幻。

这不是免费的。数据包有存储成本,且逐字记录工具响应会引发关于个人可识别信息(PII)和保留期的现实问题——你现在将客户数据存储在调试产物中,它需要与其他副本相同的访问控制和过期策略。请将此视为设计约束,而不是跳过它的理由。30 天的数据包保留窗口可以覆盖绝大多数事故并限制风险暴露。

重现环境是数据包发挥价值的地方

一个无法执行的数据包只是一个更昂贵的日志。另一半工作是重现环境:一种让智能体针对锁定的输入而非实时输入运行的模式。

在重现模式下,检索步骤不查询实时索引,而是提供数据包中冻结的代码块。工具层不访问实时 API,而是返回由调用参数索引的记录响应。提示词是从锁定的模板版本渲染的,而不是当前版本。模型是在数据包记录的版本上调用的。你唯一被允许改变的是你正在测试的变量。

最后一点是将重现从取证分析转变为工具的关键。一旦你可以锁定一切,你就可以只改变一个变量,并观察其影响:

  • 针对当前提示词重放事故,以确认你的修复是否真正堵住了漏洞——还是仅仅因为模型掷出了不同的骰子。
  • 在迁移之前,针对较新的模型版本进行重放,这样模型升级就不再是一次“信仰之跃”。
  • 在扰动检索上下文的情况下进行重放,以了解错误的决策是取决于特定的文档,还是智能体判断力的稳健性。

确定性会让这一切变得更加敏锐。你并不总能在重现时获得逐位一致(bit-for-bit identical)的模型输出——只要你对此保持坦诚,这是可以接受的。如果你的重现十次中有九次重现了错误的工具调用,你就得到了一个可重现的漏洞。如果五十次中只重现了一次,你就了解到故障是罕见的、概率性的,这本身就是一个值得写入复盘报告(postmortem)的发现。无论哪种方式,你都用一个数字代替了“无法重现”。

可重现性是你在故障发生前做的决定

复盘报告中的漏洞并不是事后可以弥补的工具缺口。当工单提交时,输入已经消失了——索引变了,提示词发布了,对话状态从未保存。没有追溯性的修复。唯一有效的版本是数据包在凌晨 02:14 之前就已经存在的版本,因为你决定每次运行都会携带一个。

所以,实际的收获是现在就要执行的清单,而不是在下次事故发生期间:

  1. 捕捉每次运行的决策上下文数据包,而不仅仅是失败的运行。内容寻址、不可变,并带有有限的保留期。
  2. 对提示词进行版本控制并锁定检索快照,使“提示词”和“上下文”成为可解析的标识,而不是移动的目标。
  3. 逐字记录工具响应并重放它们——在调查期间永远不要重新调用实时服务。
  4. 构建重现模式,锁定每一个输入并让你精确改变其中一个。
  5. 以比率而非“是/否”来报告可重现性。“针对修复前的提示词重现率为 9/10”是根因。“无法重现”是一个漏洞。

非确定性不会消失;它是这些系统运作方式所固有的,到处与之对抗是错误的战斗。正确的战斗是让你的智能体尽管如此仍可审计。一个工程化可重现性的团队,其发布的复盘报告中包含确切原因。一个继承其技术栈随缘提供的团队,其发布的复盘报告中包含“未知”一词——并默默接受下一个凌晨 02:14 也会是一个惊喜。

References:Let's stay in touch and Follow me for more thoughts and updates