RAG 的详细过程是什么?从文档加载到答案生成的完整链路

魏远标 Lv7

提问

RAG 的详细过程是什么?每个环节具体怎么做的?生产环境要注意什么?

参考答案

RAG(Retrieval-Augmented Generation,检索增强生成)的本质,是让 LLM 在回答问题前先查一下资料——把检索到的相关文档塞进 Prompt,让模型基于这些「事实」作答,而不是凭训练时学到的旧知识「瞎编」。

它解决的是 LLM 三大短板:

  1. 幻觉:模型不知道的事会一本正经胡说
  2. 知识截止:训练数据之后发生的事它完全不知道
  3. 私有数据:企业内部文档、产品手册、合同条款,LLM 从来没见过

RAG 之所以是 2024-2026 落地最多、最稳的 AI 工程方案,就是因为它不用训练模型,只做「检索 + 拼上下文」就能用。


一图流:完整流程

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
┌──────────── 离线预处理(一次性) ────────────┐    ┌──── 在线问答(每次请求) ────┐
│ │
│ 原始文档 Chunking Embedding 向量库 │ │ 用户提问 │
│ (PDF/Word/ (切块) (向量化) (存储) │ │ │ │
│ 网页/数据库) │ │ ▼ │
│ │ │ │ │ │ │ Query 改写(可选) │
│ ▼ ▼ ▼ ▼ │ │ │ │
│ 解析清洗 切 512 块 向量化 入库 │ │ ▼ │
│ 每块加 │ │ │ 向量检索(Top-K) │
│ 元数据 │ │ │ │ │
│ │ │ │ ┌──────────┐ │
│ │ │ │ │ ┌────────┴──┐ │
│ │ │ ◀────│ │ │ 向量库 │ │
│ │ │ │ │ └────────┬──┘ │
│ │ │ │ └──────────┘ │
│ │ │ │ │ Top-K 文档块 │
│ │ │ │ ▼ │
│ │ │ │ Rerank 重排序(可选) │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ Prompt 组装 │
│ │ │ │ ┌─────────────────────────┐ │
│ │ │ │ │ system: 基于以下资料回答:│ │
│ │ │ │ │ context: [文档1][文档2]… │ │
│ │ │ │ │ user: 用户问题 │ │
│ │ │ │ └─────────────────────────┘ │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ LLM 生成 │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ 答案 + 引用来源 │
└─────────────────────────────────────────────┘ └────────────────────────────────┘

两个阶段

  • 离线(左边):文档 → 切块 → 向量化 → 入库,一次性处理
  • 在线(右边):用户提问 → 检索 → 组装 Prompt → LLM 回答,每次请求处理

接下来按顺序讲每个环节。


Step 1:文档解析与清洗

目标

把 PDF/Word/网页/数据库等异构数据源,转成统一的纯文本 + 元数据。

常见文档源

文档类型 推荐工具
PDF PyMuPDF(fitz)/ Unstructured / pdfplumber
Word(.docx) python-docx / Unstructured
Markdown / HTML BeautifulSoup / markdown-it
Excel / CSV pandas / openpyxl
飞书 / 钉钉 / Confluence 官方 API + 自定义解析
数据库(MySQL/ES) 直接 SQL 查询 + 字段映射
图片 / 扫描件 PaddleOCR / Tesseract + LLM 描述

关键坑

  1. PDF 表格乱掉:PyMuPDF 提取的表格是散落的字符流,要用 pdfplumbercamelot 专门处理
  2. 编码乱码:中文 PDF 常见 GBK/UTF-8 混用,统一用 chardet 自动检测
  3. 图片里的文字:用 OCR 提取后,再合并到正文
  4. 重复内容:同一份文档在不同地方存了多份,要按 hash 去重

实战建议别贪全。先支持你最核心的 1-2 种文档格式跑通闭环,再扩展。


Step 2:Chunking(切块)— 最关键的一步

为什么 Chunking 最重要?

Embedding 和 LLM 都有上下文窗口限制:

  • Embedding 模型:512 token(很多开源模型)
  • LLM 上下文:8K-128K(GPT-4 128K、Claude 200K)

所以必须把长文档切成「不大不小」的块(典型 256-1024 token)。

切得太小:每个块缺少上下文,检索召回的内容支离破碎
切得太大:噪声多,召回不准;塞不进 LLM 上下文

