跳到主要内容

从 Bug 到行为率:没有复现步骤的 AI 事后分析

· 阅读需 10 分钟
Tian Pan
Software Engineer

用户提交了一个工单。智能体告诉一位付费客户,他们的退款将在 7 小时内处理,而文档中记录的 SLA 是 7 天。附带了截图。你调取了追踪记录,找到了准确的提示词(prompt)、准确的工具调用、准确的模型和种子值(seed)。你进行了复现。模型说是 7 天。你再次复现。7 天。你复现了 100 次。其中 98 次说是 7 天,2 次说是“今天结束前”,但从未说过 7 小时。截图是明确无误的。复现结果却不一致。周五截止的复盘报告现在有一个“根本原因”栏,但你却填不出任何根本原因。

这就是大多数进入复盘阶段的 AI 事故的形态。不是那种明显的宕机——那些会有堆栈追踪和 500 错误率图表,并以每个 SRE 都受训过的方式恢复。棘手的是那些产生了一个错误输出、留下了受害者、在退出时抹除了自身条件,且在你召唤它时拒绝再次出现的单次事件。你使用过的每一个复盘模板都假设存在一个可复现用例。但智能体并不给你提供这些。

SRE 手册中的直觉是不断挖掘,直到复现结果与报告匹配。在这种情况下,这种直觉是错误的。相同的提示词加相同的模型加相同的种子值并不是产生相同输出的充分条件,“用户看到的”与“我再次运行时看到的”之间的差距并不是调试失败——它是你所交付系统的一个属性。你正在编写的复盘报告必须吸收这一属性,而不是与其对抗。

为什么“相同输入,相同输出”从来都不是真的

关于原因最清晰的表述来自于 LLM 推理中批次不变性(batch-invariance)的研究。模型看到的不是“你的请求”。它看到的是一个批次——你的请求加上调度器在同一个前向传播中协同放置的其他人的流量。矩阵乘法中的归约核(Reduction kernels)、RMSNorm 和注意力机制在浮点运算下不满足结合律,因此求和累加的顺序会改变结果的最后几位。改变批次大小,位(bits)就会改变。逻辑值(logits)中改变几位,偶尔就会改变哪个 token 在 argmax 中胜出。温度为零关闭了采样侧的随机性;但它并不会触及执行侧的随机性,而执行侧是你请求到达那一毫秒时其他用户负载的函数。

除此之外还有:依赖于协同批次 token 的混合专家模型(MoE)路由、产生微妙差异中间状态的 KV 缓存冷热路径、跨越 GPU 世代的集群路由(逻辑上相同的模型运行在不同的内核上)、提供商侧在未更新 API 版本号的情况下发布的补丁更新。Thinking Machines 的研究表明,通过显式的批次不变内核,你可以将确定性驱动至零方差——而你为此付出的代价是大约 60% 的吞吐量损失。没有任何商业推理端点是以这种方式运行的。你的生产流量与成千上万个你无法看到其负载概况的其他租户共享一个集群,而那个负载概况正是你输出的输入之一。

诚实的版本是:当用户报告“智能体说了 X”而你无法让它再说一次 X 时,你并没有失败于寻找 Bug。你已经正确地衡量了 X 是某种分布下的低概率输出,而该分布恰好在那个时刻为那个用户掷出了一个 X。Bug(如果存在的话)并不在追踪记录中。它在于分布的形状。

复盘模板问错了问题

看看任何 SRE 复盘模板。有一个“根本原因”字段。有一个“我们将如何防止此类事件再次发生?”字段。这两者的措辞都好像事故是一个具有位置的缺陷——一行代码、一个配置值、一个配置错误的权限——而修复方法就是更改那个位置。这种框架支撑了我们行业二十年,但在面对概率系统时却无法维持。

对于一个没有可复现用例的 AI 事故,模板中三个关键的假设会同时失效。首先是原因具有单一性。事实几乎并非如此——错误的输出是提示词、那一分钟的检索状态、那一小时的模型检查点、请求落入的批次以及采样器温度共同作用的结果。其次是原因在你的代码中。一半的变量在提供商的集群内部,而不是你的,而且你没有它们的日志。第三是“我们修复了它”意味着“这个确切的失败不会再发生”。对于概率系统来说,失败是一个频率,而不是一个开关。你无法关掉它。你只能降低它。

模板必须吸收这三个失效点,否则每个复盘都会以同一段话结束:“无法复现。无行动项。”这是一个故事,而不是调查,而下一个具有相同形状的事故将以完全相同的方式被提交,因为没有任何改变。

一个可调查的 AI 复盘应该是怎样的

转变在于从“寻找 Bug”变为“刻画行为频率”。这比听起来要小一些心理跨度,但它改变了你收集什么、衡量什么以及记录什么。

