RAG 从零到一:构建你的第一个检索增强生成系统
AI 核心技能系列 · 第 5 篇
导语
大模型很强,但它不知道你公司的内部文档、最新产品手册、私有数据。你问它"我们公司的退货政策是什么?"------它只能瞎编。
RAG(Retrieval-Augmented Generation,检索增强生成) 是解决这个问题的最主流方案:先从知识库中检索相关信息,再交给大模型生成回答。简单、有效、不需要训练模型。
RAG 也是企业 LLM 落地最多的应用模式------你能在市场上看到的大部分"企业知识问答"、"智能客服"、"文档助手",底层都是 RAG。
一、为什么需要 RAG
1.1 大模型的三大局限
| 局限 | 表现 | 例子 |
|---|---|---|
| 知识截止 | 不知道训练数据之后发生的事 | "2026 年世界杯谁赢了?"→ 不知道 |
| 幻觉 | 自信地编造不存在的信息 | 编造不存在的论文引用 |
| 无法访问私有数据 | 不知道你公司的内部信息 | 公司制度、产品文档、客户数据 |
1.2 三种解决方案对比
| 方案 | RAG | Fine-tuning | 长上下文 |
|---|---|---|---|
| 原理 | 先检索后生成 | 用数据重新训练模型 | 把所有数据塞进 Prompt |
| 成本 | 低(只需向量库) | 高(需要训练) | 中(Token 费用高) |
| 实时性 | 高(更新文档即可) | 低(重新训练) | 高 |
| 适用数据量 | 大(百万级文档) | 中(万级数据) | 小(受上下文限制) |
| 最佳场景 | 企业知识问答 | 特定风格/格式 | 单次分析少量文档 |
决策指南:90% 的企业 LLM 应用,先试 RAG。Prompt Engineering + RAG 搞不定的,再考虑 Fine-tuning。
1.3 RAG 工作流程概览
arduino
用户提问: "公司的年假政策是什么?"
│
▼
┌──────────────┐
│ 1. 查询处理 │ → 将问题转换为向量
└──────┬───────┘
│
▼
┌──────────────┐
│ 2. 检索 │ → 在向量库中找最相关的文档片段
└──────┬───────┘
│
▼
┌──────────────┐ 检索到的文档:
│ 3. 增强 │ → "员工入职满一年后享有5天年假..."
└──────┬───────┘ "年假需提前3天申请..."
│
▼
┌──────────────┐
│ 4. 生成 │ → LLM 基于检索到的文档生成回答
└──────┬───────┘
│
▼
回答: "根据公司制度,入职满一年后享有5天年假,
需要提前3天向主管提交申请..."
二、RAG 完整流程拆解
2.1 文档加载
python
from langchain_community.document_loaders import (
PyPDFLoader, TextLoader, Docx2txtLoader,
UnstructuredHTMLLoader, CSVLoader
)
# 支持多种格式
pdf_docs = PyPDFLoader("company_handbook.pdf").load()
txt_docs = TextLoader("faq.txt").load()
word_docs = Docx2txtLoader("policy.docx").load()
html_docs = UnstructuredHTMLLoader("product.html").load()
csv_docs = CSVLoader("customer_data.csv").load()
all_docs = pdf_docs + txt_docs + word_docs + html_docs + csv_docs
print(f"加载了 {len(all_docs)} 个文档片段")
2.2 文本分块(Chunking)
文档太长放不进一个 Embedding,需要切分成小块:
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块最大 500 字符
chunk_overlap=50, # 相邻块重叠 50 字符(保持上下文连贯)
separators=["\n\n", "\n", "。", ",", " ", ""], # 按优先级切分
)
chunks = splitter.split_documents(all_docs)
print(f"分块后: {len(chunks)} 个块")
2.3 向量化与存储
python
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# 生成 Embedding 并存入向量库
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
2.4 检索
python
# 基础检索
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5} # 返回 Top 5 最相关的文档块
)
results = retriever.invoke("公司年假政策")
for doc in results:
print(f"[{doc.metadata.get('source', 'unknown')}] {doc.page_content[:100]}...")
2.5 生成
python
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_template("""
基于以下参考资料回答用户的问题。如果资料中没有相关信息,请明确说明"根据现有资料无法回答"。
不要编造任何不在资料中的信息。
参考资料:
{context}
用户问题:{question}
回答:
""")
# 组装 RAG 链
from langchain.schema.runnable import RunnablePassthrough
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
)
answer = rag_chain.invoke("公司的年假政策是什么?")
print(answer.content)
三、分块策略深入对比
分块策略直接影响检索质量------块太大,语义不精确;块太小,丢失上下文。
| 策略 | 做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定长度 | 每 N 个字符切一刀 | 简单 | 可能切断语义 | 快速原型 |
| 递归字符分割 | 按段落→句子→字符递归切分 | 尽量保持语义完整 | 块大小不均匀 | 通用场景(最推荐) |
| 语义分块 | 根据语义相似度变化切分 | 语义最精确 | 计算成本高 | 高质量需求 |
| 文档结构分块 | 按标题、段落、章节切分 | 保持文档结构 | 需要结构化文档 | 技术文档/手册 |
分块大小经验值:
- 通用推荐:chunk_size=500-1000,overlap=50-100
- 精确检索:chunk_size=200-500(小块更精确)
- 上下文丰富:chunk_size=1000-2000(大块信息更完整)
四、检索优化技巧
基础向量检索的效果往往不够好。以下是递进式的优化手段:
4.1 混合检索
python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# BM25 关键词检索
bm25_retriever = BM25Retriever.from_documents(chunks, k=5)
# 向量检索
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 混合:各占 50% 权重
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5]
)
4.2 查询改写(Query Rewriting)
用户的问题可能不够好------太简短、有歧义、或者用词和文档不一致。
python
rewrite_prompt = ChatPromptTemplate.from_template("""
将以下用户问题改写为更适合搜索的形式。生成 3 个不同角度的查询:
原始问题:{question}
改写后的查询(每行一个):
""")
# 用 LLM 改写查询,每个查询分别检索,合并结果
4.3 重排序(Reranking)
初步检索返回 Top 20,用更精准的 Cross-Encoder 模型重排,取 Top 5。
python
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
# 初步检索 Top 20
initial_results = retriever.invoke(query, k=20)
# 重排序
pairs = [(query, doc.page_content) for doc in initial_results]
scores = reranker.predict(pairs)
# 按分数排序,取 Top 5
reranked = sorted(zip(initial_results, scores), key=lambda x: x[1], reverse=True)[:5]
4.4 优化技巧层级
yaml
Level 0: 基础向量检索(baseline)
↓ +15% 效果
Level 1: 混合检索(向量 + BM25)
↓ +10% 效果
Level 2: 查询改写 + 多查询检索
↓ +10% 效果
Level 3: 重排序(Reranking)
↓ +5% 效果
Level 4: 上下文压缩 + 父文档检索
五、RAG 常见问题与调优
| 问题 | 症状 | 诊断 | 解决方案 |
|---|---|---|---|
| 检索不到 | 回答"无法找到相关信息" | 查看检索结果的相似度分数 | 调整分块策略/换 Embedding 模型 |
| 检索到但答案错 | 回答偏离问题 | 检查检索到的文档是否真的相关 | 添加重排序/查询改写 |
| 混入无关信息 | 回答中夹杂不相关内容 | 检索返回了噪声文档 | 提高相似度阈值/减少 Top K |
| 延迟太高 | 响应时间 > 5秒 | 定位是检索慢还是生成慢 | 向量库索引优化/流式输出 |
| 回答太泛 | 没有具体细节 | 分块太大,信息被稀释 | 减小 chunk_size |
| 幻觉 | 编造不在文档中的信息 | Prompt 约束不够强 | 强化"只基于资料回答"的指令 |
六、进阶:生产级 RAG 的注意事项
6.1 数据更新策略
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 全量重建 | 删除旧索引,重新构建 | 数据量小,更新不频繁 |
| 增量更新 | 只添加新文档/更新变化的文档 | 数据量大,实时性要求高 |
| 版本管理 | 每个版本一个索引,切换生效 | 需要回滚能力 |
6.2 权限控制
不同用户应该看到不同的数据。在 metadata 中标记权限:
python
# 存储时带上权限标签
vectorstore.add_documents(
documents=hr_docs,
metadata=[{"access_level": "hr_only", "department": "hr"} for _ in hr_docs]
)
# 检索时过滤
retriever = vectorstore.as_retriever(
search_kwargs={
"k": 5,
"filter": {"access_level": "hr_only"}
}
)
6.3 评估指标
| 指标 | 含义 | 如何计算 |
|---|---|---|
| Context Precision | 检索到的文档中有多少是相关的 | 相关文档数 / 检索文档数 |
| Context Recall | 应该被检索到的文档有多少被找到了 | 被检索到的相关文档数 / 总相关文档数 |
| Faithfulness | 回答是否忠实于检索到的文档 | LLM-as-Judge 评估 |
| Answer Relevancy | 回答是否回答了用户的问题 | LLM-as-Judge 评估 |
七、职业视角
- RAG 是目前企业 LLM 落地最多的方向------AI 应用工程师岗位几乎必考
- 面试必问:RAG 的完整流程?检索效果不好怎么优化?RAG vs Fine-tuning 怎么选?
- 相关岗位:AI 应用工程师、LLM 工程师、搜索工程师
- 建议:自己动手搭一个完整的 RAG Demo 放到 GitHub 上,比任何证书都有说服力
总结
- 为什么 RAG:解决大模型知识截止、幻觉、无法访问私有数据三大问题
- 完整流程:文档加载 → 分块 → 向量化 → 检索 → 生成
- 分块是关键:推荐递归字符分割,chunk_size 500-1000
- 检索优化:混合检索 → 查询改写 → 重排序,递进式提升
- 生产考量:数据更新、权限控制、评估指标缺一不可
本文是 AI 核心技能系列 第 5 篇,共 12 篇。上一篇:Embedding 与向量数据库 | 下一篇:Function Calling:让大模型连接真实世界
关注公众号「coft」,获取完整系列更新、配套代码和学习路线图。一起交流 AI 转行经验,助力职业跃升,迈向高薪岗位。