RAG 实战教程(一):RAG 工作原理与完整流程——分片、索引、召回、重排和生成
大模型真正落地到企业知识库、智能客服、文档问答、代码搜索等场景时,RAG 几乎是绕不开的一项技术。
这一篇先不急着上 LangChain、LlamaIndex 等框架,而是从底层把 RAG 的整个工作流程讲清楚:文档加载 → 分片 → Embedding → 索引 → 召回 → 重排 → Prompt 组装 → 大模型生成。
并且会用大量 Python 代码,把一个最小可运行的 RAG 系统一步一步实现出来。

@
- 二、为什么需要 RAG?
- 三、RAG 的完整架构
- 四、第一步:文档加载
- 五、第二步:Chunk 文本分片
- 六、最简单的固定长度分片
- 七、为什么 Chunk 要有 Overlap?
- 八、更合理的段落分片
- 九、Chunk 应该多大?
- 十、第三步:Embedding
- 十一、为什么文本可以变成向量?
- 十二、调用 Embedding API
- 十三、封装 Embedding 函数
- 十四、第四步:向量索引
- 十五、先不用向量数据库,自己实现一个
- 十六、第五步:计算相似度
- 十七、Python 实现余弦相似度
- 十八、完整向量召回
- 十九、什么叫召回?
- 二十、为什么只做向量检索还不够?
- 二十一、第六步:Reranker 重排
- 二十二、一个最简单的 LLM Rerank
- 二十三、为什么 Retrieval Top K 和最终 Context Top K 不一样?
- 二十四、第七步:组装 Context
- 二十五、第八步:构造 Prompt
- 二十六、第九步:调用 LLM 生成答案
- 二十七、把整个 RAG 串起来
- 二十八、一个完整的最小 RAG Demo
- 二十九、生产级 RAG 和这个 Demo 差在哪里?
- 三十、向量检索并不是唯一的检索方式
- 三十一、Hybrid Search
- 三十二、一个简单 Hybrid Search 思路
- 三十三、RRF 是什么?
- 三十四、RAG 最容易出问题的地方,其实不是 LLM
- 三十五、如何判断是哪一步出了问题?
- 三十六、Metadata 非常重要
- 三十七、一个更完整的数据结构
- 三十八、给答案附带引用
- 三十九、RAG 的几个核心评价指标
- 四十、RAG 的核心其实是信息压缩
- 四十一、RAG 的几个关键调优点
- 四十二、完整流程总结
- 四十三、最后
一、RAG 到底是什么?
RAG 全称:
Retrieval-Augmented Generation
中文通常翻译为:
检索增强生成
它的核心思想非常简单:
不要让大模型只依赖训练时学到的知识,而是在回答问题之前,先从外部知识库中检索相关资料,然后把资料连同问题一起交给大模型。
传统的大模型问答流程可能是:
用户问题
↓
大语言模型
↓
回答
而 RAG 的流程变成:
用户问题
↓
检索知识库
↓
找到相关文档
↓
把文档 + 用户问题交给大模型
↓
生成回答
例如用户问:
我们公司的退款规则是什么?
LLM 本身显然不可能知道你公司的内部规定。
RAG 会先从知识库中找到:
退款政策.md
售后服务规范.pdf
用户协议.docx
然后提取出类似:
用户购买服务后 7 日内,在未使用核心服务额度的情况下,
可以申请全额退款……
最终构造 Prompt:
请根据下面的企业知识回答用户问题。
已知资料:
用户购买服务后 7 日内,在未使用核心服务额度的情况下,
可以申请全额退款。
用户问题:
我们公司的退款规则是什么?
这样大模型就可以基于真实资料回答。
二、为什么需要 RAG?
很多人第一次接触 RAG 时会问:
我直接把所有文档塞给大模型不行吗?
理论上可以。
实际上很快就会遇到几个问题。
1. Context 有长度限制
假设你的知识库有:
10000 篇文章
5000 个 PDF
几十万页内部文档
不可能一次全部放进 Prompt。
即使模型支持:
128K
200K
1M Context
也不意味着应该全部塞进去。
因为 Context 越长:
- Token 成本越高
- 推理速度越慢
- 关键信息越容易被淹没
- 模型更容易受到无关信息干扰
所以正确做法应该是:
100 万条知识
↓
检索
↓
找到最相关的 5~20 条
↓
交给 LLM
这就是 Retrieval 的意义。
2. 大模型知识不是实时的
比如:
公司最新制度
产品最新价格
今天发布的公告
最新 API 文档
数据库实时数据
模型训练结束后并不会自动知道这些信息。
RAG 可以让大模型读取动态知识。
例如:
用户问题
↓
搜索最新 API 文档
↓
找到相关内容
↓
LLM 回答
这也是为什么 RAG 特别适合:
企业知识库
客服机器人
文档助手
产品手册助手
内部 Wiki
代码问答
论文问答
法律文档检索
三、RAG 的完整架构
一个完整 RAG 系统一般可以拆成两个阶段。
第一阶段:Indexing
也就是知识库建设阶段。
原始文档
↓
文档解析
↓
文本清洗
↓
Chunk 分片
↓
Embedding
↓
写入向量数据库
第二阶段:Retrieval + Generation
也就是用户真正提问时:
用户问题
↓
Query Embedding
↓
向量检索
↓
召回 Top K
↓
Rerank 重排
↓
选择最相关上下文
↓
构造 Prompt
↓
LLM
↓
最终答案
合起来就是:
┌──────────────┐
│ 原始知识库 │
└──────┬───────┘
↓
文档解析
↓
Chunk 分片
↓
Embedding
↓
┌──────────────┐
│ Vector Store │
└──────┬───────┘
↑
│ Search
│
用户问题 → Embedding → Retrieval
↓
Top K 文档
↓
Rerank
↓
Top N 文档
↓
Prompt Construction
↓
LLM
↓
最终回答
下面我们把每一步拆开。
四、第一步:文档加载
假设我们有一个文件:
knowledge.txt
内容:
RAG 是 Retrieval-Augmented Generation 的缩写。
RAG 的主要流程包括文档分片、Embedding、向量索引、
检索、重排和大模型生成。
向量数据库通常用来保存 Embedding 向量,
并提供相似度检索能力。
常见向量数据库包括 Milvus、Qdrant、Weaviate、
Pinecone、Chroma 和 FAISS。
Python 加载:
def load_text(path: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read()
text = load_text("knowledge.txt")
print(text)
真实项目中还可能处理:
PDF
Word
Markdown
HTML
Excel
PPT
网页
数据库
GitHub 仓库
Notion
Confluence
但无论输入是什么,第一步通常都是:
各种文件
↓
统一转换成文本
五、第二步:Chunk 文本分片
Chunk 是 RAG 中非常重要的一个概念。
因为通常不会把整篇文档作为一个向量,而是把文档切成很多小块。
例如原始文章:
一篇 10000 字文章
可能被拆成:
Chunk 1:500 字
Chunk 2:500 字
Chunk 3:500 字
...
Chunk 20:500 字
为什么?
假设用户问:
RAG 中为什么要进行重排?
如果整篇 10000 字作为一个向量:
整个文档只有一个 Embedding
粒度太粗。
如果切成 500 字:
Chunk 7:介绍召回
Chunk 8:介绍重排
Chunk 9:介绍 Prompt
系统就可以直接找到:
Chunk 8
六、最简单的固定长度分片
先自己实现一个简单版本。
def chunk_text(
text: str,
chunk_size: int = 500,
overlap: int = 50
):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start += chunk_size - overlap
return chunks
调用:
text = """
RAG 是 Retrieval-Augmented Generation 的缩写。
RAG 会在大模型生成回答之前检索外部知识库。
向量数据库用于保存文档 Embedding。
重排模型则用于重新计算文档和 Query 的相关性。
"""
chunks = chunk_text(
text,
chunk_size=50,
overlap=10
)
for i, chunk in enumerate(chunks):
print(f"Chunk {i}")
print(chunk)
print("-" * 50)
这里有两个关键参数:
chunk_size = 500
overlap = 50
七、为什么 Chunk 要有 Overlap?
假设一句话刚好位于两个 Chunk 之间:
Chunk 1:
RAG 系统首先对用户问题进行向量化,
然后从向量数据库中检索相关文档。为了提高
Chunk 2:
最终检索质量,系统通常还会加入 Reranker。
语义被硬生生切断。
所以经常会设置:
Chunk Size = 500
Overlap = 50
变成:
Chunk 1:
然后从向量数据库中检索相关文档。
为了提高最终检索质量,系统通常还会加入 Reranker。
Chunk 2:
为了提高最终检索质量,系统通常还会加入 Reranker。
Reranker 会进一步判断 Query 和文档之间的相关性。
这样语义连续性更好。
八、更合理的段落分片
实际项目中不要只按字符暴力切割。
可以优先按:
标题
段落
句子
Token
Markdown 层级
分割。
例如:
def split_by_paragraph(text: str):
paragraphs = text.split("\n\n")
return [
p.strip()
for p in paragraphs
if p.strip()
]
进一步组合:
def semantic_chunk(
text: str,
max_size: int = 500
):
paragraphs = split_by_paragraph(text)
chunks = []
current = ""
for paragraph in paragraphs:
if len(current) + len(paragraph) <= max_size:
current += paragraph + "\n\n"
else:
if current:
chunks.append(current.strip())
current = paragraph + "\n\n"
if current:
chunks.append(current.strip())
return chunks
这种方式通常比纯字符切割更自然。
九、Chunk 应该多大?
这是 RAG 最经典的问题之一。
没有绝对答案。
常见范围:
200 Token
300 Token
500 Token
800 Token
1000 Token
通常可以先从:
300~800 Token
开始测试。
如果知识本身很短:
FAQ
API 参数说明
商品介绍
可以使用较小 Chunk。
例如:
100~300 Token
如果知识本身需要完整上下文:
法律合同
论文
技术原理
复杂业务规则
Chunk 可以适当更大:
500~1500 Token
关键不是数字本身,而是:
一个 Chunk 最好能够表达一个相对完整的语义单位。
十、第三步:Embedding
分片完成之后,就需要把文字转换成向量。
例如:
RAG 使用向量数据库检索文档
经过 Embedding 模型后,可能变成:
[
0.124,
-0.083,
0.992,
0.231,
...
]
假设这是一个:
1536 维
向量。
于是:
文本
↓
Embedding Model
↓
Vector

十一、为什么文本可以变成向量?
Embedding 模型会把语义相近的文字映射到向量空间中相近的位置。
例如:
如何修改密码?
和:
忘记密码后怎么重置?
虽然文字不同,但语义非常接近。
Embedding 后:
Vector A
Vector B
在向量空间中的距离通常也会比较近。
而:
北京今天天气怎么样?
对应的向量就会离它们比较远。
因此我们可以通过:
向量距离
判断:
两个文本语义是否相似
十二、调用 Embedding API
使用 OpenAI SDK 风格接口:
pip install openai
Python:
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY"
)
response = client.embeddings.create(
model="text-embedding-3-small",
input="RAG 使用向量检索找到相关知识"
)
vector = response.data[0].embedding
print(len(vector))
print(vector[:10])
批量生成:
texts = [
"RAG 使用向量数据库保存知识",
"Embedding 可以把文本转换成向量",
"Reranker 用于重新计算相关性",
]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
vectors = [
item.embedding
for item in response.data
]
print(len(vectors))
生产环境中最好批量处理。
不要:
for chunk in chunks:
embedding(chunk)
每个 Chunk 请求一次接口。
而应该:
batch_size = 100
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i + batch_size]
embeddings = create_embeddings(batch)
这样效率高很多。
十三、封装 Embedding 函数
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY"
)
def embedding(texts):
if isinstance(texts, str):
texts = [texts]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts,
)
return [
item.embedding
for item in response.data
]
使用:
vectors = embedding([
"什么是 RAG?",
"什么是 Embedding?"
])
print(len(vectors))
十四、第四步:向量索引
现在我们已经有:
Chunk 1 → Vector 1
Chunk 2 → Vector 2
Chunk 3 → Vector 3
需要把它们保存下来。
一般数据结构至少包含:
{
"id": "chunk-001",
"content": "RAG 是一种检索增强生成技术",
"embedding": [0.1, 0.2, 0.3],
"metadata": {
"source": "rag.md",
"page": 1
}
}
生产环境一般会使用:
FAISS
Milvus
Qdrant
Weaviate
Pinecone
Chroma
Elasticsearch
PostgreSQL + pgvector
十五、先不用向量数据库,自己实现一个
为了真正理解原理,我们先用 Python List。
documents = []
for i, chunk in enumerate(chunks):
vector = embedding(chunk)[0]
documents.append({
"id": i,
"content": chunk,
"embedding": vector,
})
现在内存中就是:
[
{
"id": 0,
"content": "...",
"embedding": [...]
},
{
"id": 1,
"content": "...",
"embedding": [...]
}
]
实际上这已经是最原始的:
Vector Store
了。
十六、第五步:计算相似度
用户现在问:
RAG 为什么需要重排?
先把 Query 变成向量:
query = "RAG 为什么需要重排?"
query_vector = embedding(query)[0]
然后计算:
Query Vector
与所有:
Document Vector
之间的相似度。
最常见的是:
Cosine Similarity
余弦相似度。
公式:
cos(A, B) = A · B / (||A|| × ||B||)
十七、Python 实现余弦相似度
import numpy as np
def cosine_similarity(a, b):
a = np.array(a)
b = np.array(b)
return np.dot(a, b) / (
np.linalg.norm(a)
* np.linalg.norm(b)
)
测试:
a = [1, 0, 0]
b = [0.9, 0.1, 0]
score = cosine_similarity(a, b)
print(score)
越接近:
1
通常说明越相似。
十八、完整向量召回
def retrieve(query, documents, top_k=5):
query_vector = embedding(query)[0]
results = []
for doc in documents:
score = cosine_similarity(
query_vector,
doc["embedding"]
)
results.append({
"content": doc["content"],
"score": score
})
results.sort(
key=lambda x: x["score"],
reverse=True
)
return results[:top_k]
调用:
results = retrieve(
query="RAG 为什么需要 Reranker?",
documents=documents,
top_k=5
)
for item in results:
print(item["score"])
print(item["content"])
print("=" * 80)
这就是最基础的:
Vector Search
整个过程:
Query
↓
Query Embedding
↓
与所有 Document Embedding 计算距离
↓
排序
↓
Top K
十九、什么叫召回?
Retrieval 中文通常叫:
检索
或者:
召回
假设整个知识库有:
100000 个 Chunk
第一阶段从中找出:
20 个可能相关 Chunk
这 20 个就是:
Recall Results
也就是召回结果。
此时目标通常不是:
一定把最好的排第一。
而是:
尽量不要漏掉真正相关的内容。
因此第一阶段通常可以:
Top 10
Top 20
Top 50
多取一点。
之后再使用 Reranker。

