跳到主要内容

系统化调试 LLM 故障:给读不懂日志的工程师的实战指南

· 阅读需 14 分钟
Tian Pan
Software Engineer

一家金融科技初创公司在他们的系统提示词中添加了一个逗号。第二天,他们的发票生成机器人开始输出乱码,在有人追踪到原因之前,他们已经损失了 8,500 美元。没有抛出任何错误。没有触发任何报警。应用程序继续运行,虽然错了但很自信。

这就是在生产环境中调试 LLM 的真实样子。没有指向行号的堆栈追踪。没有你可以检查的核心转储(core dump)。系统不会崩溃——它在继续运行的同时,悄无声息地产生降级的输出。传统的调试直觉不再适用。大多数工程师的反应是随机调整提示词,直到看起来好一点,然后基于三个示例进行部署,并称其已修复。结果两周后,问题又以另一种形式浮出水面。

有一种更好的方法。LLM 的故障遵循系统性的模式,而这些模式对结构化的调查有反应。这就是这套方法论。

为什么你的直觉会失效

在进入方法论之前,值得理解为什么靠直觉调试 LLM 的代价如此之高。

传统调试假设故障是确定性且可追溯的。你运行代码,它崩溃了,你读取错误,你找到那一行。LLM 同时打破了这三个假设。

非确定性比你预想的更深。 设置 temperature=0 感觉像是解决可重复性的方案——理论上,你要求的是每一步概率最大的 token。在实践中,受控研究发现,即使在 temperature=0 的情况下,准确率差异也可能高达 15%,而最好与最差结果之间的差距可达 70%。非确定性并不存在于采样逻辑中,它存在于基础设施中:连续批处理(continuous batching)、前缀缓存(prefix caching)以及分布式硬件上的浮点排序。你的“确定性”测试可能通过三次,但在第四次失败,而代码没有任何更改。

上下文敏感性意味着故障无法孤立地复现。 一个通过了所有开发测试的提示词,在生产环境的流量下经常会失败。真实用户的表述方式不同,提交的文档更长,使用多种语言书写,并发送了你精心策划的测试集从未预料到的输入。故障模式仅存在于分布层级——这就是为什么 LLM 系统中最危险的 bug 是你从用户投诉中发现的,而不是从你的测试套件中发现的。

多组件的不透明性增加了故障表面积。 在 RAG 流水线或多智能体系统中,故障可能源自嵌入模型、检索索引、分块策略、提示词格式、生成模型或输出解析器。每个组件单独看起来都没问题。Bug 是它们交互产生的一种属性。如果没有隔离变量的方法论,你就是在瞎猜。

输入消融:将提示词视为电路

第一种结构化技术是输入消融(input ablation)——逐个移除或替换提示词的组件,以确定哪一部分对故障负责。

把提示词想象成一个有多条路径的电路。当电路发生故障时,你不会更换整个电路板。你会隔离出是哪个组件导致了短路。同样的原则也适用于这里。

工作流程:

  1. 从一个已知的失败提示词和一个特定的、可复现的故障案例开始。
  2. 逐个移除组件——系统消息、few-shot 示例、检索到的上下文、格式指令。
  3. 每次移除后,测试故障是否仍然存在。
  4. 当移除某个组件修复了故障时,你就找到了那个出故障的电路。

受控实验中一个反直觉的发现是:few-shot 示例经常是元凶。添加示例是通用的提示工程建议。但示例也会重定向模型的推理。一套旨在改进某种行为的示例可能会导致模型跳过之前正在执行的验证步骤,或者以某种方式与示例进行模式匹配,从而在不符合示例分布的输入上出错。

提示词消融还揭示了一类相关的故障:位置敏感性。研究一致表明,模型在处理提示词开头和结尾内容的能力强于中间内容。埋在长系统提示词中间的关键指令极易被忽视。将相同的指令移动到不同的位置会显著改变行为——即使指令文本完全相同。

行为边界测试:借鉴软件 QA

边界值分析是软件测试的标准方法。你测试预期行为的边缘:如果一个函数接受 1-100 的整数,你会测试 0、1、100 和 101。LLM 也遵循同样的原则,只是应用方式不同。

