RAG检索增强生成

0 阅读10分钟

本节目标:掌握 RAG 的完整架构与实现,让大模型能基于你的私有知识库来回答问题,解决幻觉和知识过时的问题。


一、为什么需要 RAG?

1.1 大模型的三大痛点

痛点1:知识过时
  用户:"2026年3月的GDP数据是多少?"
  模型:"我的知识截止到训练时间,无法回答最新数据。"

痛点2:缺少私有知识
  用户:"我们公司的请假制度是什么?"
  模型:"我不了解你公司的内部制度。"

痛点3:容易产生幻觉
  用户:"产品X的技术规格是什么?"
  模型:(可能编造一个看起来合理但错误的答案)

1.2 RAG 如何解决这些问题?

RAG = Retrieval-Augmented Generation = 检索增强生成

核心思想:先搜索,再回答。就像开卷考试——允许你翻书找答案。

不用 RAG(闭卷考试):
  用户提问 → 模型凭记忆回答 → 可能答错

使用 RAG(开卷考试):
  用户提问 → 先从知识库搜索相关内容 → 模型基于搜索结果回答 → 有据可依
┌────────────────────────────────────────────────────────┐
│                    RAG 核心流程                         │
│                                                        │
│   用户提问                                              │
│     │                                                  │
│     ▼                                                  │
│   ┌─────────────┐     ┌──────────────────┐             │
│   │  检索        │────→│  知识库           │             │
│   │  (Retrieval)│←────│  (向量数据库)      │             │
│   └──────┬──────┘     └──────────────────┘             │
│          │ 相关文档                                     │
│          ▼                                             │
│   ┌─────────────┐                                      │
│   │  增强        │  将检索到的文档 + 用户问题              │
│   │  (Augment)   │  组合成完整的 Prompt                  │
│   └──────┬──────┘                                      │
│          │                                             │
│          ▼                                             │
│   ┌─────────────┐                                      │
│   │  生成        │  大模型基于检索内容生成回答              │
│   │  (Generate) │                                      │
│   └──────┬──────┘                                      │
│          │                                             │
│          ▼                                             │
│       回答用户                                          │
└────────────────────────────────────────────────────────┘

二、RAG 完整架构

2.1 两个阶段

RAG 系统分为两个阶段:离线索引在线查询

阶段一:离线索引(准备阶段,只做一次)

  原始文档           处理流程
  ┌──────┐     ┌──────┐    ┌──────┐    ┌──────┐    ┌──────────┐
  │ PDF  │     │      │    │      │    │      │    │          │
  │ Word │ ──→ │ 加载  │ ─→ │ 清洗 │ ─→ │ 分块  │ ─→ │ Embedding│
  │ 网页  │     │      │    │      │    │      │    │ + 存储   │
  │ ...  │     └──────┘    └──────┘    └──────┘    └──────────┘
  └──────┘                                              │
                                                        ▼
                                                   向量数据库

阶段二:在线查询(每次用户提问时)

  用户提问 → Embedding → 向量搜索 → 取出相关文档 → 生成回答

2.2 文档分块(Chunking)—— 最关键的步骤

为什么要分块?因为一篇文档可能有几万字,但每次检索只需要其中相关的一小段。

一篇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 │                       │
  │  └─────────┘                        │
  │     ...                             │
  └─────────────────────────────────────┘

分块策略对比

┌─────────────────┬──────────────────────────────────────┐
│  分块方式        │  说明                                 │
├─────────────────┼──────────────────────────────────────┤
│  固定长度分块     │  每 500 字切一刀                       │
│  (Fixed Size)   │  简单粗暴,可能切断句子                  │
│                 │                                      │
│  按句子/段落分块   │  以句号或段落为边界                    │
│  (Sentence)     │  保证语义完整性                        │
│                 │                                      │
│  递归分块        │  先按段落切,太长再按句子切               │
│  (Recursive)    │  LangChain 默认方式,推荐              │
│                 │                                      │
│  语义分块        │  用模型判断哪里是语义边界                │
│  (Semantic)     │  效果最好但最慢                        │
└─────────────────┴──────────────────────────────────────┘

块大小建议:
  • 一般推荐 300-1000 字/块
  • 重叠 50-200 字(避免信息在边界被切断)

