← 返回文章列表

Dify 中级实验(11):高级 RAG 流水线——如何搭建多路检索与精排?

1. 业务场景

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

一家做企业级软件的厂商,产品手册、API 文档、FAQ 加起来上千页。客户和一线客服每天都在问同样的问题:「如何配置 API 密钥?」「重置密码后 API 还能用吗?」——这些问题原本要人工翻文档回答,于是他们搭了一个知识库问答机器人。结果机器人上线后,客服反馈「还不如我自己翻文档」:问「API 密钥在哪配」,它答非所问;问个跨章节的综合问题,它只引用了一部分资料;最要命的是,有些回答看起来头头是道,实际是模型自己编的。

我们第一次接这类需求时,第一反应也是「换个更强的模型试试」。真正动手把链路拆开才发现——模型是无辜的,检索链路才是瓶颈:召回、去重、精排、裁剪,每一环欠一点优化,喂给模型的上下文就差一截,模型再强也答不对。

这不是个例。任何「文档量大、问题五花八门、回答必须可溯源」的场景都是这个模式:企业内部 SOP 查询、合规条款检索、产品支持门户、医疗法律知识问答……单条检索链路解决不了这些问题,真正要解决的是「怎么把检索质量拆开、逐环节优化」。

2. 场景痛点

这个场景的痛点,在这家厂商的客服团队身上体现得最直接:

本质上,「答非所问、引用不可信」不是 LLM 的问题,而是检索链路每一环都欠优化——召回、去重、精排、裁剪、溯源,任何一环弱,答案质量就崩

3. 方案:为什么是高级 RAG 流水线

Dify 工作流里可以用节点把「检索质量」拆成一条可逐环优化的流水线:Query 改写 → 三路并行检索 → 合并去重 → Rerank 精排 → 上下文窗口裁剪 → 带引用溯源回答

选它的理由:

这篇文章我们就用它搭一个「技术文档问答系统」:用户提问后,系统改写问题生成 3 个检索查询,从技术文档库和 FAQ 库并行检索,合并去重后精排、裁剪,最后 LLM 只基于文档生成回答并标注来源 [1][2]

4. 整体架构

整条流水线的架构关系可以更直观地看成:

graph TD start["开始:user_query"] --> qa["Query 分析器(LLM:改写问题,输出 3 个检索查询 JSON)"] qa --> parse["解析检索查询(Code:正则提取 JSON,失败回退原问题 ×3)"] parse --> kb1["技术文档精确检索(KB,top_k=3,query1)"] parse --> kb2["技术文档语义检索(KB,top_k=5,query2)"] parse --> kb3["FAQ 知识库检索(KB,top_k=3,query3)"] kb1 --> merge["合并去重(Code:前 50 字符去重 → 按分降序 → 取前 10)"] kb2 --> merge kb3 --> merge merge --> trim["上下文窗口裁剪(Code:按 1.5 字符/token 估算,超限截断)"] trim --> gen["生成带引用回答(LLM:标注 [1][2],未找到则明说)"] gen --> end1["结束:result / total_unique / dedup_count"]

链路很清晰:入口收问题 → 改写扩召回 → 三路并行检索 → 合并去重精排 → 裁剪控窗口 → 带引用生成。合并去重和窗口裁剪是这条链路的两个关键闸门——前者保证 LLM 拿到的都是不重复的高分片段,后者保证上下文不撑爆。

5. 模块设计

5.1 开始节点

- data:

    type: start

    variables:

    - label: 用户问题

      required: true

      type: text-input

      variable: user_query

5.2 Query 分析器(LLM)——变量必须三花括号

prompt_template:

