← 返回文章列表

Dify 高级实验(04):RAG 增强问答——如何让知识库问答更准还能多轮追问?

1. 业务场景

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

一家智能硬件公司的技术支持团队,每天在社群里回答用户提问:「X200 手表防水吗?」「充不进电怎么办?」「它的续航怎么样?」。他们搭过一个最基础的问答机器人——知识库检索 → 拼接 → LLM 回答,结果三个问题全露馅:第一个问题检索到了对的文档却答得含糊;第二个问题引用了无关内容;第三个问题直接答非所问,因为机器人根本不记得上一轮聊的是 X200。用户追问两句就不耐烦了:「这 AI 还不如百度」。

我们第一次搭这种问答机器人时,第一反应也是「检索 → 拼接 → 回答,三步不就完了」。真正跑起来才发现——单轮孤立问题能答对,一追问答非所问,这才意识到「能检索」和「答得准、聊得下去」之间隔着一整层工程:检索词要对、召回要全、回答要能溯源、上下文要能记住,缺一环,体验就塌。

这不是个例。任何「靠知识库问答做产品支持」的场景都是这个模式:SaaS 厂商的文档问答、银行的产品手册咨询、设备厂商的售后支持——简单「拼接式 RAG」能答对单轮、孤立的问题,一旦涉及多知识库、多轮追问,检索不全、引用跑偏、上下文丢失三个毛病全冒出来。

2. 场景痛点

这个流程的痛点,在技术支持团队身上体现得最直接:

本质上,「能检索」和「答得准、聊得下去」之间隔着一整层工程——RAG 的问题从来不是知识库有没有内容,而是检索质量、上下文管理和溯源这三件事没做好。

3. 方案:为什么是这套增强型 RAG 链路

Dify 工作流里的「Query 改写 → 多路检索 → 合并去重 → 带引用回答 → 对话摘要」链路,正好把这三个毛病逐个治掉。

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

这篇文章我们就用它搭一个「产品支持问答助手」:双知识库并行检索,带来源编号回答,还支持多轮追问。

4. 整体架构

graph TD start["开始:用户输入 sys.query"] --> rw["LLM lm_rewrite:Query 改写(结合对话摘要补全指代)"] rw --> kd["知识库 kb_docs:产品文档,single 检索 TopK=3"] rw --> kf["知识库 kb_faq:技术 FAQ,single 检索 TopK=2"] kd --> mg["代码 cd_merge:合并 + 去重 + 排序 + 截断 → docs_text"] kf --> mg mg --> la["LLM lm_answer:带引用标注回答"] la --> aa["回复 ans_answer"] la --> cs["代码 cd_summary:更新对话摘要"] cs --> e1["结束:映射回 conversation_summary"]

链路很清晰:入口收问题 → 改写检索词 → 双库并行检索 → 合并去重 → 带引用回答 → 更新摘要。改写和摘要是这个架构的「记忆器官」——改写让每轮检索都对准用户真实意图,摘要让多轮对话不丢上下文。

5. 模块设计

5.1 Query 改写 lm_rewrite

