AI 基础 | | 约 26 分钟 | 10,238 字

RAG 检索增强生成完全指南

从零理解 RAG 架构,包括文档切分、Embedding、检索、生成的完整流程

什么是 RAG

RAG (Retrieval-Augmented Generation) 检索增强生成,是目前让 LLM “拥有私有知识”的最主流方案。

核心思路很简单:用户提问时,先从知识库中检索相关内容,然后把检索到的内容和用户问题一起交给 LLM,让它基于这些上下文生成回答。

传统 LLM:
  用户: "我们公司的退款政策是什么?"
  LLM:  "抱歉,我不了解你们公司的具体政策..."

RAG 增强后:
  用户: "我们公司的退款政策是什么?"
  系统: [检索到退款政策文档] → 交给 LLM
  LLM:  "根据公司政策,购买后 30 天内可以全额退款..."

为什么需要 RAG

LLM 有几个天然的局限:

局限说明RAG 如何解决
知识截止训练数据有截止日期检索最新文档
无私有知识不知道你的公司、产品、内部文档检索私有知识库
幻觉可能编造不存在的信息基于真实文档回答,可溯源
上下文限制不能把所有文档塞进 prompt只检索最相关的几段

RAG vs Fine-tuning vs 长上下文

RAG:
  ✓ 不需要训练,随时更新知识
  ✓ 回答可溯源
  ✓ 成本低
  ✗ 检索质量影响回答质量

Fine-tuning:
  ✓ 模型内化知识,响应更快
  ✓ 适合改变模型的行为风格
  ✗ 需要训练数据和计算资源
  ✗ 知识更新需要重新训练

长上下文 (塞进所有文档):
  ✓ 最简单,不需要额外架构
  ✗ 成本高(按 token 计费)
  ✗ 有上下文窗口限制
  ✗ "大海捞针"问题——文档太多时 LLM 可能忽略关键信息

RAG 架构全景

一个完整的 RAG 系统分为两个阶段:离线索引和在线查询。

架构图

┌─────────────────────────────────────────────────┐
│                  离线索引阶段                      │
│                                                   │
│  文档源 → 文档加载 → 文本切分 → Embedding → 向量数据库  │
│  (PDF,   (Loader)  (Chunking) (Model)    (Store)  │
│   MD,                                             │
│   HTML)                                           │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│                  在线查询阶段                      │
│                                                   │
│  用户提问 → Query Embedding → 向量检索 → 重排序     │
│                                  ↓                │
│            LLM 生成回答 ← 构建 Prompt ← 相关文档    │
└─────────────────────────────────────────────────┘

我们逐步拆解每个环节。


第一步:文档加载

把各种格式的文档读取为纯文本。

# 使用 LangChain 的文档加载器
from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    UnstructuredMarkdownLoader,
    WebBaseLoader,
    CSVLoader
)

# 加载 PDF
pdf_loader = PyPDFLoader("company_handbook.pdf")
pdf_docs = pdf_loader.load()

# 加载 Markdown
md_loader = UnstructuredMarkdownLoader("api_docs.md")
md_docs = md_loader.load()

# 加载网页
web_loader = WebBaseLoader("https://docs.example.com/faq")
web_docs = web_loader.load()

# 合并所有文档
all_docs = pdf_docs + md_docs + web_docs

常见文档源

格式工具注意事项
PDFPyPDF, Unstructured表格和图片需要特殊处理
Markdown直接读取保留标题结构有助于切分
HTMLBeautifulSoup需要清理标签和噪音
Wordpython-docx注意格式转换
数据库SQL 查询需要把结构化数据转为文本
APIHTTP 请求注意频率限制和认证

第二步:文本切分 (Chunking)

这是 RAG 中最关键也最容易被忽视的环节。切分策略直接影响检索质量。

为什么要切分

  • Embedding 模型有输入长度限制(通常 512-8192 tokens)
  • 太长的文本语义会被稀释
  • 检索时需要返回精确的相关段落,而不是整篇文档