三种主流切法

固定长度切(最简单)

1
2
3
4
5
6
7
8
# 最朴素,按字符数切
def fixed_size_chunk(text, chunk_size=500, overlap=50):
chunks = []
start = 0
while start < len(text):
chunks.append(text[start:start+chunk_size])
start += chunk_size - overlap
return chunks

缺点:可能把一个语义完整的句子从中间切断。

按段落 / 句子切(推荐)

1
2
3
4
5
6
7
8
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""]
)
chunks = splitter.split_text(document)

LangChain 的 RecursiveCharacterTextSplitter 是工业级默认选择。它按优先级在多个分隔符中递归切,直到块大小合适。

按语义切(最先进)

  • 用 NLP 模型(spaCy、NLTK)按句子切
  • 用 LLM 总结每个段落的关键概念,按概念切
  • 用滑动窗口 + 句子嵌入相似度,相似度低的地方切

代码示例(按句子切):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re

def semantic_chunk(text, max_chunk=512):
sentences = re.split(r'(?<=[。!?.!?])\s*', text)
chunks = []
current = ""
for sent in sentences:
if len(current) + len(sent) <= max_chunk:
current += sent
else:
chunks.append(current)
current = sent
if current:
chunks.append(current)
return chunks

Chunking 的进阶策略

场景 推荐 chunk_size overlap
法律合同 256-512 50-100
通用文档 512 50
长文 / 论文 1024 100
代码 256-512 0(按函数切)
FAQ 整条 0

黄金法则:先按段落切,再观察检索效果。90% 的场景用 RecursiveCharacterTextSplitter(512, 50) 就够了。


Step 3:Embedding(向量化)

目标

把文本块转成稠密向量(dense vector),让语义相近的文本在向量空间里距离近。

主流 Embedding 模型对比

模型 维度 中文支持 MTEB 分数 价格 推荐场景
OpenAI text-embedding-3-small 1536 $0.02/1M token 生产首选,质量稳定
OpenAI text-embedding-3-large 3072 更高 $0.13/1M token 高质量需求
BAAI/bge-large-zh-v1.5 1024 ✅ 中文专精 免费(自部署) 国内中文站
BAAI/bge-m3 1024 ✅ 多语言 免费 多语言场景
M3E 1024 免费 中文小型项目
Cohere embed-multilingual-v3 1024 $0.10/1M token 多语言 + 高质量
bge-small-zh 512 免费 资源受限场景

选型建议

  • 国内生产:BGE 系列(开源 + 中文效果好)+ 自部署,或 DeepSeek 的 embedding API
  • 国际生产:OpenAI text-embedding-3(最稳)
  • 隐私要求高:本地部署 bge-large-zh-v1.5(显存 4G 就够)

调 Embedding 的关键点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from openai import OpenAI

client = OpenAI()

def embed(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text,
encoding_format="float"
)
return response.data[0].embedding

# 批量调用(省钱 + 快 10x)
def embed_batch(texts: list[str]) -> list[list[float]]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts,
encoding_format="float"
)
return [d.embedding for d in response.data]

坑 1:Embedding 输入不要带 system prompt,只传纯文本,否则会污染向量。
坑 2:中文长文本要先断句再嵌入,否则向量会失真。


Step 4:向量数据库(Vector Store)

主流选择

数据库 类型 部署 亿级数据 特点
Milvus 独立服务 Docker/K8s 国内最流行,功能全
Qdrant 独立服务 / 嵌入式 Rust 写的,性能好 近 2 年崛起,推荐
Weaviate 独立服务 Docker 模块化强,GraphQL 接口
pgvector PostgreSQL 扩展 装 PG + 扩展 ⚠️ 亿级吃力 已用 PG 的项目首选
Chroma 嵌入式 进程内 ⚠️ 百万级 开发测试用
Pinecone SaaS 云服务 全托管,最省心
Elasticsearch 8+ 搜索引擎 集群 已有 ES 的项目可复用

选型建议

  • 个人站 / 小项目:Chroma(开发)+ pgvector(生产)
  • 中型生产:Qdrant 或 Milvus(单节点能扛千万级)
  • 大型生产:Milvus 集群 / Pinecone / 阿里云向量检索
  • 已有 PG:pgvector 直接上

