从开发者视角聊聊 RAG 技术的演进路线。这个系列的上篇已经聊过 RAG(检索增强生成)的基础原理和朴素架构,这篇“中”篇主要聚焦在它从“能用”到“好用”的进化过程。如果你正在做 RAG 应用开发,或者准备给自己的业务接入大模型知识库,这篇文章应该能帮你理清技术选型的思路,搞清楚那些花里胡哨的优化手段到底在解决什么问题。
先说一个基本判断:RAG 现在的形态已经不是最初那个“检索 + 拼接 + 生成”的三板斧了。它分化出了很多变体,有侧重检索质量的,有侧重生成策略的,还有侧重流程编排的。很多人看到一堆术语——HyDE、RAG-Fusion、Self-RAG、CRAG——就头大,其实它们都有明确的进化动因。
1. 演进主线:从朴素管线到模块化编排
1.1 朴素 RAG 的经典三段式
要说清演进,得先回顾起点。朴素 RAG 的结构非常简单,就是索引(Indexing)、检索(Retrieval)、生成(Generation)三个环节串联。
- 索引阶段:把文档切块(chunking),用 embedding 模型转成向量,写入向量数据库。
- 检索阶段:把用户 query 转成向量,在向量库里做相似度检索,取 top-k 个相关块。
- 生成阶段:把检索到的上下文块 + 原始 query 拼进 prompt,交给 LLM 生成答案。
这套管线的优点是实现快、思路直白。我当时第一次跑通的时候,也觉得“这不就是 openAI 接口包了个壳嘛”。但真正放到业务场景里,问题很快就暴露出来了。
1.2 朴素 RAG 的三个痛点
第一个痛点是检索精度不够。向量相似度并不总是等于语义相关性,尤其在处理专有名词、代码片段、缩写这类文本时,embedding 模型经常“看不懂”。结果是检索回来的 top-k 块里混着不少噪声,甚至核心信息根本没进到 top-k 里。
第二个痛点是上下文利用率低。LLM 的输入长度虽然不断在扩展,但把大量检索块全部塞进去,既浪费 token 成本,也会稀释关键信息。我见过一个案例,有人给模型塞了十几个块,答案反而被无关上下文带偏了。
第三个痛点是管线固定,缺乏自适应能力。不管用户问什么,流程都是“检索-拼接-生成”。但实际场景里,有的 query 需要多跳推理,有的 query 根本不需要检索(比如闲聊),有的 query 需要从多个异构数据源汇总。一个固定管线没法处理这些复杂情况。
1.3 三个演进方向:高级 RAG、模块化 RAG、新范式
针对这些痛点,业界逐渐分化出了几个演进方向。
- 高级 RAG(Advanced RAG):仍然保留三段式结构,但在每个环节内部做精细化优化,比如更聪明的分块策略、混合检索、重排序(rerank)、查询改写等。这是目前工程落地的主流形态。
- 模块化 RAG(Modular RAG):把原来固定管线中的每个环节抽象成独立模块,模块之间可以灵活编排、替换、增删,甚至引入搜索模块、记忆模块、路由模块。它追求的是流程的灵活性。
- 新范式(如 Self-RAG、CRAG 等):试图让系统具备自我反思和自我纠正能力。比如检索结果不可靠时怎么办,答案生成后如何做质量自检。这属于比较前沿的探索方向。
用一张表来对比这三者会更直观:
| 演进阶段 | 核心特征 | 典型手段 | 解决的核心问题 |
|---|---|---|---|
| 朴素 RAG | 固定三段式管线 | 向量检索 + prompt 拼接 | 从无到有,让 LLM 能访问外部知识 |
| 高级 RAG | 环节内精细化 | 分块调优、混合检索、重排序 | 从有到准,提升检索质量和答案准确率 |
| 模块化 RAG | 流程可编排 | 路由、多源检索、推理模块 | 从准到活,适配复杂业务场景 |
这个系列既然是“中”,重点自然放在高级 RAG 的工程落地上,因为它离实际项目最近,收益也最直接。
2. 索引优化:不是在切文档,是在设计知识结构
索引阶段看似最简单,其实是最容易出问题也最值得深挖的环节。
2.1 分块策略——切法不同,效果天差地别
分块(chunking)的核心矛盾在于:块太大,检索出来的内容噪声多、token 浪费大;块太小,语义信息不完整,embedding 难以捕捉完整表达。业界常用的分块策略大概分这么几类:
- 固定长度分块:按字符数或 token 数硬切,比如每 500 字符一块,相邻块重叠 50 字符。优点是零实现成本,缺点是容易切断句子或语义单元。
- 句级/段落级分块:按自然边界(句号、换行、markdown 标题)切分。实现也不难,效果会比固定长度好不少,特别是在文档结构良好(比如技术文档、论文)时。
- 内容感知分块:结合文档结构信息(标题层级、表格、列表)来切。这种方法效果最好,但需要对文档格式做解析,通常引入布局识别模型。对于 Markdown 或 HTML 类文档,可以用专门的解析器做结构切块。
我自己的实践经验是,先看看你的语料长什么样。如果是结构化程度高的 Markdown 或 HTML,优先做结构感知切块;如果是纯文本,句级切块基本就够了。固定长度切块不是不能用,但一定要配合重叠,否则一个完整知识点被拦腰切断后,检索效果会非常随机。
2.2 元数据与文档结构——被低估的检索加成
给每个分块挂上必要的元数据(标题、章节路径、文件来源、更新时间),能显著优化后续的检索逻辑。
- 元数据可以用于过滤。比如用户指定“只看 2024 年的政策文件”,就能直接在查询时附带元数据过滤条件,减少向量检索的候选范围。
- 章节路径信息可以用于上下文重建。检索到的某个块如果过于碎片化,可以顺着章节路径把前文或后文一并带上,补全上下文再送进 LLM。
- 来源信息可以用于引用溯源,这对企业知识库场景几乎是硬性要求。
我建议在搭建索引时,至少为每个 chunk 保存:块内文本、对应文档 ID、章节路径、摘要级别的元数据(比如文档标题、发布时间、作者)。把这些信息存进数据库,后面做过滤、加权、溯源都会方便很多。
另外,还有一个经常被忽略的做法是多粒度索引。同一个文档,可以在段落级建一层精细索引(用于精确检索),再在更高层(小节或全文)建一层粗粒度索引(用于上下文补全)。检索时先用细粒度索引定位,再到粗粒度索引取完整上下文。
2.3 Embedding 模型的选型思路
Embedding 模型这块,网上讨论很多,我这里只说几条被踩坑踩出来的经验:
- 先看领域匹配度。通用 embedding 模型在垂直领域(医疗、法律、代码)的表现可能远不如领域微调过的模型。有条件的话,用自己领域的数据对 embedding 模型做一遍评测再决定用哪个。
- 中文场景要特别注意分词粒度。部分模型对中文长文本的支持不好,embedding 出来的向量整体质量偏差。可以在自己的语料上抽一个小的评测集,人工检查 top-k 检索结果的相关性。
- 别盲目追新。新版 embedding 模型在 MTEB 或者 C-MTEB 上的分数提升,不代表你在自己场景下一定能拿到同样的收益。做一次端到端的离线评测比什么都重要。
3. 检索优化:让“找到”变成“找对”
检索环节是高级 RAG 改动最大、收益也最明显的地方。
3.1 混合检索——向量不是万能的
纯向量检索有个天然的盲区:对精确匹配(如产品型号、人名、代码变量、身份证号)不敏感。两个字符串完全一致,但语义上可能没有任何相似度。比如用户搜“RS-485 接线图”,向量检索可能把“RS-422 接线说明”也捞出来,因为语义相近。但对用户来说,他要的就是字面完全匹配的内容。
混合检索的思路很朴素:把 BM25 这类稀疏检索和向量检索(稠密检索)结合起来。BM25 擅长抓精确词项匹配,向量检索擅长抓语义相关,两者互补。
工程实现上一般有两种思路:
- 分数融合。两个检索器分别返回 top-k 结果,用 RRF(Reciprocal Rank Fusion)或加权分数融合算法合并排序。RRF 公式很简单:对每个文档,把它在不同检索结果中排名的倒数相加,最终求和排序。这个方案实现成本低,效果也比较稳。
- 多路召回 + 重排序。先让 BM25 和向量检索各自召回一批候选(可以多召回一些,比如每路 top-50),然后统一交给一个重排序模型做精细化的相关性打分,输出最终的 top-k。
从我实测的效果看,混合检索至少能在大多数业务场景里把“找不到”的问题下降一个量级。特别是用户习惯用专有名词提问的时候,BM25 那一路往往能救命。
3.2 重排序——精度提升最直接的模块
RAG 流程里,重排序(rerank)几乎是性价比最高的一项优化。思路是:向量检索和 BM25 召回的都是“可能相关”的候选集,精度不够;重排序模型会逐一计算 query 和每个候选块的相关性打分,从几十个候选中挑出真正的 top-k。
常见方案有两类:
- Cross-encoder 模型。这类模型把 query 和 doc 拼接成一个序列,输入模型计算相关性。效果最好,但速度慢,不适合对大候选集逐条重排。
- 各种 rerank API 或开源 rerank 模型。性能与延迟的权衡各有不同,可以根据业务体量选。
实操上建议把重排序放在最后一步,只对混合检索召回后的候选做重排,因为这样一来计算量可控。候选集控制在 20-50 条,重排后取 5-10 条进 prompt,准确率和延迟都能兼顾。
有一个细节值得说:重排序的分数并不是一个绝对意义的相关性概率,它只适合做排序。别试图给重排分数设绝对阈值,而是用相对排序来决定取哪些块进上下文。
3.3 查询改写与扩展——用户没说清楚的话
用户的问题往往是简短的、模糊的、包含指代词(这个、那个、它)的。直接把这类 query 拿去检索,结果自然不稳定。查询改写(Query Rewriting)就是在这里介入:
- 多查询扩展(Multi-Query)。把用户的一个查询通过 LLM 扩写成几个不同角度、不同措辞的查询,分别检索,合并结果。适合用户问题信息量足但表述不精准的场景。
- 子问题分解(Sub-Query)。针对复杂问题,拆成多个子问题分别检索,再汇总答案。适合多跳推理类问题(Multi-hop QA)。
- 假设性文档嵌入(HyDE)。先让 LLM 基于用户查询生成一个假设性回答文档,再用这个“假文档”的 embedding 去找真实文档。思路很有意思,适合查询与文档风格差异特别大的情况,但注意生成假文档会引入额外延迟和成本,需要实测评估收益。
老实说,查询改写这块的调优空间很大,但也最容易“优化过度”。我在项目里见过把简单查询复杂化之后效果变差的情况。建议先搭好基线(不加改写),确认纯检索确实存在“问法一变就检索不到”的问题,再考虑加改写模块。
4. 生成优化:上下文管理和提示词工程
检索质量上去了,最后还得看生成环节能不能把这些上下文用好。
4.1 上下文压缩——给 LLM 减负
很多时候,检索回来的块里只有三分之一甚至更少的内容跟答案直接相关,其余都是铺垫背景。把这些都塞给 LLM,不仅浪费 token,反而会引入“上下文偏移”(上下文噪声干扰判断)。
上下文压缩的思路有几种:
- 按块压缩。把每个块单独交给 LLM,让它提取与问题相关的核心信息,压缩后再统一拼接。效果好,但会明显增加调用次数和延迟。
- 按内容选择性截取。利用摘要或关键词信息,只取块中与 query 语义最接近的句子或片段。效率高,但需要一定的文本切分能力。
- 硬截断。设定 token 预算,超出就从相关性较低的块开始丢弃。最简单粗暴,适合对成本敏感的场景。
我个人建议,在系统早期不要做过于激进的上下文压缩,因为每次压缩都可能丢失关键信息。先把检索和重排做到足够精准,让进入 prompt 的本身就已经是高相关度内容,再做压缩才有意义。
4.2 提示词结构——把“参考答案”摆稳
RAG 的提示词一般需要包含:系统指令、上下文内容、用户问题、输出约束(比如要求严格基于上下文回答,无法回答时明确说不知道)。
几个我踩过坑之后的切身体会:
- 上下文和问题之间要有明确的标记分隔。LLM 才能准确区分“哪里是参考资料,哪里是待回答的问题”。用清晰的 XML 标签或 Markdown 引用块都可。
- 指令不要让模型对上下文进行“思考题式”的重新组织。如果你的目标是中文回答,就让它直接把基于上下文的答案写出。别让它“先分析再回答”,容易让输出膨胀。
- 如果业务要求答案简明,在 prompt 里明确给出目标字数或格式(一段话/要点列表),效果比让模型自由发挥稳定得多。
4.3 生成后的自检——最后的兜底
前面提到的新范式里,Self-RAG 的思路就是在生成之后让模型判断“检索到的上下文是否足够支撑答案”。工程上可以低成本实现一个简化版:
生成答案后,再给模型发一个独立的校验请求,给它原文答案、检索上下文和用户问题,让它判断“回答是否完全基于提供的上下文”以及“上下文是否包含足够的信息来回答问题”。如果校验不通过,可以触发补充检索或拒答。
这个做法的代价是额外一次 LLM 调用,但能显著降低幻觉率。对回答准确性要求高的业务(医疗咨询、法律问答、技术支持),这笔开销是值得的。
5. 实操环节:搭一个支持混合检索与重排序的 Mini RAG
理论说了这么多,还是得落到代码上。这里分享一个我在模拟项目里使用过的简化实现,技术选型相对常见:语言模型接口统一封装,向量数据库用轻量级实现,重排序使用公开 API 或本地推理均可以。
5.1 系统设计和依赖
整体流程:
- 文档解析与分块。
- embedding 建索引。
- 查询阶段同时做向量检索和 BM25 检索。
- 合并后用重排序模型精排。
- 拼 prompt 给 LLM 生成最终答案。
工程结构清单(假设场景是一个内部知识库问答):
- 文档处理:支持 Markdown 和纯文本。
- 分块:按 Markdown 标题层级切分,段落过长时再按句号切割。
- 存储:向量存储 + 文档元数据,便于过滤和溯源。
- 检索层:向量检索 + 关键词检索(用轻量全文索引即可)。
- 重排序:本地加载一个小型 cross-encoder 模型。
- 生成层:统一风格的 prompt 模板。
5.2 分块与索引实现要点
分块模块的关键是“保留结构信息”。我处理 Markdown 时,会先按标题层级构建一个树状结构,然后从叶子节点开始切块。每个块的元数据里记录它的上级标题路径。这样后续既方便过滤,也方便做上下文重建。
伪代码思路:
def parse_markdown(filepath): # 解析标题层级,构建文档树 # 叶子节点按内容切块,保留标题路径 return blocks # 每个 block 包含 text, metadata向量化时,我习惯把“标题 + 块正文”拼起来做 embedding,因为标题往往包含关键的领域信息,能提升检索效果。
5.3 混合检索的实现
先定义一个检索统一接口,内部同时调用两个检索器,再用 RRF 融合。
def hybrid_search(query, top_k=20): vector_hits = vector_store.search(query, top_k=top_k) bm25_hits = bm25_index.search(query, top_k=top_k) rrf_scores = {} for rank, hit in enumerate(vector_hits): rrf_scores[hit.id] = rrf_scores.get(hit.id, 0) + 1 / (60 + rank + 1) for rank, hit in enumerate(bm25_hits): rrf_scores[hit.id] = rrf_scores.get(hit.id, 0) + 1 / (60 + rank + 1) ranked_ids = sorted(rrf_scores, key=rrf_scores.get, reverse=True) return [doc_by_id[i] for i in ranked_ids]RRF 公式里的常数 60 是常见的默认值,不用过分调优。如果你发现某种检索器某一方明显更可靠,可以对不同路召回的分数设置权重,但一般用 RRF 已经足够稳。
之后接重排序:
def rerank(query, docs): pairs = [[query, doc.text] for doc in docs] scores = rerank_model.predict(pairs) scored_docs = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True) return [doc for doc, _ in scored_docs[:5]]需要注意,这里重排序的候选集规模建议控制在 20-50 条。如果候选太多,cross-encoder 的推理延迟会高到影响真实用户体验。
5.4 生成阶段的 Prompt 设计
一个经过多轮实测稳定可用的 prompt 模板(简化示意):
请仅根据给定的参考资料回答问题。 参考资料: <context> {context} </context> 用户问题: <question> {question} </question> 要求: 1. 如果参考资料中没有足够的信息,请直接回答"信息不足,无法回答"。 2. 回答必须用中文。 3. 回答控制在 300 字以内。这个模板看起来简单,但“仅根据”、“无法回答时明确说不知道”这两条约束能明显降低幻觉。我不建议在模板里加入过多“思考链”要求,会拉长输出,影响响应速度。
5.5 离线评测与参数调优经验
搭好系统后第一要务不是调 prompt,而是做一个小的离线评测集。我从项目里总结的经验是:准备 30-50 条真实业务问答,标注好标准答案(或者说至少标注清楚“应该在哪个文档的哪个区域里找”)。每次改动检索参数或模型,都跑一遍评测集,统计:
- 检索命中率:标准答案对应的文档片段是否出现在最终进 prompt 的上下文里。
- 端到端准确率:生成回答能否覆盖标准答案中的关键点。
这两项是硬指标。不少工程团队上线前只看几个 demo 问题过了就放行,之后遇到真实流量就会翻车。
分块大小和重叠度是影响检索命中率最敏感的参数。我一般以 200-400 token 的块大小为起点,用一个固定评测集多组对照,看命中率的边际变化。如果一个块超过 500 token,检索命中率会下降;低于 100 token,又容易丢失实体间的关联信息。具体数值和 embedding 模型、语料风格都有关系,只能拿评测数据说话。
6. 常见问题与排查实录
RAG 系统的问题排查,十次里有七八次是检索链路的问题,而不是模型的问题。下面这些坑是我在真实项目和社区交流里见了无数次的。
6.1 检索结果不相关
症状:用户问 A,系统答非所问;或者召回结果里混入大量无关块。
排查顺序:
- 先看 query 的 embedding 向量和召回块的相似度分数。如果分数普遍很低,说明 query 和语料的表达差异太大。这时候加查询改写(HyDE 或多查询)或者换一个领域更强的 embedding 模型。
- 如果分数不低但内容还是不对,那就是 embedding 模型本身对语义理解不足,或者分块太粗导致块内语义分散。尝试升级模型,或者把召回候选集调大一点(比如从 10 调到 30),交给重排序兜底。
- 如果只有个别 query 不对,把它们加入评测集,针对性分析是哪一环的问题。
6.2 检索到了但答案仍然错误
症状:上下文里明明有正确答案,LLM 还是输出错误。
这种一般不是检索问题,而是上下文使用效率问题。可能是 prompt 结构不佳,模型没分清哪里是参考材料;也可能是块内信息太密集,关键信息被埋没。可以尝试:
- 调整 prompt 格式,明确上下文与问题边界。
- 用上下文压缩,突出与问题直接相关的句子。
- 在 prompt 中强调“直接引用参考材料中的事实”。这一步很微妙,措辞不同,效果区别很大。
6.3 系统延迟过高
症状:接口响应很慢,尤其是加了重排序和查询改写之后。
这类问题的原因往往很直接:
- cross-encoder 重排序的候选集太大了。真没必要对 100 个候选都做重排,控制在 20-30 个就够。
- 查询改写调用了多轮 LLM 推理。如果业务时效要求高,可以把多查询数量从 3 个减到 2 个,或者仅在命中低置信度时才启用改写。
- 向量库索引参数不合理。部分向量库的召回效率和索引类型、数据量大小直接相关,这个需要具体分析。
6.4 成本暴涨
RAG 的成本大头通常不是向量检索,而是 LLM 调用次数和输入 token 数量。
- 如果查询改写、上下文压缩、生成后校验都用了 LLM,一次用户请求可能触发 4 次以上模型调用。成本能不涨吗?建议分级启用:简单问题走快速通道,只有复杂问题才走全链路优化。
- 进 prompt 的上下文也值得盯。一次 token 支出 5000 和 1500,价格差三倍多。重排序 + 压缩是省 token 最直接的手段。
6.5 相似度检索质量好但召回率低
这种情况听上去矛盾,实际很常见:向量检索捞回来的都是高相似度但没有必要信息的块,真正信息所在的块被挤出去了。常见原因是分块粒度不合理,或者车辆检索模型的“语义精度”和“语义召回”不可兼得。
解决套路是:
- 把索引粒度调细(多*粒度索引)。
- 混合检索之后扩大召回候选集,再靠重排序从大候选集中找回“信息块”。
7. 结尾的几句实在话
这个系列走到“中篇”,其实就是想表达一个核心观点:RAG 的挺进不是靠某一个“聪明模型”,而是一整套检索工程、上下文工程和评测体系的持续打磨。我在接项目时最深的体会是,很多团队把大量精力去追最新的检索模型或生成策略,却连一个像样的评测集都没有。没有评测,你根本说不清楚一个改动到底是变好了还是变坏了——这种感觉比技术栈陈旧还要让人难受。
如果你正在做 RAG 应用,建议从简单的基线起步,先把“专利检索 + BM25 + 重排”这套扎实的组合搭建起来,再逐步引入查询改写、上下文压缩等高级特性。每一层改动都用一个固定评测集去验证收益。实际做下来,我见过很多项目单靠混合检索加重排,效果就比原来纯向量检索提升了一大截。
接下来的系列下篇,我们再聊模块化 RAG 的流程编排,以及 Self-RAG 这类带反思机制的镜形态。到时候会结合实际代码讲讲,怎样才能让 RAG 从“知识检索工具”进化成“会思考的知识助手”。