Dify 韧性验证实验(01):故障注入与降级链——如何用故障注入验证 AI 应用的降级链?
1. 业务场景
先讲一个我们实际遇到的场景。
一家做客服工单 SaaS 的公司,凌晨两点,用户在 App 里提交了一个「账号无法登录」的工单。系统本该自动创建工单、生成摘要、给用户发一条「已受理」通知——但当晚通知 API 恰好挂掉了(http 500)。结果呢?用户没收到任何反馈,以为工单没提交成功,又连着提交了两次——同一故障变成了三张重复工单,第二天客服光合并工单就花了半小时。更糟的是另一种情况:模型服务 429,摘要节点直接失败,整个流程崩掉,工单压根没建上,用户的求助石沉大海。
我们第一次接这类需求时,第一反应是「把降级分支配上就完事了」。真正动手才发现——配了降级分支,不等于降级链能用。没人真正走过降级路径,真出事时降级分支自己也是坏的;后来我们干脆把「主动制造故障、逐路验证降级」写进上线前检查项,这个实验就是这么来的。
这不是个例。任何「用户提交 → 系统处理 → 通知用户」的线上链路都是这个模式:电商下单后的支付回调、物流系统的签收通知、SaaS 平台的告警推送——下游任何一个环节抖动,用户感知到的都是「系统坏了」。
2. 场景痛点
这套链路的问题,在工单系统上体现得最直接:
- happy path 全绿掩盖了真实风险:功能测试跑的都是「通知 API 正常、模型正常」的顺利路径,通知服务一挂,流程直接失败、工单没建上——「好的时候能用」证明不了「坏的时候不崩」。
- 降级分支配了等于没配:很多系统配置了降级分支却从没真正走过,真出事时降级分支自己也是坏的——容错应用自身不容错,是生产事故里最讽刺的一类。
- 模型异常时裸奔:模型 429/超时/空输出时,系统要么整体崩溃,要么给用户返回一段空白——用户不知道发生了什么,只能反复重试,问题越滚越大。
- 知识库空结果直接中断:工单分类检索没有参考结果时,流程直接断掉,连降级的机会都没有——「没查到」和「系统坏了」在用户那里,是同一件事。
本质上,上线前最该被证明的不是「好的时候能用」,而是「坏的时候不崩」——而「坏的时候不崩」只能靠主动制造故障来证明。
3. 方案:为什么是故障注入
Dify 工作流里验证降级链最直接的方法,就是故障注入:主动制造故障(http 节点失败 / 模型异常 / 知识库空结果),验证应用的降级路径真的兜住——而不是只在 happy path 下跑通。
选它的理由:
- 平台原生可注入:http 节点有
error_strategy: fail-branch、LLM 节点可配置不可用模型、知识库检索可问库外内容——三条故障注入路都不需要改代码,改输入即可触发; - 可验证可兜底:降级是否生效可以直接从出口判断(
result_degraded必须含明确降级信息,不静默),配合 C1-A4 静态检查,断链在生成时就能抓出来; - 是 104 批次实测教训的反向工程:先故意埋断链,再用静态检查抓出来——把「容错应用自身不容错」的坑,变成可重复的验证方法。
这篇文章我们就用它搭一个「工单流程应用」:创建工单 → 校验 → 通知,然后主动注入三类故障,逐路验证降级链真的兜得住。
4. 整体架构
链路很清晰:入口收工单 → 参数校验 → 知识库参考 → LLM 摘要 → 通知 → 多出口结束。关键设计是每一处可能失败的点(模型、http、知识库)都接了降级分支,且降级信息显式输出——不静默、不裸奔。
5. 模块设计
5.1 降级链三件套
降级链设计是本实验核心,三路降级缺一不可:
- LLM 降级:LLM 节点后置 IF-ELSE 判断
text为空 → 固定话术分支(模型异常时不裸奔) - http 降级:通知节点
error_strategy: fail-branch→ 失败分支接降级 code(记录 + 话术)→ 降级 end - 知识库降级:检索节点后 IF-ELSE 判断结果为空 → 无参考分支(流程不中断)
5.2 http 节点 fail-branch 配置
失败分支的 handle 必须写作 fail-branch,不是
fail(实测 104 修正——写错 UI
不画失败分支线、后端不执行降级):
- id: http_notify
data:
type: http-request
title: 通知API
url: '{{#start.notify_url#}}'
method: GET
error_strategy: fail-branch
timeout:
max_connect_time: 5
max_read_time: 10
enable_retry: true
retry_interval: 1000
retry_times: 35.3 多出口 end 变量唯一
每个出口有唯一输出变量名(104 多 end
教训):result_ok / result_degraded /
result_reask /
result_fail——下游消费不歧义,排障时可从出口名直接判断走了哪条路径。
5.4 故障注入方法(三路)
| 注入路 | 触发方式 | 预期降级行为 |
|---|---|---|
| http 故障 | notify_url 指向不可达地址(如
http://172.19.0.50:9999/none) |
fail-branch 降级链 →
result_degraded 含明确降级信息 |
| 模型故障 | LLM 节点配置不可用模型 / mock 空输出 | text 为空 → 固定话术分支 |
| 知识库空 | 提问库外内容(如「量子计算集群的冷却液配方」) | 无参考分支,流程不中断 |
6. 运行验证
| 用例 | 输入要点 | 预期 | 结果 |
|---|---|---|---|
| 正常路径 | order_id=OD2026080501 + notify_url=KV /echo | succeeded → result_ok「通知成功,工单已全链路完成」 | 通过(实测) |
| 注入 http 故障 | notify_url=不可达地址 | partial-succeeded → result_degraded 含「(已降级)通知通道异常」 | 通过(实测,fail-branch 降级链真实生效,非静默) |
| 注入模型故障 | (LLM 空分支) | 固定话术降级节点存在且 reachable | 通过(实测结构验证) |
| 注入 KB 空 | 提问库外内容 | succeeded → end_ok(无参考分支不中断流程) | 通过(实测) |
| 参数缺失 | order_id 空 | succeeded → result_reask「请提供订单号后再创建工单」 | 通过(实测) |
| 反例验证 | 故意断降级 code 出边 → check_dsls C1-A4 | 静态检查抓出「fail 分支不可达 end」 | 通过(实测,生成时按完整降级链设计无断链) |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
| 容错应用自身不容错 | http 500→exception 后链路断裂、partial-succeeded 且出口空 | 故障注入逐路验证降级链,出口空 = P1(实测,104-17) |
| 失败分支 handle 写错 | 写 fail 而不是
fail-branch → 降级分支不执行 |
统一写
error_strategy: fail-branch + 失败分支 handle
同名(实测,104-03) |
| 降级分支静默 | 降级只返回默认值不报「已降级」→ 用户不知情 | 降级分支必须输出明确降级信息(record + 话术)(实测,102-06) |
| 静态检查查不出类型问题 | code 输出类型不匹配导入不报错、运行才炸 | 降级链可达性用 C1-A4 静态检查兜底,类型问题靠运行验证(实测,104-01) |
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-105-01:故障注入与降级链验证.md
- 源码(可直接导入):dify105_01_工单流程.yml
- 全部实验文档目录:dify-105/experiments
- 全部源码目录:dify-105/dsl
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
- Dify 韧性验证实验(01):故障注入与降级链——如何用故障注入验证 AI 应用的降级链?
- Dify 韧性验证实验(02):契约与消费一致性——多应用协作时契约变了如何第一时间发现?
- Dify 韧性验证实验(03):多轮记忆边界——对话记忆在哪些场景会失效?
- Dify 韧性验证实验(04):安全对抗——如何给 AI 应用做安全对抗测试?
- Dify 韧性验证实验(05):并发与可靠性——高并发下 AI 应用如何保证可靠?
- Dify 韧性验证实验(06):全链路追踪——一次请求如何在 Dify 中被完整追踪?
- Dify 韧性验证实验(07):六模块综合验收——AI 应用上线前如何做六维度验收?
- Dify 韧性验证实验(08):上线后回归——应用上线后如何持续回归验证?