从踩坑到定理(三):架构决策轴——为什么状态会散落、失败靠运气、验收翻车?
从踩坑到定理:Dify 应用工程的通用理论 · 3/7
📖 摘要:架构决策没有唯一解,但有一条经过验证的优选路径:状态最小化 → 失败外置 → 测试分层。三条定理顺序依赖,分别回答状态怎么管、失败怎么办、怎么证明它可靠,并落地为可直接使用的评审清单。
📌 本文要解决的核心痛点
- 工单应用状态散落各处,多轮对话后错乱?
- 失败路径靠运气,线上偶发崩溃?
- 验收时「看起来能跑」,上线就翻车?
- 本文用三条设计法则(状态最小化、失败外置、测试分层)回答:状态怎么管、失败怎么办、怎么证明它可靠。
前两个维度都是「约束」:平台约束违反必错,LLM 行为违反必不稳定。架构决策轴是自由度——平台不规定你怎么做,模型也不限制你怎么做。状态存哪、失败怎么处理、验证怎么分层,都是我们的选择。
自由度的坏消息是:没有唯一正确答案,方案好坏全看设计功力。好消息是:自由度的空间里藏着可复用的设计法则——不是「必须这样」,是「这样经过验证更优」。
场景
做一个企业级工单流转应用:用户提交工单 → 状态机流转(待处理/处理中/已完成)→ 过程中可能失败(外部系统不可用、模型超时)→ 多轮对话收集信息。
如果设计时每个决策都「临时想」,会出现什么?状态散落在各处(有的在会话变量、有的在临时拼的参数里)、失败路径靠运气(没想过外部系统挂了会怎样)、验证靠手工点几遍「看起来能跑」。
我们第一版就长这样。直到验收阶段被一连串问题打回:状态不一致、失败后重试把数据搞脏、验证漏掉的边界在线上炸了。这版经验直接逼出了这一篇的三条定理。
结论
架构决策没有唯一解,但有一条经过验证的优选路径:状态最小化 → 失败外置 → 测试分层。
三条定理,一条比一条靠后——只有前一条做对了,后一条才有意义。
定理 3:状态最小化——无状态优先,状态必须显式化。
定理 4:失败外置——失败路径是设计出来的,不是运行时发现的。
定理 5:测试分层——节点级形状断言 + 链路级端到端,两种失效模式要两种验证手段。
推导链:三条定理怎么推出来的
定理 3 的推导。 LLM 应用里「状态」是最危险的东西:它不可复现(模型输出是采样)、它容易污染(跨轮次上下文传染)、它难以排查(看不到中间值)。所以状态越少越好,且每一份状态必须显式化——放在看得见、查得到的地方(会话变量、外部存储),而不是散落在节点参数或隐式上下文里。
显式化的标准动作是:需要跨运行保存的状态,放显式存储(如 Dify 的会话变量、外部 KV 容器),而不是依赖「这次运行刚好还留着」。状态最小化不是「不用状态」,是「每一份状态都有名字、有出处、有生命周期」。
定理 4 的推导。 LLM 应用的外部依赖多(模型 API、知识库、外部系统),任何一步都可能失败。失败不是异常,是默认情形。如果不设计失败路径,失败时应用会怎样?报错、卡死、返回垃圾数据——都是运行时才发现。失败外置的意思是:在设计期就把「失败时走哪条路」定好——重试、降级、兜底、明确报错,每条失败路径都是设计出来的,不是撞出来的。
定理 5 的推导。 这来自一个被反复验证的事实:节点是局部的、链路是全局的。LLM 节点的非确定性是局部的(这个节点输出可能漂移),链路的可靠性是全局的(数据流跨多个节点,某处断裂全链失败)。两种失效模式需要两种验证手段:节点级验证用形状断言(输入输出是否符合契约),链路级验证用端到端用例(真实业务路径是否通)。只用一种验证,必然漏掉另一种失效模式。
正例实证:工单流转应用的完整正确设计
按三条定理重做后的工单应用,验收一次通过:
- 状态最小化:工单状态唯一存在会话变量里,状态机流转全部用确定性节点实现,每步状态迁移有明确的输入输出。多轮对话中状态不丢、不重、不串。
失败外置:外部系统不可用时走降级路径(缓存数据兜底 + 明确提示);模型超时走重试(有限次 + 退避);LLM 输出不合法走归一化(重试一次 + 规则兜底)。每条失败路径在设计文档里画得清清楚楚,上线后没有一条是运行时才发现的新失败路径。
测试分层:节点级——每个确定性节点配形状断言,LLM 节点配输出校验;链路级——按业务路径枚举端到端用例(提交→流转→完成、提交→外部失败→降级→完成、提交→超时→重试→完成)。两层验证覆盖了「节点坏了」和「链路断了」两类问题。
反例实证:第一版怎么翻的车
第一版的三宗罪,正好是三条定理的反例:
反例 1(状态最小化):状态散落。
现象:工单状态同时存在会话变量、拼接参数、临时字段里,互相不同步,多轮对话后状态错乱。
根因:没做状态设计——「用的时候随手放一个地方」。
修复:状态收敛到唯一显式存储,其余全部删掉。删完状态相关 bug 归零。
反例 2(失败外置):失败路径靠运气。
现象:外部系统测试时正常,上线后偶发不可用,应用直接报错卡死,用户看到一屏英文错误。
根因:设计时没想过「外部系统会挂」——失败路径是空的。
修复:补全降级/重试/兜底路径。之后外部系统故障时,应用行为是「设计好的行为」,不是「崩溃」。
反例 3(测试分层):验证漏边界。
现象:验收时手工点了几遍主路径「看起来能跑」,上线的第一周,链路边界场景(半路失败、极端输入)连续出问题。
根因:只做了链路级「能不能跑」,没做节点级断言,也没按路径枚举用例。
修复:补节点级形状断言 + 路径枚举用例。之后边界场景由用例覆盖,不再是线上「惊喜」。
扩展:三条定理怎么落地成评审清单
理论要可操作,就得能变成评审时的检查项。我们实际用的评审清单,就是三条定理的直接展开:
定理 3(状态最小化)检查项:
这份状态必须存在吗?——能不能现场算出来?能,就别存。
存在哪?——是显式存储(会话变量/外部存储),还是散落在节点参数里?散落的,收敛。
谁改它?——多个节点都能改同一个状态?改出冲突谁负责?只有一个写入方,最稳。
定理 4(失败外置)检查项:
这个环节可能失败吗?——模型调用、外部 API、知识库检索、数据解析,四个默认高危点。
失败走哪条路?——重试?降级?兜底?明确报错?必须有设计好的路径,不能是「报错就行」。
失败路径测试过吗?——每条失败路径至少有一个用例,不能只测成功路径。
定理 5(测试分层)检查项:
节点级断言覆盖了哪些节点?——确定性节点全覆盖,LLM 节点形状断言。
链路级用例覆盖了哪些路径?——按拓扑枚举:主路径、分支、失败路径、边界路径。
两层有空白吗?——只测链路不测节点(节点坏了测不出来)、只测节点不测链路(链路断了测不出来),都是空白。
清单化的价值在于:评审不再靠「感觉对不对」,而是逐项过检查点。我们实测过,带清单评审比不带清单,漏检率显著下降——因为人的记忆会漏,清单不会。
实践动作
设计时:三问——「这份状态必须存在吗?存在哪?谁改它?」(定理 3);「这个环节可能失败吗?失败走哪条路?」(定理 4);「这条链路的失效模式是局部的还是全局的?」(定理 5)。
评审时:检查状态是否有唯一显式存储;检查每个外部依赖是否有设计过的失败路径;检查用例是否同时覆盖节点级与链路级。
排障时:状态错乱先查「状态是不是散落了」;偶发失败先查「失败路径设计了没」;验证漏网先查「用例分层全不全」。
边界与版本
版本无关:三条定理是 LLM 应用的普遍设计法则,与平台无关。Dify 只是实现它们的载体之一。
边界说明:定理 4 的验证覆盖了模型超时、外部 API 不可用、LLM 输出不合法三类失败;其他失败类型(如平台自身故障、数据源损坏)按同样方法设计,但具体路径未全部实测。
收尾
三条定理合起来回答了架构决策轴的核心问题:状态怎么管、失败怎么办、怎么证明它可靠。 它们之间是顺序依赖——状态最小化让失败路径简单(状态少,失败面小),失败外置让链路可控(每条路都有设计),测试分层让这一切可证明(两层验证全覆盖)。
下一篇进入第四个维度——数据/记忆。这是四个维度里坑源最密集的一个:RAG 的检索质量、知识新鲜度、query 归一化,每一个都是「看起来简单,做起来全是坑」。
💬 讨论区:你的应用里状态散落过吗?失败路径是设计出来的还是线上撞出来的?验收时有没有「看起来能跑、交付就翻车」?评论区聊聊你的架构决策故事。
本文基于真实项目交付经验撰写(Dify 1.16.x 环境、69 个实验与验收记录)。文中数据均来自我们自己的实测记录,理论部分以「已验证 / 推断待验证」标注边界。