为什么我们的 RAG 还打不过 Dify 原生助手
最近在做一条自建 RAG 链路:Java 负责知识库工具调用,Python 负责编排和回答生成。链路能跑,双知识库也已经接上了,但同一个问题丢给 Dify 原生聊天助手,回答还是更稳,引用也更像“已经把资料看明白了再说话”。
这个差距一开始看着像模型问题,后来排下来,主要不是模型本身,而是检索编排层差得比较多。Dify 用的是一条完整的问答链路,我们现在更像是“从知识库捞几段文本出来,再让模型自己总结”。
这篇把这次对比里真正拉开差距的地方写清楚,也顺手把后面的优化方向收口一下。
先看现象
拿一个很典型的定义题来说:
1 | |
Dify 原生助手给出的回答有几个明显特征:
- 开头先给一句标准定义
- 后面再补工作原理、优点和适用场景
- 能引用教材类资料,也能带图片
- 整体像是在回答“这是什么”,而不是在复述一段检索结果
我们这边在接上双知识库之后,回答质量确实上来了一点,但整体风格还是更像论文摘要:
- 会提到 907A 模铸、球墨铸铁研磨盘之类的应用案例
- 会解释补缩、放热、延缓凝固这些机理
- 但定义句通常不够靠前
- 结构更像“根据检索结果总结”,而不是“先把问题答准”
这说明两个问题已经分开了:
- 双知识库确实能补覆盖面。
- 覆盖面补上以后,链路本身的检索和组织方式还是不如 Dify 原生。
第一个误判:以为差距主要在知识库数量
最开始 Java 工具只查了一个 dataset,结果里甚至能直接看到只有一个 dataset_id。把第二个知识库接进去以后,召回确实更好了,说明这个方向没错。
但这件事只能解决“有没有看到那份资料”,解决不了“看到了以后怎么排、怎么喂给模型、怎么让模型按正确的问法回答”。
也就是说,双知识库更像补地基,不是把 Dify 效果复刻出来的那一步。
真正的差距在检索编排层
1. Dify 是多路召回 + 全局 rerank,我们是分库检索后本地拼
Dify 的知识库问答默认不是只跑一条检索通道。按实际配置看,它会做多路召回,然后再统一 rerank:
- 语义召回
- 关键词召回
- 多知识库一起取候选
- 用 rerank 模型统一重排
- 最后再把候选喂给回答模型
我们这边当时的做法更接近:
- Java 针对每个 dataset 调一次 retrieve
- 每个库先拿各自的 top-k
- 把结果按返回分数拼在一起
- 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。