从踩坑到定理(二):LLM 行为轴——为什么 JSON 解析偶发失败、同样输入时对时错?
从踩坑到定理:Dify 应用工程的通用理论 · 2/7
📖 摘要:LLM 输出是概率性的——违反不必然错,但必然不稳定。本文用两条定理(控制流与内容分离、形状契约)教你管理概率性:确定性逻辑交给确定性节点,LLM 输出做形状断言 + 归一化兜底,把漂移挡在业务逻辑之外。
📌 本文要解决的核心痛点
- LLM 输出 JSON 解析偶发失败,为什么?
- 多轮对话后输出格式开始「传染」,怎么防?
- 同样的输入,为什么时对时错?
- 本文用两条定理(控制流与内容分离、形状契约)教你管理模型的概率性——不赌运气,造稳压器。
平台约束轴是确定性——违反必错,查规则就行。LLM 行为轴完全不同:模型不给你确定性,它给你概率。 同样的提示词,这轮输出 JSON,下轮可能多一句解释;这轮格式工整,下轮可能跨轮次「传染」上上一轮的格式。
这个维度没有「查文档」的解法。只有设计边界。
场景
做一个「从自然语言中提取结构化数据」的应用:用户输入一段简历描述,应用输出姓名、职位、年限、技能四个字段。
第一版设计:「让 LLM 直接输出 JSON,解析一下就能用。」听起来天经地义——模型不是号称会输出 JSON 吗?
上线后数据流开始花式翻车:有时输出被 markdown 代码块包着;有时字段名变了("job_title" 变 "title");有时多出个逗号;最麻烦的是——多轮对话后,输出格式开始「继承」用户输入的风格,字段顺序全乱。下游的解析代码一崩,整个应用就卡住。
这就是 LLM 行为轴的全部故事:模型的输出是概率性的,你设计时必须假设它会在任何一次调用中漂移。 不是「可能漂移」,是「必然在某个输入下漂移」。
结论
LLM 输出是概率性的:违反不必然错,但必然不稳定。
两条推论,构成这个维度的两条核心定理:
定理 1:控制流与内容分离——确定性逻辑(分支、循环、状态流转)必须交给确定性节点,LLM 只生产内容,不生产结构。
定理 2:形状契约——所有跨节点数据必须显式声明形状;LLM 输出必须做形状断言,而不是严格断言。
推导链:为什么「让 LLM 决定一切」必然翻车
先推导定理 1。Dify 工作流的本质是确定性图执行引擎:if-else 按 case_id 跳、迭代按列表循环、状态按变量流转。这些逻辑一旦交给 LLM 输出「下一步动作」来决定,会发生什么?
LLM 每次输出的是一个采样结果——同样的输入,概率分布相同,但采样不同。于是「下一步跳 A」和「下一步跳 B」都可能是同一输入的合法输出。用采样结果驱动控制流,等于把确定性的执行引擎变成了掷骰子。 一次两次可能没事,长期运行必然出现无法复现的分支行为——排障时最可怕的情况:同样的输入,上次对,这次错。
所以定理 1 的机制性表述是:控制流要求可复现,LLM 输出不保证可复现,两者不相容。 解法不是「调好提示词」,是结构上分离——LLM 只回答「内容是什么」,路由由确定性节点根据结构化的内容做判断。
再推导定理 2。LLM 输出是采样,采样就没有「必然的格式」。即便提示词写「必须输出 JSON」,模型也可能给出带代码块、带注释、字段顺序乱、甚至字段名同义替换的「近似 JSON」。严格断言(相等)必然失败,因为概率不为零的漂移必然在某次出现。
解法是形状断言:不要求「等于我期望的精确值」,要求「符合我期望的形状」——解析后校验字段存在、类型正确、值合法,不合法就走归一化/兜底。形状断言把「模型给我什么」的不可控,转化为「我接受什么形状」的可控。
正例实证:形状契约的正确打开方式
我们把「形状断言 + 归一化」用在了多个数据提取类应用上,链路稳定后,多轮对话场景下连续测试几十轮,数据提取零失败。
正确做法是这样的:
LLM 输出原始文本(不直接要求严格 JSON,允许它自然表达)。
解析节点提取字段,逐字段校验:存在性、类型、枚举合法性。
非法/缺失字段走归一化:重试一次、或规则兜底(如固定映射表)。
只有形状合法的数据才进入下游。
关键点在第 2 步的「校验」和第 3 步的「归一化」——它们是确定性节点,是应用对 LLM 概率性的「稳压器」。概率性的输出经过确定性校验后,数据流就是确定性的。 这就是管理概率性的方法:不是消灭漂移(做不到),是把漂移挡在业务逻辑之外。
# 形状断言示例(解析后校验,不逐字比对)
fields = json.loads(text) # 容错解析
required = ["name", "years", "skills"]
ok = all(f in fields for f in required) # 形状断言:字段存在
ok = ok and isinstance(fields["years"], (int, str)) # 类型合法
if not ok:
fields = retry_or_default(fields) # 归一化兜底反例实证:两个真实事故
事故 1:LLM 直接输出分支标签(定理 1 的反例)。
现象:多轮对话路由混乱、分支跳转不可复现,同样输入时对时错。
根因:让 LLM 输出「下一步动作」,路由节点按它跳——控制流被采样结果驱动。
修复:LLM 只输出意图内容(如「用户要查订单」),路由用确定性规则(关键词/结构化字段)判断分支。之后行为完全可复现。
事故 2:LLM 输出 JSON 直接解析(定理 2 的反例)。
现象:解析偶发失败;多轮后输出格式被用户输入「传染」(跨轮次格式传染),字段顺序全乱。
根因:把「输出 JSON」当成了必然,直接 json.loads——严格断言,一次漂移就崩。
修复:解析后逐字段形状断言 + 缺失归一化;跨轮次场景显式约束输出格式、隔离上下文。
这两个事故都满足可证伪断言:不做形状断言的节点,必然在某轮输入下崩溃。 我们实际验证过——不是「可能」,是「必然」,只是崩溃的输入不可预测而已。
扩展:漂移的三个层次,稳压器要分级设计
「形状断言 + 归一化」听起来简单,但实践中我们发现漂移有三个层次,稳压器必须分级设计,否则只挡得住第一层:
第一层:格式漂移。 多出来的代码块、多余的逗号、字段顺序乱。这一层用解析容错 + 形状断言就能挡住——解析时容忍前后缀噪声,断言时只查关键字段存在性和类型。
第二层:字段漂移。 字段名同义替换("job_title" 变 "title")、字段合并(两个字段合成一个)。这一层形状断言挡不住——字段名对不上,断言直接失败。需要归一化映射:解析后把同义字段名归一到一个标准名,或者用「候选字段名集合」匹配。
第三层:语义漂移。 模型理解了指令,但输出内容本身偏了——提取的年限是「5年以上」而非「5年」,分类结果是「疑似」而非「是/否」。这一层是模型能力边界,断言和归一化都挡不住,只能靠设计上游约束:把开放问题改造成选择题(给候选枚举让模型选),把模糊问题改造成明确问题(提示词里定义清楚「年限只输出数字」)。
三层的教训是:断言只是第一道闸,不是全部。 设计时先问「这个输出可能在哪一层漂移」,再决定用哪种稳压器。我们的经验:90% 的漂移问题靠第一层就能解决,但剩下 10% 的顽固问题,必须靠第二层的归一化映射和第三层的输入改造才能根治。
实践动作
设计时:凡是 LLM 节点的输出要进下游逻辑的,先问「下游对它的形状有要求吗?」有——必配形状断言 + 归一化。凡是「让 LLM 决定走哪条路」的设计,直接否决,改确定性路由。
评审时:检查每个 LLM 节点——输出有没有被断言?断言是形状级还是严格级?控制流有没有被 LLM 输出驱动?三项里任何一项不满足,打回。
排障时:「同样输入时对时错」先怀疑控制流被 LLM 驱动;「解析偶发失败」先怀疑严格断言。这两条能定位 LLM 行为轴 90% 的问题。
边界与版本
版本无关:定理 1、2 是 LLM 应用的普遍规律,不依赖 Dify 版本,甚至不依赖 Dify 平台——任何 LLM 应用(无论 LangChain、自建 Agent)都适用。版本相关的部分只是 Dify 上「形状断言用什么节点实现」这类平台细节。
边界说明:本维度的验证记录基于 Dify 1.16.x 上的文本类 LLM 节点。图像/多模态模型的漂移模式未在本系列实测范围内,推断待验证。
收尾
平台约束教你「平台怎么说,你就怎么做」;LLM 行为教你「模型会漂移,所以你要造稳压器」。这两个维度合起来,定义了应用的「骨架」:确定性框架 + 概率性内容,两者之间用形状契约衔接。
下一篇进入第三个维度——架构决策。这里没有「违反必错」,只有「怎么选更优」。状态怎么存、失败怎么处理、验证怎么分层,是自由度最大的地方,也是设计功力最见高下的地方。
💬 讨论区:你遇到过「让模型决定分支」翻车的事故吗?或者「JSON 解析偶发失败」「多轮对话格式传染」?评论区聊聊你的漂移时刻,我们用定理复盘。
本文基于真实项目交付经验撰写(Dify 1.16.x 环境、69 个实验与验收记录)。文中数据均来自我们自己的实测记录,理论部分以「已验证 / 推断待验证」标注边界。