常见切分策略

1. 固定大小切分

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)

chunk_overlap 很重要——它确保跨块的信息不会丢失。

2. 按语义切分

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

# 根据语义相似度自动找到切分点
semantic_splitter = SemanticChunker(
    OpenAIEmbeddings(),
    breakpoint_threshold_type="percentile"
)

chunks = semantic_splitter.split_documents(all_docs)

3. 按文档结构切分

from langchain.text_splitter import MarkdownHeaderTextSplitter

# 按 Markdown 标题切分
headers_to_split_on = [
    ("#", "h1"),
    ("##", "h2"),
    ("###", "h3"),
]

md_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on
)

chunks = md_splitter.split_text(markdown_text)

切分参数怎么选

参数推荐值说明
chunk_size200-1000 字符取决于文档类型和查询粒度
chunk_overlapchunk_size 的 10-20%防止信息丢失
分隔符优先级段落 > 句子 > 词尽量保持语义完整

经验法则:如果你的用户问题通常比较具体,用小块(200-500);如果问题比较宽泛,用大块(500-1000)。


第三步:Embedding 与索引

把切分好的文本块转换为向量,存入向量数据库。

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

# 初始化 Embedding 模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 创建向量存储(自动 Embedding + 存储)
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

print(f"已索引 {len(chunks)} 个文本块")

第四步:检索

用户提问时,把问题转换为向量,在向量数据库中搜索最相似的文本块。

基本检索

# 创建检索器
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4}  # 返回 Top 4
)

# 检索
query = "公司的年假政策是什么?"
relevant_docs = retriever.invoke(query)

for doc in relevant_docs:
    print(f"来源: {doc.metadata.get('source', '未知')}")
    print(f"内容: {doc.page_content[:200]}...")
    print()

混合检索

单纯的向量检索有时会漏掉关键词精确匹配的结果。混合检索结合了向量搜索和关键词搜索:

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# 关键词检索器 (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 4

# 向量检索器
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

# 混合检索器(各占 50% 权重)
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5]
)

results = ensemble_retriever.invoke("年假政策")

重排序 (Reranking)

检索到的结果可以用 Reranker 模型重新排序,提高精度:

from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank

# 使用 Cohere Reranker
reranker = CohereRerank(model="rerank-v3.5", top_n=3)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=vector_retriever
)

results = compression_retriever.invoke("年假政策")

第五步:生成

把检索到的文档和用户问题组合成 prompt,交给 LLM 生成回答。

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 定义 prompt 模板
template = """基于以下上下文回答用户的问题。如果上下文中没有相关信息,请说"我在知识库中没有找到相关信息"。

上下文:
{context}

用户问题: {question}

回答:"""

prompt = ChatPromptTemplate.from_template(template)

# 初始化 LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 构建 RAG 链
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 使用
answer = rag_chain.invoke("公司的年假政策是什么?")
print(answer)

完整的简易 RAG Pipeline

把上面的步骤串起来,这是一个可以直接运行的完整示例:

"""
简易 RAG Pipeline
依赖: pip install langchain langchain-openai chromadb
"""
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# ---- 1. 准备文档 ----
documents = [
    "公司年假政策:入职满一年的员工享有 10 天带薪年假,满三年 15 天,满五年 20 天。年假需提前一周申请,经直属主管批准后生效。未使用的年假不可跨年累积。",
    "报销流程:员工需在费用发生后 30 天内提交报销申请。单笔金额超过 5000 元需要部门经理审批,超过 20000 元需要 VP 审批。报销款项将在审批通过后的下一个工资周期发放。",
    "远程办公政策:公司支持混合办公模式。员工每周可选择最多 2 天远程办公,需提前在系统中报备。远程办公日需保证正常工作时间在线,参加所有已安排的会议。",
]

# ---- 2. 切分 ----
splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,
    chunk_overlap=30
)
chunks = splitter.create_documents(documents)

# ---- 3. 索引 ----
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