对 LLM 系统而言,重要的边界不是数值范围,而是语义和结构阈值:

  • Token 计数边界:上下文质量在何处开始下降?每一个测试过的尖端模型都会随着输入长度的增加而变差——不是在 token 限制处,而是从开始就持续发生。对于你的具体用例,退化的阈值是多少?
  • Schema 复杂度边界:在输出结构退化之前,模型可以可靠地填充 JSON schema 中的多少个字段?
  • Few-shot 示例数量:示例在达到多少个时停止提供帮助,并开始出现错误的模式匹配?
  • 指令数量边界:指令列表到什么程度会变得太长,从而导致对早期指令的遵循度下降?

对于每一个边界,测试模式都是相同的:找到标称情况,找到边缘,就在边缘处进行探测。记录哪些故障是持续发生的,哪些是偶尔发生的。边界处的持续故障表示结构性限制;偶尔发生的故障则表明与其他提示词组件存在交互。

这也是你调试多智能体协作故障的方法。一份关于智能体故障的文档分类法确定了三个类别的 14 种不同故障模式:规范故障(specification failures)、智能体间失调(inter-agent misalignment)和任务验证故障。在智能体交互的边界进行测试——具体来说,当一个智能体的输出处于下一个智能体 schema 预期的边缘时会发生什么——这就是你发现故障属于哪一类别的方法。

中间输出检查:当推理链说谎时

对于思维链(chain-of-thought)系统和智能体流水线(agentic pipelines),存在一类消融实验(ablation)无法捕捉的故障:推理轨迹是错误的,但最终答案看起来是正确的,反之亦然。

对思维链忠实度(faithfulness)的研究发现,CoT 推理并不总是与最终答案存在因果连接。模型可能会生成一个看起来合理的推理轨迹,以此对它们无论如何都会产生的输出进行事后合理化。这对调试很重要,因为它意味着仅检查推理链是不够的 —— 你需要测试推理链是否真的驱动了输出。

这种测试是一种扰动检查(perturbation check):在中间推理步骤中故意引入一个错误,并观察最终答案是否改变。如果你破坏了思维链,但最终答案保持不变,那么推理就是装饰性的。模型并没有使用它。这对你设计智能体系统具有重要意义 —— 如果中间步骤不因果地影响最终输出,记录它们只会给你一种缺乏实质内容的可观测性幻觉。

对于多步流水线,实际的准则是记录每一个跨度(span):每一次 LLM 调用、每一次工具调用、每一次检索查询以及每一个中间结果。而不仅仅是最终的输入和输出。当流水线失败时,你需要重建每一步的准确状态。如果没有细粒度的追踪,你就像是在读故事的最后一行,然后猜测前面的章节说了什么。

拒绝模式分析:独立电路问题

拒绝调试(Refusal debugging)是一个独立的类别,因为 LLM 中的拒绝行为在结构上被证明与有害性检测是不同的。研究表明,这两者是分别编码的:模型可能会拒绝一个无害的请求,同时允许一个有害的请求,因为拒绝电路(refusal circuit)是由表面模式 —— 关键词、措辞、语境 —— 触发的,而不是由语义有害性评估触发的。

这解释了一个常见的生产环境失败模式:在更新了添加安全导向语言的系统提示词后,模型开始拒绝合法的请求。这次更新强化了拒绝触发器,而它原本要处理的底层有害内容并没有发生任何实际变化。

调试方法是系统性的措辞变体分析。对于被错误拒绝的请求:

  1. 将请求简化为最简形式,测试拒绝是否仍然存在。
  2. 改变人格设定(persona)上下文 —— 如果系统提示词将用户设定为领域专家,拒绝是否会改变?
  3. 改变框架 —— 重新排列句子顺序是否会改变拒绝边界?
  4. 测试应当触发类似处理的相邻请求 —— 拒绝是针对这种特定措辞,还是针对该语义类别?

通过系统性的变体分析来映射拒绝边界,可以让你识别触发器是关键词、结构模式还是语义类别。每种诊断都会导向不同的修复方案。

评估驱动循环:什么取代了 “看起来不错”

所有这些技术都会产生假设。验证这些假设的方法是评估驱动循环(eval-driven loop),而不是手动检查。

反模式是研究人员所说的 “凭感觉检查”(vibe checking)—— 基于少量例子微调提示词,观察到输出看起来更好,然后部署。这就是 “逗号事件” 发生的原因。提示词在工程师的抽查中 “看起来不错”。没有人运行完整分布的测试。

