引言:当大模型遇到”知识盲区”

如果你玩过 Agent,大概率遇到过这样的尴尬:你让 Agent 回答一个公司内部的问题,比如”我们组这周的值班表是谁排的?”,它却一本正经地编出一个根本不存在的人名。

这不是模型不够聪明,而是它根本没见过这些信息。大模型的知识来自训练数据,训练完成那一刻,它的”记忆”就冻结了。之后发生的所有事——新产品文档、最新财报、公司内部规章——它统统不知道。

面对这种”知识盲区”,直觉上有三条路:

  1. 重新训练或微调模型,把新知识”塞”进参数里;
  2. 把所有资料塞进 Prompt,让模型一次性读完;
  3. 让模型先去”查资料”,查到相关内容再回答。

第一条路成本高、更新慢,还可能引发灾难性遗忘;第二条路撞在上下文窗口的墙上,资料一多就装不下,而且模型在长文本里容易”走神”。第三条路,就是本文的主角——RAG(Retrieval-Augmented Generation,检索增强生成)

一、RAG 究竟是什么

RAG 这个概念由 Lewis 等人在 2020 年的论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》中正式提出。论文的核心主张一句话就能讲清楚:

把”参数化记忆”(模型自身学到的知识)和”非参数化记忆”(外部检索到的文档)组合在一起,用来生成回答。

拆开 RAG 三个字母,正好对应它的三步动作:

  • R(Retrieval,检索):根据用户问题,从外部知识库中检索出相关文档片段;
  • A(Augmented,增强):把检索到的内容作为上下文,拼接到 Prompt 里,增强模型的输入;
  • G(Generation,生成):大模型基于”问题 + 检索到的资料”生成最终回答。

一句话概括:**RAG让大模型从”闭卷考试”变成了”开卷考试”**。闭卷时它只能凭记忆作答,容易编造;开卷时它可以先翻到对应页码,再据此作答,既准确又能溯源。

二、核心思想:两种”记忆”的协奏

理解 RAG,关键是理解它区分了两种记忆。这一点论文里讲得很透彻,也是后续所有技术选型的根基。

参数化记忆:指模型权重里编码的知识。它来自训练数据,是”内化”在模型里的。优点是回答快、能泛化;缺点是知识会过时、无法解释来源、容易产生幻觉,而且更新必须重新训练。

非参数化记忆:指外部知识库(向量数据库里的文档片段)。它独立于模型存在,可以随时增删改,检索结果可以展示给用户作为引用。

打个比方:参数化记忆就像一个学生大脑里已经记住的知识,非参数化记忆就像考试时桌上摆着的参考书。学生答题时,既调用脑中已有的一般推理能力,又翻阅参考书查找具体事实——前者负责”怎么想”,后者负责”想什么”。

这个分工带来一个重要推论:**RAG 不是要替代大模型,而是给大模型配一个”外接硬盘”**。模型本身的推理、语言能力照旧使用,只是事实性知识改从外部按需取用。于是 RAG 的行为可以用一个公式表达:

1
Answer = LLM(Query + Retrieved_Context)

模型不再凭”记忆”回答,而是基于”查到的资料”回答。这一改变,直接缓解了大模型的几个老大难问题:知识过时、幻觉、无法溯源、领域知识缺失。

三、RAG 的完整工作流程

RAG 的流程可以清晰地分成两个阶段:离线的知识库构建在线的检索生成。前者做一次(或定期更新),后者每次提问都执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────── 离线:构建知识库 ───────────────────┐
│ │
│ 原始文档 → 解析 → 文本分块 → 向量化 → 存入向量数据库 │
│ │
└───────────────────────────────────────────────────────┘

│ (知识库就绪)

┌─────────────────── 在线:检索生成 ─────────────────────┐
│ │
│ 用户提问 → 向量化 → 向量库检索 Top-K → 构建 Prompt │
│ → 大模型生成 → 答案 + 引用来源 │
│ │
└───────────────────────────────────────────────────────┘

