一次 LLM 线上故障复盘
LLM 应用的线上故障经常不是“接口彻底挂了”,而是回复变慢、工具调用偶发失败、结构化输出错误增加,几种小问题叠在一起后才让用户明显感知。只看 HTTP 状态码,很容易把事故归因成“上游模型不稳定”;只看最终输出,又很难解释为什么同一请求重放后恢复正常。
本文用一个经过抽象的典型事故复盘:客服摘要功能在一次发布后,超时率和重复回答同时升高。重点不是讲某个供应商的具体故障,而是演示如何建立证据链、区分触发因素与根因,并把“以后加强监控”改成可以验收的工程动作。
一、先写清影响,不要急着猜根因
事故开始后的第一份记录应该回答“谁受影响、影响什么、持续多久”,而不是讨论责任。这个案例的初始现象如下:
| 观察维度 | 现场信号 | 需要继续确认 |
|---|---|---|
| 用户体验 | 部分摘要等待时间明显变长 | 哪类输入最容易触发 |
| 接口状态 | 5xx 小幅上升,但不足以解释全部失败 | 是否存在客户端主动断开 |
| 输出质量 | 少量回复重复相同段落 | 重试是否复用了不完整状态 |
| 成本 | 单请求平均 token 用量上升 | 是输入膨胀还是重复调用 |
| 范围 | 新版本实例更明显 | 配置变更还是代码变更 |
这里要避免两个常见误区。第一,不要只统计“完全失败”的请求;用户等待很久后自行关闭页面,也是事故影响。第二,不要用全站平均值掩盖长尾,至少拆出 P95/P99 延迟、任务类型、模型路由和版本维度。
可操作结论是:先定义影响口径,再统一时间窗口。否则产品、后端和供应商控制台各说各的数字,复盘无法对齐。
二、止血优先于完整诊断
现场团队确认异常集中在新版本后,先把新版本流量从 100% 降到小比例,并暂停非关键的自动重试。这样做不是提前认定发布有错,而是用最小代价缩小爆炸半径,同时保留少量样本继续观察。
本次止血按以下顺序执行:
- 冻结新发布和配置变更,记录当前版本号与路由规则。
- 把关键摘要请求切回已验证版本,保留内部测试流量在新版本。
- 降低单任务最大重试次数,避免超时请求在后台继续堆积。
- 对非核心的长文本任务启用排队提示,而不是同步等待。
- 检查队列积压是否开始下降,确认止血动作没有制造第二个故障。
回滚之后延迟快速恢复,只能证明“问题与新版本路径相关”,不能直接证明某一行代码就是根因。把相关性写成确定性结论,是很多复盘后来推翻自己的起点。
三、用统一时间线恢复事实
时间线必须把代码、配置、指标和人工动作放在同一时区。建议精确到分钟,但不要为了显得严谨而编造秒级顺序。这个案例整理出的关键节点是:发布开始、首批新实例接流、长尾延迟抬升、告警触发、值班人员确认、降流、恢复基线。
每个节点至少附一种原始证据:部署记录、Trace ID、指标截图对应的查询条件,或配置审计日志。聊天记录只能说明团队何时意识到问题,不能代替系统证据。
一个实用的时间线结构是:
时间 | 事实 | 证据 | 当时判断 | 后续是否证实
10:05 | 新版本接入首批流量 | deployment_id | 发布正常 | 是
10:12 | P99 延迟开始上升 | dashboard query | 上游波动 | 否
10:21 | 降低新版本权重 | config revision | 缩小影响 | 是
10:28 | 队列积压回落 | queue metric | 止血有效 | 是
把“当时判断”与“事实”分栏很重要。值班期间的错误猜测并不可耻,反而能暴露监控或 Runbook 为什么会把人引向错误方向。
四、根因不是最后一个报错
Trace 对比显示,新版本把历史对话和本轮待摘要文本一起放入 Prompt;输入变长后,部分请求越过应用层预设的软超时。客户端取消连接,但任务执行器没有及时收到取消信号,随后又按通用网络错误策略重试。重试使用了已经追加过一次的会话状态,于是输入进一步变长,并在少数请求中造成重复段落。
因此,“模型响应慢”只是触发条件,“取消未传播 + 非幂等重试 + 状态重复追加”才构成可修复的根因链。可以用五个为什么继续追问:
- 为什么用户超时?因为长输入的生成耗时超过软超时。
- 为什么后台还在运行?因为取消信号没有传播到执行器。
- 为什么又调用一次?因为通用重试器把客户端断开归类为临时网络错误。
- 为什么出现重复内容?因为重试前已经写入中间会话状态。
- 为什么上线前没发现?因为测试只覆盖短输入和单次调用,没有覆盖取消后的状态一致性。
这条链同时解释延迟、调用次数和重复输出,且能被 Trace 与重放实验验证,比“上游偶发抖动”更接近根因。
五、区分修复项、预防项和侦测项
有效的行动项必须有负责人、截止条件和验收方法。把所有措施都写成“优化稳定性”,下次复盘时无法判断是否完成。
本次修复分三层:
- 直接修复:取消请求时终止下游调用;重试前不持久化中间状态;为每个摘要任务生成幂等键。
- 预防复发:增加长输入、客户端取消、重试并发和版本回滚的集成测试;限制可拼接的历史轮数与总 token 预算。
- 提前发现:按版本监控取消率、每任务调用次数、输入 token 分布和队列年龄;告警同时附带可检索的 Trace 样本。
如果团队还没有统一的错误分类,可先参考 LLM API 错误重试策略设计,把可重试、不可重试和需要人工介入的错误分开。复盘不必重写重试算法,而要说明本次为何落入了错误分支。
六、用反事实测试验收复盘
复盘会议结束不等于事故关闭。最可靠的验收方式,是把根因改写成可以失败的测试:如果取消信号不传播,测试必须失败;如果相同幂等键触发第二次执行,状态不得重复写入;如果输入超过预算,系统必须在调用模型前明确截断或转异步。
上线修复时可先回放脱敏后的事故请求,再做小流量灰度。观察的不只是成功率,还包括每任务模型调用次数、输入长度、取消后的后台存活时间和输出重复检测结果。只有这些指标回到基线,行动项才算关闭。
同时安排一次故障演练:人为延迟模型响应并主动断开客户端,确认系统不会留下幽灵任务。多供应商切换类演练可结合 多模型供应商故障演练 的方法,但本次重点应放在应用自身状态,而不是把流量切走就算成功。
七、可直接复用的复盘模板
一份足够实用的 LLM 故障复盘应包含:事故摘要、用户影响、检测方式、时间线、止血动作、根因链、促成因素、做得好的地方、需要改进的地方,以及行动项表格。每项结论都尽量指向证据,每项行动都写验收信号。
还要明确哪些内容不能写进广泛流转的复盘:真实 API Key、完整用户 Prompt、个人信息和供应商账户细节。需要展示样本时先脱敏,并保留受控的原始证据索引。复盘的价值是让系统变可靠,不是扩大一次事故的数据暴露范围。
最后,用一句话检查质量:如果六个月后换一批值班人员,他们能否仅凭这份文档复现问题、验证修复并正确止血? 如果答案是否定的,就继续补证据和验收条件。