pgvector 示例(推荐给已经有 PG 的项目)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 启用扩展
CREATE EXTENSION IF NOT EXISTS vector;

-- 建表
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536), -- OpenAI 维度
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);

-- 创建 HNSW 索引(推荐,性能好)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

-- 检索
SELECT id, content, metadata,
1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 10;

关键参数:距离度量

度量 公式 适用
Cosine(余弦) 1 - cos(θ) 最常用,文本 embedding 默认
L2(欧氏距离) √(Σ(a-b)²) 图像 embedding
Dot Product(点积) Σ(a·b) 已归一化的向量

OpenAI、BGE、Cohere 都用 Cosine,所以建索引时选 vector_cosine_ops


Step 5:检索(Retrieval)— 召回阶段

5.1 向量召回(最基本)

1
2
3
4
5
6
7
8
9
# Python 伪代码
def retrieve(query: str, top_k=10):
query_embedding = embed(query)
results = vector_db.search(
vector=query_embedding,
top_k=top_k,
filters={"category": "tech-doc"} # 可选的元数据过滤
)
return results

问题:纯向量检索有两个硬伤:

  1. 关键词丢失:搜「Redis Cluster 部署」,可能召回到「Redis Cluster 介绍」(不匹配)
  2. 长 query 失真:用户的复杂问题,向量化后语义模糊

5.2 Hybrid Search(混合检索)— 生产必备

关键词检索(BM25)+ 向量检索的结果合并,互补。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def hybrid_retrieve(query: str, top_k=10):
# BM25(关键词)
bm25_results = elasticsearch.search(query, top_k=top_k)

# 向量(语义)
vector_results = vector_db.search(embed(query), top_k=top_k)

# 互惠排序融合(Reciprocal Rank Fusion, RRF)
merged = reciprocal_rank_fusion([
bm25_results,
vector_results
], k=60)

return merged[:top_k]

RRF 算法(极简但好用):

1
2
3
4
5
6
7
def reciprocal_rank_fusion(rank_lists, k=60):
"""k 是常数,60 是经验值"""
scores = {}
for rank_list in rank_lists:
for rank, doc_id in enumerate(rank_list):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])

为什么 RRF 这么流行?

  • 不需要训练,不用学权重
  • 对不同检索源的可信度不敏感(BM25 和向量谁更重要不用纠结)
  • 实现简单,10 行代码搞定

5.3 Reranking(重排序)— 让 Top-K 更精准

向量召回 Top-10 之后,再用专门的重排序模型精排,效果提升 10-30%。

1
2
向量召回(快、模糊)     Rerank(慢、精准)
1000 文档 → Top-100 → Top-10

主流 Rerank 模型:

模型 性能 速度 价格
BAAI/bge-reranker-v2-m3 免费自部署
Cohere rerank-3 最高 $2/1000次
Jina jina-reranker-v2 $0.018/1K
1
2
3
4
5
6
7
8
9
10
# 用 bge-reranker 精排
from sentence_transformers import CrossEncoder

reranker = CrossEncoder('BAAI/bge-reranker-v2-m3')