离线阶段做的是”把资料整理好放进图书馆”:把各种格式的文档解析成纯文本,切成大小合适的片段,每段转成一个向量,连同原文一起存进向量数据库。

在线阶段做的是”读者来图书馆查资料”:把用户的问题也转成向量,在数据库里找最相似的几段,把它们和问题一起拼成 Prompt 交给大模型,模型据此生成答案。

接下来几节,我们逐步拆解这条流水线上的每个关键环节。为了让你看清”为什么这么做”,我会先讲每个环节要解决的问题,再讲它怎么做。

四、第一步:文档处理与文本分块

为什么不把整篇文档直接存进去?三个原因:大模型有上下文长度限制,整篇文档塞不下;检索时整篇文档粒度太粗,匹配不准;而且只把相关片段送给模型,能省下大量 Token 成本。

所以必须把长文档切成小块。这一步叫文本分块(Chunking),听起来简单,却是影响 RAG 效果的最大变量之一。分块分得好,检索精准;分得不好,要么切碎了语义,要么块太大混入无关内容。

分块的核心原则是:在语义完整性和块大小之间找平衡。一个理想的块,应该能独立表达一个完整的意思,又不至于过长。

常见的分块策略有四种,复杂度递增:

  • 固定长度分割:按字符数硬切。实现最简单,但会粗暴切断句子,适合日志这种没有语义结构的文本。
  • 递归字符分割:按分隔符优先级递归切分,先按段落(\n\n),再按句子(\n),再按空格,最后按字符。这是最常用的通用策略,能较好地保留段落和句子完整性。
  • 文档结构感知分割:按 Markdown 标题、HTML 标签、PDF 章节等结构切分。适合结构化文档,能保留逻辑层次。
  • 语义分割:用 Embedding 计算相邻句子的语义相似度,在语义跳变处切分。质量最高,但计算成本也最高。

这里有个容易踩的坑:块边界可能正好切断关键信息。比如一句话”退款需在购买后 7 天内申请”被切成”退款需在购买后”和”7 天内申请”两块,单独检索到任何一块都没用。

解决办法是重叠窗口(Overlap):相邻块之间保留一部分重复内容。比如块大小 500 字符、重叠 50 字符,那么块 2 的开头会重复块 1 的结尾 50 字符。这样即使关键信息落在边界,也能在至少一个块里完整出现。

一个实操建议:块大小一般选 256 到 1024 字符(约 100 到 500 Token),重叠取块大小的 10% 到 20%。具体数值要看你的文档特点——FAQ 类短问答可以用小块,长篇技术文档可以用大块。

五、第二步:向量化(Embedding)

分块之后,每段文本还是一串字符,计算机没法直接算”相似度”。需要把它转成数值向量,这一步叫向量化Embedding

Embedding 模型的作用,是把一段文本映射成一个固定长度的浮点数数组,比如 [0.12, -0.34, 0.56, ..., 0.07](通常几百到几千维)。关键性质是:语义相近的文本,向量也相近;语义无关的文本,向量相距甚远

举个例子,三句话经过 Embedding 后:

1
2
3
"如何退款"   → [0.12, -0.34, 0.56, ...]
"退货流程" → [0.11, -0.32, 0.58, ...] ← 与"如何退款"向量很接近
"今天天气" → [0.89, 0.12, -0.45, ...] ← 与前两者向量相差很大

“如何退款”和”退货流程”用词不同,但意思相近,向量也靠近;”今天天气”完全无关,向量就远离。这正是语义检索的基础。

这里有个容易混淆的点:稀疏向量和稠密向量。它们代表了文本数字化的两种思路。

稀疏向量(如词袋模型、TF-IDF 生成)像一份清单:维度对应词典里的每个词,某个词出现就在对应位置记个数,没出现就是 0。一份文档的向量绝大多数位置都是 0,所以叫”稀疏”。它的本质是”这篇文档里有哪些词、各出现几次”。

