← 返回文章列表

Dify 高级实验(05):多步推理——如何把复杂问题拆解成子问题逐个击破?

1. 业务场景

先讲一个我们实际遇到的场景。

一位用户在产品社区里问:「我该买 iPhone 还是华为?」这个看似简单的问题,直接问 LLM 往往得到一段含糊其辞的「各有优劣,看你的需求」——因为「买哪个」背后其实是一串子问题:价格差多少?性能谁强?续航谁久?生态谁全?最后还要结合用户自己的偏好做判断。一步回答,模型要么信息不全,要么夹带私货,用户看完还是不知道买哪个。

我们第一次遇到这类问题时,第一反应也是「让模型多思考一会儿不就行了」。后来才发现——问题不在思考时长,而在上下文:模型在同一个上下文里既要回忆事实、又要对比、还要下判断,互相干扰,一步回答天生不可靠。把问题拆开、逐个击破、最后综合,才是人类分析问题的方式,也是 LLM 该有的方式。

这不是个例。任何「一步答不准」的问题都是这个模式:产品对比(A 和 B 选哪个)、行业分析(新能源车会不会取代燃油车)、研究型问答(这个技术方案值不值得采用)——问题越复杂,一步到位的回答越不可靠,因为模型在同一个上下文里既要回忆事实、又要对比、还要下判断,互相干扰。

2. 场景痛点

这个流程的痛点,在问问题的人身上体现得最直接:

本质上,一步回答的瓶颈不是模型不够聪明,而是「把复杂问题当简单问题处理」——复杂问题需要先拆解、再逐个击破、最后综合,这是人类分析问题的方式,也是 LLM 该有的方式。

3. 方案:为什么是这套多步推理范式

Dify 工作流里的「拆解 → 迭代推理 → 综合」链路,把「一步回答」变成「先拆后答」,正好复刻人类分析问题的过程。

选它的理由,我们实际对比过:

这篇文章我们就用它搭一个「多步推理问答助手」:复杂问题先拆成子问题逐个推理,再综合成结构化回答。

4. 整体架构

graph TD start["开始:user_query / query_subject"] --> dec["LLM 问题拆解:输出子问题 JSON"] dec --> prep["Code 准备迭代:解析 JSON → items 数组"] prep --> it["迭代「逐子问题分析」:itstart0 → LLM 子问题回答"] it --> sum["Code 汇总子回答:拼接 + 检测「不确定」缺口"] sum --> fin["LLM 综合回答:引用分步结果,标注缺口"] fin --> e1["结束"]

链路很清晰:入口收复杂问题 → 拆解成子问题 → 逐个子问题推理 → 汇总检测缺口 → 综合回答。拆解和迭代是这个架构的核心——拆得好,每个子问题都能独立求解;迭代让子问题逐个处理,互不干扰。

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: iteration

5.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_idparentIdisInIterationsourcePositiontargetPosition 被静默销毁,校验报错 内部节点补全五件套;修改用 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_prepareitems[:5] 截断留余量(dify103_09 实测)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过