检索增强生成(Retrieval-Augmented Generation, RAG)是目前把大模型接入私有知识最实用的方案。它不改模型权重,却能让模型“读到”你的文档后再回答,从而缓解幻觉、支持实时更新。
本文从工程视角拆解 RAG 的完整链路,并给出一份可以直接照做的实践清单。
RAG 的核心链路
一个最小可用的 RAG 系统由五步组成:
- 切分(Chunking):把长文档拆成语义完整的片段。
- 向量化(Embedding):把每个片段编码成向量。
- 检索(Retrieval):用问题向量在向量库里找最相近的片段。
- 重排(Rerank):用更精细的模型对候选片段重新排序。
- 生成(Generation):把命中的片段拼进 Prompt,交给大模型作答。
一句话总结:检索决定上限,生成决定体验。大多数“答非所问”其实是检索没召回对内容。
切分策略
切分是最容易被忽视、却最影响效果的一步。经验法则:
- 按语义边界切(标题、段落),而不是按固定字数硬切。
- 片段大小控制在 300–800 tokens,配合 10%–20% 的重叠(overlap)。
- 保留结构化元数据(标题层级、来源、更新时间),检索时可用于过滤。
def chunk(text: str, size: int = 500, overlap: int = 80) -> list[str]:
chunks, start = [], 0
while start < len(text):
end = start + size
chunks.append(text[start:end])
start = end - overlap # 保留重叠,避免语义被切断
return chunks
常见坑
- 切太碎:片段失去上下文,生成时容易“断章取义”。
- 切太大:噪声进 Prompt,稀释真正有用的证据。
- 不带元数据:上线后无法按目录/时间过滤,也难做引用溯源。
向量化与索引
Embedding 模型决定“语义相似度”怎么被计算。选型时关注:
- 中文效果是否经过实测(别只看英文榜单)。
- 向量维度与索引成本(维度越高,存储与检索越贵)。
- 是否支持本地部署(数据合规场景尤其重要)。
索引侧常见选择:FAISS(本地实验)、pgvector(已有 Postgres)、专用向量库(Milvus / Qdrant / Weaviate)。先保证正确性,再谈吞吐。
检索与重排
单纯的向量检索(dense retrieval)在关键词精确匹配上偏弱,实践中常用混合检索:向量召回 + BM25 关键词召回,再合并去重。
召回之后,用一个 cross-encoder 重排模型对 Top-K 候选打分,能显著提升命中率:
scores = reranker.predict([(query, c.text) for c in candidates])
top = [c for _, c in sorted(zip(scores, candidates), reverse=True)][:5]
召回数量怎么定
- 初召回:20–50 条(给重排留空间)。
- 送进 Prompt:3–8 条(看模型上下文窗口与任务复杂度)。
- 空召回必须有兜底话术,而不是硬编答案。
生成阶段的 Prompt 组织
把检索结果拼进 Prompt 时,请务必:
- 明确要求模型只依据给定材料回答,材料中没有就说不知道。
- 为每段材料标注来源编号,方便模型引用、也方便前端展示出处。
- 控制上下文长度,宁可少而准,不要塞满噪声。
示例骨架:
你只能依据【资料】回答【问题】。
若资料不足,回答:资料未提及。
回答末尾列出引用编号。
【资料】
[1] ...
[2] ...
【问题】
...
评测:没有数字就没有迭代
上线前至少准备两类评测:
- 检索评测:问题 → 期望命中的文档/片段(Recall@K、MRR)。
- 端到端评测:问题 → 期望要点 / 人工打分 / LLM-as-Judge。
把失败样本沉淀成回归集:每次改切分、换模型、调 Top-K,都跑一遍。
一份落地清单
- 混合检索(向量 + 关键词)
- Rerank 重排 Top-K
- 引用出处、可溯源
- 对“检索为空”做兜底话术
- 离线评测集(问题 → 期望片段)持续回归
- 文档更新流水线(增量切片 + 重新索引)
- 延迟与成本监控(检索 P95、生成 tokens)
做好这几点,你的 RAG 就已经超过大多数 Demo 级实现了。下一步通常是:加评测、加缓存、再考虑 Agent 化编排。
