RAG 的详细过程是什么?从文档加载到答案生成的完整链路
提问
RAG 的详细过程是什么?每个环节具体怎么做的?生产环境要注意什么?
参考答案 RAG(Retrieval-Augmented Generation,检索增强生成)的本质,是让 LLM 在回答问题前先查一下资料 ——把检索到的相关文档塞进 Prompt,让模型基于这些「事实」作答,而不是凭训练时学到的旧知识「瞎编」。
它解决的是 LLM 三大短板:
幻觉 :模型不知道的事会一本正经胡说
知识截止 :训练数据之后发生的事它完全不知道
私有数据 :企业内部文档、产品手册、合同条款,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 描述
关键坑
PDF 表格乱掉 :PyMuPDF 提取的表格是散落的字符流,要用 pdfplumber 或 camelot 专门处理
编码乱码 :中文 PDF 常见 GBK/UTF-8 混用,统一用 chardet 自动检测
图片里的文字 :用 OCR 提取后,再合并到正文
重复内容 :同一份文档在不同地方存了多份,要按 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 RecursiveCharacterTextSplittersplitter = 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 redef 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 OpenAIclient = 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 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 ), metadata JSONB, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);SELECT id, content, metadata, 1 - (embedding <=> $1 ::vector) AS similarity FROM documentsORDER BY embedding <=> $1 ::vectorLIMIT 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 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
问题 :纯向量检索有两个硬伤:
关键词丢失 :搜「Redis Cluster 部署」,可能召回到「Redis Cluster 介绍」(不匹配)
长 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_results = elasticsearch.search(query, top_k=top_k) vector_results = vector_db.search(embed(query), top_k=top_k) 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 from sentence_transformers import CrossEncoderreranker = 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 : citations = extract_citations(answer) confidence_score = verify_answer(answer, source_docs) 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) { List<Document> docs = new PagePdfDocumentReader (pdfResource).read(); List<Document> chunks = textSplitter.split(docs); chunks.forEach(c -> { c.getMetadata().put("source" , pdfResource.getFilename()); c.getMetadata().put("category" , "tech-doc" ); c.getMetadata().put("ingested_at" , Instant.now().toString()); }); vectorStore.add(chunks); return chunks.size(); } public String ask (String question) { List<Document> relevantDocs = vectorStore.similaritySearch( SearchRequest.query(question).withTopK(5 ) ); String context = relevantDocs.stream() .map(d -> "[%s] %s" .formatted(d.getId(), d.getContent())) .collect(Collectors.joining("\n\n" )); 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 PyPDFLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_openai import OpenAIEmbeddings, ChatOpenAIfrom langchain_community.vectorstores import Chromafrom langchain.chains import RetrievalQAloader = 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 评分)
工具:
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 hashlibfrom functools import lru_cachedef 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) 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