- role: system

  text: |

    你是一个检索优化专家。分析用户问题,输出检索用查询。

    用户问题:{{#start.user_query#}}

    任务:

    1. 识别核心实体和关键概念

    2. 生成 3 个检索查询:查询1(精确版)、查询2(扩展版)、查询3(关键词版)

    3. 输出纯 JSON(不要 Markdown 围栏,不要其他内容)

model:

  completion_params: {max_tokens: 2000, temperature: 0.7}

  name: deepseek-v4-flash

  provider: langgenius/deepseek/deepseek

5.3 解析检索查询(Code)

LLM 输出常带 ```json 围栏或多余文字,代码节点用正则提取第一个 {...} 并做兜底:

def main(llm_text: str, user_query: str) -> dict:

    import json, re

    text = (llm_text or "").strip()

    queries = None

    m = re.search(r'\{.*\}', text, re.DOTALL)

    if m:

        try:

            data = json.loads(m.group(0))

            qs = data.get("search_queries")

            if isinstance(qs, list) and qs:

                queries = [str(x) for x in qs]

        except Exception:

            queries = None

    if not queries:

        queries = [user_query, user_query, user_query]   # 解析失败兜底

    queries = (queries[:3] + [queries[0]] * 3)[:3]

    return {"query1": queries[0], "query2": queries[1],

            "query3": queries[2], "original_query": user_query}

5.4 三路知识库检索

三路并行,query 分别取 query1/query2/query3;精确路 top_k=3、语义路 top_k=5、FAQ 路 top_k=3。output_retrieval_result 必须为 true,否则结果为空:

- data:

    dataset_ids: [459c4981-6b76-43e7-b44f-c430f33ef035]   # 技术文档库 ID

    output_retrieval_result: true

    query_variable_selector: [cd_parse, query1]

    retrieval_mode: single

    top_k: 3

    type: knowledge-retrieval

  id: kb_exact

5.5 合并去重与窗口裁剪(Code)

合并节点做前 50 字符模糊去重、按分数降序、截取前 10 条,同时拼出带编号的 docs_text 供 LLM 引用:

def main(results_a: list, results_b: list, results_c: list) -> dict:

    seen, merged = set(), []

    for results, source in [(results_a, "tech_doc"), (results_b, "tech_doc_semantic"), (results_c, "faq")]:

        for item in (results if isinstance(results, list) else []):

            content = str(item.get("content", ""))

            key = content[:50].strip()                 # 前 50 字符模糊去重

            if key and key not in seen:

                seen.add(key)

                merged.append({"content": content, "title": item.get("title", ""),

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

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

    merged = merged[:10]

    lines = []

    for i, d in enumerate(merged):

        lines.append("[{}] {} (来源: {}, 相关度: {})".format(i + 1, d["title"], d["source"], d.get("score", 0)))

        lines.append(d["content"]); lines.append("---")

    total_in = sum(len(x) for x in (results_a, results_b, results_c) if isinstance(x, list))

    return {"merged_results": merged, "total_unique": len(merged),

            "dedup_count": total_in - len(merged), "docs_text": "\n".join(lines)}

窗口裁剪节点按 1.5 字符 ≈ 1 token(中文)估算累计长度,超过 max_chars=8000 就截断最后一条并打 truncated 标记,防止检索结果撑爆上下文。

5.6 生成带引用回答(LLM)

你是一个技术问答专家。根据提供的文档资料回答用户问题。

回答要求:

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

2. 每个关键信息末尾标注来源序号,如 [1][2]

3. 如果文档中没有相关信息,明确说"提供的文档中未找到相关信息"

参考资料:

{{#cd_window.selected_text#}}

用户问题:{{#start.user_query#}}

6. 运行验证

测试输入 预期 实测
如何配置 API 密钥? 从技术文档回答,带 [1] 引用 命中 API 文档分段,引用标注正确
重置密码后 API 还能用吗? 综合技术文档 + FAQ 两路来源 同时引用 [1][2],跨库信息综合
你们的公司食堂在几楼? 明确回答「未找到相关信息」 无幻觉,如实说明
任意问题(观察统计) total_unique ≤ 10 且 dedup_count ≥ 0 三路共 11 条去重后剩 8 条,去重生效

7. 实战坑

现象 修复
LLM 提示词用双花括号 {{var}} 变量不替换,{{var}} 原样传给模型 节点内变量引用一律 {{#node.field#}} 三花括号
Rerank 模型名写短名 知识库配置报「模型不兼容」 reranking_provider_name 必须三段式全名 langgenius/xxx/xxx,且 reranking_enable + reranking_model 在知识库数据集层面配置
分段不完整 检索命中半截内容,回答断章取义 导入文档时用「自包含分段」,保证每段语义完整
LLM 输出带 ```json 围栏 json.loads 直接抛异常 代码节点正则提取第一个 {...},失败回退原问题
检索节点没开 output_retrieval_result result 为空,合并全丢 该开关必须 true,结果才是 list[dict]

其中「双花括号」这个坑值得展开看——它是 RAG 链路里最容易犯、又最隐蔽的错误。第一次搭这条流水线时,Query 分析器的提示词里写的是:

用户问题:{{user_query}}   <!-- 双花括号,错误写法 -->

结果运行后模型收到的不是用户问题原文,而是字面量 {{user_query}}——改写出来的 3 个检索查询全部基于这个占位符文本,检索结果自然全错。最迷惑人的是:工作流不报错,链路正常跑完,只是答案质量莫名差。后来把变量引用改成三花括号直接引用后才恢复正常:

用户问题:{{#start.user_query#}}   <!-- 三花括号,正确写法 -->

排查口诀:Dify 1.16 的 LLM 节点只替换 {{#节点id.字段#}} 三花括号引用,{{变量名}} 双花括号会原样传给模型——出现「模型回答里带着 {{xxx}} 占位符」或「答案质量莫名变差」时,先查提示词里的变量引用形态。

8. 实验文档及源码获取

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

本系列 · Dify 实验 · 中级
  1. Dify 中级实验(01):参数提取器实战——如何从自然语言中提取结构化数据?
  2. Dify 中级实验(02):问题分类器——智能路由引擎如何四路分发?
  3. Dify 中级实验(03):模板转换实战——如何用零 Token 完成文本加工?
  4. Dify 中级实验(04):迭代进阶——如何批量处理数据并守住性能边界?
  5. Dify 中级实验(05):并行执行——如何让多路任务同时跑?
  6. Dify 中级实验(06):变量聚合——如何确定性合并多路分支结果?
  7. Dify 中级实验(07):子工作流——如何把公共逻辑做成可复用积木?
  8. Dify 中级实验(08):代码节点进阶——如何用标准库处理文件与数据?
  9. Dify 中级实验(09):HTTP 节点进阶——如何搞定认证、分页与错误重试?
  10. Dify 中级实验(10):知识库深度调优——如何科学评估检索质量?
  11. Dify 中级实验(11):高级 RAG 流水线——如何搭建多路检索与精排?
  12. Dify 中级实验(12):Agent 深度配置——如何让智能体自主调用工具?
  13. Dify 中级实验(13):多 Agent 协作——如何编排多个智能体分工干活?
  14. Dify 中级实验(14):对话变量与状态管理——如何让工作流记住多轮对话的状态?
  15. Dify 中级实验(15):条件分支高阶策略——多条件路由如何避免分支爆炸?
  16. Dify 中级实验(16):错误处理与降级——工作流如何有尊严地失败?
  17. Dify 中级实验(17):调试监控与性能优化——响应慢和 Token 超支如何定位?
  18. Dify 中级实验(18):插件开发入门——如何把工作流变成 Agent 可调用的工具?
  19. Dify 中级实验(19):综合实战——如何把 19 个实验串成一条生产级流水线?
  20. Dify 中级实验(20):综合实战——自动化报告生成流水线如何从数据到周报一步到位?

联系我

15088711270

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

微信二维码

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