改写节点引用对话变量(注意是 {{#conversation.xxx#}} 前缀,不是 start)与 {{#sys.query#}}

你是一个检索优化专家。根据用户当前问题和对话历史,生成一个更适合知识库检索的查询。

对话摘要:{{#conversation.conversation_summary#}}

用户当前问题:{{#sys.query#}}

要求:

1. 如果用户追问(如"它的续航怎么样"),补充上下文(如"智能手表X200的续航怎么样")

2. 去掉口语化表达

3. 如果有多个意图,拆成多个查询,用分号隔开

4. 输出仅检索查询文本

5.2 并行检索 kb_docs / kb_faq

两个知识库检索节点共用改写后的文本做 query,检索模式 single、阈值 0.0,TopK 不同(文档 3、FAQ 2):

kb_docs:  dataset_ids: [产品文档库]  retrieval_mode: single  top_k: 3  score_threshold: 0.0

kb_faq:   dataset_ids: [技术FAQ库]   retrieval_mode: single  top_k: 2  score_threshold: 0.0

query_variable_selector: [lm_rewrite, text]   # 两节点都用改写后的查询

5.3 合并去重排序 cd_merge

按内容前 80 字符去重、分数降序、超 3000 字符截断前 4 条,并拼成 docs_text 供 LLM 引用:

def main(docs_a, docs_b):

    seen, merged = set(), []

    for docs, source in [(docs_a, "product_doc"), (docs_b, "faq")]:

        for doc in docs or []:

            key = (doc.get("content", "") or "")[:80]

            if key not in seen:

                seen.add(key)

                merged.append({"content": doc.get("content", ""),

                               "title": doc.get("title", ""),

                               "score": doc.get("score", 0), "source": source})

    merged.sort(key=lambda x: x.get("score", 0), reverse=True)

    if sum(len(d["content"]) for d in merged) > 3000:

        merged = merged[:4]

    docs_text = "\n".join("[{}] {}(来源: {}\n{}".format(

        i + 1, d["title"], d["source"], d["content"]) for i, d in enumerate(merged))

    return {"merged_docs": merged, "docs_text": docs_text,

            "doc_count": len(merged), "total_chars": sum(len(d["content"]) for d in merged)}

5.4 带引用回答 lm_answer

直接引用 {{#cd_merge.docs_text#}}(已带 [1][2] 编号的文本),要求关键信息标注来源、找不到就明说:

你是一个产品技术支持。根据提供的文档资料回答用户问题。

参考资料:

{{#cd_merge.docs_text#}}

回复要求:

1. 仅基于提供的文档回答,不要自行编造

2. 关键信息后标注来源编号,如 [1][2]

3. 如果提供的信息不足以回答,明确说"提供的文档中未找到相关信息"

4. 如果有对话上下文,结合上下文理解用户的意图

5.5 更新对话摘要 cd_summary → 结束节点

每轮把「用户问 + 回答」追加进摘要并截断(超 1000 保留末尾 800),防止摘要无限膨胀污染上下文;结束节点把 updated_summary 映射回对话变量 conversation_summary,实现跨轮记忆:

end.outputs:

  - variable: conversation_summary

    value_selector: [cd_summary, updated_summary]

6. 运行验证

轮次 输入 期望行为 实测
第 1 轮 「X200 手表防水吗?」 改写后检索产品文档,回答 IP68 防水并标 [1] 与预期一致
第 2 轮 「充不进电怎么办」 检索 FAQ,给出排查步骤并标来源编号 与预期一致
第 3 轮 「它的续航怎么样」 Query 改写补全为「X200 续航」,命中产品文档规格段 与预期一致,多轮上下文生效

7. 实战坑

现象 修复
追问不补全直接检索 「它的续航怎么样」检索不到「智能手表 X200 续航」相关内容 Query 改写节点结合 conversation_summary 补全指代后再检索(dify103_04 实测)
知识库分段不自包含 single 检索单段命中,片段缺上下文,LLM 答不全或答错 入库分段保证每段自含答案(自包含分段),TopK 检索才有效
两个知识库内容重叠 同一信息被检索两次,回答重复引用、上下文被浪费 cd_merge 按内容前 80 字符去重 + 分数降序 + 超 3000 字符截断(dify103_04 实测)
对话摘要不截断 多轮后摘要无限增长,token 爆炸、旧信息淹没新问题 摘要超 1000 保留末尾 800,结束节点映射回 conversation_summary(dify103_04 实测)
DSL 导入后直接跑 对话变量映射丢失,conversation_summary 为空,多轮上下文失效 导入后先按导入验证流程跑一轮端到端(查节点/边/变量映射),再正式测试
开启 rerank 精排缺配置 检索报 provider 错误,召回直接失败 rerank 需三段式 provider 配置(rerank 模型 + API 地址 + 密钥),缺一不可;本实验用 single 检索可不开

8. 实验文档及源码获取

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

联系我

15088711270

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

微信二维码

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