稠密向量(如 BERT、BGE 等 Embedding 模型生成)像一份指纹:维度是固定的几百到上千维,每个位置都是一个连续的小数,没有具体对应哪个词,整体编码了这段话的”意思”。它不再记录”有哪些词”,而是捕捉”说了什么”。

两者的差别直接决定了检索方式的不同:稀疏向量擅长精确的关键词匹配(你查”退款”它就找含”退款”的文档),但理解不了同义词;稠密向量能理解”退货”和”退款”意思相近,但对罕见专有名词的精确匹配反而不如稀疏向量。现代 RAG 通常用稠密向量,并在需要时配合稀疏向量做混合检索(后面会讲)。

六、第三步:向量数据库与相似度检索

向量存到哪里?传统数据库擅长精确匹配(WHERE name = '张三'),但语义检索要的是”找和这个向量最像的几个向量”,这是传统数据库不擅长的。于是有了向量数据库——专门存储、索引和检索高维向量的系统,常见的有 Milvus、Chroma、Weaviate、Pinecone、FAISS 等。

向量数据库的核心能力有三:能集成 Embedding 模型自动生成向量;能同时存向量和元数据(作者、日期、来源等标量字段);能做向量检索和标量过滤的联合查询(比如”只在 2024 年的文档里找语义相似的”)。

检索时怎么判断两个向量”像不像”?靠相似度度量,最常用的有两种:

  • 余弦相似度:衡量两个向量的方向是否一致,不管长度。可以理解为”两段文本的话题方向是否一致”。
  • 欧氏距离:衡量两个向量在空间中的绝对距离。可以理解为”两段文本在语义空间里的直线距离”。

一个直观的类比:余弦相似度像在判断”两个人是不是朝同一个方向走”,欧氏距离像在判断”两个人站得有多近”。大多数语义检索场景用余弦相似度就够了。

但纯向量检索有个软肋:它”看不懂”用户的真实意图。下一节我们就来解决这个问题。

七、检索进阶:混合检索与重排序

来看一个会让 RAG 翻车的场景。用户问”如何退款?”,向量检索返回的 Top-3 结果是:

  1. “无法退款的情况说明”
  2. “退款政策条款”
  3. “退款流程操作指南”

用户明明想问”怎么操作退款”,应该排第一的是”退款流程操作指南”,可向量检索把”无法退款的情况”排在了第一——因为这三段都含”退款”这个词,向量上很接近,检索器分不清谁更贴合”如何”这个意图。

这个问题有两层:第一层是召回层面,纯稠密检索对关键词不够敏感,可能漏掉含精确术语的文档;第二层是排序层面,初检结果的相关性排序不够精准。

混合检索(Hybrid Search)解决第一层。它同时跑两路检索:

  • 稠密检索:用 Embedding 向量做语义匹配,擅长理解同义、近义;
  • 稀疏检索(BM25):基于词频统计做关键词匹配,擅长精确匹配专业术语。

两路各返回 Top-K,再用RRF(Reciprocal Rank Fusion,倒数排名融合)把结果合并。RRF 的思路很朴素:一个文档如果在两路检索里都排名靠前,它大概率真的相关;如果只在一路里靠前,可信度就低些。公式是 score = Σ 1/(k + rank_i),k 通常取 60。这样既不丢语义理解,也不丢关键词精度。

重排序(Reranking)解决第二层。初检(无论是稠密、稀疏还是混合)为了速度,用的是双编码器(Bi-Encoder):问题和文档各自独立编码成一个向量,再算相似度。这种方式快,但问题和文档之间没有”细粒度交互”。

重排序用的是交叉编码器(Cross-Encoder):把”问题 + 候选文档”拼在一起送进模型,让模型逐字衡量两者的相关性。精度高很多,但计算量大,所以只能用在初检召回的小批量候选上。