三、从 Naive RAG 到 Advanced RAG

3.1 Naive RAG(基础版)

"""最简单的 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 的问题

问题1:用户提问质量差
  用户:"那个东西怎么用?" → 检索不到有用的内容

问题2:检索结果不准
  检索到的文档可能和问题表面相似但实际不相关

问题3:上下文不足
  一个问题可能需要多个文档的信息才能完整回答

问题4:答案不完整
  模型可能只用了部分检索结果

3.2 Advanced RAG 技术

(1)查询改写(Query Rewriting)

用户的原始提问可能不够精确,先优化一下再去搜索:

原始提问:"那个东西怎么用?"
                │
        ┌───────┴───────┐
        ▼               ▼
   LLM 改写          HyDE(假设文档嵌入)
        │               │
        ▼               ▼
  "公司OA系统      生成一个假设的
   如何提交        完美答案文档,
   请假申请?"     再用它去搜索
# 查询改写示例
def rewrite_query(original_query: str) -> str:
    prompt = f"""请将以下模糊的用户问题改写为更清晰、更具体的搜索查询。
保持原意,但使关键信息更明确。

原始问题:{original_query}
改写后的查询:"""

    return llm.generate(prompt)

# "那个东西怎么用" → "公司OA系统的使用方法和操作流程"
# 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)

把一个问题拆成多个角度,分别检索再合并:

原始问题:"Python 和 Java 哪个更适合做 Web 开发?"
                      │
              ┌───────┼───────┐
              ▼       ▼       ▼
         子查询1    子查询2   子查询3
         "Python   "Java    "Web开发
         Web开发   Web开发   语言对比"
         框架"     框架"
              │       │       │
              ▼       ▼       ▼
         检索结果1  检索结果2  检索结果3
              │       │       │
              └───────┼───────┘
                      ▼
                 合并去重排序
                      │
                      ▼
                 生成最终回答

(3)Self-RAG(自反思 RAG)

让模型自我判断是否需要检索、检索结果是否有用:

┌────────────────────────────────────────────────────┐
│                 Self-RAG 流程                       │
│                                                    │
│  用户提问                                           │
│     │                                              │
│     ▼                                              │
│  需要检索吗? ──否──→ 直接回答                         │
│     │是                                            │
│     ▼                                              │
│  检索文档                                           │
│     │                                              │
│     ▼                                              │
│  检索结果相关吗? ──否──→ 重新检索/换个方式              │
│     │是                                            │
│     ▼                                              │
│  生成回答                                           │
│     │                                              │
│     ▼                                              │
│  回答有事实支撑吗? ──否──→ 重新生成                    │
│     │是                                            │
│     ▼                                              │
│  输出回答                                           │
└────────────────────────────────────────────────────┘

四、Graph RAG(知识图谱增强 RAG)

4.1 传统 RAG 的局限

问题:"谁是李明的直接上级的妻子?"

传统 RAG 检索到:
  文档1"李明在技术部工作,向张伟汇报"
  文档2"张伟已婚,妻子是王丽"

传统 RAG 可能找不到完整信息链,因为这两个文档的关联性不高。

4.2 Graph RAG 的解决方案

知识图谱:

  李明 ──上级──→ 张伟 ──妻子──→ 王丽
   │              │
   └──部门──→ 技术部

通过图谱可以沿着关系链找到:
  李明 → 上级 → 张伟 → 妻子 → 王丽 ✓
┌─────────────────────────────────────────────────────┐
│            Graph RAG vs 传统 RAG                     │
├─────────────────┬───────────────────────────────────┤
│  传统 RAG        │  Graph RAG                        │
├─────────────────┼───────────────────────────────────┤
│ 基于文本块检索    │ 基于实体和关系检索                   │
│ 适合简单问答      │ 适合多跳推理                        │
│ 可能遗漏关联信息  │ 能追踪实体间的关系链                  │
│ 上下文是平铺的    │ 上下文是结构化的                     │
└─────────────────┴───────────────────────────────────┘

五、RAG 评估

5.1 评估维度

┌─────────────────────────────────────────────────────┐
│                RAG 评估四维度                         │
│                                                     │
│  1. 检索质量                                         │
│     └── 检索到的文档真的和问题相关吗?                   │
│         指标:Precision@K, Recall@K, MRR             │
│                                                     │
│  2. 回答正确性                                        │
│     └── 模型的回答和标准答案一致吗?                     │
│         指标:Accuracy, F1                           │
│                                                     │
│  3. 回答忠实性(Faithfulness)                        │
│     └── 模型的回答是否基于检索到的文档?                 │
│         还是自己编造的?                              │
│         指标:Faithfulness Score                     │
│                                                     │
│  4. 回答相关性                                        │
│     └── 模型的回答是否切中了用户的问题?                  │
│         指标:Answer Relevancy                       │
└─────────────────────────────────────────────────────┘

5.2 使用 RAGAS 评估

# 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 实战

"""
完整的 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 长上下文

