ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG系统真正拉开差距的六大分水岭

RAG系统真正拉开差距的六大分水岭

1. 流水线烂大街,到底烂在哪儿

1.1 每个 Demo 都长一个样

这两年如果你说自己做 RAG,大概率听到的反应是“哦,又是个 RAG”。我算了下,光是我进过的大小群里,至少有十个朋友发过“基于 LangChain 的客服知识库”Demo,八个人用的是同一张架构图:加载文档、切分、Embedding、写进向量库、查询时做相似度检索、拼到 Prompt 里让大模型回答。你说它错吗?完全没错。但你说它能解决实际问题吗?大多数情况,不能。

这话说出来大概率要得罪一批刚入门的朋友,但数据摆在那里。很多人把 RAG 项目上线之后,最大的反馈是“召回的东西不相关”“答案看着挺顺溜,但细节全是编的”“换个问法就崩了”。这不是模型出了问题,而是流水线本身只能保证“跑通”,并不保证“跑对”。真正让一个 RAG 系统从玩具变成生产工具的,从来不是那条谁都能搭的流水线,而是藏在流水线背后的几个关键决策。

1.2 框架让我变强,也让我变懒

我并不是反对框架,我自己也在用 LangChain、Dify 这些东西,它们确实把大量重复工作变成了配置项。但框架最大的副作用,是让人误以为“配置完就是完成”。框架像是一套预制菜,你按说明书加热三分钟,端出来的确实是一盘能吃的菜,但离“私房菜”还有很大距离。大多数 RAG 翻车案例,翻的不是流水线本身,而是流水线前面的知识体系设计、中间的检索策略、后面的评估闭环。

我记得有个朋友用 Dify 拉了一条知识库流水线,接了云端的 Embedding 模型,测试时问“公司年假怎么休”,答案基本能对上,于是他就上线了。结果业务方问的是“去年十二月入职的员工,今年二月离职,年假怎么折算”——这种牵扯时间、人员状态、制度变更的组合问题,直接把流水线给绕晕了。他后来跑来找我排查,我才发现他对知识库里的文档连版本日期字段都没打,季度更新后的制度老版本还在索引里,检索结果新旧混杂,模型完全是“看心情”回答。

所以我想聊的真正主题是:RAG 烂大街不假,但烂大街的只是那条“读进来、切一切、存进去、搜出来、拼起来”的流水线。真正拉开差距的,是六个分水岭:知识表示、查询理解与路由、索引设计、检索与重排、评估闭环、生成与答案治理。下面我一个个拆开讲,这些都是我在实际项目里踩坑踩出来的,希望对正在搭 RAG 的同学有参考价值。

2. 分水岭一:知识表示,文本切片和知识图谱是两个物种

2.1 纯文本切片的边界

大部分 RAG 教程的第一步是“把 PDF 切了”,但很少有人问你:你的知识到底是什么形态?如果是一堆手册、公告、FAQ,纯文本切片没问题。但你的知识一旦涉及实体之间的依赖关系,比如产品参数与维修流程的关联、制度条款之间的引用,纯文本的 embedding 就会变得非常勉强。

我在做一个设备维修知识库时,把 200 多页的手册切成 500 字的块,Embedding 之后检索效果还行。但用户问“这台设备报警 E20,需要什么工具和零件?”,系统召回的是“E20 报警描述”那一块,却漏了“维修步骤详细清单”和“备件清单”。原因是这几个信息散落在不同章节,向量上离得很远。文本切片本身没有错,错误的是把文本切片当成了唯一的知识表示。你想想,一篇手册里有几十个报警代码,每个代码对应原因、工具、步骤、备件,这些关系其实是一张网,硬切成一段段文字后,网就断了。

2.2 何时上图谱、本体和结构化知识

