本节目标:掌握 RAG 的完整架构与实现,让大模型能基于你的私有知识库来回答问题,解决幻觉和知识过时的问题。
一、为什么需要 RAG?
1.1 大模型的三大痛点
arduino
痛点1:知识过时
用户:"2026年3月的GDP数据是多少?"
模型:"我的知识截止到训练时间,无法回答最新数据。"
痛点2:缺少私有知识
用户:"我们公司的请假制度是什么?"
模型:"我不了解你公司的内部制度。"
痛点3:容易产生幻觉
用户:"产品X的技术规格是什么?"
模型:(可能编造一个看起来合理但错误的答案)
1.2 RAG 如何解决这些问题?
RAG = Retrieval-Augmented Generation = 检索增强生成
核心思想:先搜索,再回答。就像开卷考试------允许你翻书找答案。
不用 RAG(闭卷考试):
用户提问 → 模型凭记忆回答 → 可能答错
使用 RAG(开卷考试):
用户提问 → 先从知识库搜索相关内容 → 模型基于搜索结果回答 → 有据可依
scss
┌────────────────────────────────────────────────────────┐
│ RAG 核心流程 │
│ │
│ 用户提问 │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ 检索 │────→│ 知识库 │ │
│ │ (Retrieval)│←────│ (向量数据库) │ │
│ └──────┬──────┘ └──────────────────┘ │
│ │ 相关文档 │
│ ▼ │
│ ┌─────────────┐ │
│ │ 增强 │ 将检索到的文档 + 用户问题 │
│ │ (Augment) │ 组合成完整的 Prompt │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 生成 │ 大模型基于检索内容生成回答 │
│ │ (Generate) │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ 回答用户 │
└────────────────────────────────────────────────────────┘
二、RAG 完整架构
2.1 两个阶段
RAG 系统分为两个阶段:离线索引 和 在线查询。
markdown
阶段一:离线索引(准备阶段,只做一次)
原始文档 处理流程
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
│ PDF │ │ │ │ │ │ │ │ │
│ Word │ ──→ │ 加载 │ ─→ │ 清洗 │ ─→ │ 分块 │ ─→ │ Embedding│
│ 网页 │ │ │ │ │ │ │ │ + 存储 │
│ ... │ └──────┘ └──────┘ └──────┘ └──────────┘
└──────┘ │
▼
向量数据库
阶段二:在线查询(每次用户提问时)
用户提问 → Embedding → 向量搜索 → 取出相关文档 → 生成回答
2.2 文档分块(Chunking)------ 最关键的步骤
为什么要分块?因为一篇文档可能有几万字,但每次检索只需要其中相关的一小段。
markdown
一篇10000字的文档不能整个塞进 Embedding:
1. Embedding 模型有最大长度限制(通常 512-8192 Token)
2. 太长的文本,Embedding 质量会下降
3. 检索时返回整篇文档,会包含大量无关内容
所以需要"切块":
┌─────────────────────────────────────┐
│ 一篇 10000 字的文档 │
│ │
│ ┌─────────┐ ← 块1:第1-500字 │
│ │ Chunk 1 │ │
│ └─────────┘ │
│ ┌─────────┐ ← 块2:第400-900字 │
│ │ Chunk 2 │ (有100字重叠) │
│ └─────────┘ │
│ ┌─────────┐ ← 块3:第800-1300字 │
│ │ Chunk 3 │ │
│ └─────────┘ │
│ ... │
└─────────────────────────────────────┘
分块策略对比:
scss
┌─────────────────┬──────────────────────────────────────┐
│ 分块方式 │ 说明 │
├─────────────────┼──────────────────────────────────────┤
│ 固定长度分块 │ 每 500 字切一刀 │
│ (Fixed Size) │ 简单粗暴,可能切断句子 │
│ │ │
│ 按句子/段落分块 │ 以句号或段落为边界 │
│ (Sentence) │ 保证语义完整性 │
│ │ │
│ 递归分块 │ 先按段落切,太长再按句子切 │
│ (Recursive) │ LangChain 默认方式,推荐 │
│ │ │
│ 语义分块 │ 用模型判断哪里是语义边界 │
│ (Semantic) │ 效果最好但最慢 │
└─────────────────┴──────────────────────────────────────┘
块大小建议:
• 一般推荐 300-1000 字/块
• 重叠 50-200 字(避免信息在边界被切断)
三、从 Naive RAG 到 Advanced RAG
3.1 Naive RAG(基础版)
python
"""最简单的 RAG 实现"""
# 1. 加载文档
documents = load_documents("./docs/")
# 2. 分块
chunks = split_into_chunks(documents, chunk_size=500, overlap=100)
# 3. 生成 Embedding 并存储
embeddings = embedding_model.encode(chunks)
vector_db.add(chunks, embeddings)
# 4. 用户提问时检索并回答
def ask(question):
# 检索最相关的3个块
relevant_chunks = vector_db.search(question, top_k=3)
# 组装 Prompt
prompt = f"""基于以下参考资料回答用户的问题。
如果参考资料中没有相关信息,请说"我在知识库中没有找到相关信息"。
参考资料:
{chr(10).join(relevant_chunks)}
用户问题:{question}
"""
return llm.generate(prompt)
Naive RAG 的问题:
arduino
问题1:用户提问质量差
用户:"那个东西怎么用?" → 检索不到有用的内容
问题2:检索结果不准
检索到的文档可能和问题表面相似但实际不相关
问题3:上下文不足
一个问题可能需要多个文档的信息才能完整回答
问题4:答案不完整
模型可能只用了部分检索结果
3.2 Advanced RAG 技术
(1)查询改写(Query Rewriting)
用户的原始提问可能不够精确,先优化一下再去搜索:
arduino
原始提问:"那个东西怎么用?"
│
┌───────┴───────┐
▼ ▼
LLM 改写 HyDE(假设文档嵌入)
│ │
▼ ▼
"公司OA系统 生成一个假设的
如何提交 完美答案文档,
请假申请?" 再用它去搜索
python
# 查询改写示例
def rewrite_query(original_query: str) -> str:
prompt = f"""请将以下模糊的用户问题改写为更清晰、更具体的搜索查询。
保持原意,但使关键信息更明确。
原始问题:{original_query}
改写后的查询:"""
return llm.generate(prompt)
# "那个东西怎么用" → "公司OA系统的使用方法和操作流程"
python
# HyDE(假设文档嵌入)示例
def hyde_search(query: str):
# 第1步:让 LLM 生成一个假设的完美答案
hypothetical_doc = llm.generate(
f"请写一段详细的文档来回答:{query}"
)
# 第2步:用假设文档的 Embedding 去搜索(而不是用原始问题)
results = vector_db.search(hypothetical_doc, top_k=5)
return results
(2)多查询检索(Multi-Query)
把一个问题拆成多个角度,分别检索再合并:
arduino
原始问题:"Python 和 Java 哪个更适合做 Web 开发?"
│
┌───────┼───────┐
▼ ▼ ▼
子查询1 子查询2 子查询3
"Python "Java "Web开发
Web开发 Web开发 语言对比"
框架" 框架"
│ │ │
▼ ▼ ▼
检索结果1 检索结果2 检索结果3
│ │ │
└───────┼───────┘
▼
合并去重排序
│
▼
生成最终回答
(3)Self-RAG(自反思 RAG)
让模型自我判断是否需要检索、检索结果是否有用:
rust
┌────────────────────────────────────────────────────┐
│ Self-RAG 流程 │
│ │
│ 用户提问 │
│ │ │
│ ▼ │
│ 需要检索吗? ──否──→ 直接回答 │
│ │是 │
│ ▼ │
│ 检索文档 │
│ │ │
│ ▼ │
│ 检索结果相关吗? ──否──→ 重新检索/换个方式 │
│ │是 │
│ ▼ │
│ 生成回答 │
│ │ │
│ ▼ │
│ 回答有事实支撑吗? ──否──→ 重新生成 │
│ │是 │
│ ▼ │
│ 输出回答 │
└────────────────────────────────────────────────────┘
四、Graph RAG(知识图谱增强 RAG)
4.1 传统 RAG 的局限
arduino
问题:"谁是李明的直接上级的妻子?"
传统 RAG 检索到:
文档1:"李明在技术部工作,向张伟汇报"
文档2:"张伟已婚,妻子是王丽"
传统 RAG 可能找不到完整信息链,因为这两个文档的关联性不高。
4.2 Graph RAG 的解决方案
知识图谱:
李明 ──上级──→ 张伟 ──妻子──→ 王丽
│ │
└──部门──→ 技术部
通过图谱可以沿着关系链找到:
李明 → 上级 → 张伟 → 妻子 → 王丽 ✓
┌─────────────────────────────────────────────────────┐
│ Graph RAG vs 传统 RAG │
├─────────────────┬───────────────────────────────────┤
│ 传统 RAG │ Graph RAG │
├─────────────────┼───────────────────────────────────┤
│ 基于文本块检索 │ 基于实体和关系检索 │
│ 适合简单问答 │ 适合多跳推理 │
│ 可能遗漏关联信息 │ 能追踪实体间的关系链 │
│ 上下文是平铺的 │ 上下文是结构化的 │
└─────────────────┴───────────────────────────────────┘
五、RAG 评估
5.1 评估维度
sql
┌─────────────────────────────────────────────────────┐
│ RAG 评估四维度 │
│ │
│ 1. 检索质量 │
│ └── 检索到的文档真的和问题相关吗? │
│ 指标:Precision@K, Recall@K, MRR │
│ │
│ 2. 回答正确性 │
│ └── 模型的回答和标准答案一致吗? │
│ 指标:Accuracy, F1 │
│ │
│ 3. 回答忠实性(Faithfulness) │
│ └── 模型的回答是否基于检索到的文档? │
│ 还是自己编造的? │
│ 指标:Faithfulness Score │
│ │
│ 4. 回答相关性 │
│ └── 模型的回答是否切中了用户的问题? │
│ 指标:Answer Relevancy │
└─────────────────────────────────────────────────────┘
5.2 使用 RAGAS 评估
python
# pip install ragas
from ragas import evaluate
from ragas.metrics import (
faithfulness, # 回答是否基于检索文档
answer_relevancy, # 回答是否切题
context_precision, # 检索精度
context_recall, # 检索召回率
)
# 准备评估数据
eval_data = {
"question": ["公司的年假制度是什么?"],
"answer": ["公司规定员工入职满一年后可享受5天年假..."],
"contexts": [["员工手册第3章:年假制度..."]],
"ground_truth": ["入职满一年享受5天年假,满三年10天..."]
}
results = evaluate(
dataset=eval_data,
metrics=[faithfulness, answer_relevancy,
context_precision, context_recall]
)
print(results)
# {'faithfulness': 0.92, 'answer_relevancy': 0.88,
# 'context_precision': 0.85, 'context_recall': 0.78}
六、完整 RAG 实战
python
"""
完整的 RAG 系统实现(使用 LangChain + Chroma + OpenAI)
"""
from langchain_community.document_loaders import (
PyPDFLoader, TextLoader, DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# ========== 阶段一:离线索引 ==========
# 1. 加载文档
loader = DirectoryLoader(
"./docs/",
glob="**/*.pdf",
loader_cls=PyPDFLoader
)
documents = loader.load()
print(f"加载了 {len(documents)} 页文档")
# 2. 分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块最大500字
chunk_overlap=100, # 块之间重叠100字
separators=["\n\n", "\n", "。", "!", "?", ",", " "]
)
chunks = text_splitter.split_documents(documents)
print(f"切分为 {len(chunks)} 个块")
# 3. 存入向量数据库
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
# ========== 阶段二:在线查询 ==========
# 4. 创建检索器
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3} # 返回最相关的3个块
)
# 5. 定义 Prompt 模板
prompt = ChatPromptTemplate.from_template("""
你是一个专业的知识库问答助手。请基于以下参考资料回答用户的问题。
规则:
1. 只基于参考资料中的信息回答,不要编造
2. 如果参考资料中没有相关信息,明确告知用户
3. 回答要简洁准确,并标注信息来源
参考资料:
{context}
用户问题:{question}
""")
# 6. 构建 RAG Chain
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 7. 提问!
answer = rag_chain.invoke("公司的年假制度是什么?")
print(answer)
七、RAG vs 长上下文
markdown
┌────────────────────────────────────────────────────────────┐
│ RAG vs 长上下文 ------ 什么时候用哪个? │
├──────────────┬──────────────┬──────────────────────────────┤
│ 维度 │ RAG │ 长上下文(1M Token) │
├──────────────┼──────────────┼──────────────────────────────┤
│ 知识库大小 │ 不限 │ 受窗口限制(几本书) │
│ 实时更新 │ ✓ 随时加文档 │ ✗ 每次都要重传 │
│ 精确度 │ 取决于检索 │ 模型直接看原文,更准 │
│ 成本 │ 检索免费 │ 全部按 Token 付费,贵 │
│ 延迟 │ 检索很快 │ 处理长文本较慢 │
│ 适合场景 │ 大量文档 │ 少量文档需深度理解 │
│ │ FAQ、客服 │ 代码审查、合同分析 │
└──────────────┴──────────────┴──────────────────────────────┘
结论:大多数企业场景用 RAG;
少量文档需要深度分析时用长上下文。
两者也可以结合使用。
八、常见问题排查
arduino
问题1:检索到了文档但回答不对
→ 检查分块大小是否合理(太小丢失上下文,太大混入噪音)
→ 检查 Prompt 是否明确要求基于文档回答
问题2:检索不到相关文档
→ 试试查询改写(Query Rewriting)
→ 检查 Embedding 模型是否适合你的语言/领域
→ 尝试混合检索(向量 + 关键词)
问题3:回答说"没有找到相关信息"但其实有
→ 增大 top_k(检索更多结果)
→ 降低相似度阈值
→ 检查文档是否被正确加载和分块
问题4:回答太慢
→ 检查是否每次都在重新计算 Embedding
→ 使用更小的 Embedding 模型
→ 加缓存(相同问题直接返回之前的答案)
九、本篇小结
ini
┌─────────────────────────────────────────────────────┐
│ 本篇知识地图 │
│ │
│ RAG = 先搜索再回答(开卷考试) │
│ │
│ 核心流程:加载 → 分块 → Embedding → 存储 → 检索 → 生成 │
│ │
│ 演进路线: │
│ Naive RAG → Advanced RAG → Modular RAG │
│ │ │ │ │
│ 简单搜索 查询改写 组件可插拔 │
│ 直接回答 多路检索 自适应策略 │
│ 重排序 │
│ │
│ 高级变种: │
│ ├── Graph RAG → 多跳推理、关系追踪 │
│ ├── Self-RAG → 自我反思、质量控制 │
│ └── Agentic RAG → Agent 驱动的智能 RAG │
│ │
│ 评估关注:检索质量 + 回答正确性 + 忠实性 + 相关性 │
└─────────────────────────────────────────────────────┘
十、扩展学习资源
必读
- RAG 原始论文 ------ Facebook 提出的 RAG 方法
- LangChain RAG 教程 ------ 官方 RAG 教程
- LlamaIndex RAG 教程 ------ 另一个优秀的 RAG 框架
推荐
- Advanced RAG 技术综述 ------ RAG 最新技术总结
- Graph RAG(微软) ------ 微软的 Graph RAG 开源实现
- RAGAS 文档 ------ RAG 评估框架
- Chunking 策略指南 ------ 文档分块最佳实践
动手实践
- 用自己公司的文档搭建一个 RAG 问答系统
- 对比不同分块大小(200/500/1000字)对检索质量的影响
- 实现查询改写,对比改写前后的回答质量
下一篇章预告 :将讲解 Function Calling 与工具使用(Tool Use)------让大模型不仅能"说",还能"做",比如查天气、查数据库、发邮件。
觉得有用的话,点个关注吧!大模型方面你想看什么?留言区说,我来写。
声明:本博客内容素材来源于网络,文章由AI技术辅助生成。如有侵权或不当引用,请联系作者进行下架或删除处理。