为什么我们的 RAG 还打不过 Dify 原生助手

最近在做一条自建 RAG 链路:Java 负责知识库工具调用,Python 负责编排和回答生成。链路能跑,双知识库也已经接上了,但同一个问题丢给 Dify 原生聊天助手,回答还是更稳,引用也更像“已经把资料看明白了再说话”。

这个差距一开始看着像模型问题,后来排下来,主要不是模型本身,而是检索编排层差得比较多。Dify 用的是一条完整的问答链路,我们现在更像是“从知识库捞几段文本出来,再让模型自己总结”。

这篇把这次对比里真正拉开差距的地方写清楚,也顺手把后面的优化方向收口一下。

先看现象

拿一个很典型的定义题来说:

1
发热冒口是什么

Dify 原生助手给出的回答有几个明显特征:

  • 开头先给一句标准定义
  • 后面再补工作原理、优点和适用场景
  • 能引用教材类资料,也能带图片
  • 整体像是在回答“这是什么”,而不是在复述一段检索结果

我们这边在接上双知识库之后,回答质量确实上来了一点,但整体风格还是更像论文摘要:

  • 会提到 907A 模铸、球墨铸铁研磨盘之类的应用案例
  • 会解释补缩、放热、延缓凝固这些机理
  • 但定义句通常不够靠前
  • 结构更像“根据检索结果总结”,而不是“先把问题答准”

这说明两个问题已经分开了:

  1. 双知识库确实能补覆盖面。
  2. 覆盖面补上以后,链路本身的检索和组织方式还是不如 Dify 原生。

第一个误判:以为差距主要在知识库数量

最开始 Java 工具只查了一个 dataset,结果里甚至能直接看到只有一个 dataset_id。把第二个知识库接进去以后,召回确实更好了,说明这个方向没错。

但这件事只能解决“有没有看到那份资料”,解决不了“看到了以后怎么排、怎么喂给模型、怎么让模型按正确的问法回答”。

也就是说,双知识库更像补地基,不是把 Dify 效果复刻出来的那一步。

真正的差距在检索编排层

1. Dify 是多路召回 + 全局 rerank,我们是分库检索后本地拼

Dify 的知识库问答默认不是只跑一条检索通道。按实际配置看,它会做多路召回,然后再统一 rerank:

  • 语义召回
  • 关键词召回
  • 多知识库一起取候选
  • 用 rerank 模型统一重排
  • 最后再把候选喂给回答模型

我们这边当时的做法更接近:

  1. Java 针对每个 dataset 调一次 retrieve
  2. 每个库先拿各自的 top-k
  3. 把结果按返回分数拼在一起
  4. Python 再拿这些片段生成答案

问题在于,这两条链路不是一个级别的东西。

多知识库的原始检索分数通常不适合直接横向比较。一个库里的 0.72,和另一个库里的 0.68,很可能不是一个统一标尺。Dify 这里多做了一步全局 rerank,把所有候选重新放到同一个尺度上打分。我们当时没有这一步,所以跨库排序天然更飘。

2. Dify 有关键词通道,定义题更容易命中教材式答案

“发热冒口是什么”这种问题,最优答案通常不是来自案例论文,而是来自教材、手册、工艺说明里的定义段。

这类段落有几个特点:

  • 词面非常稳定
  • 通常直接写“X 是一种……”
  • 结构化程度高

这对关键词召回很友好。

我们这边一开始更偏语义检索,命中的内容就更容易往“相关工艺研究”“应用效果”那边偏。于是模型拿到上下文以后,能总结出一个差不多的答案,但很难像 Dify 一样先把定义句拿出来,再往下展开。

所以定义题上,Dify 看起来更像“答题”,我们更像“摘要”。

3. 每个库先截 top-k,会把好 chunk 提前裁掉

这次还有一个很具体的问题:我们当时是每个 dataset 各取一批结果,再在本地合并。

这个策略的问题不在“能不能工作”,而在于它会让局部排序过早生效。

举个简单例子:

  • 最终只想给模型 6 条 chunk
  • 每个库各拿 5 条
  • 某个库里最有价值的定义段刚好排第 7

那这条定义段在进入全局视野之前就已经没了。后面不管怎么提示模型,它都答不出最像样的定义句。

Dify 这类原生助手一般会先放大候选池,再做统一排序,局部截断没有那么早。

4. 我们喂给模型的是“被压扁的片段”,不是完整证据

Java 侧最开始返回给 Python 的信息很薄:

  • 片段文本
  • 文档名
  • 文档 ID
  • 分数

片段本身还做了截断。

