跳到主要内容

崩溃是承重性的:大语言模型 (LLM) 的容错性如何掩盖了破碎的数据契约

· 阅读需 12 分钟
Tian Pan
Software Engineer

五十年来,数据管道通过“宕机”来强制执行它们的契约。上游团队重命名了一个列,下游解析器抛出异常,任务崩溃,有人在凌晨 2 点收到告警,到了早上,契约要么被修复,要么被正式重新协商。没有人将其设计为一种治理机制。它只是源于这样一个事实:僵化的代码无法处理它预期之外的输入。崩溃就是执行。告警器就是审计追踪。

![](https://opengraph-image.blockeden.xyz/api/og-tianpan-co?title=%E5%B4%A9%E6%BA%83%E6%98%AF%E6%89%BF%E9%87%8D%E6%80%A7%E7%9A%84%EF%BC%9A%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%20(LLM%29%20%E7%9A%84%E5%AE%B9%E9%94%99%E6%80%A7%E5%A6%82%E4%BD%95%E6%8E%A9%E7%9B%96%E4%BA%86%E7%A0%B4%E7%A2%8E%E7%9A%84%E6%95%B0%E6%8D%AE%E5%A5%91%E7%BA%A6)

接着,我们让 LLM 坐上了消费者的位置,于是破损不再导致崩溃。

模型在读取格式错误的数据记录时不会抛出解析错误。它会“自适应”。缺失的字段变成了看似合理的猜测。重命名的字段变成了略微错误的解读。单位的变化——从分到元,从 UTC 到本地时间——变成了一个自信的答案,而其偏差倍数模型从未提及。管道端到端显示为绿色,仪表盘保持静默,而破碎的契约在三周后才浮出水面,表现为一种谁也无法进行二分定位(bisect)的弥散性质量投诉。我们没有消除故障。我们消除的是“信号”。

崩溃从来不是契约系统中的 Bug

值得精确审视一下崩溃曾经为你带来了什么,因为它带来的价值比大多数团队意识到的要多。

在 Schema 边界发生的硬故障是一个“定位型”事件。堆栈跟踪指出了字段,时间戳指出了部署,且爆炸半径是受限的:坏数据停留在了解析器处,而不是流向六个下游表。它也是一个“强制型”事件。生产团队无法悄悄地发布一个破坏性变更然后走人,因为消费团队的事故会在几小时内找上门来。每一次 Schema 迁移会议、每一份弃用通知、每一封“请评审我们提议的字段重命名”邮件的存在,都是因为某地某人吸取了教训:未经宣布的变更会导致告警。

这与长期以来对波斯塔尔鲁棒性原则(Postel's robustness principle)——“对自己接收的东西要宽容,对自己发送的东西要保守”——的批评有着同样的见解。数十年的协议经验(总结在 IETF 对该原则的重新审视中)表明,宽容的接收并不会让系统变得鲁棒;它会让畸形行为变得根深蒂固,因为没有任何机制告诉发送者他们错了。你容忍的每一个怪癖都会变成你必须永远支持的怪癖。相比之下,严格的解析器是生态系统保持诚实的方式:拒绝就是反馈。

LLM 是有史以来部署的最宽容的解析器。这正是它们的价值所在——它们能从混乱、规范不足、具有人类特征的输入中提取含义。但当你将它接入管道作为结构化数据的消费者时,你就在原本需要严格性来承重的地方,将波斯塔尔法则调到了最高档。

“自适应”究竟长什么样

这些失败模式值得分类列举,因为它们看起来都不像失败。

缺失字段变成了推断。 你的提示词模板渲染了 Customer tier: {tier},而上游服务从上周二开始停止发送 tier 字段。传统的模板引擎可能会因为缺失键而抛出异常;更可能的情况是你的代码渲染了 Customer tier: None 或一个空字符串。模型不会停止。它会根据其他上下文(客户的消费历史、他们的邮箱域名)推断出等级,并且其准确率高到足以让支持工单量不会激增。它们只是在漂移。

重命名字段变成了误读。 上游将 account_status 重命名为 lifecycle_stage 并带有新的枚举值。你的提取提示词仍然在询问账户状态。模型在你传递给它的 JSON 数据块中找到了 lifecycle_stage: "dormant_reactivated",并将其映射到它自己的本体中看起来最接近的内容。有时这种映射符合你的业务逻辑预期,有时则不然。无论哪种情况,都不会产生日志行。

语义变化被直接放行。 这是最微妙的情况:Schema 完全没变,但含义变了。revenue(收入)从毛利切换到了净利。timestamp 从事件时间切换到了摄取时间。一个严格的系统会在此时崩溃(虽然可能太晚或根本不崩溃),但严格的系统也不会对其碰巧注意到的差异进行“粉饰”。当要求模型总结异常时,它会抹平它们:“本期收入略有下降,”它针对 40% 的定义变更这样说道。

垃圾变成了流畅的垃圾。 截断的 JSON、双重编码的字符串、编码不匹配导致的乱码——这些足以杀死任何反序列化器的输入,被模型读取的方式就像人类阅读一封受潮受损的信件。它会重构。重构的内容看似合理。而“看似合理”正是问题所在。

在每种情况下,模型都在准确执行它被训练去做的事情:针对混乱的输入产生最有可能有用的输出。这种行为不是模型的缺陷。它是模型的容忍度与你的架构假设之间的错配——你的架构假设“在这个边界上有人在检查”。

为什么你无法对一种“感觉”进行二分定位

当一个严格的管道崩溃时,你会得到一个有明确开始时间的事故。而当一个 LLM 管道吸收了一个破碎的契约时,你会得到更糟糕的东西:一个没有边界的质量退化。

这种下降是弥散性的——它只影响那些缺失字段至关重要的记录,因此聚合指标只会有个位数的波动。它是延迟的——用户在任何指标变动之前就会感觉到输出不对劲,而当有人提交工单时,上游的变更已经发生了数周,且期间又发布了三个其他的部署。而且它是不可归因的——当质量确实出现可衡量的下降时,嫌疑列表会很长:模型版本升级、提示词修改、检索索引陈旧、季节性输入偏移。上游团队的字段重命名甚至排不上号,因为那个团队的变更日志存在于另一个组织的仓库里。

这就是为什么“我们会在评估(evals)中捕获它”在这里行不通。你的评估集是基于历史输入构建的——这些输入符合旧的契约。上游契约的破碎改变了生产输入的分布,而不是模型在你固定的测试用例上的行为。评估结果显示绿色,生产环境却在退化,而它们之间的差距正是 Schema 偏移的形状。

那些确实捕获到这些退化的团队通常是以一种令人尴尬的方式捕获的:客户把两个输出并排粘贴在一起,一个是变更前的,一个是变更后的。这就是你现在的监控系统——客户的记忆。

重建崩溃机制:推理前的验证

这个修复方案在概念上很简单,但在组织执行上却很令人烦恼:在架构失去严谨性的边界处(即模型之),重新引入严谨性。

具体来说,这是一个在消耗任何 token 之前运行的验证层:

  • 带有必填字段而不仅仅是类型的 Schema 验证。 契约应当声明 Prompt 实际依赖哪些字段,如果缺少这些字段,应当拒绝处理,而不是将其视为 None。如果你的 Prompt 模板引用了 12 个字段,你的验证器就必须断言这 12 个字段的存在。这听起来显而易见;但几乎没人这么做,因为 Prompt 模板和数据摄取代码由不同的人负责,这种依赖关系是不可见的。
  • 对高风险字段进行分布和语义检查。 空值率激增、枚举值超出已知范围、数值字段的数量级发生偏移。这些是标准的数据质量检查 —— 关键在于将它们配置为推理路径上的闸门,而不是某人随便扫一眼的每日报告。
  • 用死信队列代替“耸耸肩”。 验证失败的记录不应被静默丢弃,也不应被英雄主义式地强行处理。它们应该被停放、计数并触发告警。死信队列恢复了过去崩溃机制所提供的特性:异常输入变得可见且有据可查,同时不会拖累那些正常的记录。

注意这个层级本质上是什么:它是作为基础设施重建的“崩溃”。你并不是在让模型变得更严格 —— 你做不到,也不应该想这么做。你是在确保当数据到达模型时,容错性(tolerance)是一种功能,而不是一种掩饰。

组织层面的工作更难。验证层的好坏取决于它所验证的契约,而 LLM 消费者需要声明一种新型的依赖:Prompt 本身。当一个 Prompt 引用一个字段时,这就是一个消费者契约,它值得在 Schema 注册表、dbt 契约或你技术栈使用的任何工具中进行注册,就像任何服务依赖一样。上游团队无法遵守他们看不见的依赖,而今天大多数 Prompt 级别的字段依赖对于数据平台中的任何工具来说都是不可见的。

金丝雀断言:选择你的“脆弱性”

验证闸门捕获结构性破坏。对于语义破坏 —— 比如毛利与净收入混淆的情况 —— 你需要更有杀伤力的手段:金丝雀断言(canary assertions),让你的 LLM 消费者根据告警需求表现出精确的“脆弱性”。

模式如下:在生产流量的同时,持续运行一小部分已知答案的探测器 —— 即正确输出已经固定的输入。例如,一个层级为 platinum 的合成客户记录,其摘要必须包含相应说明。或者一个固定的交易批次,提取的总额必须等于已知的常数。这些不是模型质量意义上的评估(evals);它们是数据路径上的触线告警。如果固定答案变了而模型没变,说明输入契约发生了变动。

第二种变体成本更低:让模型本身将契约异常作为结构化输出呈现。在你的提取 Schema 中增加一个字段 —— input_anomalies —— 并指示模型报告缺失的预期字段、无法识别的枚举值或单位不一致的值,而不是静默地将它们标准化。模型非常擅长发现这些问题;默认情况下,它只是没有报告渠道,所以它的应对过程是不可见的。给这种应对机制一个渠道,当任何字段的异常率超过阈值时触发告警,你就把模型从隐藏偏差的工具转变成了偏差探测器。

“脆弱(brittle)”这个词在这里是刻意使用的。脆弱性并非可靠性的对立面 —— 它是一个刻度。传统流水线将其固定在最大值(任何异常都是停机)。天真的 LLM 流水线将其固定为零(任何异常都不算事)。正确的设置是根据每个字段、每个用例进行选择:与计费相关的字段设置硬性闸门和实时告警;装饰性字段设置计数器和每周回顾。不可接受的是让模型的“脾气”替你做决定,因为模型的脾气总是:默默应对,绝不提起。

容错性是预算,而非恩赐

LLM 处理畸形输入的能力具有真正的价值 —— 这正是这些系统能够处理人类生成的混乱数据的核心原因。错误在于将这种容错性消耗在你自己的基础设施上。为了消化合作伙伴不一致的 CSV 导出而消耗的容错性是产品能力。而为了消化上游团队未告知的 Schema 变更而消耗的容错性则是延迟的故障响应,且在不断积累利息。

因此,审计一下以前容易发生崩溃的地方。在每个 LLM 取代了解析器来消费结构化数据的地方,问问自己:如果一个必填字段消失了,今天会发生什么?如果诚实的回答是“模型会靠猜,且不会有任何告警”,那么你就发现了一个没有执行力的契约 —— 而执行力不会自动回归。在推理前设置验证闸门,将 Prompt 的字段依赖注册为真实的依赖,在涉及资金或信任的路径上部署一些金丝雀断言。

旧系统因为无法应对而崩溃。新系统因为不会崩溃而应对。这两种默认状态都是错误的。那些能把这件事做好的团队,是那些意识到崩溃从来不是失败 —— 而是火警报告 —— 并能在烟雾还没地方躲藏之前,不辞辛劳地安装新警报器的团队。

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