二十、为什么只做向量检索还不够?
Embedding 非常强,但并不是完美的。
例如用户问:
GPT-5 API 价格是多少?
可能召回:
GPT API 使用教程
GPT-4 API 定价
GPT-5 模型介绍
API Token 计算规则
GPT-5 API 价格说明
这些内容在向量空间中都很接近。
但真正最需要的显然是:
GPT-5 API 价格说明
所以需要第二层排序。
也就是:
Rerank
二十一、第六步:Reranker 重排
完整流程变成:
Query
↓
Vector Search
↓
召回 Top 20
↓
Reranker
↓
重新排序
↓
Top 5
Embedding 检索通常是:
Bi-Encoder
Query 和 Document 分别编码:
Query → Vector
Document → Vector
计算向量距离。
优点:
快
而 Reranker 通常直接同时判断:
Query + Document
例如:
Query:
RAG 中为什么需要重排?
Document:
重排模型会进一步判断初步召回结果和用户问题之间的相关性……
Reranker 输出:
0.97
另一个:
Query:
RAG 中为什么需要重排?
Document:
Embedding 是将文本转换为高维向量的技术……
可能只有:
0.35
所以重新排序。
二十二、一个最简单的 LLM Rerank
甚至可以让大模型自己打分。
例如:
def rerank_prompt(query, document):
return f"""
请判断下面的文档是否能够回答用户问题。
用户问题:
{query}
候选文档:
{document}
请返回 0~10 的相关性评分。
只返回数字。
"""
然后:
from openai import OpenAI
client = OpenAI()
def rerank_with_llm(query, docs):
results = []
for doc in docs:
prompt = rerank_prompt(
query,
doc["content"]
)
response = client.chat.completions.create(
model="gpt-5",
messages=[
{
"role": "user",
"content": prompt
}
]
)
score = float(
response.choices[0]
.message.content.strip()
)
results.append({
**doc,
"rerank_score": score
})
results.sort(
key=lambda x: x["rerank_score"],
reverse=True
)
return results
当然,这种方式:
成本高
速度慢
生产环境更常见的是专门的:
Cross Encoder
Reranker Model
二十三、为什么 Retrieval Top K 和最终 Context Top K 不一样?
一个经典配置是:
Retrieve Top 20
Rerank Top 5
流程:
100000 个 Chunk
↓
向量数据库
↓
20 个候选
↓
Reranker
↓
5 个最相关
↓
LLM
为什么不是直接向量检索 Top 5?
因为:
Vector Search
的排序可能不够精准。
先取 20 个,是为了保证:
Recall
然后让更精确但更慢的 Reranker 从 20 个里面挑 5 个。
这是一种非常经典的:
粗排 + 精排
架构。
二十四、第七步:组装 Context
假设 Rerank 后得到:
results = [
{
"content": "RAG 首先通过向量检索得到候选知识..."
},
{
"content": "Reranker 会进一步计算 Query 和文档..."
},
{
"content": "最终只将最相关内容传递给大模型..."
}
]
可以组装:
def build_context(results):
contents = []
for i, item in enumerate(results):
contents.append(
f"[资料 {i + 1}]\n"
f"{item['content']}"
)
return "\n\n".join(contents)
最终得到:
[资料 1]
RAG 首先通过向量检索得到候选知识……
[资料 2]
Reranker 会进一步计算 Query 和文档……
[资料 3]
最终只将最相关内容传递给大模型……
二十五、第八步:构造 Prompt
一个基础 RAG Prompt:
def build_prompt(query, context):
return f"""
你是一个知识库问答助手。
请严格根据提供的资料回答用户问题。
如果资料中没有答案,请直接回答:
“根据当前知识库无法确定。”
不要编造不存在的信息。
======== 知识库资料 ========
{context}
======== 用户问题 ========
{query}
======== 回答 ========
"""
这里很重要的一句话:
如果资料中没有答案,不要编造。
因为 RAG 并不能彻底解决幻觉。
需要通过 Prompt 对生成行为进行约束。
二十六、第九步:调用 LLM 生成答案
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY"
)
def generate_answer(query, context):
prompt = build_prompt(
query,
context
)
response = client.chat.completions.create(
model="gpt-5",
messages=[
{
"role": "user",
"content": prompt
}
]
)
return (
response
.choices[0]
.message.content
)
最终:
query = "RAG 为什么需要重排?"
retrieval_results = retrieve(
query,
documents,
top_k=20
)
rerank_results = rerank_with_llm(
query,
retrieval_results
)[:5]
context = build_context(
rerank_results
)
answer = generate_answer(
query,
context
)
print(answer)
至此,一个最基础的 RAG 就已经跑起来了。
二十七、把整个 RAG 串起来
我们可以进一步封装:
class SimpleRAG:
def __init__(self):
self.documents = []
def add_documents(self, texts):
vectors = embedding(texts)
for i, (text, vector) in enumerate(
zip(texts, vectors)
):
self.documents.append({
"id": len(self.documents),
"content": text,
"embedding": vector
})
def search(self, query, top_k=10):
query_vector = embedding(query)[0]
results = []
for doc in self.documents:
score = cosine_similarity(
query_vector,
doc["embedding"]
)
results.append({
"content": doc["content"],
"score": score
})
results.sort(
key=lambda x: x["score"],
reverse=True
)
return results[:top_k]
def ask(self, query):
docs = self.search(
query,
top_k=10
)
context = build_context(
docs[:5]
)
return generate_answer(
query,
context
)
使用:
rag = SimpleRAG()
rag.add_documents([
"RAG 是 Retrieval-Augmented Generation 的缩写。",
"Embedding 可以将文本映射到高维向量空间。",
"向量数据库用于存储 Embedding,并执行相似度搜索。",
"Reranker 可以对初次召回结果进行更加精确的排序。",
"RAG 最终会将检索到的知识和用户问题一起交给大模型。",
])
answer = rag.ask(
"Reranker 在 RAG 中有什么作用?"
)
print(answer)
二十八、一个完整的最小 RAG Demo
下面直接给出一个相对完整的版本。
import numpy as np
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY"
)
# =========================
# Embedding
# =========================
def create_embeddings(texts):
if isinstance(texts, str):
texts = [texts]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
return [
item.embedding
for item in response.data
]
# =========================
# Cosine Similarity
# =========================
def cosine_similarity(a, b):
a = np.array(a)
b = np.array(b)
return np.dot(a, b) / (
np.linalg.norm(a)
* np.linalg.norm(b)
)
# =========================
# Chunk
# =========================
def chunk_text(
text,
chunk_size=500,
overlap=50
):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start += chunk_size - overlap
return chunks
# =========================
# Vector Store
# =========================
class VectorStore:
def __init__(self):
self.documents = []
def add(self, texts):
vectors = create_embeddings(texts)
for text, vector in zip(
texts,
vectors
):
self.documents.append({
"content": text,
"embedding": vector
})
def search(
self,
query,
top_k=10
):
query_vector = (
create_embeddings(query)[0]
)
results = []
for doc in self.documents:
score = cosine_similarity(
query_vector,
doc["embedding"]
)
results.append({
"content": doc["content"],
"score": score
})
results.sort(
key=lambda item: item["score"],
reverse=True
)
return results[:top_k]
# =========================
# Context
# =========================
def build_context(results):
return "\n\n".join([
f"[资料 {i + 1}]\n"
f"{item['content']}"
for i, item in enumerate(results)
])
# =========================
# Generation
# =========================
def generate(query, context):
prompt = f"""
你是一个专业的知识库问答助手。
请严格根据下面提供的知识库资料回答问题。
要求:
1. 不允许编造资料中不存在的信息。
2. 如果知识库中没有答案,请明确说明。
3. 优先使用知识库中的事实。
4. 回答尽量清晰、准确。
知识库:
{context}
用户问题:
{query}
"""
response = client.chat.completions.create(
model="gpt-5",
messages=[
{
"role": "user",
"content": prompt
}
]
)
return (
response
.choices[0]
.message.content
)
# =========================
# Main
# =========================
if __name__ == "__main__":
with open(
"knowledge.txt",
"r",
encoding="utf-8"
) as f:
text = f.read()
chunks = chunk_text(
text,
chunk_size=500,
overlap=50
)
print(
f"Chunk 数量: {len(chunks)}"
)
vector_store = VectorStore()
vector_store.add(chunks)
query = input(
"请输入你的问题:"
)
results = vector_store.search(
query,
top_k=5
)
print("\n召回结果:")
for result in results:
print(
f"{result['score']:.4f}"
)
print(
result["content"][:200]
)
print("-" * 50)
context = build_context(
results
)
answer = generate(
query,
context
)
print("\n最终回答:")
print(answer)
安装依赖:
pip install openai numpy
准备:
knowledge.txt
然后:
python rag.py
输入:
RAG 为什么要进行重排?
就可以完成一次完整的:
知识库 → Chunk → Embedding → Search → Context → LLM
流程。
二十九、生产级 RAG 和这个 Demo 差在哪里?
我们上面的系统虽然可以运行,但是距离生产系统还有很大差距。
生产级架构通常是:
┌─────────────────┐
│ Document Loader │
└────────┬────────┘
↓
Parser / OCR
↓
Text Cleaning
↓
Chunking
↓
Embedding
↓
┌─────────────────┐
│ Vector Database │
└─────────────────┘
用户 Query
↓
Query Rewrite
↓
Embedding
↓
Hybrid Search
├── Vector Search
└── BM25 Search
↓
Fusion
↓
Top 20~100
↓
Reranker
↓
Top 5
↓
Context Compression
↓
Prompt Construction
↓
LLM
↓
Citation
↓
Final Answer
所以真正做好一个 RAG,需要优化很多环节。
三十、向量检索并不是唯一的检索方式
很多入门教程会让人产生一个误解:
RAG = Vector Database
其实并不是。
RAG 的核心是:
Retrieval
至于怎么检索,可以有很多方案。
例如:
Vector Search
BM25
关键词检索
全文搜索
SQL
Graph Search
Web Search
API Search
甚至:
SELECT *
FROM products
WHERE price < 1000
也可以属于 RAG 系统的数据获取过程。
三十一、Hybrid Search
现在很多生产 RAG 会使用:
Hybrid Search
也就是:
Vector Search
+
BM25 Search
原因是向量搜索擅长:
语义
BM25 擅长:
关键词
例如用户搜索:
ERR_CONNECTION_RESET
对于这种精确错误码:
BM25
通常会比 Embedding 更靠谱。
而:
浏览器访问网页时连接突然断开是什么原因?
这种自然语言查询:
Vector Search
通常更有优势。
所以:
Hybrid Search
会同时召回两路结果。
三十二、一个简单 Hybrid Search 思路
假设:
vector_results = vector_search(query)
bm25_results = bm25_search(query)
可以做简单的加权:
final_score = (
0.7 * vector_score
+ 0.3 * bm25_score
)
比如:
def hybrid_score(
vector_score,
bm25_score,
alpha=0.7
):
return (
alpha * vector_score
+ (1 - alpha) * bm25_score
)
但真实系统经常使用:
RRF
也就是:
Reciprocal Rank Fusion
三十三、RRF 是什么?
公式:
score(d) =
Σ 1 / (k + rank(d))
例如:
Vector Search:
A
B
C
BM25:
B
D
A
A 和 B 在两种检索器里面都有不错的排名。
最终 RRF 会自然提高它们的分数。
Python:
def reciprocal_rank_fusion(
result_lists,
k=60
):
scores = {}
for results in result_lists:
for rank, doc_id in enumerate(
results,
start=1
):
score = 1 / (
k + rank
)
scores[doc_id] = (
scores.get(doc_id, 0)
+ score
)
return sorted(
scores.items(),
key=lambda x: x[1],
reverse=True
)
三十四、RAG 最容易出问题的地方,其实不是 LLM
很多人做 RAG,一发现回答不好,就开始换:
GPT
Claude
Gemini
DeepSeek
Qwen
但很多情况下问题根本不在模型。
而是在:
Retrieval
举个例子。
用户问:
公司员工差旅住宿标准是多少?
正确资料是:
一线城市住宿标准最高 600 元/晚。
但系统检索出来的是:
出租车报销标准
飞机票报销规则
餐饮补贴规则
那么哪怕后面换成再强的模型,也不可能凭空得到:
600 元
所以 RAG 调优时要先判断:
是 Retrieval 错了?
还是 Generation 错了?
这两个是完全不同的问题。
三十五、如何判断是哪一步出了问题?
可以把整个流程打印出来。
query = "公司差旅住宿标准是多少?"
results = vector_store.search(
query,
top_k=10
)
for result in results:
print(result["score"])
print(result["content"])
print("=" * 100)
如果正确答案根本没有进入:
Top 10
说明问题在:
Retrieval
可能要优化:
Chunk
Embedding
Hybrid Search
Query Rewrite
Metadata Filtering
如果正确答案已经在:
Top 3
但是模型还是回答错:
Generation
才需要检查:
Prompt
Context
LLM
三十六、Metadata 非常重要
真实文档不能只保存:
content
embedding
一般还应该保存:
{
"id": "doc-001-chunk-003",
"content": "...",
"source": "员工手册.pdf",
"page": 16,
"title": "差旅管理制度",
"department": "财务部",
"created_at": "2026-08-01",
"version": "v3",
"embedding": []
}
这样以后可以做:
Metadata Filter
例如:
只搜索财务部文档
或者:
只搜索 2026 年以后发布的资料
甚至:
只允许用户访问自己部门的文档
这是企业 RAG 中非常重要的一部分。
三十七、一个更完整的数据结构
可以定义:
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Chunk:
id: str
content: str
source: str
page: Optional[int]
title: Optional[str]
embedding: Optional[List[float]]
例如:
chunk = Chunk(
id="handbook-16-01",
content="一线城市住宿标准最高为 600 元/晚。",
source="员工手册.pdf",
page=16,
title="差旅管理制度",
embedding=None
)
三十八、给答案附带引用
RAG 最好的实践之一是:
Answer + Citation
例如:
根据公司的差旅管理制度,一线城市住宿标准最高为
600 元/晚。
来源:《员工手册》P16
因此 Context 里最好把来源一起交给模型。
def build_context(results):
blocks = []
for i, item in enumerate(results):
source = item.get(
"source",
"unknown"
)
page = item.get(
"page",
"unknown"
)
block = f"""
[资料 {i + 1}]
来源:
{source}
页码:
{page}
内容:
{item["content"]}
"""
blocks.append(block)
return "\n\n".join(blocks)
然后 Prompt:
回答时请注明引用的是哪一个资料编号。
模型就可以生成:
根据资料 [1],一线城市住宿费用最高为 600 元/晚。
三十九、RAG 的几个核心评价指标
如果以后真正做 RAG 系统,不能只靠:
“感觉回答还不错”
进行评估。
至少要关注几个指标。
Recall@K
例如:
Recall@10
意思是:
正确答案所在的文档有没有被召回到前 10 个结果里。
Precision
召回的结果里有多少是真的相关。
比如:
Top 10
里面:
3 个相关
7 个垃圾
Precision 就比较差。
MRR
Mean Reciprocal Rank。
关注:
第一个正确结果出现在哪里
例如正确文档排名:
第 1 位
显然比:
第 10 位
好。