结构化的替代方案:

  1. 立即建立黄金数据集(Golden Set)。 20 个手动标注的例子就足以开始。你不需要基础设施;你需要一个包含输入、预期输出和通过/失败标签的电子表格。
  2. 使用二元通过/失败评估,而不是评分。 李克特量表(Likert scales)和 1-5 分制会导致标注漂移和虚假精度。二元标签迫使你进行更清晰的思考:这个输出是否满足要求?
  3. 在 LLM 评委之前添加基于代码的检查。 JSON Schema 校验、对必需模式的正则表达式、结构化断言 —— 这些方法快速、廉价,且能捕捉大部分失败。将 “LLM 作为评委” 的评估器留给需要语义判断的情况。
  4. 每次提示词更改都要运行黄金数据集。 不是运行样本,是运行全集。运行 20 次评估的成本微不足道。错过一次回归的成本却很高。

有项记录在案的发现值得强调:传统的提示词工程改进并不具备普适的迁移性。在对照实验中,添加带有明确行为规则的 “得力助手” 框架(任何提示词工程指南都会推荐的那种包装)已被证明会将提取准确率降低 10%,将 RAG 引用合规性降低 13%,尽管它能将通用指令遵循能力提高 13%。一个维度的改进会引入其他维度的回归。如果不测量整个任务分布的回归情况,你就无法改进提示词。

为可调试性进行日志记录

如果没有支持它的基础设施,任何方法论都无法生存。对于 LLM 系统,最低日志记录要求比传统服务更丰富。

每次推理调用都应记录:完整的输入(包括系统提示词、对话历史、检索到的上下文)、完整的输出、模型版本、提示词版本、Token 计数、延迟以及任何工具调用的参数和结果。这是不可逾越的底线。当事故发生时,你需要能够重现产生该故障的精确调用。

除了单次调用,聚合监控需要追踪:按失败类型分类的错误率(解析错误、拒绝、格式违规)、评估分数的滚动平均值、输出长度分布以及 RAG 系统的检索召回率。关键信号与传统 API 不同。延迟飙升的危险性低于逐渐发生的准确率退化,后者表现为数周内评估分数的缓慢漂移。

最昂贵的失败模式是隐性退化(silent degradation):质量逐渐下降,错误率监控无法察觉,只能通过持续的评估运行来发现。用户会比你更早感觉到这一点,到那时,你已经失去了最难恢复的信任。运营准则是定期在生产环境中运行你的黄金数据集 —— 不仅仅是在 CI 中,而是针对实时流量持续运行。

当一切乱成一团时,从哪里开始

当生产环境中的 LLM 故障被报告,而你没有日志也没有评估 (eval) 基础设施时,优先级顺序如下:

  1. 使用固定输入复现故障。 找到能够稳定触发故障的确切输入。在这个阶段,输入的不确定性意味着你每次迭代都在调试不同的问题。
  2. 检查最近的提示词变更。 大多数生产环境中的 LLM 退化是由提示词变更、模型版本更新或输入分布偏移引起的——按此顺序排列。逐一检查。
  3. 对提示词进行消融实验 (Ablate the prompt)。 将其简化到最简形式,然后逐个组件重新构建,直到故障再次出现。
  4. 检查上下文长度。 如果故障是间歇性的且取决于输入长度,那么你可能遇到了上下文性能衰减 (context degradation),而不是提示词逻辑错误。
  5. 测试相邻输入。 一旦找到导致故障的输入,测试其微小的变体。了解故障的边界比盯着故障案例看个没完能让你学到更多关于原因的信息。

每一步的目标都是产生一个关于故障原因的精确假设——而不是去修复它。在建立假设之前进行修复,所产生的提示词更改只会掩盖症状,而无法解决根本原因。

结语:可以迁移的工程纪律

LLM 调试之所以困难,并不是因为 LLM 本身有多么神秘,而是因为工程师们从传统软件开发中引入了错误的心理模型。置信度 (Confidence) 和正确性 (Correctness) 是同一个模型的独立输出。系统不会在出错时主动告诉你。

这里的论述方法——消融实验、边界测试、中间检查、拒绝映射、评估驱动的验证——并不局限于任何特定的模型或框架。它适用于任何故障表面是分布式 (distributional) 而非确定性 (deterministic) 的系统。随着这些系统成为基础设施,维护它们的纪律需要变得像代码审查和测试套件一样常规。现在建立起这种纪律的工程师将能够把时间花在交付功能上。而那些不这样做的工程师,则会把时间花在调试逗号上。

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