RAG 不是「接个向量库」就完事。它是一条从文档解析到引用生成的完整管线,任何一环出问题,最终都会表现为「模型胡说」。
RAG 解决什么问题
模型不知道你的私有数据,也不知道训练截止之后的事。RAG(Retrieval-Augmented Generation)在生成前检索相关资料,放入上下文,让回答有依据。
它主要解决:
- 知识不在模型里:内部文档、产品手册、工单记录;
- 知识会过期:价格、政策、库存;
- 需要引用:用户要看到来源;
- 降低幻觉:给模型可核对的事实。
它不解决:
- 检索不到的问题(覆盖不足);
- 文档本身错误;
- 需要复杂推理而非查找的问题;
- 权限与数据隔离(要单独设计)。
完整管线
清洗、分节
1. 解析:先把文档读对
PDF 是最容易出问题的格式:
- 双栏排版被读成一行;
- 表格变成乱序文本;
- 页眉页脚混入正文;
- 扫描件没有文本层;
- 图表标题与正文分离。
处理策略:
| 文档类型 | 建议 |
|---|---|
| 原生 PDF | 用布局感知解析器,保留标题层级与表格 |
| 扫描 PDF | OCR,必要时人工校对关键页 |
| HTML | 去导航/广告,保留正文与标题 |
| Markdown | 按标题分节,保留代码块 |
| 表格 | 单独抽取为结构化行,不要混进段落 |
| 图片 | 图表用多模态模型生成描述,再索引 |
解析质量决定上限。如果一段政策被读成「退款需在 7 天内申请」和「退款需在 7 天内」,后者会直接导致错误回答。
2. 切分:语义完整优先
固定长度切分会切断论证。更稳的做法是结构优先、长度兜底:
- 按标题层级切(H1 → H2 → H3);
- 段落超过上限时再按句子切;
- 保留 10%–15% 重叠,避免边界丢上下文;
- 每块带元数据:文档 ID、标题路径、页码、版本、日期、权限标签。
经验参数:
| 内容 | chunk 大小 | 重叠 |
|---|---|---|
| 政策/合同 | 400–800 token | 10%–15% |
| 技术文档 | 300–600 token | 10% |
| 对话/工单 | 按轮次或问题切 | 1 轮 |
| 表格 | 一行或一组行 | 表头重复 |
父子块(parent-child) 很实用:用小块做检索(精度高),命中后返回它所属的大块(上下文完整)。
def chunk_by_heading(doc, max_tokens=600, overlap=80):
sections = split_by_headings(doc) # 保留标题路径
chunks = []
for sec in sections:
for piece in split_sentences(sec.text, max_tokens, overlap):
chunks.append({
"text": piece,
"doc_id": doc.id,
"title_path": sec.path, # 例如:退款政策 > 时效
"page": sec.page,
"version": doc.version,
"updated_at": doc.updated_at,
"acl": doc.acl,
})
return chunks没有标题路径和页码,模型无法引用,用户无法核对,过滤也无法按版本/权限做。元数据不是可选项。
3. Embedding:选型与注意点
选择 Embedding 模型时看:
- 语言:中文/多语言能力;
- 领域:法律、医疗、代码可能需要领域模型;
- 维度:768–3072,越高越贵;
- 最大输入长度:要覆盖你的 chunk;
- 是否支持指令/查询前缀:有些模型要求
query:/passage:前缀; - 部署方式:API 还是本地。
实践要点:
- 文档与查询必须用同一个模型;
- 向量做归一化,使用余弦相似度;
- 换模型要全量重建索引;
- 记录模型版本,便于回溯;
- 对专有名词、编号,向量检索不可靠,必须配合关键词检索。
4. 索引:向量库只是其中一部分
| 组件 | 作用 | 常见选择 |
|---|---|---|
| 向量索引 | 语义召回 | pgvector、Qdrant、Milvus、Pinecone |
| 倒排索引 | 关键词/BM25 | Elasticsearch、OpenSearch、Postgres FTS |
| 元数据存储 | 过滤、权限、版本 | PostgreSQL |
| 对象存储 | 原文、附件 | S3 / R2 |
选型看四件事:过滤能力、更新成本、混合检索支持、运维复杂度。小规模(< 100 万块)用 pgvector 往往最省事;需要强过滤与大规模并发再考虑专用向量库。
5. 检索:召回阶段要「宁多勿漏」
稠密 + 稀疏
- 稠密:语义相似,擅长「意思相近但用词不同」;
- 稀疏(BM25):精确匹配,擅长编号、专有名词、代码;
- 混合:两路各取 top 20–50,融合排序。
常用的融合算法是 RRF(Reciprocal Rank Fusion):
def rrf(rank_lists, k=60):
scores = {}
for ranks in rank_lists:
for rank, doc_id in enumerate(ranks, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)RRF 不需要两路分数同尺度,简单稳定。
过滤与多样性
- 按租户、权限、版本、日期过滤(在检索前做,避免越权);
- 用 MMR 或相似度阈值去重,避免返回 5 段几乎一样的内容;
- 多文档问题时,保证每个来源至少有一条。
top-k 怎么定
召回阶段取 20–50 条,重排后取 4–8 条进入上下文。太少容易漏,太多会稀释注意力并增加成本。
6. 重排:把「相关」变成「最相关」
向量相似度是粗排。Cross-encoder 或 LLM 重排能看到查询与文档的交互,精度更高。
召回 50 条 → 重排模型打分 → 取 top 6 → 组装上下文代价是延迟。常见折中:
- 只对 top 20 重排;
- 用小型重排模型;
- 对简单查询跳过重排;
- 缓存相同查询的结果。
7. 组装上下文:顺序、引用、预算
- 按相关性排序,但把最关键的来源放在开头或结尾(中间容易被忽略);
- 每段加编号与来源:
[1] 退款政策 v3 · p.2; - 去重、截断,控制在预算内;
- 明确告诉模型:「只根据以下资料回答;资料不足时说不知道」;
- 要求每个事实性陈述标注引用编号。
[1] 来源:退款政策 v3 · 第 2 页 · 2026-08-01
退款申请需在签收后 7 天内提交,商品需保持完好。
[2] 来源:FAQ · 退款时效
审核通过后 1–3 个工作日原路退回。8. 生成:允许说不知道
RAG 的提示词要明确:
只使用提供的资料回答。
每个事实性陈述后标注来源编号,如 [1]。
如果资料不足以回答,输出「根据现有资料无法确定」,并说明缺什么。
不要使用资料之外的知识补充细节。同时做输出校验:
- 引用编号是否存在于本次上下文;
- 引用的段落是否真的支持该结论(可用小模型校验);
- 是否出现资料中没有的数字/日期。
评估:分开看检索与生成
| 层级 | 指标 | 说明 |
|---|---|---|
| 检索 | Recall@k | 正确文档是否在 top-k 中 |
| 检索 | MRR / nDCG | 正确文档排得有多靠前 |
| 检索 | 覆盖率 | 测试问题是否都有对应文档 |
| 生成 | Faithfulness | 回答是否忠于资料 |
| 生成 | Answer Relevance | 是否回答了问题 |
| 生成 | Citation Accuracy | 引用是否支持结论 |
| 端到端 | 通过率 | 人工或 LLM 评审 |
| 运维 | P95 延迟、单次成本 | 检索 + 重排 + 生成 |
一个实用做法:先构造 50 条「问题 + 正确文档 ID + 标准答案」,分别测检索召回和生成质量。如果召回率只有 60%,再怎么调提示词也没用。
失败模式对照
| 现象 | 根因 | 修复 |
|---|---|---|
| 答「资料中没有」但明明有 | 召回失败 / 过滤过严 | 检查 Recall@k,放宽过滤,加关键词检索 |
| 引用了不支持的段落 | 重排差 / 提示不严 | 加强重排,要求逐句引用,做引用校验 |
| 答案混入旧版本 | 版本过滤缺失 | 元数据加版本,检索时只取有效版本 |
| 多文档问题只答一半 | top-k 太少 / 去重过度 | 增加多样性,按来源配额 |
| 表格数字错 | 解析错乱 | 表格结构化抽取,单独索引 |
| 延迟高 | 重排 + 大模型 + 多轮 | 缓存、缩小 top-k、模型路由 |
| 越权检索 | 过滤在生成后 | 检索前按 ACL 过滤 |
成本与延迟预算
一个可参考的预算:
查询改写 100–300 ms
向量检索 20–100 ms
关键词检索 10–50 ms
重排(20 条) 100–400 ms
生成(流式) 首 token 0.5–2 s
--------------------------------
总计 1–3 s(不含生成完)如果要求 1 秒内,考虑:缓存、跳过重排、小模型、预检索。
一个最小技术栈
解析:unstructured / pymupdf4llm / Docling
切分:自定义(按标题 + 句子)
Embedding:多语言模型(按评测选)
索引:PostgreSQL + pgvector + 全文索引
重排:bge-reranker / Cohere Rerank / LLM
生成:通用模型 + 引用提示
评估:Ragas / 自建脚本 + 人工抽检不要一上来就上复杂架构。先用 100 份文档 + 50 条问题跑通,再根据失败案例加组件。
检查清单
- 解析保留标题、页码、表格结构
- 切分语义完整,带元数据与重叠
- 文档与查询使用同一 Embedding 模型
- 混合检索(向量 + 关键词)
- 检索前做权限/版本/租户过滤
- 有重排,top-k 合理
- 上下文带编号与来源
- 提示要求引用、允许说不知道
- 分别评估检索与生成
- 有延迟与成本预算
小结
- RAG 是完整管线,解析与切分决定上限。
- 召回宁多勿漏,重排负责精度,组装负责可读与可引用。
- 检索和生成要分开评估,先看 Recall@k。
- 权限过滤必须在检索前。
- 先用小规模跑通,再按失败案例加复杂度。
下一篇处理更难的查询:一次检索不够时,Agent 如何自己决定检索什么。