┌────────────────────────────────────────────────────────────┐
│          RAG vs 长上下文 —— 什么时候用哪个?                   │
├──────────────┬──────────────┬──────────────────────────────┤
│  维度         │  RAG         │  长上下文(1M Token)          │
├──────────────┼──────────────┼──────────────────────────────┤
│  知识库大小    │  不限        │  受窗口限制(几本书)            │
│  实时更新     │  ✓ 随时加文档  │  ✗ 每次都要重传                │
│  精确度       │  取决于检索   │  模型直接看原文,更准            │
│  成本         │  检索免费     │  全部按 Token 付费,贵         │
│  延迟         │  检索很快     │  处理长文本较慢                │
│  适合场景      │  大量文档     │  少量文档需深度理解            │
│              │  FAQ、客服    │  代码审查、合同分析            │
└──────────────┴──────────────┴──────────────────────────────┘

结论:大多数企业场景用 RAG;
     少量文档需要深度分析时用长上下文。
     两者也可以结合使用。

八、常见问题排查

问题1:检索到了文档但回答不对
  → 检查分块大小是否合理(太小丢失上下文,太大混入噪音)
  → 检查 Prompt 是否明确要求基于文档回答

问题2:检索不到相关文档
  → 试试查询改写(Query Rewriting)
  → 检查 Embedding 模型是否适合你的语言/领域
  → 尝试混合检索(向量 + 关键词)

问题3:回答说"没有找到相关信息"但其实有
  → 增大 top_k(检索更多结果)
  → 降低相似度阈值
  → 检查文档是否被正确加载和分块

问题4:回答太慢
  → 检查是否每次都在重新计算 Embedding
  → 使用更小的 Embedding 模型
  → 加缓存(相同问题直接返回之前的答案)

九、本篇小结

┌─────────────────────────────────────────────────────┐
│                   本篇知识地图                        │
│                                                     │
│  RAG = 先搜索再回答(开卷考试)                         │
│                                                     │
│  核心流程:加载 → 分块 → Embedding → 存储 → 检索 → 生成  │
│                                                     │
│  演进路线:                                           │
│  Naive RAG → Advanced RAG → Modular RAG             │
│    │              │              │                  │
│    简单搜索        查询改写        组件可插拔           │
│    直接回答        多路检索        自适应策略           │
│                   重排序                             │
│                                                     │
│  高级变种:                                          │
│  ├── Graph RAG → 多跳推理、关系追踪                   │
│  ├── Self-RAG → 自我反思、质量控制                    │
│  └── Agentic RAG → Agent 驱动的智能 RAG              │
│                                                    │
│  评估关注:检索质量 + 回答正确性 + 忠实性 + 相关性        │
└─────────────────────────────────────────────────────┘

十、扩展学习资源

必读

推荐

动手实践

  • 用自己公司的文档搭建一个 RAG 问答系统
  • 对比不同分块大小(200/500/1000字)对检索质量的影响
  • 实现查询改写,对比改写前后的回答质量

下一篇章预告:将讲解 Function Calling 与工具使用(Tool Use)——让大模型不仅能"说",还能"做",比如查天气、查数据库、发邮件。


觉得有用的话,点个关注吧!大模型方面你想看什么?留言区说,我来写。

声明:本博客内容素材来源于网络,文章由AI技术辅助生成。如有侵权或不当引用,请联系作者进行下架或删除处理。