重排序的角色可以用一个类比理解:初检像海选,重排序像复试。海选要快,用粗筛从百万文档里捞出几十篇;复试要精,对这几十篇逐一细看,排出真正的先后。常见的重排序模型有 bge-reranker、Cohere Rerank 等。

经过”混合检索召回 + 重排序精排”,检索质量会有明显提升。这也是生产环境 RAG 的推荐配置。

八、从论文看 RAG 的两种范式

讲完工程细节,我们回到论文,看看 RAG 在学术上最初是怎么设计的。这部分能帮你理解 RAG 的”原汁原味”,也能解释一些你在框架里看到的配置项。

论文提出了两种 RAG 范式,区别在于”检索到的文档如何参与生成”:

RAG-Sequence:检索出 Top-K 篇文档后,对每一篇文档分别让生成器生成一个完整答案,再把 K 个答案的概率加权求和。也就是说,**整个答案序列由同一篇文档”负责”**。

RAG-Token:生成答案时,每一个 Token 都可以参考不同的文档。也就是说,答案的不同部分可以来自不同文档

用一个类比来区分:RAG-Sequence 像”写一篇作文只能翻一本书”,整篇答案都基于同一份参考资料;RAG-Token 像”写每个句子都可以换一本书参考”,答案可以综合多份资料。

哪种更好?看任务。如果答案的事实集中在某一篇文档里(比如事实问答),RAG-Sequence 更合适;如果答案需要综合多篇文档(比如”对比 A 产品和 B 产品的优缺点”),RAG-Token 更灵活。

值得一提的是,现在工程实践中用的大多数 RAG 框架(LangChain、LlamaIndex、Dify 等)采用的是一种简化版本:检索 Top-K 文档,把它们全部拼进 Prompt,再让模型一次性生成。这更接近 RAG-Sequence 的思路,但省去了对每篇文档分别解码的开销。论文里的精细概率建模在工程上往往被”拼上下文 + 单次生成”取代,因为现代大模型的上下文窗口已经足够容纳多篇文档。

九、上下文构建与提示词工程

检索回来的文档片段,不能直接丢给模型,要组织成结构化的 Prompt。这一步叫上下文构建(Context Assembly),它直接决定模型能不能”用好”检索结果。

上下文构建要回答四个问题:

选哪些片段? 一般取重排序后的 Top-K(如 Top-5)。可以再加一道相似度阈值过滤,只保留相似度足够高的;可以做去重,避免内容雷同;可以兼顾多样性,避免所有片段都来自同一篇文档。

怎么排序? 把最相关的片段放在 Prompt 前面。这不是随意的——研究表明模型对 Prompt 开头和结尾的内容注意力更强,中间部分容易被忽略,这叫”Lost in the Middle”现象。所以重要的内容往前放,或者往前和往后放,别埋在中间。

怎么控制长度? 上下文有 Token 预算:上下文 Token = 模型上下文窗口 - 系统提示 - 用户问题 - 预留输出。超预算时,从相关性最低的片段开始截断;如果单段太长,可以先做摘要压缩。

Prompt 怎么组织? 一个好的 RAG Prompt 通常包含:系统角色定义、行为约束(如”基于参考资料回答,不要编造”)、边界处理(如”资料里没有就说明”)、输出格式要求、参考资料、用户问题。其中动态部分(问题和检索到的资料)只占少数,大部分是预定义的结构化指令。

一个典型的 Prompt 模板长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
你是一个专业的知识问答助手。请基于以下参考资料回答用户问题。

要求:
1. 如果参考资料中有相关信息,请准确回答;
2. 如果参考资料中没有相关信息,请明确说明"根据现有资料无法回答";
3. 回答时引用来源,格式:[来源文档, 页码];
4. 不要编造或推测任何信息。