四十、RAG 的核心其实是信息压缩
如果从更抽象的角度理解 RAG:
整个系统实际上是在做:
海量知识
↓
检索
↓
少量候选知识
↓
重排
↓
更少、更准确的知识
↓
LLM
例如:
100 GB 文档
↓
1000000 Chunk
↓
Vector Search
↓
Top 50
↓
Reranker
↓
Top 5
↓
大约 3000 Token
↓
LLM
所以 RAG 本质上可以理解为:
从一个巨大的知识空间中,压缩出和当前问题最相关的少量信息,再交给大模型进行推理和生成。
四十一、RAG 的几个关键调优点
一个 RAG 最终效果好不好,主要取决于:
Document Parsing
↓
Chunk Strategy
↓
Embedding Model
↓
Retrieval
↓
Hybrid Search
↓
Reranker
↓
Context Construction
↓
Prompt
↓
LLM
任何一个环节出问题,都可能影响最终结果。
可以总结成:
Garbage In
↓
Garbage Retrieval
↓
Garbage Context
↓
Garbage Answer
所以不能只盯着最后的大模型。
四十二、完整流程总结
到这里,RAG 的整个核心流程其实已经非常清楚。
1. 文档加载
PDF
Markdown
Word
HTML
数据库
转换成文本。
2. Chunk
Document
↓
Chunk 1
Chunk 2
Chunk 3
3. Embedding
Chunk
↓
Embedding Model
↓
Vector
4. Index
Vector
↓
Vector Database
5. Retrieval
Query
↓
Embedding
↓
Vector Search
↓
Top K
6. Rerank
Top K
↓
Reranker
↓
Top N
7. Context
Top N Document
↓
Prompt Context
8. Generation
Context
+
Question
↓
LLM
↓
Answer
最终完整链路就是:
Document
↓
Chunk
↓
Embedding
↓
Vector Store
↓
─────────────────────────
↓
User Query
↓
Query Embedding
↓
Retrieval
↓
Top K
↓
Reranker
↓
Top N
↓
Context
↓
Prompt
↓
LLM
↓
Answer
四十三、最后
很多人第一次学习 RAG,会急着安装:
LangChain
LlamaIndex
Dify
FastGPT
RAGFlow
然后几行代码把知识库跑起来。
这当然没有问题。
但如果以后遇到:
为什么搜不到?
为什么搜出来不相关?
为什么知识明明存在,模型还是回答错?
为什么 Chunk 越切效果反而越差?
为什么换了更强的 LLM 还是没效果?
为什么需要 BM25?
为什么需要 Reranker?
最终还是需要回到底层。
真正理解下面这一条链路:
分片
↓
Embedding
↓
索引
↓
召回
↓
重排
↓
Context
↓
生成
基本就理解了 RAG 的骨架。
而实际生产级 RAG,就是不断围绕这几个步骤进行工程优化。
下一篇,我们可以正式开始搭建一个真正可使用的 RAG 项目:
Python
+
Embedding
+
Qdrant / Milvus
+
Reranker
+
大语言模型
把本文中的内存版 Vector Store 替换成真正的向量数据库,并实现一个可以持续导入 Markdown、PDF 文档的知识库问答系统。
RAG 实战教程系列
第一篇:RAG 工作原理与完整流程
第二篇:Embedding 与向量数据库实战
第三篇:使用 Qdrant / Milvus 搭建知识库
第四篇:Hybrid Search 与 BM25
第五篇:Reranker 重排模型实战
第六篇:Query Rewrite 与 Multi-Query
第七篇:PDF / Markdown 企业知识库实战
第八篇:从 Demo 到生产级 RAG 系统

浙公网安备 33010602011771号