RAG: 检索增强生成 —— 让大模型先查资料, 再开口回答
前面几篇文章里,LLM 都是靠 prompt 里塞的材料干活:DAG 把构建日志喂给它做分析, Agent 把工具返回喂给它做决策。但有一个问题一直没回答:材料从哪来? 用户随口一句"我们公司的住宿报销标准是多少",模型 training 数据里怎么可能有你们公司的报销制度。
这篇文章介绍 AI 应用里最常用的一种补料姿势 —— RAG(Retrieval-Augmented Generation,检索增强生成),并给出一个零依赖、单文件、已实际跑通的最小实现(核心 50 行)。
结论先放在前面
RAG 不神秘:先检索、后生成。把用户问题拿去知识库查出最相关的几段资料,连同原问题一起塞进 prompt,让模型"开卷作答";
它治大模型的三个硬伤:知识截止(训练数据有截止日期, 之后的事不知道)、私有数据(你的公司制度、个人笔记不在训练集里)、幻觉(不知道也会一本正经地编);
管线就五步:加载 → 分块 → 向量化入库 → 检索 → 拼 prompt。demo 用纯 Python(TF-IDF 当 embedding)全链路跑通, 实测"住宿费报销标准"这个问题精准命中报销制度那一块;
边界:RAG 的天花板是检索质量 —— 查不到就必然答不出, 垃圾进垃圾出;它是"考试时给模型发参考资料", 不是"把知识灌进模型脑子", 后者靠微调。
从 demo 到生产,差距不在流程而在用料:embedding 模型(入库查询同一个、换模型重建全库)、按结构分块、混合检索+rerank、元数据与权限;建库里 AI 的刚需只有向量化那一步;向量没有直接质量指标,要用标注评测集算 HitRate@k 间接评估(见
ai/rag_demo/eval_rag.py的对照实验)。
为什么需要 RAG: 大模型的三个硬伤
直接问模型三个问题, 就能看清问题所在:
问题 |
模型裸答的表现 |
根因 |
|---|---|---|
"2026 年的新规是什么?" |
不知道, 或者把旧规当新规 |
训练数据有知识截止日期, 截止后的事它天然不知道 |
"我们公司的年假几天?" |
开始编造一个听起来合理的数字 |
你的私有数据从没进过它的训练集 |
"XX 库的某个冷门函数怎么用?" |
一本正经地给出不存在的参数 |
幻觉: 语言模型的本职是"续写像人话的话", 不是"保证每句是真的" |
三条根因有一条共同点:缺失的知识不是模型的推理能力不行, 是它脑子里没有这些事实。那就有两条路:
把知识灌进去:微调(fine-tuning), 用领域数据继续训练模型 —— 贵、慢、新知识一来就要重训;
用的时候再查:RAG, 回答前临时把相关资料找出来发给它 —— 知识库随时更新, 模型本身不动。
维度 |
微调 |
RAG |
|---|---|---|
知识更新 |
重训一遍, 小时到天级 |
改知识库文件, 即时生效 |
私有数据 |
数据要进训练集, 有泄露面 |
数据只在检索时用, 不训练 |
回答可追溯 |
难, 学了什么说不清 |
容易, 引用了哪几段就是哪几段 |
适合补什么 |
风格、格式、任务模式("怎么说") |
事实("说什么") |
成本 |
高(训练) |
低(一次向量检索) |
一句话:补"说法"靠微调, 补"事实"靠 RAG, 两者不互斥, 生产里经常混着用。
RAG 是什么: 离线建库 + 在线查询两段管线
RAG 全程分两个阶段, 用一张图看清:
flowchart TB
subgraph OFFLINE[离线: 建库, 一次或增量做]
D[文档<br/>PDF/Markdown/网页] --> C[分块<br/>chunking]
C --> E[向量化<br/>embedding]
E --> V[(向量库<br/>向量+原文)]
end
subgraph ONLINE[在线: 每次提问都做]
Q[用户问题] --> QE[向量化]
QE --> R[检索 top-k<br/>向量距离排序]
V --> R
R --> P[拼 prompt<br/>资料+问题]
P --> L[LLM 开卷作答]
end
离线阶段(建库), 把知识预处理成可检索的形态:
分块(chunking):文档切成几百字的小块。为什么不能整篇入库?——检索的粒度是块, 块太大, 命中一块等于啥也没筛出来;
向量化(embedding):每块文本送入 embedding 模型, 得到一个高维向量。核心性质:语义相近的文本, 向量距离也近。"住宿报销怎么算"和"差旅住宿标准"用词不同, 但向量很近;
入库:向量连同原文一起存进向量数据库(Chroma/PGVector/ES 都行, 本质是"带距离索引的表")。
在线阶段(查询), 每次提问:
检索:用户问题同样向量化, 在库里按向量距离取 top-k 块;
增强 + 生成:把 top-k 块作为"资料"和问题拼成 prompt, 让模型"根据以下资料回答, 资料里没有的就直说不知道"。
RAG 三个词各对应一段:Retrieval 是第 4 步, Augmented 是第 5 步的拼 prompt, Generation 是模型最后生成的答案。
最小 Demo: 一个文件跑通全链路
ai/rag_demo/rag_demo.py 一个文件, 零第三方依赖(用 TF-IDF 代替 embedding 模型演示"文本→向量"), 流程和生产的 RAG 一模一样:
"""最小 RAG: 分块 -> 向量化(TF-IDF) -> 索引 -> 检索 -> 拼 prompt。零依赖, 直接 python3 rag_demo.py"""
import math
import re
def chunk(text, size=40, overlap=10):
"""按句切, 滑窗分块, 块间保留 overlap 个字的重叠上下文"""
chunks, cur = [], ""
for s in re.findall(r"[^。;]+[。;]?", text.strip()):
if len(cur) + len(s) > size and cur:
chunks.append(cur)
cur = cur[-overlap:] # 尾部重叠, 防止答案恰好被切断
cur += s
if cur.strip():
chunks.append(cur)
return chunks
def tokenize(doc):
# 演示级分词: 英文按单词, 中文按单字。生产里换成正经分词器/embedding 模型
return re.findall(r"[a-zA-Z]+", doc.lower()) + re.findall(r"[\u4e00-\u9fff]", doc)
class Store:
"""向量库: 文档入库时统计词频, 查询时套用同一份 IDF 表, 按余弦相似度取 top-k"""
def __init__(self):
self.docs, self.df = [], {}
def _vec(self, tokens, n):
tf = {}
for w in tokens:
tf[w] = tf.get(w, 0) + 1
return {w: c * math.log((n + 1) / (self.df.get(w, 0) + 1)) for w, c in tf.items()}
def add(self, doc):
self.docs.append(doc)
for w in set(tokenize(doc)):
self.df[w] = self.df.get(w, 0) + 1
def search(self, query, k=2):
qv = self._vec(tokenize(query), len(self.docs))
scored = [cosine(qv, self._vec(tokenize(d), len(self.docs))) for d in self.docs]
top = sorted(enumerate(scored), key=lambda x: x[1], reverse=True)[:k]
return [(self.docs[i], round(s, 3)) for i, s in top]
def cosine(a, b):
dot = sum(v * b.get(w, 0) for w, v in a.items())
na = math.sqrt(sum(v * v for v in a.values()))
nb = math.sqrt(sum(v * v for v in b.values()))
return dot / (na * nb) if na and nb else 0.0
业务侧的使用只有四行, 真实输出附在代码后面:
KNOWLEDGE = """
公司报销制度(2025 版): 差旅住宿一线城市每晚不超过 500 元, 二线城市不超过 350 元。
交通费按职级补贴, 总监以下每月 800 元, 总监及以上每月 1500 元。
发票必须在费用发生后 30 天内提交, 超期不予受理。
年假按工龄计算, 满 1 年 5 天, 满 10 年 10 天, 满 20 年 15 天。
服务器部署规范: 生产环境所有变更必须走工单审批, 变更窗口为每周三凌晨。
数据库慢查询超过 2 秒的必须提交优化说明, 由 DBA 团队复核。
"""
QUESTION = "住宿费报销标准是多少?"
store = Store()
for c in chunk(KNOWLEDGE):
store.add(c)
hits = store.search(QUESTION)
prompt = f"""根据以下资料回答用户问题, 资料里没有的就直说不知道。
资料:
{chr(10).join(doc for doc, _ in hits)}
问题: {QUESTION}
"""
实测输出(直接运行 python3 rag_demo.py, 未删减):
== 知识库分块 ==
[0] 公司报销制度(2025 版): 差旅住宿一线城市每晚不超过 500 元, 二线城市不超过 350 元。
[1] 不超过 350 元。
交通费按职级补贴, 总监以下每月 800 元, 总监及以上每月 1500 元。
[2] 每月 1500 元。
发票必须在费用发生后 30 天内提交, 超期不予受理。
[3] 交, 超期不予受理。
年假按工龄计算, 满 1 年 5 天, 满 10 年 10 天, 满 20 年 15 天。
[4] 20 年 15 天。
服务器部署规范: 生产环境所有变更必须走工单审批, 变更窗口为每周三凌晨。
[5] 更窗口为每周三凌晨。
数据库慢查询超过 2 秒的必须提交优化说明, 由 DBA 团队复核。
== 检索结果 (top-2) ==
0.186 公司报销制度(2025 版): 差旅住宿一线城市每晚不超过 500 元, 二线城市不超过 350 元。
0.030 每月 1500 元。
发票必须在费用发生后 30 天内提交, 超期不予受理。
== 拼好的 prompt(传给 LLM 的就是它) ==
根据以下资料回答用户问题, 资料里没有的就直说不知道。
资料:
公司报销制度(2025 版): 差旅住宿一线城市每晚不超过 500 元, 二线城市不超过 350 元。
每月 1500 元。
发票必须在费用发生后 30 天内提交, 超期不予受理。
问题: 住宿费报销标准是多少?
几个值得注意的观察, 全是踩过坑才写出来的:
分块重叠不能省:看分块 [0] 和 [1], 接界处"不超过 350 元"出现了两次。如果不重叠, 一条规则恰好被切断, 两块各自只剩半句话, 检索命中哪块都读不懂。生产里 chunk size / overlap 是核心调参旋钮;
检索不是匹配:问题里没有一个字和"报销制度"四字相同, 但它还是排到了第一 —— 这就是向量检索的价值, 比的是语义不是字面;
top-2 的第二名是噪声(0.030 vs 0.186, 分差很大, 是"补贴/费"这类通用字撞出来的)。真实系统里这一步靠**重排(rerank)**和拉高阈值过滤掉, demo 为了极简没做;
给模型的"资料里没有就说不知道"那句 prompt 很关键。不写这句, 检索失手时模型照样现场编造, RAG 就白装了。
RAG 的天花板: 检索质量
demo 里第二名是噪声这件事, 指向 RAG 最重要的一个认知:整条链路是"检索先行"结构, 生成质量的上限在检索那一刻就定了。
相关资料没被检索出来 → 模型根本没见过事实, 只能编;
相关资料排进了 top-k 但被噪声淹没 → 模型可能采信错的;
资料本身就没有(用户问的知识库外的全新问题)→ RAG 无能为力, 该走 Agent 去现查。
所以生产级 RAG 的优化, 大头都不在"生成"环节, 而在检索环节:混合检索(向量 + 关键词 BM25 并行, 互补语义近和精确匹配)、查询改写(把口语问题改成好检索的查询)、重排(用一个专精模型对 top-50 重排序取 top-5)。这套组合拳做完, 才轮到调 prompt 和换更强的 LLM。
从 demo 到生产: 算法重要吗、建库要不要 AI、向量怎么评
demo 跑通了流程, 真上生产前还有四个问题要答清楚。先放结论:
追问 |
一句话回答 |
|---|---|
向量化算法重要吗? |
重要。TF-IDF 只是演示替身, 生产用神经网络 embedding 模型;三条铁律——入库查询同一个模型、换模型重建全库、模型升级按换模型对待 |
生产环境一般怎么处理? |
五步管线不变, 每步换用料:正经解析器、按结构分块、embedding 模型、向量库+元数据、混合检索+rerank |
建库要先用 AI 分析资料吗? |
向量化本身就是 AI(神经网络), 这步绕不开;LLM 预处理(摘要/假问题/元数据)是可选增强, 贵 10~100 倍, 按需加 |
向量好坏怎么判断? |
向量本身没有直接指标, 用标注评测集算 HitRate@k / MRR 间接评估;失败 case 按"查不到/查错/被淹没"三类归因 |
向量化算法重要吗: 重要, 但要分清"换参数"和"换家族"
rag_demo.py 里的"向量化"是 TF-IDF——词袋统计, 没有理解。它不知道"出差住酒店"和"差旅住宿"是一回事, 只知道哪些字常一起出现;它甚至不算真正的语义向量——每个块是一张稀疏词频表, "向量"只是演示口径。
生产用的是 embedding 模型, 本身就是一个小型神经网络(BERT 系的 sentence-transformers、OpenAI text-embedding-3、BGE-M3、Qwen3-Embedding 等), 训练目标就是"语义相近的文本, 向量距离近"。词袋和它的差距集中在两处:
同义词: "出差住酒店" vs "差旅住宿", 词袋的交集约等于零, embedding 很近;
一词多义: "苹果发布了新手机"和"苹果富含维生素", 词袋算出几乎相同的向量, embedding 模型会给出两个不同的向量。
选型看四个维度:语言(中文为主选 BGE-M3 / Qwen3-Embedding 这类开源双语模型, 或云厂商的 embedding API)、维度(512~4096, 维度越高越占存储, 精度提升趋缓)、领域(代码/医疗/法律都有专用模型)、部署(API 省心 vs 本地开源, 数据不出域)。
三条铁律, 全是踩坑换来的:
入库和查询必须用同一个模型。两个模型的向量空间互不兼容, 混用等于随机排序;
换模型 = 重建整个库。旧向量全部作废, 千万级块的库重算一遍是小时到天级的活;
模型版本升级按"换模型"对待。哪怕厂商说"效果更好", 向量空间也可能变了——升级前先小流量评测(方法见下文最后一节)。
"算法重要"到底重要到什么程度?ai/rag_demo/eval_rag.py 做了个对照实验:同一份知识库(12 块)、同一套标注问题(12 题——前 5 题字面提问, 后 7 题口语/同义改写, 库里故意放了 发票×3、住宿×2、过期×2 的干扰块), 只换分词方案, 实测:
=> HitRate @1=10/12 @3=12/12 (单字分词)
=> HitRate @1= 9/12 @3=11/12 (双字分词)
三个读法:
词袋家族内部调参, 收益就是"±1 题"这个量级——单字双字互有胜负, 不值得纠结。真正的数量级差距在"词袋 → 神经网络 embedding"这一跳;
字面提问两边都稳(前 5 题全对), 一改写就开始翻车:双字方案把"发票过期了还能报销吗"的正确块排到第 5——它分不清"过期"在"发票超期"和"调休过期"两个语境里谁相关, 词袋永远只会数字面;
字面题也没全对:"住宿费报销标准是多少"在两个方案里都没排第一——库变大、干扰块变多后, 字面撞车是常态不是意外。这就是上一节"天花板是检索质量"的量化版, 也是生产要加 rerank 的直接动机。
把 eval_rag.py 里的 TF-IDF 换成 embedding API, 评测集一行不用改——这套脚本就是"新模型到底好不好"的验收工具。
生产环境一般怎么处理: 五步不变, 每步换用料
环节 |
demo |
生产 |
|---|---|---|
加载解析 |
一段写死的字符串 |
PDF/Word/网页/Markdown 解析器;表格转文本、图片 OCR;解析丢字漏表, 后面全白搭 |
分块 |
按句+固定字数滑窗 |
按文档结构切(标题层级、语义段落), 块大 200~500 token、overlap 10~15%;先清洗(去页眉页脚、归一化全半角) |
向量化 |
TF-IDF 现算 |
embedding API 或本地开源模型;批量+并发+断点续传, 块和向量一起落库 |
入库 |
一个 list |
向量库:专用(Qdrant/Milvus/Weaviate)或 PGVector/ES 插件;距离索引用 ANN(HNSW/IVF);每条存 向量+原文+元数据(来源/部门/生效时间/权限) |
检索 |
余弦相似度 top-2 |
混合检索:向量 + BM25 关键词并行、RRF 合并(向量管语义, BM25 管精确词和编号);再 rerank(bge-reranker / Cohere)对 top-50 重排取 top-5 |
生成 |
一句"不知道就说不知道" |
同左, 外加引用标注、追问澄清、按权限过滤可见块 |
更新 |
一次性 |
增量:文档变更检测 → 只重算变化的块 → 旧向量连带删除 |
权限 |
无 |
检索时按用户权限过滤元数据——不做这步, 等于把全员资料发给每个人 |
建库要先用 AI 分析资料吗: 一半是刚需, 一半是可选增强
先拆开"AI 分析"这个词, 它指两件不同的事:
刚需的那半, 就是向量化本身。 embedding 模型是神经网络, 分块后的每一块都要过它——这是 RAG 建库里 AI 唯一绕不开的介入点。所以"要不要 AI 参与建库"的答案是:要, 但向量化这一步是唯一的刚需。demo 用 TF-IDF 替它演示了流程, 生产换上真模型即可, 管线形状不变。
可选的那半, 是用 LLM 预处理文档, 生产里常见三种:
增强 |
做法 |
解决什么 |
|---|---|---|
LLM 生成元数据 |
给每块抽标题、部门、生效时间、文档类型 |
检索可按字段过滤("只要 2025 版制度");权限控制也靠它 |
LLM 生成假问题 |
让每个块附带几个"用户可能怎么问"(HyDE 思路) |
口语问题和正式条文的措辞 gap, 检索锚点变多 |
LLM 结构化改写 |
表格转文字叙述、图片内容描述 |
解析器啃不动的内容, LLM 兜底 |
成本账要算清:一块过一次 embedding 是毫厘级, 过一次 LLM 改写贵 10~100 倍。所以默认管线 = 解析+分块+embedding, LLM 预处理只在 bad case 分析发现"这类问题查不到"时定向补, 不做全系标配。
最后一个直觉校准, 很重要:AI 并不是先"读懂"整篇文档再编一份索引——那是人脑编目录。它是把每一块独立压成向量, 块与块的语义关系全靠向量距离涌现, 没有全局理解, 也没有"重点"概念。所以分块切断上下文是 RAG 的经典损失点(上面 12 块的库, 块 7 就把"采购"和"招待"两个主题缝进了一块), 结构感知分块就是在补这个。
向量好坏怎么判断: 没有直接指标, 只有间接评估
先破除一个执念:单个向量"好不好"不存在直接度量。一百个数字排成一列, 没有标签告诉你它"语义够不够准"。向量的质量只能通过"好问题能不能查到对的块"间接体现——评估对象是检索效果, 不是向量本身。由轻到重四层:
第 1 层: 公开榜单(选模型时用)。 MTEB / C-MTEB 把上百个 embedding 模型放在统一任务集上打分, 初筛好用。但记住:榜单分高 ≠ 在你的数据上分高——榜单语料是百科和网页, 你的知识库是报销制度和运维手册。榜单只负责缩小候选, 最终要用第 2 层在自己的数据上复测定生死。
第 2 层: 离线评测集(最常用、最该建)。 标注一批"问题 → 正确答案所在块"(20~50 题就能起步, 字面问和口语问都要有), 对检索算:
HitRate@k / Recall@k: 正确块有没有进 top-k。k 取你实际拼 prompt 的块数, 不是取得越大越好;
MRR: 正确块排得多靠前——排第 1 和排第 5 虽然都"进了 top-5", 喂给 LLM 的可信度完全不同。
eval_rag.py 就是这一层的可跑样板:两个方案同库同题对比, 谁好谁坏一目了然;把分词器换成 embedding API, 就是新模型的验收单。
第 3 层: bad case 归因(调优时用)。 失败 case 只可能是三类, 处方各不同:
症状 |
根因 |
处方 |
|---|---|---|
查不到 |
库里没有 / 分块切断 / 没进 top-k |
补文档;调分块和 overlap;上混合检索 |
查错 |
字面撞车("过期"撞"调休过期") |
换更强 embedding;BM25+向量双路;上 rerank |
查到了被淹没 |
正确块在 top-k 但排名低, LLM 采信噪声 |
rerank;相似度阈值过滤 |
第 4 层: 在线指标(上线后盯)。 检索命中率、答案忠实度(LLM-as-judge 打"答案是否只来自所给资料")、该答"不知道"时硬答的比例。
两个误区别踩:一是拿相似度绝对值当质量——它不能跨模型比(各家模型打分松紧完全不同), 能跨方案比的是"同一套评测集上的命中率";二是只看 HitRate 不看 MRR——进了 top-5 但垫底, 和排第 1, 对最终答案是两回事。
边界: 三个常见追问
1. 上下文窗口越来越长, 直接把全部文档塞进去不行吗?
小库可以, 大了不行, 而且贵。1 万页的文档库就算装得进 128K 窗口, 每问一次都要全量读一遍 —— 成本、延迟、注意力稀释全是问题。RAG 本质是用一次几毫秒的距离计算, 替代几十万 token 的重复阅读。
2. RAG 和微调怎么选?
前面给过:补事实用 RAG, 补"说法/风格/任务模式"用微调。比如"让模型学会按我司格式写周报"是微调, "让模型知道我司年假制度"是 RAG。
3. RAG 和本仓库其他文章什么关系?
RAG 是喂料的一种方式, 不是工作流形态。搭在 DAG 里, 它就是"检索"那个节点;搭在 Agent 里, 它就是检索工具 —— 典型如"联网搜索"工具, 本质就是拿搜索引擎当知识库的 RAG。
参考
提出 RAG 的论文: Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks(Lewis et al., 2020), arXiv:2005.11401
本文 demo:
ai/rag_demo/rag_demo.py,python3 rag_demo.py直接可跑, 上文输出为实测未删减检索质量对照实验:
ai/rag_demo/eval_rag.py,python3 eval_rag.py, 两套向量化方案在同一评测集上比 HitRate@k, 上文数字为实测MTEB / C-MTEB 榜单: embedding 模型初筛用, 不能替代自建评测集