参考资料:
[文档1] 来源:员工手册_v3.2.pdf 第15页
内容:年假天数按工龄计算,满1年5天,满3年10天,满10年15天。

[文档2] 来源:员工手册_v3.2.pdf 第22页
内容:年假需提前3个工作日在OA系统提交申请。

用户问题:我入职2年,能休几天年假?怎么申请?

回答:

注意第 2 条”没有就说明”——这是抑制幻觉的关键。没有这条约束,模型在资料不全时仍会”硬编”一个答案出来。

十、RAG、微调与长上下文:该如何选择

讲到这里,你可能会问:现在大模型上下文窗口动辄几十万、上百万 Token,直接把所有资料塞进去不就行了?微调也能教模型新知识,为什么非要用 RAG?

这三种方案不是互斥的,而是各有适用场景。理解它们的差别,才能在工程中做对选型。

微调改变的是模型的参数(参数化记忆)。它适合让模型学会某种”能力”或”风格”——比如学会用公司的话术写邮件、学会某种推理范式。但微调不适合注入频繁变化的事实知识:成本高、更新慢、还可能遗忘旧知识。打个比方,微调像”送员工去培训”,改变的是他的能力,不是他的资料库。

长上下文把资料直接塞进 Prompt。它适合资料量不大、单次查询的场景,比如”帮我总结这份合同”。但资料量一大就力不从心:Token 成本随资料量线性增长,模型在超长上下文里检索能力会下降(Lost in the Middle),而且每次提问都要重新传一遍资料,既慢又贵。打个比方,长上下文像”每次考试都把整柜子的书搬到考场”,搬不动也翻不过来。

RAG把资料存在外部,按需检索。它适合知识量大、频繁更新、需要溯源的场景。每次只检索相关片段送进模型,成本低、速度快、知识可随时增删、答案可引用来源。打个比方,RAG 像”开卷考试配索引”,需要哪页翻哪页。

实际工程中,三者常常组合使用:用微调让模型掌握领域推理风格,用 RAG 注入实时事实知识,对个别超长文档用长上下文做精读。但如果只能选一个解决”知识更新”问题,RAG 几乎总是性价比最高的那个。

十一、动手实现:一个最小 RAG 示例

讲了一堆原理,不如动手跑一遍。下面用一个最小可运行的示例,把前面讲的分块、向量化、检索、生成串起来。读者有 Python 基础,看这段代码应该很轻松。

我们用 LangChain 框架,它把 RAG 的各环节封装成了可组合的组件。先装依赖:

1
2
pip install langchain langchain-community langchain-openai
pip install chromadb sentence-transformers

然后是一段完整的最小 RAG:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 1. 加载文档(这里用本地 txt 举例,实际可换成 PDF/Markdown 等)
loader = TextLoader("company_faq.txt", encoding="utf-8")
docs = loader.load()

# 2. 文本分块:块大小 500,重叠 50,使用递归字符分割
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ",", " ", ""],
)
chunks = splitter.split_documents(docs)

# 3. 向量化 + 存入向量数据库(这里用本地 Chroma 和开源 BGE 模型)
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectorstore = Chroma.from_documents(chunks, embeddings)

# 4. 构建检索器:每次召回 Top-3
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 5. 准备 Prompt 模板
template = """你是一个专业的知识问答助手。请仅基于以下参考资料回答用户问题。
如果资料中没有相关信息,请回答"根据现有资料无法回答",不要编造。

参考资料:
{context}

用户问题:{question}

回答:"""
prompt = ChatPromptTemplate.from_template(template)

# 6. 初始化大模型
llm = ChatOpenAI(model="deepseek-chat", temperature=0)

# 7. 把检索到的文档拼成一段文本
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)

# 8. 用 LCEL 串成一条 RAG 链
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)

# 9. 提问
question = "入职2年能休几天年假?"
answer = rag_chain.invoke(question)
print(answer)

