Dify 高级实验(05):多步推理——如何把复杂问题拆解成子问题逐个击破?
1. 业务场景
先讲一个我们实际遇到的场景。
一位用户在产品社区里问:「我该买 iPhone 还是华为?」这个看似简单的问题,直接问 LLM 往往得到一段含糊其辞的「各有优劣,看你的需求」——因为「买哪个」背后其实是一串子问题:价格差多少?性能谁强?续航谁久?生态谁全?最后还要结合用户自己的偏好做判断。一步回答,模型要么信息不全,要么夹带私货,用户看完还是不知道买哪个。
我们第一次遇到这类问题时,第一反应也是「让模型多思考一会儿不就行了」。后来才发现——问题不在思考时长,而在上下文:模型在同一个上下文里既要回忆事实、又要对比、还要下判断,互相干扰,一步回答天生不可靠。把问题拆开、逐个击破、最后综合,才是人类分析问题的方式,也是 LLM 该有的方式。
这不是个例。任何「一步答不准」的问题都是这个模式:产品对比(A 和 B 选哪个)、行业分析(新能源车会不会取代燃油车)、研究型问答(这个技术方案值不值得采用)——问题越复杂,一步到位的回答越不可靠,因为模型在同一个上下文里既要回忆事实、又要对比、还要下判断,互相干扰。
2. 场景痛点
这个流程的痛点,在问问题的人身上体现得最直接:
- 一步回答信息不全:模型一次回答只能覆盖部分维度——对比手机只聊了价格没聊续航,用户拿到的结论是残缺的,决策依据不足。
- 不可解释:模型直接给结论「推荐华为」,但没说清楚基于哪些维度、每个维度怎么比较的——用户无法判断这个推荐是否适合自己,也不敢信。
- 复杂问题答不准:事实、对比、判断混在一个回答里,模型容易顾此失彼——说了价格忘了性能,说了优点不提缺点,回答质量随问题复杂度直线下降。
- 无法定位错误:回答错了,用户不知道错在哪一步——是事实记错了,还是对比维度漏了,还是判断逻辑有问题,完全无法追溯。
本质上,一步回答的瓶颈不是模型不够聪明,而是「把复杂问题当简单问题处理」——复杂问题需要先拆解、再逐个击破、最后综合,这是人类分析问题的方式,也是 LLM 该有的方式。
3. 方案:为什么是这套多步推理范式
Dify 工作流里的「拆解 → 迭代推理 → 综合」链路,把「一步回答」变成「先拆后答」,正好复刻人类分析问题的过程。
选它的理由,我们实际对比过:
- 拆解让每个子问题都可独立求解:LLM 把复杂问题拆成 3-5 个可独立检索或推理的子问题,每个子问题单独回答——单点准确率远高于一次答全;
- 迭代节点逐个击破:Dify 原生迭代节点对每个子问题单独跑一轮 LLM,子问题之间不互相污染,答案可逐条核对;
- 综合阶段显式标注缺口:子回答出现「不确定」时,汇总代码检测出来,最终答案显式标注信息缺口——不掩盖不确定性,用户知道哪些结论可靠、哪些要核实。
这篇文章我们就用它搭一个「多步推理问答助手」:复杂问题先拆成子问题逐个推理,再综合成结构化回答。
4. 整体架构
链路很清晰:入口收复杂问题 → 拆解成子问题 → 逐个子问题推理 → 汇总检测缺口 → 综合回答。拆解和迭代是这个架构的核心——拆得好,每个子问题都能独立求解;迭代让子问题逐个处理,互不干扰。
5. 模块设计
5.1 拆解 LLM(lm_decompose)
系统提示词限定输出纯 JSON,并要求 3-5 个子问题、先事实后判断:
你是一个问题拆解专家。将用户的问题拆解成 3-5 个独立的子问题。
用户问题:{{#start.user_query#}}
拆解规则:
1. 每个子问题必须是可独立检索或推理的
2. 子问题之间不要重叠
3. 按逻辑顺序排列(先事实后判断)
输出格式(纯 JSON,不要 Markdown):
{"sub_questions": [{"id": 1, "question": "xxx", "type": "fact/compare/judge"}],
"reasoning_path": "从基本信息到综合判断的逻辑链描述"}
5.2 准备迭代代码(cd_prepare)
解析拆解结果并兜底:<think> 剥离
→ json.loads → 失败则按行正则提取子问题,最后
items[:5] 截断(防迭代 30 元素上限):
def main(sub_questions_json, query_subject):
import json, re
text = sub_questions_json if isinstance(sub_questions_json, str) else str(sub_questions_json)
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()
m = re.search(r'\{[^}]', text)
if m:
text = text[m.start():]
try:
questions = json.loads(text)
items = questions.get("sub_questions", [])
if isinstance(items, list) and len(items) > 0:
return {"items": items, "total": len(items),
"reasoning_path": questions.get("reasoning_path", "")}
except:
pass
lines = text.split('\n')
items = []
for ln in lines:
m2 = re.match(r'^\d+[\.\、\,]\s*(.+)$', ln.strip())
if m2 and len(m2.group(1).strip()) > 5:
items.append({"id": len(items)+1, "question": m2.group(1).strip(), "type": "fact"})
return {"items": items[:5], "total": min(len(items), 5), "reasoning_path": ""}5.3 迭代节点(it)
三件套缺一不可:iterator_selector 指向 items
数组、output_selector
只选可见类型(string)、start_node_id 与迭代起始标记 id
一致:
- data:
desc: ''
iterator_selector: [cd_prepare, items]
output_selector: [lm_sub, text]
start_node_id: itstart0
title: 逐子问题分析
type: iteration5.4 子问题 LLM(lm_sub)
迭代内部 LLM 引用 {{#it.index#}}(序号)与
{{#it.item.子字段#}},这是迭代标准机制;必须配
reasoning_format: separated:
你是一个专题分析师。请回答以下子问题。
主题:{{#start.query_subject#}}
子问题(第{{#it.index#}}个):{{#it.item.question#}}
问题类型:{{#it.item.type#}}
回答要求:
1. fact 类型基于知识回答;compare 列出对比维度;judge 给出判断理由
2. 控制在 100 字以内
3. 如不确定,明确说"不确定"
4. 直接给出回答,不要重复问题
5.5 汇总代码(cd_summary)
逐条拼接 Q/A,并检测信息缺口——has_information_gaps
展平为字符串(boolean 在变量选择器不可见):
def main(sub_results, original_query, reasoning_path):
lines = []
has_gaps = False
for item in (sub_results if isinstance(sub_results, list) else []):
q = item.get("question", "?") if isinstance(item, dict) else "?"
a = item.get("answer", item.get("text", "未回答")) if isinstance(item, dict) else str(item)
lines.append(f"Q: {q}\nA: {a}\n")
if "不确定" in a:
has_gaps = True
return {"intermediate_results": "\n".join(lines),
"has_information_gaps": "true" if has_gaps else "false",
"gap_note": "部分信息未确认,建议核实" if has_gaps else "信息完整"}5.6 综合 LLM(lm_final)
引用 {{#cd_summary.intermediate_results#}} 与
{{#cd_prepare.reasoning_path#}},要求先给直接答案、再引关键论据、最后按缺口标注。
6. 运行验证
| 输入 | 期望行为 | 实测 |
|---|---|---|
| user_query:我该买 iPhone 还是华为?query_subject:手机对比 | 拆解出 5 个子问题(价格/性能/续航/生态/推荐),逐个回答后给出综合对比与推荐 | 与预期一致,回答结构化、含缺口标注逻辑 |
| user_query:AI 会不会取代程序员?query_subject:职业分析 | 拆解出「AI 能力现状/程序员工作内容/替代分析/趋势」等子问题,输出行业分析 | 与预期一致 |
| 构造一个让子回答出现「不确定」的输入 | gap_note =
部分信息未确认,建议核实,最终回答末尾标注 |
与预期一致(信息缺口检测生效) |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
迭代内部 LLM 缺
reasoning_format: separated |
思考过程混入
text,污染整个迭代输出数组;cd_summary
里 "不确定" in a 把思考中的「不确定」误判为信息缺口 |
迭代内 LLM 显式加
reasoning_format: separated(dify103_05 实测) |
迭代五件套缺失或 yaml.dump()
重写 |
内部节点 UI 叠在一起 /
iteration_id、parentId、isInIteration、sourcePosition、targetPosition
被静默销毁,校验报错 |
内部节点补全五件套;修改用
patch 精确定位,不用 yaml round-trip |
prompt 变量用 {{变量名}}
双花括号 |
1.16 不替换双花括号,字面传给模型,回答「参考资料为空」 | LLM prompt 一律
{{#节点id.字段#}} 三花括号(dify102_12 实测) |
| 拆解出过多子问题 | 迭代运行报
then length of var "item" must be less than 30 elements |
拆解 prompt 限 3-5 个 +
cd_prepare 里 items[:5] 截断留余量(dify103_09
实测) |
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-103-05:多步推理工作流.md
- 源码(可直接导入):dify103_05_多步推理工作流.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
- Dify 高级实验(01):智能文档处理——如何把非结构化文档变成结构化数据?
- Dify 高级实验(02):对话式 BI 分析——如何让业务人员用自然语言直接查数?
- Dify 高级实验(03):智能客服——如何让 AI 自动应答并把疑难问题转人工?
- Dify 高级实验(04):RAG 增强问答——如何让知识库问答更准还能多轮追问?
- Dify 高级实验(05):多步推理——如何把复杂问题拆解成子问题逐个击破?
- Dify 高级实验(06):自动化报告——如何让系统定时自动生成周报?
- Dify 高级实验(07):智能审批——如何让机器自动完成流程审批?
- Dify 高级实验(08):智能告警——如何用 AI 替代固定阈值监控?
- Dify 高级实验(09):测试用例生成——如何让 AI 自动产出测试用例?
- Dify 高级实验(10):综合实战——如何从零搭建一个企业级智能客服平台?