后来我们给这个维修库加了一层图谱,把报警代码、故障原因、工具、备件、工序节点拆成实体和关系,用轻量本体约束类型。比如“E20 使用部件为 S 型密封圈”“E20 对应维修工序 M3”“M3 需要扭矩扳手”,这些三元组存进图数据库。查询时,先用 LLM 把用户的自然语言解析成结构化路径,再去图数据库里把路径取出来,最后把取到的结构信息喂给生成模块。同样的查询,召回质量直接上了一个台阶,因为模型拿到的不再是几段可能相关的话,而是一条完整的“报警 -> 工序 -> 备件”证据链。

所以说,知识表示是第一个分水岭。它要求你先判断领域里是“文”重要还是“关系”重要。比如法规制度类,条款之间的“引用”“修订”“废止”关系比原文更重要;电商客服类,商品属性、库存状态、售后策略之间的联动关系比商品文案重要。这时候只靠向量库是不够的,就应该考虑引入图数据库,或者至少做一个知识图谱映射,这也是 ontology RAG 的核心思路。关于 RAG 知识库和结构化知识库的区分,我常用的判断标准很简单:如果答案依赖多个分散文档的数据推理,就得往结构化方向靠;如果答案基本能在某一段原文里找到,纯文本切片就够。

2.3 关键实操:混合知识表示怎么落地

在真实项目里,很少有人一上来就建全量图谱,太重了。我更推荐的做法是两段式:先用文本流水线保证覆盖度,然后把高频查询里暴露出的“跨段落依赖”挑出来,针对这些依赖建小图谱。这么做成本低很多,见效也快。一个经验参考:故障类、法律类、医疗类知识库,图谱的投入回报率远高于纯文本优化;常识型、资料型、百科型知识库,做好切片和重排就行。

这里能看出 RAG 和知识图谱不是对立关系,而是互补。GraphRAG 或 Ontology RAG 本质都不是替代向量检索,而是给生成模型提供更准确的“骨架”。我在工程上习惯把结果分为三类:需要文本证据的走向量召回,需要关系推理的走图查询,两者都需要走混合——先图后文或者先文后图都可以,具体取决于查询路由的判断,这个在分水岭二展开聊。

3. 分水岭二:查询理解与路由,别再“一段话打天下”

3.1 简单查询 vs 复杂查询

几乎所有流水线示例都是同一个动作:拿到 query,算出向量,去向量库里做 topk。这种做法对付“年假有几天”没问题,但对付“去年十二月入职、今年二月离职的员工,年假怎么折算”就开始吃力了。问题不在检索,而在查询本身需要拆分:它包含实体(员工、时间、制度)、约束(入职时间、离职时间),还隐含一个计算规则。你直接拿整句话去向量化,必然把语义摊平成一片模糊,最后检索出来的全是“年假”“入职”“离职”这几个词分别出现的文章片段。

所以第二个分水岭,是你有没有认真对待“查询”这个环节。我的经验是:把用户输入当作一等公民处理,先判断是简单事实型、多跳推理型、计算型还是闲聊型,再决定走哪条路。简单事实型可以直接向量检索;多跳推理型要拆解成子查询;计算型要到结构化表格里取数,不能到文档里硬找;闲聊型最好直接走模型对话,别进知识库,免得召回一堆无关片段把答案带偏。分类这件事不需要特别大的模型,几个典型话术模板加一个小分类器就能跑起来。

3.2 查询改写与路由策略

具体做法也不复杂。可以用一个小模型做意图分类,分到三五个固定路由;也可以让主 LLM 先做一次查询结构化,输出一个 JSON,包含意图、实体、时间约束、需要的字段。比如前面那个年假问题,结构化之后可能是:查询年假折算规则、适用入职时间为去年十二月、离职时间为今年二月。基于这个结构化结果,我们再决定是去原文搜索、去图数据库找规则,还是去人资系统接口查数。