# ---- 4. 构建 RAG 链 ----
template = """根据以下上下文回答问题。只使用上下文中的信息,不要编造。

上下文:
{context}

问题: {question}"""

prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(model="gpt-4o", temperature=0)

def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# ---- 5. 使用 ----
questions = [
    "年假有多少天?",
    "报销需要谁审批?",
    "可以每天都远程办公吗?",
]

for q in questions:
    print(f"Q: {q}")
    print(f"A: {rag_chain.invoke(q)}")
    print()

RAG 的常见问题与优化

问题 1:检索不到相关内容

原因和解决方案:

  • 切分粒度不对 → 调整 chunk_size
  • Embedding 模型不适合 → 换一个模型试试
  • 查询和文档的表述差异大 → 使用 Query Rewriting
# Query Rewriting: 让 LLM 改写查询
rewrite_prompt = "把以下用户问题改写为更适合搜索的形式:{question}"

问题 2:检索到了但回答不对

  • 检索到的内容不够相关 → 增加 Reranking 步骤
  • 上下文太多导致 LLM 困惑 → 减少 k 值,只保留最相关的
  • Prompt 模板不够好 → 优化 system prompt

问题 3:幻觉问题

# 在 prompt 中明确要求
template = """
规则:
1. 只基于提供的上下文回答
2. 如果上下文中没有答案,明确说"我不知道"
3. 引用具体的来源段落

上下文: {context}
问题: {question}
"""

问题 4:延迟太高

优化方向:
1. 缓存常见查询的结果
2. 使用更快的 Embedding 模型
3. 预计算和预热向量索引
4. 流式输出 LLM 回答

RAG 评估指标

怎么知道你的 RAG 系统好不好?需要从两个维度评估:

检索质量

指标含义计算方式
Recall@KTop K 结果中包含正确答案的比例命中数 / 总问题数
MRR正确答案的平均排名倒数1/排名 的平均值
NDCG考虑排名位置的相关性评分越靠前的相关结果权重越高

生成质量

指标含义评估方式
Faithfulness回答是否忠于上下文LLM 评估或人工评估
Relevance回答是否切题LLM 评估或人工评估
Completeness回答是否完整人工评估
# 使用 ragas 库评估(推荐)
# pip install ragas
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision

result = evaluate(
    dataset=eval_dataset,
    metrics=[faithfulness, answer_relevancy, context_precision]
)
print(result)

进阶:RAG 的演进方向

1. Naive RAG → Advanced RAG → Modular RAG

Naive RAG:     检索 → 生成(最基础)
Advanced RAG:  预处理 → 检索 → 后处理 → 生成
Modular RAG:   可插拔的模块化架构,按需组合

2. 多跳检索 (Multi-hop)

有些问题需要多次检索才能回答:

问题: "负责 AI 项目的部门经理的联系方式是什么?"

第一跳: 检索 → AI 项目由技术部负责
第二跳: 检索 → 技术部经理是张三
第三跳: 检索 → 张三的联系方式是 xxx

3. Self-RAG

让 LLM 自己决定是否需要检索,以及检索结果是否有用:

LLM 判断: "这个问题我需要检索吗?" → 是 → 检索
LLM 判断: "检索结果有用吗?" → 是 → 使用;否 → 重新检索或直接回答

总结

RAG 是目前最实用的 LLM 增强方案。它不需要训练模型,不需要大量计算资源,却能让 LLM 拥有最新的、私有的知识。

构建一个好的 RAG 系统,关键在于:

  • 切分策略要合理——这是最容易被忽视但影响最大的环节
  • 检索要准确——考虑混合检索和重排序
  • Prompt 要明确——告诉 LLM 只基于上下文回答
  • 持续评估和优化——用指标驱动改进

RAG 让 LLM 从”博学但不知道你”变成了”既博学又了解你”。掌握了 RAG,你就掌握了构建企业级 AI 应用的核心能力。下一篇我们来聊另一条路——Fine-tuning 微调。

评论

加载中...

相关文章

分享:

评论

加载中...