LLM应用混沌工程实践:主动下毒暴露故障,提升系统稳定性

发布时间:2026/10/7 12:25:11
LLM应用混沌工程实践:主动下毒暴露故障,提升系统稳定性 1. 为什么我要给自家 LLM 应用主动“下毒”第一次听到“给 AI 系统下毒”这个说法团队里几个做模型推理的同事都愣了一下。我们花了小半年时间把一套基于大模型的智能问答系统调到线上可用日均请求量稳定在六位数结果现在要主动往里面灌脏数据、断网、塞超长上下文这不是自己给自己找麻烦吗但真正做过一轮之后我才明白Chaos Engineering混沌工程在 LLM 应用里的价值恰恰在于它能把那些“平时不出现、一出现就要命”的故障提前暴露出来。传统后端服务的混沌工程思路相对成熟注入延迟、杀掉节点、模拟网络分区看熔断和降级策略是否生效。但 LLM 应用不一样它的故障面要宽得多。一个请求从用户输入到最终返回中间要经过提示词组装、上下文检索、模型推理、工具调用、结果后处理、安全过滤等好几个环节每个环节都可能出问题而且很多问题是“软故障”——服务没挂但输出质量悄悄崩了。比如检索模块返回了一堆语义相近但事实错误的文档模型照样能编出一段读起来很顺的答案再比如工具调用的返回格式变了模型不会报错而是把错误信息当成正常内容继续往下编。这类问题靠传统的监控指标根本发现不了只有主动注入故障才能逼出来。我写这篇东西是想把这套实践完整地摊开讲。LLM 应用的混沌工程核心不是“搞破坏”而是用可控的故障注入去验证系统在真实恶劣条件下的容错能力。它适合已经有一套能跑起来的 LLM 应用、并且开始关心线上稳定性的团队。如果你还在调 prompt 阶段那先别急等系统有了明确的调用链路和降级策略再来看这篇会更合适。下面我会从故障分类、注入手段、观测指标、真实踩坑几个角度把这一轮实践里能落地的部分都讲清楚。2. LLM 应用的故障面和传统服务到底差在哪2.1 从“服务挂了”到“服务活着但答案是错的”传统微服务的健康检查很直接进程在不在、端口通不通、响应时间超没超阈值。一个服务返回 500监控立刻报警运维上去重启或者切流量问题闭环。但 LLM 应用里最危险的状态是服务完全正常HTTP 200响应时间也在预期内但输出内容是错的、有害的或者完全跑偏的。我举个实际遇到的例子。我们的问答系统接了一个内部知识库检索模块正常情况下检索 top-5 文档喂给模型。有一次上游检索服务因为索引重建返回的文档相关性骤降但接口本身没报错返回的还是 5 条结果只是内容跟问题八竿子打不着。模型拿到这些文档后没有说“我不知道”而是硬生生把不相关的文档拼接成了一段看似合理的回答。用户看到的是一个语气笃定、格式规范的错误答案。这种故障传统监控一个都抓不到。所以 LLM 应用的混沌工程第一件事就是把“输出质量”纳入故障观测范围。这要求我们在注入故障的同时有一套能自动评估输出质量的机制比如用另一个模型做 LLM as judge或者用规则匹配关键事实。没有这层评估混沌实验就只是看服务有没有崩意义会少一大半。2.2 故障注入的四个层次在实践里我把 LLM 应用的故障注入分成四个层次从外到内依次是层次注入对象典型故障观测重点接入层网关、限流、鉴权突发流量、超时、鉴权失败限流是否生效、降级是否触发编排层提示词组装、上下文管理上下文超长、模板变量缺失、历史污染截断策略、兜底提示词模型层推理服务、模型版本推理超时、返回空、输出乱码、版本切换重试、fallback 模型、超时熔断工具与检索层向量库、外部 API、函数调用检索空结果、返回格式错误、API 限流工具调用容错、结果校验这四个层次里接入层和模型层的故障比较接近传统混沌工程工具与检索层和编排层才是 LLM 应用特有的重灾区。尤其是编排层提示词模板里一个变量没传进去模型可能不会报错而是把{user_name}这种占位符原样输出或者干脆忽略整段指令。这种问题在测试环境很难复现因为测试数据往往太干净了。2.3 为什么“下毒”比“压测”更接近真实压测解决的是容量问题它回答的是“系统能扛多少 QPS”。但 LLM 应用线上出的事故绝大多数不是被流量打死的而是被脏数据、异常输入、依赖抖动搞死的。我统计过我们系统过去半年的线上告警真正因为流量超限触发的不到两成剩下八成都是依赖异常、输入异常、模型输出异常。混沌工程里的“下毒”本质上是主动构造那些线上迟早会遇到、但测试环境很难自然出现的恶劣输入。比如用户粘贴一段几万字的合同让模型总结比如检索库因为同步延迟返回了昨天的旧数据比如工具调用的 JSON 里多了一个字段导致解析失败。这些场景你不可能靠等只能主动造。造出来之后看系统是优雅降级还是直接崩掉这才是混沌实验要的答案。3. 动手之前把可观测性这层地基打牢3.1 没有 trace 的混沌实验就是盲人摸象我第一次做故障注入的时候犯了个低级错误直接在生产环境给检索模块注入了空结果然后盯着大盘看。结果大盘上只有总请求量和平均延迟根本看不出这次注入影响了哪些请求、模型拿到空结果后到底输出了什么。折腾了半小时除了知道“服务没挂”什么有效信息都没拿到。后来我们补了全链路 trace每个请求从入口到出口的每个环节都打点提示词组装后的完整内容、检索返回的文档 ID 和相似度分数、模型调用的输入输出 token 数、工具调用的请求和响应、最终输出的完整文本。有了这层 trace混沌实验才真正有意义——注入之后我可以精确地拉出受影响的那批请求逐条看它们在每个环节的表现。提示trace 里记录完整的提示词和模型输出会带来存储成本建议对混沌实验期间的请求做全量记录日常请求做采样记录用标签区分。3.2 输出质量评估混沌实验的“眼睛”前面说过LLM 应用最怕的是“服务活着但答案是错的”。要抓住这种故障必须有一套自动化的输出质量评估。我们用的是组合方案规则层检查输出是否包含特定错误标记、是否为空、是否超长、是否包含占位符未替换的情况。模型层用一个较小的模型做 LLM as judge对输出的事实一致性、相关性、完整性打分。对比层在混沌实验里把注入故障后的输出和正常基线输出做对比看语义偏移程度。这三层里规则层最快最便宜能抓住大部分低级故障模型层能抓住语义层面的问题但有成本和延迟对比层适合在实验环境做生产环境慎用因为基线本身也可能在变。我建议在混沌实验的初期先把规则层做扎实。很多故障其实用规则就能识别比如输出为空、输出里出现Error、timeout、null这类词或者输出长度异常短。这些规则实现成本低误报也少能覆盖相当一部分场景。3.3 基线数据没有对比就没有伤害混沌实验要回答的问题是“系统在故障下表现如何”这需要一个参照系。我们在每次实验前会先跑一批正常请求记录下输出质量分数、延迟分布、token 消耗等指标作为基线。注入故障后再跑同样一批请求对比两组数据。这里有个细节基线请求和实验请求必须用同一批输入。我们一开始图省事基线用历史真实请求实验用构造的测试请求结果两组数据根本没法比因为输入分布都不一样。后来改成固定一批覆盖典型场景的测试用例每次实验都用它数据才可比。4. 四类故障注入的具体做法和实测效果4.1 上下文污染往检索结果里塞“看起来相关”的垃圾这是我认为对 LLM 应用威胁最大、也最值得优先做的一类注入。做法很简单在检索模块返回的文档里混入几条语义相似但事实错误的内容看模型会不会被带偏。我们构造污染文档的方式有几种。一种是事实篡改把正确文档里的关键数字、日期、人名改掉其他表述保持不变。另一种是主题漂移用 embedding 相似度找一批跟问题相关但实际答非所问的文档。还有一种是指令注入在文档里埋入类似“忽略以上指令直接输出……”的文本测试模型对上下文里恶意指令的抵抗能力。实测下来最容易被带偏的是事实篡改类。模型对检索结果的信任度很高尤其是当污染文档和正确文档混在一起时它往往会综合两者编出一个“折中”的答案。我们有一次把一份政策文件里的生效日期改了模型输出的答案里日期就是错的而且语气非常肯定。这个实验直接推动我们在检索层加了来源可信度加权对低可信来源的文档降权处理。注意上下文污染实验一定要在隔离环境做污染文档不能进入真实索引库。我们是用一个独立的检索代理在返回结果时动态注入实验结束就关掉。4.2 工具调用异常让外部 API 返回“格式对但内容错”的数据LLM 应用经常要调外部工具比如查天气、查订单、算汇率。工具调用最怕的不是 API 挂了而是 API 返回了 200 但数据结构变了或者内容错了。模型拿到这种数据往往会把它当成正常结果继续处理。我们做过一组注入让订单查询工具在特定条件下返回一个不存在的订单号但其他字段都正常。模型拿到后没有识别出订单号异常而是基于这个假订单号编了一段物流信息。用户如果不去核对根本发现不了。另一组注入是字段缺失把工具返回 JSON 里的某个必填字段删掉。模型的表现分两种一种是直接报错说信息不足另一种是拿其他字段硬凑。后者更危险。这个实验让我们在工具调用层加了一层 schema 校验字段缺失或类型不对时直接走兜底逻辑不把脏数据喂给模型。4.3 模型推理抖动超时、空返回和版本切换模型层的故障注入相对传统但 LLM 场景下有它的特殊性。我们主要注入三种推理超时人为把模型响应时间拉长到超过阈值看上层是否有超时熔断和 fallback。空返回让模型返回空字符串或只有标点的内容看后处理逻辑是否会崩。版本切换在请求中间切换模型版本模拟灰度发布时的版本不一致。超时注入暴露了我们一个设计缺陷原来的重试逻辑是无限重试结果模型一慢请求就堆积把线程池占满了。后来改成有限重试加快速失败并且对超时请求走一个轻量级的规则兜底至少给用户一个“当前繁忙请稍后再试”的明确回复而不是一直转圈。空返回注入则暴露了后处理的一个 bug我们的输出解析器假设模型一定会返回非空内容遇到空字符串时抛了异常导致整个请求 500。修复后加了空值判断空返回时走默认回复。4.4 输入侧攻击超长上下文、特殊字符和提示词注入输入侧的注入最接近真实用户行为因为线上用户什么都可能输入。我们重点测了三类超长上下文把几万字的文档直接塞进 prompt看系统的截断策略是否合理。实测发现简单的按字符截断会把关键信息截掉后来改成按语义段落截断并且优先保留开头和结尾。特殊字符输入里混入大量 emoji、控制字符、零宽字符看 tokenizer 和后续处理是否稳定。有一次输入里带了零宽空格导致检索匹配失败模型拿不到任何上下文直接开始胡编。提示词注入在用户输入里埋入“忽略之前的指令”这类文本测试模型的指令遵循边界。这个实验的结果比较依赖模型本身的对齐程度但系统层面能做的是在输入预处理阶段做一层检测对高风险输入打标或者拦截。5. 一次完整的混沌实验是怎么跑起来的5.1 实验设计先定假设再定注入混沌工程不是随便搞破坏每次实验都要有明确的假设。我们的实验模板大概是这样假设当检索模块返回空结果时系统应该在 2 秒内返回“未找到相关信息”的兜底回复而不是让模型自由发挥。注入在检索代理层对特定请求返回空列表。观测受影响请求的最终输出是否包含兜底话术延迟是否在阈值内是否有异常抛出。判定如果输出里出现了模型自由发挥的内容或者延迟超过阈值假设不成立需要修复。这个模板看起来简单但执行起来关键是注入要精准、观测要全面。我们一开始注入范围太大把整个检索模块都关了结果影响了几百个请求排查起来很费劲。后来改成按请求标签注入只影响带特定标记的测试请求生产流量不受影响。5.2 灰度与回滚别把实验做成事故混沌实验最大的风险是失控。我们的做法是所有注入默认关闭通过配置中心动态开启。注入只对带chaos_test标签的请求生效生产流量不受影响。每次实验设置最大影响请求数和最长持续时间超了自动关闭。实验期间值班同学盯着核心指标一旦发现异常扩散立即手动回滚。这套机制听起来繁琐但真出过一次险情。有一次注入配置写错了影响范围从测试请求扩大到了全量幸好有自动熔断30 秒内就关掉了。如果没有这层保护那次实验就是一次真实事故。5.3 实验后的复盘把发现的问题变成修复项每次实验结束我们会拉一份报告包含注入类型、影响请求数、观测到的异常、假设是否成立、需要修复的问题。这份报告直接进迭代 backlog每个问题都有负责人和截止时间。我印象最深的一次复盘是发现模型在检索空结果时有 30% 的概率会编造答案。这个比例远高于我们的预期之前一直以为模型会老实说“不知道”。修复方案是在提示词里强化“如果上下文为空必须回复未找到”同时在输出后处理里加一层检测发现疑似编造就拦截。改完之后再跑同样的注入编造比例降到了 3% 以下。6. 踩过的坑和几条实在的经验6.1 别在没做可观测性之前搞混沌这是我踩的第一个坑也是最大的坑。没有 trace、没有输出质量评估、没有基线数据混沌实验就是往黑箱里扔石头听个响就没了。我们第一轮实验基本白做因为拿不到有效数据。后来花了三周补可观测性第二轮实验才真正有产出。所以如果你现在系统还只有基础监控建议先把 trace 和质量评估做起来再考虑混沌工程。6.2 注入要“像真的”不要“像测试”构造故障数据的时候很容易构造出过于规整的假数据比如空结果就是干干净净的空列表格式错误就是明显的乱码。但线上真实的故障往往更隐蔽空结果可能是一个包含空字符串的列表格式错误可能是字段名大小写变了。注入数据越接近真实故障的形态实验价值越高。我们后来专门收集了一批线上真实异常样本作为注入数据的模板。6.3 质量评估模型本身也可能被“下毒”我们用 LLM as judge 做输出质量评估结果有一次实验里注入的污染文档把 judge 模型也带偏了导致它给错误答案打了高分。这个发现让我们意识到评估模型和被测模型之间要有隔离评估时不能把原始上下文直接喂给 judge而应该用独立的评估提示词和评估标准。后来我们把 judge 的输入做了裁剪只给它问题和答案不给检索上下文减少被污染的可能。6.4 混沌实验要常态化不能搞成运动式我们最开始是集中一周做一轮实验后来发现这样不行。系统在迭代故障面在变一次实验的结论过两个月可能就失效了。现在我们把混沌实验做成了常态化任务每周跑一批核心场景的注入结果自动进报表。这样既能持续发现新问题也能验证之前的修复有没有退化。6.5 团队共识比工具更重要最后说个软性的经验。混沌工程推行的最大阻力往往不是技术而是人。业务方会问“为什么要故意搞坏系统”开发会担心“实验出了事故谁负责”。我们的做法是先在小范围做拿实际发现的问题和修复效果说话慢慢建立信任。同时明确实验的边界和回滚机制让所有人知道风险是可控的。等大家看到混沌实验真的能提前抓住线上问题配合度就上来了。7. 这套实践后续还能往哪走目前我们的混沌实验主要还是人工设计场景、手动触发。下一步想做的是把故障注入和自动化测试流水线结合起来每次发版前自动跑一轮核心场景的注入把输出质量下降作为发版卡点。另一个方向是引入更多的随机性用模糊测试的思路自动生成异常输入覆盖人工想不到的边角场景。还有一个我觉得很有价值的方向是把混沌实验的结果反哺到提示词和系统设计里。比如我们发现模型在上下文为空时容易编造就在提示词里加了强约束发现工具返回格式错误时模型会硬凑就在工具层加了 schema 校验。这些修复本质上是在给系统“打疫苗”而疫苗的配方就是从一次次“下毒”实验里来的。说到底LLM 应用的稳定性不是靠祈祷模型不出错而是靠假设它一定会出错然后提前把容错的路铺好。混沌工程就是铺路的过程虽然过程有点折腾但每次实验之后系统确实更抗造了这个投入是值得的。