这里有一个很容易踩的坑:查询改写不是越复杂越好。我见过有人每次查询都让 LLM 扩写成五个查询再合并结果,结果延迟从 1 秒涨到 6 秒,召回反而因为噪音更多而变差。对于大部分场景,一个精确的意图分类加一个必要时的子查询拆分,足够了。路由的目的是“用最少步骤拿到最准的上下文”,不是秀模型能力。做路由之后,还有个附带好处:你可以针对不同路由单独调优各自的检索策略和阈值,而不必用一个万能参数死磕所有查询。

3.3 实操:一个多路召回的路由设计

我举一个简化的路由设计,这种结构我在几个项目里都验证过。用户输入先交给意图分类器,得到路由标签,再按标签进对应索引:

  • fact:直接查向量库,取 top5 文档块;
  • graph:先解析实体关系,图查询取结果,再关联文本证据;
  • table:查结构化数据库或表格,取数值之后生成;
  • chat:不进知识库,直接走对话模型;
  • mixed:先查意图涉及的多个索引,再用重排环节统一排序。

这个设计看起来简单,但解决了一个大问题:它让你不再用一个检索器去面对所有问法。多路召回之后,哪怕每个单路的精度不高,最终候选集里也大概率包含正确答案,剩下的交给重排。路由本身可以是极小的模型,跑在本地,不影响主链路延迟。路由的准确率也不需要 100%,关键是错误路由要能兜底——比如把 fact 误判成 graph,顶多是多查一步;把 graph 误判成 fact,可能就查不到答案,所以我会在 fact 路由里也带一个“是否需要额外图查询”的二次判断开关。

4. 分水岭三:索引设计,决定检索天花板的细节

4.1 块的大小、父子块、元数据

说完查询,再往底层走一步:索引。很多人觉得索引就是“切块存向量”,但切块是个很讲究的活。大小切多少,没有标准答案,但有几个规律。小 chunk(比如 256 字)召回粒度细,但上下文缺失;大 chunk(比如 1000 字以上)上下文全,但噪声也大。我的习惯是:FAQ 类小 chunk,手册类中 chunk,制度类“父子块”结合——父块存整章、子块存整段,检索命中子块后,把父块内容一起送进模型,这样既保证命中精度,又保证上下文完整。这个技巧在 LangChain 里叫 parent document retriever,很多框架都有现成实现,新手容易忽略。

元数据是另一个被低估的东西。给每个 chunk 打上来源、章节、标题、日期、实体标签之后,可以做过滤式检索。比如“只查去年的制度”这个需求,如果没元数据,纯靠语义检索经常搜出老版本;加了日期元数据并按时间过滤之后,召回质量稳定很多。真实场景里,“版本”“状态”“适用范围”这几个字段,比嵌入向量还重要。一旦知识库维护过三个月,你就会明白,版本混乱才是让 RAG 答案失真的大杀器。

4.2 多路索引与时间维度

有时候一份知识库里既有长期稳定的制度,又有频繁变动的公告,放在同一个向量空间里互相干扰。我的做法是分索引:稳定区一个集合,动态区一个集合,查询的时候按路由决定查哪边或都查。多路索引的引入也给时间维度留了接口:文档更新时间、生效时间都可以作为排序权重。这在法规库和排班类知识库里特别管用。

多路索引另一个好处是方便做权限隔离。比如企业内部知识库,有的部门文档只能被对应部门的人检索。如果你把所有内容塞进一个向量集合,就无法在召回阶段做权限过滤。分索引之后,按用户角色路由到对应索引,权限天然隔离。这个点很多教程不讲,但真实企业场景几乎是刚需。

4.3 实操示例:图书知识库索引

举一个图书类知识库的例子。如果直接整本切,切出来的块经常横跨两个主题,搜索“主角的性格转变”会召回各种支离破碎的段落。我后来给小说库设计了三级结构:章节为父块、场景为子块、每个场景带人名和关系标签,再把全书的人物关系单独抽成一张小图谱。这样问“某角色在第几章第一次见到另一个角色”,可以走图查询定位章节,再去父块里取全文上下文。效果比纯文本切片好非常多,原因只有一个:索引的结构跟用户真正想问的问题对齐了。