捕获提供商的响应元数据,而不只是响应内容。 输出文本是用户看到的,但元数据是使事故可调查的关键。每个现代 LLM API 都会返回大多数团队丢弃的字段:请求时的系统指纹(system fingerprint)或模型版本字符串、安全过滤器触发标志、结束原因、提示词 token 和补全 token 计数、提供商暴露的缓存命中指示器。提供商集群会发布静默更新——关于模型补丁间 API 稳定性的研究发现,当提供商更新底层权重时,超过一半的提示词和模型组合会出现退化,而 API 合同并没有给你提供版本升级来定位。如果你拥有事故窗口期的系统指纹和复现时的指纹,你就有了可以追踪的假设。如果你只有响应文本,你便无从下手。

对分布进行采样,而不是对点进行复现。 单次复现只是底层行为的一次抽取。一个有用的调查会运行相同的输入 50 次或 100 次——不改变用户可以改变的任何变量——并报告直方图。问题不再是“这发生了吗?”,而是“这发生的频率是多少,该频率是否与报告一致?”如果错误的输出在 1000 次复现中出现一次,那么用户只是运气不好,行动项就是让长尾变得更安全。如果它在 10 次中出现一次,那么你面临的问题比截图显示的要大得多,其他用户群也正处于静默暴露中。

将一次错误的输出视为频率的证据,而不是确定性的 Bug。 这是一个倒置。报告是一个样本,而不是总体。调查的工作是估计产生该样本的频率。置信区间比二元原因更诚实,而表述为“将异常工具调用率从 0.4% 降低到 0.1% 以下”的行动项比“修复导致此输出的 Bug”更具辩护性,也更具可测试性。

记录你不受控的输入。 提供请求服务的检索索引版本。那一分钟向量数据库的新鲜度延迟。工具 schema 的哈希值。周围时间窗内的流量组合——是否存在多意图查询的峰值,将你的调度器推入了它很少触及的范畴?预留金丝雀集上的每分钟漂移评分。这些是在 LLM 技术栈内部静默移动的变量,一个不列举这些变量的复盘模板将无法捕捉到它们。大多数团队不捕获它们并不是因为工程实现困难,而是因为他们继承的 SRE 模板中从未有过这些字段。

尚未出现在任何模板中的章节

写一个类似“我们无法确定的事项”的章节。在其中放入:你无法从事故窗口中恢复哪些变量、原因,以及你对捕获流水线做了哪些修改,以便下次能获取这些变量。这一章打破了“讲故事”与“调查”之间的僵局。如果一个无法复现的事故能为你的可观测性覆盖范围带来增量,那么它仍然是有用的。如果一个事故既无法复现,也没有带来任何可观测性上的改进,那只是你给团队讲的一个故事,而不是一份事后分析(postmortem)。

然后写一个“无论如何我们都做了哪些改变”的章节。人们往往倾向于在查明原因后再执行行动项。对于 AI 事故,这意味着永远推迟。能够改变故障率的行动项通常与锁定具体故障无关:针对该失败模式收紧系统提示词(system prompt)、在模型下游添加确定性的防护栏(guard rail)以捕获畸形模式、将错误的输出添加到评测集(eval suite)中以便后续发现回归问题、减少周围上下文的熵。这些操作都不需要确切知道为什么那次会产生错误的输出。它们降低了错误发生的总概率,而对于一个概率性系统来说,降低频率才是唯一重要的事情。

最后一章是大多数团队都会遗忘的:写下如果要再次发生这种情况,需要满足哪些条件,以及你的监控需要达到什么状态,才能在用户发现之前捕获第二次事故。如果你无法回答第二部分,说明你的监控还没有衡量正确的事物,这本身就是一个行动项。

关于诚实的注记

有一种值得点名的文化失败模式。当事后分析无法找到根本原因时,人们往往会不自觉地弱化措辞,直到掩盖这种缺失。例如:“调查正在进行中”、“可能是瞬态问题”、“正在与模型提供商确认”。“去责化”(blameless)传统的存在是为了让人能安全地记录下实际发生的情况。而在许多 AI 事故中,实际发生的情况是:系统产生了一个低概率的错误输出,你无法复现它,而且你不知道原因。写下这句话是将该系统作为概率性系统来认真对待的前提。隐瞒这一点会让你的团队一直认为他们调试的方法不对。

未来十年的事故响应将由那些能够坦然说出“我们无法复现,但无论如何我们都做了这些改变”的团队来构建。那些一直在等待确定性的根本原因才采取行动的团队,将不断提交相同的事故报告,面对相同的受害者,并留下同样的一段内容空白。

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