一、引言:上线稳了,故障还是会来
上一期我们聊完了灰度发布:评测决定"能不能上",灰度决定"怎么上",变更全程小步快跑、随时可回滚。但有个问题灰度解决不了------故障不全是变更引起的。模型 API 突然限流、第三方订单接口超时、网络抖动、知识库索引损坏......这些故障跟你的发布节奏无关,它们随时可能发生,而且多 Agent 系统对这种故障格外敏感。
举一个真实发生的例子:某客服系统的编排者调用订单查询 Agent 时,订单服务刚好在升级,接口延迟从 200ms 飙到 8 秒。单看这个接口,只是"慢了一点";但在多 Agent 系统里,编排者会同步等待这个子 Agent 的结果,8 秒超时后重试,重试又叠加了模型调用,最终用户等到的不是答案,是 30 秒后的一声"抱歉,服务繁忙"。一个接口抖动,顺着链路放大成了整条链路的不可用。
更可怕的是另一种情况:你以为系统有降级逻辑------模型挂了走规则引擎兜底,工具挂了返回"暂时不可用"。但这条降级路径从上线起就没被真正触发过,第一次触发就是线上事故:降级代码有 bug、兜底数据是空的、日志打不出来。没有演练过的降级路径,等于没有降级。
这就是这一期的主题:多 Agent 系统的故障演练与混沌工程。我们主动给系统注入故障,在可控范围内让它"炸"一次,提前发现那些只有故障时才会暴露的问题。这是《华为云Flexus+DeepSeek征文》系列的第二十一篇。
二、为什么多 Agent 系统必须做故障演练
先说结论:单 Agent 系统可以不做故障演练,多 Agent 系统必须做。三个理由。
2.1 依赖面指数级扩大
单 Agent 系统的依赖就一个模型 API。多 Agent 系统呢?编排者模型 + 多个子 Agent 模型 + 知识库 + 各类工具/插件 + 会话存储 + 网关......任何一个环节故障,都可能波及整条链路。而且这些依赖大多是第三方的:模型 API 不是你的、订单接口不是你的、知识库服务可能是你的但也在持续变化。你控制不了故障发生,只能控制故障发生时的表现。
2.2 故障沿着链路级联放大
这是多 Agent 最危险的地方。一个子 Agent 超时,编排者等待;编排者重试,模型调用翻倍;重试成功但上下文乱了,后续 Agent 基于错误上下文继续工作------错误被逐级放大。更隐蔽的是降级连锁:A 挂了触发降级,降级逻辑又依赖 B,B 也刚好在故障,于是降级本身也失败了。这些连锁反应,靠读代码看不出来,只有真实注入故障才能暴露。
2.3 你的降级代码可能从来没跑过
人都有侥幸心理:超时设了 10 秒,想着"应该够了吧";重试设了 3 次,想着"总有一次会成功吧";兜底逻辑写了,但测试时用的是 mock 数据,从来没走真实故障路径。故障演练的核心价值就是:在故障真实发生之前,让降级路径先真实跑一遍。 你会发现超时设置不合理、重试没有退避导致重试风暴、兜底逻辑返回了错误格式------这些全是"线上事故预演"。
记住:故障演练不是制造麻烦,是提前排雷。演练中炸掉的每个问题,都是线上事故的免单券。
三、混沌工程的基本原理:先定义"稳态"
混沌工程不是乱搞破坏。正规的混沌工程(参考 Netflix Chaos Monkey 的方法论)遵循一套严谨流程,核心是四个词:稳态假设、爆炸半径、最小化影响、自动恢复。
3.1 稳态假设:先定义"什么算正常"
没有"正常"的定义,"异常"就没有意义。演练前必须明确系统的稳态指标(SLI)和目标值(SLO)。多 Agent 系统建议盯这四组:
| 指标组 | 具体指标 | 建议 SLO |
|---|---|---|
| 业务组 | 任务完成率、端到端正确率(抽样评测) | 完成率 ≥ 99%、正确率 ≥ 95% |
| 性能组 | P95/P99 延迟、平均轮次、超时率 | P95 ≤ 5s、超时率 ≤ 1% |
| 成本组 | 单任务 token 消耗、工具调用次数 | 单任务成本 ≤ 预算 1.1 倍 |
| 系统组 | 错误率、队列积压、CPU/内存水位 | 错误率 ≤ 1% |
演练期间持续采集这些指标,注入故障后观察指标如何偏离稳态、偏离多少、多久恢复。指标口径必须和线上监控一致------演练时用另一套观测口径,得出的结论线上不适用。
3.2 爆炸半径:从最小范围开始
第一次演练就在全链路注入故障,那是自爆不是演练。爆炸半径从小到大:单个子 Agent → 单条链路 → 1% 流量 → 全链路。原则是"能小则小":先在预发环境练熟,再在生产环境用最小半径验证;先注入"可恢复"的故障(延迟、限流),再注入"难恢复"的故障(进程崩溃)。
3.3 自动恢复:演练必须有"一键还原"
任何演练都必须有自动恢复机制:注入的故障到点自动解除,或者有明确的一键还原脚本。没有还原能力的演练,就是在制造事故。 建议所有故障注入都设置 TTL(最长持续时间),超时强制恢复,避免演练人员忘了还原、把演练故障留到线上。
3.4 最小化影响:选对演练时间
生产环境的演练要选流量低峰(比如凌晨),并且提前通知相关方(客服、值班、业务方)。多 Agent 系统链路长,故障传导有滞后,低峰期演练能最大限度降低对真实用户的影响,同时保留真实流量做验证。
四、多 Agent 系统的故障注入点全景
多 Agent 系统的故障可以从五个层面注入,这也是排查线上故障时的五层模型。
4.1 模型层
- 超时:模型 API 延迟飙高(最常见、最致命------编排者同步等待);
- 限流:返回 429,触发重试逻辑;
- 空输出/截断:模型返回空字符串或半截 JSON;
- 格式错误:该输出 JSON 却输出了自然语言(破坏强制解析);
- 幻觉污染:返回看似合理但错误的内容(最难发现,靠评测兜底)。
4.2 工具/插件层
- 超时:第三方 API 无响应;
- 错误码:返回 500/503 或业务错误;
- 脏数据:返回格式正确但内容错误/过期数据;
- 重复副作用:工具被重复调用(重试导致重复下单、重复扣款)。
4.3 知识库层
- 检索超时:向量检索服务无响应;
- 空结果:索引损坏或 embedding 服务挂掉,检索返回空;
- 相似度全低:检索有结果但相关性极差(RAG 幻觉高风险)。
4.4 编排层
- 节点卡死:某个工作流节点执行不返回;
- 死循环:编排逻辑在子 Agent 之间反复循环;
- 状态丢失:会话上下文在节点传递中丢失。
4.5 基础设施层
- 网络:延迟、丢包、连接重置;
- 资源:CPU 打满、内存不足、磁盘写满(日志把磁盘写爆很常见);
- 实例故障:某台 Flexus 实例宕机、容器重启。
注入优先级建议:先模型层(最高频)→ 工具层(次高频)→ 知识库层 → 编排层 → 基础设施层(低频但影响大)。
五、故障注入工具链:从零搭建一套"破坏工具"
工具不在多,能用就行。三件套足以覆盖 90% 的注入场景。
5.1 Chaos Mesh:K8s 原生混沌工具
如果 Dify 部署在 K8s(或 Flexus 的 CCE 容器集群),Chaos Mesh 是最省事的注入工具:支持 Pod 故障(kill 容器)、网络故障(延迟/丢包/带宽限制)、IO 故障(磁盘读写错误)、压力注入(CPU/内存占满)。声明式配置,一个 YAML 就能注入:
# chaos-mesh 示例:注入 5s 网络延迟,持续 10 分钟,影响 30% 请求
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: delay-model-api
spec:
action: delay
mode: percentage
value: "30"
duration: "10m"
selector:
namespaces: ["dify-prod"]
labelSelectors:
app: model-gateway
delay:
latency: "5000ms"
jitter: "1000ms"
5.2 tc netem:轻量网络注入
没有 K8s 时,直接在 Flexus 实例上用 tc 命令注入网络延迟/丢包,简单直接:
# 对 eth0 出方向注入 200ms 延迟 + 5% 丢包(对目标 IP)
tc qdisc add dev eth0 root netem delay 200ms loss 5%
# 演练结束,立即恢复
tc qdisc del dev eth0 root
5.3 应用层注入:模型代理 + 工具 Mock
网络层注入管不到"模型返回乱码""工具返回脏数据"这类应用层故障,需要更精细的注入点。两个做法:
-
模型网关代理:在编排者和模型 API 之间加一层轻量代理(Python/FastAPI),支持按比例注入超时、429、乱码输出、空响应------这层代理平时透明转发,演练时按配置注入;
-
工具 Mock 开关:给关键工具(订单查询、支付)加"故障注入开关",演练时让工具返回预设的故障响应(超时/错误码/脏数据),不依赖真实第三方。
模型代理注入示例:按比例返回 429 限流
import random
FAULT_RATIO = 0.2 # 演练参数:20% 请求注入限流
async def proxy(model_request):
if random.random() < FAULT_RATIO:
return {"error": "rate_limit", "status": 429}, 429
# 正常转发到真实模型端点
return await real_call(model_request)
关键原则:注入逻辑和生产逻辑完全隔离。故障开关走独立配置(环境变量/配置文件),演练结束必须关闭;绝不能让故障注入逻辑成为线上代码的一部分,否则就是"自己给自己埋雷"。
六、设计一次故障演练:六步标准流程
一次规范的故障演练,走完六步才算数。
6.1 第一步:选场景
从风险清单里挑。风险清单来自三个地方:历史线上事故、评测集发现的薄弱点、架构评审的隐患点。优先级:出过事故的环节 > 高依赖环节 > 新上线的环节。每个季度至少覆盖一次模型层 + 一次工具层。
6.2 第二步:定稳态指标
明确"这次演练关注什么"。比如演练"模型限流",稳态指标就是任务完成率、错误率、重试次数;演练"工具超时",稳态指标就是 P95 延迟、超时率、用户等待时长。一次演练只关注 2-3 个指标,盯太多等于没盯。
6.3 第三步:定爆炸半径
- 注入对象:哪个子 Agent、哪条链路;
- 注入比例:10%、30%、50%;
- 持续时间:10 分钟、30 分钟(必须带 TTL);
- 影响范围:预发环境 / 生产低峰 / 生产特定租户。
6.4 第四步:执行注入
用上面三件套执行,同时开启全程 trace(接回 08-07 的可观测性体系)和指标采集。注入动作要脚本化、可回放------同一场景的注入参数记录在案,下次复现时用同一套参数,才有可比性。
6.5 第五步:观察与记录
记录三件事:指标怎么变(偏离多少、什么时候开始偏离)、系统怎么反应(降级触发了吗、重试了几次、告警响了吗)、用户感知(最终答案质量、等待时长)。重点记录"意外":演练时出现的非预期行为,才是演练真正的产出。
6.6 第六步:恢复与复盘
TTL 到期自动恢复 + 人工确认恢复。然后复盘,输出一份演练报告:
| 项目 | 内容 |
|---|---|
| 演练场景 | 模型限流 30% × 15 分钟 |
| 稳态指标 | 任务完成率 ≥ 99% |
| 实际表现 | 完成率跌到 87%,45 分钟才恢复 |
| 发现的问题 | ①重试无退避,重试风暴;②降级路径 5xx;③告警延迟 10 分钟 |
| 改进项 | 指数退避+抖动 / 修复降级 / 告警阈值下调 |
| 责任人/时限 | 张三 / 一周内 |
演练报告是演练唯一的产出物。没有跟进改进项的演练,等于白演。
七、实战案例:客服系统三场演练,炸出三个大坑
把流程串起来,看一个多 Agent 客服系统(意图识别 → 订单查询 → 售后处理 → 质检)的演练实例。
7.1 演练一:模型 API 限流 30%
注入方式:模型网关代理对 30% 请求返回 429,持续 15 分钟。
预期:重试逻辑生效,任务完成率小幅下降后恢复。
实际发现 :
-
完成率直接跌到 87%,远超预期;
-
根因:重试是"立即重试"没有退避------30% 请求限流,立即重试又撞上限流窗口,形成重试风暴 ,把有限的配额全吃掉了,连带正常请求也超时;
-
告警 10 分钟后才发出(阈值设置过高),值班人员错过了黄金干预期。
改进:重试改为指数退避 + 随机抖动(1s→2s→4s,±20% 抖动);告警阈值下调;限流时快速失败而非无限重试。
7.2 演练二:订单查询工具超时 8 秒
注入方式:工具 Mock 对 50% 请求注入 8 秒延迟,持续 10 分钟。
预期:编排者超时后走"暂时无法查询"的降级话术。
实际发现 :
-
降级话术确实触发了,但兜底回答是空的 ------降级逻辑里引用的"订单状态缓存表"从来没被写入过(写入逻辑在另一个服务里,一直没联调);
-
用户收到的是"抱歉,暂时无法查询订单状态:""------冒号后面是空的,比不降级还难看。
改进:修复兜底数据链路;降级回答必须经过评测集校验(接回 08-10 评测体系,新增"降级回答完整性"用例)。
7.3 演练三:知识库检索空结果
注入方式:向量检索服务对 20% 请求返回空结果,持续 10 分钟。
预期:系统明确告知"知识库暂无相关内容"。
实际发现 :
-
系统没有说"不知道",而是编造了一个答案 ------编排者收到空检索结果后,没走"空结果分支",直接让模型基于 prompt 里的系统知识硬答,出现了明显幻觉;
-
这类问题评测集很难覆盖(评测集都是"有答案"的用例),只有故障演练能逼出来。
改进:工作流加"空结果显式分支":检索结果为空 → 明确告知用户 + 引导转人工;新增"空检索"评测用例进回归集。
三场演练的共性发现 :真正的坑不在"故障本身",而在"系统对故障的反应"。这就是故障演练和线上事故的唯一区别------演练时炸的坑,是免费的。
八、演练逼出来的容错设计:五个必做项
三场演练炸出的问题,收敛成五个多 Agent 系统必做的容错设计。
8.1 超时与重试:指数退避 + 抖动
重试必须带退避和抖动,否则并发故障下必现重试风暴:
import random, time
def retry_with_backoff(attempt, base=1.0):
# 指数退避 + 随机抖动,防重试风暴
return base * (2 ** attempt) + random.uniform(0, base * 0.2)
for attempt in range(3): # 最多重试 3 次
try:
return call_model()
except RateLimitError:
time.sleep(retry_with_backoff(attempt))
8.2 熔断:连续失败快速失败
子 Agent 连续失败 N 次后,熔断器打开,后续请求直接快速失败(不再尝试),避免把故障无限放大。熔断半开状态(放少量探测请求)自动恢复。多 Agent 场景熔断粒度建议到"子 Agent 级别"------一个子 Agent 熔断不影响其他子 Agent。
8.3 降级路径:宁可说"不知道",不要编造
多 Agent 的降级三原则:
- 模型挂 → 规则引擎兜底(只回答 FAQ 类问题)或明确告知"AI 服务暂不可用,已转人工";
- 工具挂 → 明确告知"该功能暂时无法使用",绝不编造数据;
- 知识库空 → 明确告知"暂无相关内容",引导用户换种问法。
8.4 幂等与补偿:防重复副作用
重试可能造成重复副作用(重复下单、重复扣款)。工具调用必须幂等:请求带唯一 request_id,服务端按 request_id 去重。无法幂等的操作(支付)要加"确认"环节,或者用补偿机制(失败时回滚)。
8.5 会话恢复:故障后的状态续接
故障期间进行中的会话,恢复后要能续接:会话上下文存共享存储(Redis),故障恢复后读取上下文继续回答,而不是让用户重新说一遍。这是多 Agent 系统的"会话韧性",和灰度篇讲的蓝绿共用状态层是同一件事。
九、把演练变成常态化机制
演练不是"搞一次就完",要形成制度。
9.1 演练日历
- 每周:预发环境小演练(模型超时、工具错误码,10 分钟级);
- 每月:生产环境低峰演练(一个场景,30 分钟级);
- 每季度:GameDay 大演练(红蓝对抗:故障组注入、业务组处置,半天级)。
9.2 演练结果进评测集
每次演练发现的问题,都固化成评测用例:演练一的重试风暴 → 评测集加"限流场景下重试次数 ≤ 3"用例;演练三的空检索幻觉 → 加"空检索不编造"用例。演练发现的问题,必须变成自动化回归的一部分,否则下次改代码又会犯。
9.3 指标基线管理
每次演练的指标表现都记录在案,形成"故障表现基线"。下次演练同一场景时对比基线:变好了说明改进有效,变差了说明有回归。演练数据是衡量系统韧性的唯一客观依据。
9.4 演练即培训
演练也是团队培训:新同学通过演练快速理解系统依赖和降级路径;值班同学通过演练熟悉告警处置流程。演练的价值一半在发现问题,一半在练人。
十、踩坑清单:故障演练常见的 7 个坑
- 演练变成表演:注入的故障太温柔(5% 延迟 100ms),系统毫无反应,演练成了走过场。故障参数要有压力------能触发降级逻辑才算数。
- 没有自动恢复就演练:注入后忘了还原,演练故障留在线上变成真事故。所有注入必须带 TTL,超时强制恢复。
- 爆炸半径失控:本来说好 1% 流量,参数写错变成 100%,真实用户集体中招。注入前必须 double check 比例参数,先小后大。
- 只注入模型层:模型层最容易注入(代理好加),工具层、知识库层常年不演练,真出事全在没演练过的层。每季度覆盖不同层。
- 演练问题不跟进:报告写完了,改进项没人认领,下季度同样的坑再炸一次。演练报告必须带责任人 + 时限,下季度演练先验证上季度改进项。
- 重试风暴没防住:并发故障下,重试没有退避抖动,把故障放大 N 倍。所有重试必须有指数退避 + 抖动,演练第一个验证的就是它。
- 观测口径不一致:演练用临时脚本看指标,线上用监控大盘看指标,两边数字对不上,演练结论没有参考价值。演练必须用线上同一套监控。
十一、FAQ 6 问
Q1:故障演练会不会影响真实用户?
会,但可控。三原则控制影响:选低峰期、控制爆炸半径(比例 + 持续时间 + TTL)、演练前通知相关方。成熟团队的做法是先在预发环境演练到滚瓜烂熟,生产只做最小验证。
Q2:生产环境演练和预发环境演练怎么选?
预发演练解决"逻辑问题"(降级代码 bug、超时配置不合理),生产演练解决"环境问题"(真实流量下的并发行为、第三方真实依赖)。顺序是:预发练熟 → 生产最小半径验证。只做预发不做生产,等于没演练------预发环境没有真实流量,重试风暴这类问题根本复现不出来。
Q3:没有 Chaos Mesh,能演练吗?
能。最小方案:模型网关代理(应用层注入)+ tc netem(网络注入)+ 工具 Mock(依赖注入),三件套覆盖 90% 场景。Chaos Mesh 只是更省事,不是必需品。先从代理 + Mock 开始,工具链可以在演练中逐步补齐。
Q4:演练频率多少合适?
底线是"每月至少一次生产演练 + 每周一次预发演练"。多 Agent 系统链路长、变更频繁,每月一次是保证"故障表现不回归"的最低频率。重大变更(模型升级、编排重构)前后各加一次专项演练。
Q5:怎么让业务方支持故障演练?
用数据说话:演练发现的问题 = 避免的事故。把演练报告里的"发现的坑"翻译成"如果不演练,这些会在线上炸"的后果(用户投诉、赔付、口碑)。另外把演练放在低峰、爆炸半径控制到最小,业务方看到影响可控,自然会支持。
Q6:演练、评测、灰度三者什么关系?
三道防线分工:评测 管"已知场景不回归"(离线、全量、每次变更必跑);灰度 管"变更怎么安全上线"(在线、小流量、逐档放大);演练管"故障来了怎么应对"(主动注入、验证容错、常态化)。评测和灰度守的是"变更关",演练守的是"故障关"------三者缺一不可,共同构成多 Agent 系统的质量防线。
十二、总结
这一期把"多 Agent 系统怎么面对故障"讲透了:
- 为什么必须演练:依赖面指数级扩大、故障级联放大、降级路径可能从没跑过------多 Agent 系统不做故障演练就是在裸奔;
- 混沌工程四原则:稳态假设(先定义正常)、爆炸半径(能小则小)、最小化影响(低峰 + TTL)、自动恢复(一键还原);
- 五层注入点:模型、工具、知识库、编排、基础设施------先模型后工具,每季度覆盖全;
- 三件套工具:Chaos Mesh(K8s)/ tc netem(网络)/ 模型代理 + 工具 Mock(应用层),注入与生产逻辑严格隔离;
- 六步流程:选场景 → 定指标 → 定半径 → 注入 → 观察 → 复盘,报告带责任人和时限;
- 五个容错设计:指数退避重试、子 Agent 熔断、诚实降级(不编造)、幂等防重复副作用、会话状态续接;
- 常态化机制:演练日历、问题进评测集、指标基线、演练即培训。
如果只能带走一句话:故障一定会来,区别只在于你是第一次见它,还是在演练里见过它。主动让系统在可控范围内炸一次,线上才稳得住。
下一期我们聊聊多 Agent 系统的会话管理与上下文工程------当会话越来越长、跨 Agent 状态越来越多,怎么让智能体"记住该记住的、忘掉该忘掉的"。
DeepSeek 实战指南系列:
Dify 实战系列: