RAG(Retrieval-Augmented Generation)已经是私有知识问答的默认方案。但「跑通 demo」和「上线可用」之间,差距往往比想象中大:检索到的内容看似相关,回答却答非所问;模型引用了来源,但来源里根本没有那句话。
这篇文章按数据流顺序,把 RAG 拆成五步,并给出每一步的判断标准和最小实现。
一、先明确:RAG 解决什么问题
RAG 适合三类需求:模型训练数据里没有的私有文档、需要时效性的知识、以及必须给出引用来源的回答。它的本质是把「记忆」外置成可检索的索引,让模型在生成前先查资料。
反过来,如果需求涉及实时事务、复杂计算或跨系统写操作,工具调用通常比 RAG 更合适。把两者混为一谈,是架构设计阶段最常见的错误。
二、数据切分:按语义而不是按字数
切分决定了检索的上限。一个实用的默认策略是:
- 先按标题层级切,让每个 chunk 自带上下文,而不是从段落中间硬切。
- 控制长度:中文 400 到 800 字为宜,重叠 10% 到 15%。
- 保留标题路径:把「第三章 > 3.2 退款规则」拼在正文前面,召回质量通常会有肉眼可见的提升。
- 特殊内容单独处理:表格按行切并保留表头,代码块不要切断。
def build_chunks(markdown: str, max_chars: int = 600, overlap: int = 80):
"""按标题切分,再按长度兜底,并保留标题路径。"""
sections = split_by_headings(markdown) # [(标题路径, 正文), ...]
chunks = []
for path, text in sections:
step = max_chars - overlap
for i in range(0, len(text), step):
piece = text[i:i + max_chars]
chunks.append({
"text": f"标题路径:{path}\n\n{piece}",
"metadata": {"path": path, "offset": i},
})
return chunks
切完不要急着嵌入。先抽 20 个 chunk 人工读一遍:如果单独看这一段,你能回答它想表达什么吗?如果人都看不懂,模型更看不懂。
三、检索:向量只是起点
向量检索擅长语义相近,但对订单号、产品型号、法条编号这类精确匹配并不敏感。实际系统里,最常见的组合是「向量 + 关键词」的混合检索:
| 方式 | 优势 | 短板 |
|---|---|---|
| 向量检索 | 语义泛化,能处理同义表达 | 对精确编号、罕见词不敏感 |
| 关键词检索 | 精确命中,可解释性强 | 换个说法就召回不到 |
| 混合检索 | 互补,通常优于单一策略 | 需要融合与调参 |
两路结果可以用 RRF(Reciprocal Rank Fusion)合并,不需要关心两边的分数尺度是否可比:
def retrieve(question: str, k: int = 8):
vector_hits = vector_store.search(embed(question), k=k)
keyword_hits = bm25.search(question, k=k)
merged = reciprocal_rank_fusion(vector_hits, keyword_hits)
return rerank(question, merged)[:4]
另一个容易被忽略的点是 metadata 过滤。多租户系统必须在检索前按租户过滤,而不是检索完再筛。否则既浪费算力,也可能把别人的文档带进上下文。
四、重排与上下文组装
初筛可以宽一些,比如召回 20 到 50 条,再用交叉编码器重排,取前 4 到 6 条。重排在中文场景的收益通常比换更大的嵌入模型更直接。
组装上下文时,注意三件事:
- 去重:相邻 chunk 可能内容重叠,重复信息会稀释注意力。
- 标注来源:给每段资料编号,要求模型在回答中引用编号。
- 预留指令:明确告诉模型「只根据资料回答;资料不足时直接说不知道」。
把检索到的资料放在用户问题之前,并在资料末尾重复一次问题。长上下文模型对「末尾指令」的遵循度通常更好。
五、评测:没有评测就没有优化
RAG 的优化必须能分层定位问题。建议准备 50 到 100 条真实问答,至少记录四个指标:
- Recall@k:正确资料有没有被召回,衡量检索质量。
- MRR:正确资料排在第几位,衡量排序质量。
- Faithfulness:回答是否忠于资料,衡量幻觉程度。
- 引用准确率:引用的编号是否真的支持对应结论。
当回答出错时,先看检索结果里有没有正确资料:没有,就是检索问题;有但没答对,才是生成或提示词问题。这个分流能省下大量盲目调参的时间。
六、常见误区
- 只调提示词:检索没召回,提示词再漂亮也变不出答案。
- chunk 越大越好:上下文被无关内容占满,模型反而更容易漏掉关键句。
- 一次塞二十条资料:噪音增加,成本上升,效果下降。
- 索引不更新:文档改了、删了,索引还是旧的,回答自然过时。
- 忽略权限:检索层不做租户隔离,是最危险也最容易被忽视的问题。
RAG 不是一个模型问题,而是一个数据工程问题。把切分、检索、重排、评测四件事分别做好,效果通常比换一个更大的模型更明显。