def rerank(query, candidates, top_k=10):
pairs = [[query, c['content']] for c in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
return [doc for doc, _ in ranked[:top_k]]

5.4 元数据过滤(Filter)

实际场景几乎一定要加元数据过滤:

1
2
3
4
5
6
7
8
9
10
# 只搜某个时间范围的
results = vector_db.search(
vector=query_embedding,
top_k=10,
filters={
"created_at": {"$gte": "2026-01-01"},
"category": "tech-doc",
"author": "sherwin"
}
)

常见过滤维度:

  • 时间范围(按 created_at / updated_at)
  • 文档类型(按 category / doc_type)
  • 部门 / 作者(按 org / author)
  • 权限(按 access_level)

Step 6:Query 改写(Query Rewriting)

为什么需要?

用户的问题往往表述模糊

  • 口语化:「Redis 挂了咋办?」
  • 缺上下文:「这个 bug 怎么修?」(没有上下文,根本不知道哪个 bug)
  • 多跳问题:「A 公司和 B 公司的区别,以及 C 公司的策略」

Query 改写 / 扩展 / HyDE(生成假设文档再检索)能显著提升召回。

主流做法

A. Query 扩展(最简单)

1
2
3
4
5
6
def expand_query(query: str) -> list[str]:
prompt = f"""请生成 3 个与下面问题语义相同但表述不同的查询,用于检索增强。
原查询:{query}
输出(每行一个):"""
expanded = llm.invoke(prompt)
return [query] + expanded.split('\n')

B. HyDE(Hypothetical Document Embeddings)

用 LLM 先根据问题生成一段假设性答案,再把这段答案去检索(因为答案的语义空间比问题更接近目标文档)。

1
2
3
4
def hyde_retrieve(query, top_k=10):
hypothetical_answer = llm.invoke(f"用 3 句话回答:{query}")
embedding = embed(hypothetical_answer)
return vector_db.search(embedding, top_k)

C. Multi-Query(推荐生产用)

并行跑多个改写后的 query,合并结果:

1
2
3
4
5
6
def multi_query_retrieve(query, top_k=10):
queries = expand_query(query)
all_results = []
for q in queries:
all_results.extend(retrieve(q, top_k=5))
return dedupe_and_rerank(all_results)[:top_k]

Step 7:Prompt 组装

模板

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

要求:
1. 只基于参考资料中的信息回答,不要编造
2. 如果参考资料不包含答案,请直接说「资料中没有相关信息」
3. 回答末尾引用来源(用 [1][2] 这样的角标)

【参考资料】
[1] {doc1.content}
[2] {doc2.content}
[3] {doc3.content}
...

【用户问题】
{user_query}

【回答】

进阶:Few-shot

如果同一个 RAG 系统服务多个场景,可以在 system 里加 few-shot:

1
2
3
4
示例 1:
问:Redis 怎么持久化?
答:Redis 提供两种持久化方式:RDB(快照)和 AOF(追加日志)[1]。
RDB 是定时把内存数据 dump 到磁盘...

关键参数

  • Context 长度:最多塞 3-5 个文档块,超过要 Rerank 砍掉
  • Prompt 顺序:参考资料放在用户问题之前,效果更好
  • 引用标号:让模型自然引用 [1][2],便于前端展示

Step 8:生成与后处理

生成

直接调 LLM API(OpenAI / Claude / DeepSeek / 通义 / 文心)

后处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def post_process(answer: str, source_docs: list) -> dict:
# 1. 提取引用
citations = extract_citations(answer) # [1][2] → 文档对象

# 2. 验证:answer 里出现的实体是否都在 source 里(防幻觉)
confidence_score = verify_answer(answer, source_docs)

# 3. 兜底:置信度低时给个免责提示
if confidence_score < 0.6:
answer += "\n\n⚠️ 以上回答置信度较低,建议人工核实。"

return {
"answer": answer,
"sources": [source_docs[i] for i in citations],
"confidence": confidence_score
}

Java 实战:Spring AI 版

现在 Java 生态也能写 RAG 了。Spring AI 1.0 GA 后,RAG 已经成为一等公民。

Maven 依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
embedding:
options:
model: text-embedding-3-small
datasource:
url: jdbc:postgresql://localhost:5432/javai
username: javai
password: ${DB_PASSWORD}

完整 RAG 服务(不到 100 行)

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
60
61
62
63
64
65
@Service
public class RagService {

private final ChatClient chatClient;
private final VectorStore vectorStore;
private final DocumentReader documentReader;
private final TokenTextSplitter textSplitter;

public RagService(ChatClient.Builder builder, VectorStore vectorStore) {
this.chatClient = builder.build();
this.vectorStore = vectorStore;
this.textSplitter = new TokenTextSplitter(512, 50);
}

/**
* 离线:加载文档入向量库
*/
public int ingest(Resource pdfResource) {
// 1. 读取文档
List<Document> docs = new PagePdfDocumentReader(pdfResource).read();

// 2. 切块
List<Document> chunks = textSplitter.split(docs);

// 3. 加元数据(用于过滤)
chunks.forEach(c -> {
c.getMetadata().put("source", pdfResource.getFilename());
c.getMetadata().put("category", "tech-doc");
c.getMetadata().put("ingested_at", Instant.now().toString());
});

// 4. 向量化 + 入库(Spring AI 自动调 Embedding API)
vectorStore.add(chunks);

return chunks.size();
}

/**
* 在线:RAG 问答
*/
public String ask(String question) {
// 1. 检索 Top-5
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5)
);

// 2. 拼 context
String context = relevantDocs.stream()
.map(d -> "[%s] %s".formatted(d.getId(), d.getContent()))
.collect(Collectors.joining("\n\n"));

// 3. 调 LLM(带检索结果作为 system 上下文)
return chatClient.prompt()
.system("""
你是 javai.tech 的技术助手。只基于以下资料回答。
不知道就直说,不要编造。

资料:
%s
""".formatted(context))
.user(question)
.call()
.content();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {
private final RagService ragService;

@PostMapping("/ingest")
public ApiResponse<Integer> ingest(@RequestParam("file") MultipartFile file) {
Resource resource = file.getResource();
return ApiResponse.success(ragService.ingest(resource));
}

@PostMapping("/ask")
public ApiResponse<String> ask(@RequestBody QuestionRequest req) {
return ApiResponse.success(ragService.ask(req.getQuestion()));
}
}

整个 RAG 服务就这两段:离线入库 + 在线问答


LangChain 版(Python 对照)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA

# 离线
loader = PyPDFLoader("docs/tech-handbook.pdf")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)
chunks = splitter.split_documents(docs)

