RAG 检索增强生成的工程实践 封面
AI 系列

RAG 不是魔法:一条可靠检索链路的工程拆解

换更大的模型往往救不了“答非所问”。真正决定 RAG 上限的,是你能否把答案锚定到自己的资料——本文把检索链路拆开,讲清每一步的取舍与踩坑。

RAG 核心概念示意图
RAG:核心组成与职责边界
RAG 工作流程示意图
RAG:从查询到带引用答案的链路
RAG 实践检查示意图
RAG:可落地的实践检查清单

先回答一个问题:RAG、微调还是长上下文?

很多人把这三种当成同一件事的竞争者,其实它们解决的是不同维度的问题。RAG 解决的是“知识不在权重里”:你的内部文档、今天刚更新的价格表、某个客户自己的历史工单,模型训练时根本没见过,也不可能塞进权重。微调解决的是“行为/格式/风格不对”:你希望它稳定输出某种 JSON 结构、用特定语气、遵循公司术语,这类“能力形状”的改变靠喂数据比靠检索更稳。长上下文(200K+ token)解决的是“窗口内就能放下全部材料”:当整本文档能一次性塞进上下文,检索的必要性确实下降。

但工程上三者不是互斥的。一个常见的组合是:长上下文承载“当前会话的全量材料”,RAG 负责“从海量历史库里精确定位”,微调负责“固定输出契约”。选型的第一性原理是——数据是否在训练权重中、是否频繁变化、是否需要被引用溯源。会频繁变动、需要给出处、体量远超上下文窗口的知识,RAG 是不二之选。

维度RAG微调长上下文
适用知识外部/易变/需溯源固定风格与格式窗口内全量材料
更新成本极低(改索引)高(重训)低(改输入)
可溯源强(带引用)
成本/延迟检索+生成仅生成高算力
为什么重要:先用 RAG 把“知识从哪来”这件事做实,再谈微调和长上下文。多数企业问答失败,不是模型不行,是检索链路在漏答案。

切分:被低估的第一道关口

切分(chunking)直接决定了“召回的片段是否包含完整答案”。常见三种策略:固定切分按字符数(如 512 token)硬砍,实现简单但容易把一句话拦腰截断;递归切分按段落→句子→词的层级递归切,优先在标点/换行处断开,是 LangChain 等框架的默认选择,对大多数 Markdown/HTML 文档很稳;语义切分用_embedding_在相似度突降处断句,能保留完整语义单元,但计算更贵、边界更不可预测。

chunk size 是核心权衡:太小会丢失上下文(一个 definition 被拆成两半,检索只命中一半);太大则单块噪声多、召回精度下降、且占用上下文窗口。经验起点是 256–512 token,重叠(overlap)设为 10–20% 以避免边界信息丢失。对代码、表格这类结构敏感内容,应优先按语法结构(函数、表格行)切,而不是按字符。

踩坑提醒:别忘了在每块元数据里保留 source、章节路径、更新时间。没有元数据的 chunk,召回后模型无法引用,用户也无法溯源,信任直接崩塌。

检索:稠密 + 稀疏的混合才是常态

纯向量检索(稠密)擅长“语义相似”:用户问“怎么退款”,能命中“取消订单后钱多久到账”。但它对精确字符串(产品型号、错误码、人名)很弱。稀疏检索(BM25)正好相反:精确关键词命中极强,但理解不了同义表达。所以生产环境几乎都用混合检索(hybrid):两者各召回一批,再融合。

融合常用 RRF(Reciprocal Rank Fusion):score = Σ 1/(k + rank),不依赖各自分数量纲、实现简单。稠密向量模型优先选领域适配好的,如 BGE、E5、GTE 系列,中文场景建议用专门的中文版并做归一化。检索后还要重排(rerank):用 Cross-Encoder(如 BGE-reranker)对 top-k 候选做精细打分,它把 query 和 doc 拼在一起算相关性,精度远高于双塔召回,但慢,所以只作用在召回的几十个候选上。