这段代码里每一步都对应前面讲的环节:第 1、2 步是离线的文档处理与分块,第 3 步是向量化与存储,第 4 步是检索器,第 5 步是 Prompt 工程,第 8 步用 LangChain 的 LCEL 表达式把”检索 → 拼上下文 → 填模板 → 调模型 → 解析输出”串成一条链。

想进一步升级,可以把第 4 步的检索器换成混合检索(EnsembleRetriever 同时接 BM25 和向量检索),在第 4 步和第 8 步之间加一个重排序器(ContextualCompressionRetriever 配合 reranker 模型)。结构不变,只是组件升级。

十二、RAG 的局限与演进方向

RAG 不是银弹,它也有自己的局限,了解这些局限才能避免误用。

检索质量是天花板。RAG 的回答质量上限由检索质量决定——如果相关文档没被召回,模型再强也答不对。这就是为什么前面花那么多篇幅讲分块、混合检索、重排序:它们都是在抬高这个天花板。

召回但不相关的内容会干扰模型。检索回来的片段如果其实不相关,模型可能被带偏,硬从无关内容里”编”出答案。这就是为什么 Prompt 里要加”没有就说明”的约束,以及为什么相似度阈值过滤很重要。

多跳推理是难点。像”我们公司去年营收增长最高的产品线,今年的负责人是谁?”这种需要先查去年财报、再查今年人事任命的问题,单次检索很难搞定,需要把问题拆解成多步、多次检索。这就是 Agent + RAG 的结合点——用 Agent 做规划,用 RAG 做每一步的事实查询。

结构化数据不擅长。表格、数据库里的数值,用文本向量化效果不好。这类场景更适合 Text-to-SQL,让模型把问题转成 SQL 去查数据库,而不是把表格切成文本块做向量检索。

正因为这些局限,RAG 本身也在演进。学术界已经把 RAG 的发展分成三个阶段:

  • 朴素 RAG(Naive RAG):就是本文讲的基础流程,检索-生成。
  • 高级 RAG(Advanced RAG):在朴素 RAG 基础上,在检索前加查询改写、查询扩展,在检索后加重排序、上下文压缩,全面提升检索质量。
  • 模块化 RAG(Modular RAG):把 RAG 拆成可插拔的模块(检索、记忆、路由、融合、排序等),按需组合,甚至引入多轮检索、迭代检索。

更前沿的方向还有 Graph RAG(用知识图谱代替纯文本块,捕捉实体间关系)、Self-RAG(让模型自己判断要不要检索、检索结果相不相关)等。这些方向的核心思路没变——都是围绕”怎么更准地找到、更聪明地用上外部知识”做文章。理解了本文的基础流程,再去读这些前沿工作会顺畅很多。

结语

回到开头的那个值班表问题。用 RAG 解决它,思路其实很朴素:把公司的排班文档整理进知识库,每次提问时先检索出本周的值班记录,再让模型据此回答。模型不再需要”记住”这些信息,只需要”查到”它们。

这就是 RAG 的本质——**让大模型从”凭记忆作答”转向”凭证据作答”**。它没有改变模型的推理能力,只是给模型配了一个随时可更新的外接知识库。这个看似简单的改变,却精准地击中了大模型在知识密集场景下的几个核心痛点:过时、幻觉、不可溯源、领域盲区。

如果你正在做 Agent,RAG 几乎是必备的能力——Agent 的”工具调用”解决”做事”,RAG 解决”知道事”。两者结合,才能做出真正可用的知识型应用。

理解 RAG,从这篇文档开始;用好 RAG,则要在自己的数据上反复调参、反复对比。希望这篇梳理能帮你建立一个清晰的知识框架,剩下的,就交给实践。

参考资料

  • Lewis P, Perez E, Piktus A, et al. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. arXiv:2005.11401, 2020. https://arxiv.org/abs/2005.11401
  • LangChain 官方文档:https://python.langchain.com
  • Karpukhin V, et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP 2020.