vectorstore = Chroma.from_documents(chunks, OpenAIEmbeddings())
vectorstore.persist()

# 在线
qa = RetrievalQA.from_chain_type(
llm=ChatOpenAI(model="gpt-4o-mini"),
retriever=vectorstore.as_retriever(search_type="mmr", k=5),
return_source_documents=True
)
answer = qa.invoke({"query": "Redis 怎么持久化?"})

生产级必须考虑的事

1. 召回质量评估

别凭感觉上线。必须有量化指标。

主流指标:

  • Recall@K:检索 Top-K 中包含正确答案的比例
  • MRR(Mean Reciprocal Rank):正确答案的排名倒数平均
  • nDCG@K:考虑相关性的排序质量
  • Faithfulness:答案是否基于资料(用 LLM 评分)

工具:

  • RAGAS — 最流行的 RAG 评估框架
  • ARES — 学术派

2. 权限隔离(企业必做)

企业内部 RAG 不能「人人看到所有文档」:

  • 入库时打 access_level / department 标签
  • 检索时强制带 user_department 过滤条件
  • 否则就是数据泄露事故
1
2
3
4
5
6
7
8
9
def retrieve_with_permission(user, query):
return vector_db.search(
vector=embed(query),
filter={
"department": {"$in": user.allowed_departments},
"access_level": {"$lte": user.clearance_level}
},
top_k=10
)

3. 缓存与去重

同一问题反复问?缓存:

1
2
3
4
5
6
7
8
9
10
11
import hashlib
from functools import lru_cache

def ask_with_cache(question):
cache_key = hashlib.md5(question.encode()).hexdigest()
if cache_key in redis_cache:
return redis_cache[cache_key]

answer = rag_service.ask(question)
redis_cache.setex(cache_key, 3600, answer) # 缓存 1 小时
return answer

4. 监控告警

必备指标:

  • 每秒查询数(QPS)
  • 平均响应时间(拆 Embedding / 检索 / LLM 三段)
  • 召回率(用 RAGAS 跑离线测试集)
  • 答案被点「无帮助」的比例
  • 成本:每千次查询消耗多少 token / 元

工具:Prometheus + Grafana,或者用 Langfuse / LangSmith。

5. 成本控制

一次 RAG 查询的成本:

1
2
3
4
5
6
7
Embedding 输入:500 token
Embedding 输出:1 次 API 调用
LLM 输入:2000 token(system + context + question)
LLM 输出:300 token
────────────────────
DeepSeek-V3:约 0.002 元
GPT-4o:约 0.05 元

10 万次/月:

  • DeepSeek:200 元
  • GPT-4o:5000 元

生产初期用 DeepSeek / 通义千问 / Claude Haiku 这种性价比模型跑通,质量不够再升级。


一句话总结

RAG 的本质 = Embedding 检索 + Prompt 上下文拼接
难点不在「调通 API」,而在 Chunking 策略、Hybrid Search、Rerank、评估、权限、性能这五大生产环节。

进阶阅读


最后更新:2026-09-01