# 混合检索 + 重排的精简示意(Python / 伪代码)
from rank_bm25 import BM25Okapi
import numpy as np

def hybrid_search(query_vec, query_tokens, dense_idx, bm25, chunks, k=20):
    dense = dense_idx.search(query_vec, top_k=50)        # 稠密召回 top50
    sparse = bm25.get_top_n(query_tokens, chunks, n=50)  # 稀疏召回 top50
    # RRF 融合
    rrf = {}
    for rank, doc in enumerate(dense + sparse, 1):
        rrf[doc] = rrf.get(doc, 0) + 1 / (60 + rank)
    fused = sorted(rrf, key=rrf.get, reverse=True)
    return reranker.rank(query_tokens, fused[:k])        # 重排后取 top-k

# 上线建议:向量库用 pgvector / Milvus,reranker 本地或独立服务部署
小贴士:召回阶段宁多勿少(top 30–50),把“筛选”交给 reranker;生成阶段再收紧到 4–8 块,避免上下文被噪声淹没。

查询改写与 HyDE:让“问法”对齐“写法”

用户的问题是口语化的,文档是书面化的,二者存在表达鸿沟。两种解法:查询改写(Query Rewriting)用一个轻量模型把用户问题改写成更贴近文档索引风格的检索词,或拆成多个子问题(multi-query)分别检索再合并;HyDE(Hypothetical Document Embeddings)让模型先“假设”一个答案,再用这个虚构答案的 embedding 去检索——因为生成的答案措辞更接近真实文档,反而能拉回更相关的块。HyDE 对事实型问答有效,但对需要精确数据的场景可能引入幻觉偏差,需配合重排兜底。

图 RAG 与多路召回:当关系本身就是答案

普通 RAG 召回的是“片段”,但如果问题本质是“A 和 B 有什么关系”“某实体的上下游影响链”,片段检索就力不从心。GraphRAG 先对文档做实体/关系抽取,构建知识图谱,检索时沿图遍历,把子图结构一并喂给模型。它特别适合合规、科研、企业组织关系这类“强关联”场景,代价是索引构建成本高、需要图数据库。更轻量的做法是多路召回:同一查询并行走“摘要库 / FAQ 库 / 全文库 / 图谱”多条路,最后由 reranker 或路由模型统一排序。

一张图看清整条检索链路

RAG 检索链路
用户查询查询改写/HyDE稠密+稀疏混合召回RRF 融合重排 Rerank上下文拼装(带元数据)带引用生成
任意一环漏检,最终都表现为“模型在胡说”;把每一环的指标量化,才能定位瓶颈。

怎么知道 RAG 真的变好了:评估与生产坑

评估要分两段看。检索侧看召回率(Recall@k):正确答案是否出现在召回的 k 个块里——这决定“模型有没有机会答对”。生成侧看答案忠实度(Faithfulness)上下文利用率:模型是否真的基于检索内容作答、有没有编造上下文之外的信息(幻觉率)。实用做法是用 LLM-as-judge 对“答案是否可由给定片段推导”打分,但需定期人工抽检校准,避免评审模型本身有偏差。

生产坑集中在三处:其一是上下文污染,召回块太多太杂,模型反而被带偏,要靠 reranker 收紧;其二是元数据丢失导致无法溯源,前面已强调;其三是冷启动无评估集,建议上线第一天就人工收集 50–100 条真实问答作为黄金集(golden set),每次改动只变一个变量(切分/Prompt/模型/重排阈值)跑回归。

踩坑提醒:不要只看“平均分变高”。把最易失败的 20% 长尾问题单独盯,它们往往决定用户是否信任整个系统。

RAG 的核心不在模型多大,而在你能否把答案锚定到自己的资料。把切分、混合召回、重排、引用和量化评估每一环做扎实,模型才真正成为你知识的延伸,而不是又一个会自信胡说的对话框。