如果你用的是现成框架,比如 Dify 或 FastGPT,也一定要研究它们的索引配置,而不只是默认切分。很多框架支持自定义元数据注入、父子块模式,这些能力用起来之后,知识库的检索上限会明显提高。索引这件事,表面上是在处理文本,实际上是在为你的知识体系建模。多花半小时设计索引结构,可以省下后面几周调参的时间。

5. 分水岭四:检索与重排,TopK 不等于质量

5.1 为什么 top-k 会骗你

向量检索的 TopK 有个很经典的毛病:它善于找“像”,不善于找“对”。用户问“设备停机怎么恢复”,召回的可能是“停机的原因分类”“停机记录表填写说明”,因为它们在语义空间里离得很近,但真正有用的“恢复操作步骤”反而排在后面。如果你直接拿 top3 去生成,答案大概率是正确但无用的废话。

另一个常见坑是“局部相似”造成的重复。一个 chunk 里只有一句话相关,其余全是背景描述,但向量分很高。于是你取了 5 个 chunk,真正有用的信息可能只有 5 句话。此时生成模型很容易被不相关的内容带偏,甚至出现把 A 文档的信息安到 B 实体头上的幻觉。所以,检索只是获得了“一组可疑的相关内容”,真正的排序必须靠重排。

5.2 混合检索与重排序器

我的标准做法是混合检索:BM25 负责精确词匹配,向量检索负责语义召回,两者各自取回一批候选,再用交叉编码器做重排。BM25 抓得住“E20”这种编号和专有名词,向量抓得住“报警之后机器不转了”这种说法,合并之后候选集质量会提高很多。重排阶段,现在常用的 bge-reranker 或者相关榜单上的 reranker 模型,会把 query 和 chunk 拼接成一对去打分,比向量相似度更接近“实质相关性”。

加入重排之后一定要控制输入给 LLM 的 token 量。重排只负责排序,不负责去重。如果两个 chunk 高度重叠,重排器可能给出相近的高分,最终上下文全是重复信息。我会在重排后加一次简单的去重和压缩,把重复段落过滤掉,必要时只保留每个高分 chunk 里最相关的那段。这一步做得好,生成效果提升非常明显。

5.3 实操示例:BM25 + 向量 + 重排

我用一个简化伪代码展示链路,方便你一比一复现思路:

def search(query, top_k=5): bm25_hits = bm25.search(query, top_k=10) vector_hits = vector_store.search(embed(query), top_k=10) candidates = merge(bm25_hits, vector_hits, dedup=True) pairs = [[query, chunk] for chunk in candidates] scores = reranker.compute_score(pairs) ranked = sort(candidates, by=scores, reverse=True)[:top_k] context = compress(ranked) # 去掉重复与无关片段 return context

实测下来,这套链路比单独用向量检索的召回命中率一般能提升两到三成。代价是会增加几百毫秒延迟。如果对速度敏感,可以用一个轻量分类器先判断是否需要重排:简单查询跳过重排,复杂查询才走完整链路。这又是一个“按需付出成本”的工程取舍,而不是一刀切地追求最高精度。

6. 分水岭五:评估闭环,不能只靠“感觉”

6.1 离线评估指标

很多人做完 RAG 之后,评价标准是“看起来还行”,这就埋了大雷。用户问十句,有一两句回答得不对,他可能不会告诉你,但下次就不用了。所以第五个分水岭是评估闭环——你有没有一套能持续告诉你“这版比上一版强”的机制。

离线评估我会看三类指标。一是检索质量,最常用的是命中率,即正确答案是否出现在召回的 top5 里,还有 MRR。二是生成质量,主要看忠实性,也就是答案内容是否都有证据支撑,以及完整性,该覆盖的点有没有覆盖。三是端到端可用率,把答案交给模拟用户或人工打标,按“完全正确、部分正确、错误”三等分。这几个指标互相补充,单纯看一个会骗人。