这样一来,Python 看到的是几段已经被压扁的证据:

  • 缺章节结构
  • 缺相邻段上下文
  • 缺图片链接
  • 缺更完整的原始引用

模型在这种上下文里通常会做一件很自然的事:概括。

概括不是错,但定义题、图片题、引用题最怕只剩概括。Dify 那边之所以更像“真的懂你在问什么”,一个很重要的原因是它给回答模型的上下文通常更完整,不只是几个短片段。

5. Dify 的回答 prompt 约束更明确

这次对比里还有一层很容易被忽略:Dify 原生助手的 prompt 其实已经替这条链路做了不少约束。

比如:

  • 严格根据知识库回答
  • 有图片链接就返回图片链接
  • 用户问题模糊时要求继续澄清
  • 不把知识库内容误当成用户输入

这类约束会直接影响最后的回答组织方式。

我们自己的 Python 链路之前更像是一个通用回答器,检索结果拿到了就让模型去写。这样在开放问答上问题不大,但在“定义题要像教材”“图片题要保留链接”“模糊题要先追问”这些场景上,就会慢慢输给 Dify 原生。

为什么接上双知识库后,还是只能算“好了一点”

因为补双知识库只修了一层:召回覆盖面。

它没有修下面这些问题:

  • 没有全局 rerank
  • 没有关键词通道
  • 没有更宽的候选池
  • 没有保留完整证据和图片
  • 没有把回答 prompt 收紧到 grounded QA 模式

所以效果变好是正常的,差距还在也正常。

从工程上看,这个阶段继续在 Java 上补洞,性价比已经不高了。因为接下来的每一项优化都越来越像 AI 编排层的职责,而不是业务网关的职责。

后面怎么优化

方向一:继续留在 Java,但把整条链路补全

这条路不是不能走,但会越来越像在 Java 里重搭一套小型 RAG 编排器。

至少要补这些东西:

  • 多路召回,而不是只靠一条语义检索
  • 跨知识库统一 rerank
  • 更大的候选池
  • 更完整的 chunk 和引用信息
  • 图片链接透传
  • 针对定义题、比较题、模糊题的回答模板

做到这一步以后,Java 这边不只是工具网关了,已经开始承担 AI orchestration 的活。复杂度会往 Java 里长,而且调参、排障、灰度都不如 Python 顺手。

方向二:把知识库检索收回 Python

这次讨论下来,我更倾向这个方向。

原因很直接:真正需要频繁调的部分,都在 Python 这边更自然。

包括:

  • dataset 选择策略
  • query rewrite
  • 多知识库召回
  • DashScope rerank
  • prompt 收敛
  • grounded answer 生成
  • 图片和 citation 处理

Java 保留它本来就更适合做的事:

  • 鉴权
  • 审计
  • 用户和租户上下文
  • SSE / gateway
  • MCP tools 的 runtime scope

这样职责会更清楚,后面要把效果往 Dify 靠,也会顺很多。

我现在更倾向的落地方案

我会把这次优化拆成两步。

第一步:先把检索编排搬到 Python

目标不是一步到位重做全部能力,而是先把最影响效果的环节统一起来:

  • Python 直接接知识库
  • Python 控制多 dataset 检索
  • Python 调 DashScope rerank
  • Python 决定最终喂给模型哪些 chunk

这样做以后,至少链路从“Java retrieve + Python 总结”变成“Python 检索编排 + Python 回答生成”,问题会收敛很多。

第二步:再把回答策略做细

等检索编排收口以后,再按问题类型去补策略:

  • 定义题:优先定义句,再补原理和优点
  • 模糊题:先澄清,不急着答
  • 图片题:保留并原样返回图片链接
  • 对比题:明确比较维度和时间范围

这一步如果还分散在 Java 和 Python 两边做,调一次要来回改两层,维护成本很快就上去。

这次复盘里最重要的判断

这次不是 Dify 模型比我们强,而是 Dify 把“检索、排序、提示、回答”这条链路做成了一个完整产品。

我们之前那条链路能用,但结构上更像把几段检索文本递给模型,让模型自己想办法组织答案。它能跑,也能答,但在定义题、图片题、教材型问答这些场景里,天然就会弱一点。

所以后面的重点不该再停留在“再加一个 dataset”“再把 top-k 调大一点”,而是把检索编排层收回到一个地方,按完整 RAG 链路来调。

按现在这个项目的边界,那个地方更应该是 Python,不是 Java。


为什么我们的 RAG 还打不过 Dify 原生助手
https://willfordzhan.github.io/2026/03/15/rag-vs-dify-native-assistant/
作者
詹文杰
发布于
2026年3月15日
许可协议