6.2 端到端评估与线上反馈

离线评估的局限是数据太少,覆盖不了真实用户千奇百怪的问法。所以线上反馈必须接进来。最简单的做法是在答案底部加一个“有用/没用”按钮,或者记录用户是否继续追问。别小看这个粗糙的信号,它能在两周内帮你发现检索器是不是对某些高频问题系统性地失效。项目上线后,我一般会单独维护一个“坏案例库”:凡是用户点没用的、答错的、怼回来的,都结构化记下来,然后定期用这些坏案例重跑评估集。

线上反馈光记录还不够,要形成迭代动作。我习惯每两周做一次复盘:把坏案例库里的问题重新跑一遍当前系统,统计有多少被修复、有多少还是坏的,坏的集中在哪个环节。这样你就能很清楚地知道,下一步是调整 embedding、重排逻辑,还是清洗知识文档。RAG 的优化本质上就是一个持续反馈闭环,谁先建立这个闭环,谁就能更快地把系统修到可用状态。

6.3 实操:一套最小可用的评测集

最小可用的评测集不用太多,50 到 100 条就可以起步。每条包含四列:标准问题、查询类型、期望证据位置、参考答案。我把模板放在下面的表格里,你可以直接抄。

问题路由类型期望召回内容参考答案要点
去年12月入职、今年2月离职,年假怎么算graph/table离职年假折算规则、入职时间字段按当年剩余日历天数折算
E20 报警需要什么备件graph报警对应工序的备件清单需要 XX 型号滤芯及密封圈
公司附近的餐饮优惠chat不召回,直接对话无

这个表看起来简单,但价值极高。每次调完切块参数、换 embedding、改路由规则,都跑一遍,对比“这版对几个、错几个”。十次迭代下来,你会有一种“捏着数据做优化”的安全感,而不是靠玄学调参。评估集建好之后,建议存成 JSON 或 CSV,纳入版本管理,每次改动系统都留一个评估记录,方便回溯到底哪次改动提升了效果。

7. 分水岭六:生成与答案治理,防止一本正经胡说八道

7.1 Prompt 与上下文压缩

最后一道分水岭在生成端。哪怕前面检索做得很好,如果 Prompt 不会调,答案照样翻车。首先,上下文不是越多越好。我见过有人把 10 个 chunk 全塞进去,结果模型在无关信息里挑挑拣拣,挑出个片面的答案。正确思路是先压缩再生成:只保留与 query 明显相关的句子,去掉背景、重复、无关实体。你也可以在 Prompt 里明确告诉模型“输出只能依据以下材料,如果材料不包含答案,直接说无法回答”。

Prompt 里放哪些内容也很关键。我的固定套路是:先放任务定义,再放检索到的证据,每段证据标注来源章节,最后放用户问题。顺序错了效果会差很多,因为模型对靠后的指令更敏感,把用户问题放最后更能聚焦答案方向。这里有个小技巧:在证据段落前加上“以下是知识库检出的参考片段,它们可能相关也可能无关,请自行判断”,这句话看起来多余,但能给模型一个“批判性使用材料”的暗示,减少机械照搬导致的错误。

7.2 引用、校验与幻觉兜底

答案治理还包括引用溯源。生产环境里,我强烈建议答案里的每个关键结论带上来源编号。这样用户能看到“这条依据来自《设备手册》第三章第 2 节”,一旦发现错误,也方便排查是证据问题还是模型理解问题。引用不只是用户体验,更是一条调试链路。

还有一个容易被忽略的操作:设置“拒答”路径。很多 RAG 系统幻觉高发,是因为模型在自己都不知道的情况下生成了答案。给 Prompt 加一条“材料不足时,请回答‘知识库中未找到相关信息’”,并将召回分数低于阈值的情况单独拦截,直接走兜底话术,能显著降低一本正经胡说八道的概率。我把它称为答案治理的守门员。别看这个设定简单,它能让错误从“编造”变成“坦白”,用户体验完全不同。

8. 避坑实录:从 Mac 搭建到图片需求那些事

8.1 Mac 本地搭建 RAG 的关键

很多人问我怎么在 Mac 上搭 RAG 知识库,特别是没有 GPU 的情况下。其实现在本地搭一套最小系统不难:用 Ollama 跑 embedding 和 chat 模型,用 Chroma 或 Qdrant 做向量库,用 LangChain 或 Dify 串流程。M 系列 Mac 跑 7B、8B 参数量级的模型完全没问题,速度虽然比云端慢,但做实验足够。

我踩过最大的坑是 embedding 模型和中文字符。早期用某些英文优化模型处理中文,召回效果惨不忍睹。后来换成了对中文友好的 embedding 模型,并统一在入库前做文本清洗和同义归一,效果立竿见影。Mac 上另一个问题是内存占用量,切块大的时候嵌入一批长文档很容易把内存吃满。我的习惯是先小批量入库,观察内存水位,再逐步放大 batch。如果内存实在吃紧,可以把 embedding 模型换小一点,或者用 API 方式做向量化,本地只负责存储和检索。

8.2 知识库能不能存图片?我试过的结果

最近总有人问 RAG 知识库能不能存储图片。我直接说结论:传统 RAG 流水线不能直接把图片塞进文本向量库,但可以通过两个方向实现图片能力。一是提取图片里的文字,用 OCR 转成文本再进入知识库,适合截图、扫描件、表格图片。二是为图片单独建立视觉向量,检索图片本身而不是文本,再配合多模态模型生成答案,适合设计图、产品图这种“看图说话”的场景。所以答案是“能,但不是把图片和文本混在一个 chunk 里,而是分索引、分流处理”。

我在一个售后知识库里处理过大量设备照片和解说截图,做法是 OCR 文本进一个向量索引,图片原图单独存对象存储并保留链接,生成答案时带上“相关图片链接”字段。用户体验很好,也避免了多模态模型对每张图都读一遍的高延迟。如果你要处理的是图文混排的 PDF,建议先按页面把图文关系保留,再决定哪些内容走 OCR 文本、哪些图片走视觉模型,这个设计比“整页塞进模型”可控得多。

8.3 常见问题速查表

我把几个真实项目里反复被问到的问题整理一下,给后面的人少踩几个坑。

现象可能原因处理建议
召回文件都对,但答案不相关生成的上下文里混入了无关 chunk做重排+上下文压缩,减少喂给模型的噪声
换一种问法就查不到依赖单一向量检索加入 BM25+向量混合召回
制度版本总是回答老版缺少时间元数据过滤给 chunk 打生效日期,检索时按版本过滤
答案给得很完整但全在编没有做引用与拒答约束加事实校验、来源标注与“无法回答”兜底
本地 Mac 跑得慢模型大或 batch 太大换小参数模型,或在入库时用分批并限制并发
用户问“图片里写了什么”答不出没有 OCR 或视觉索引单独接 OCR 管道或按需调用多模态模型

这张表不是万能药,但它对应的每一个问题,我都亲眼见过有人在生产环境里踩过。RAG 的水很深,但只要你顺着“知识表示、查询理解、索引设计、检索重排、评估闭环、答案治理”这六个分水岭逐项检查,就大概率能定位到问题出在哪个环节。

我在做第一个 RAG 项目的时候,也天真地以为“搭好流水线就算完事”,后来被用户一连串“这还是不对”的反馈,硬生生逼着把六个环节逐个啃了一遍。现在回头看,那些能在生产环境里稳定输出的 RAG 系统,没有一个是在流水线层面胜出的,全部赢在流水线之外的各种脏活累活上。前面这套经验如果你能照着梳理一遍,至少可以少走几个月弯路。最后再提醒一句:无论用什么框架起步,一定要把“好问题、好证据、好评估”这三件事想清楚,流水线反而最不需要担心。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进