什么是 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
常见文档源
| 格式 | 工具 | 注意事项 |
|---|---|---|
| PyPDF, Unstructured | 表格和图片需要特殊处理 | |
| Markdown | 直接读取 | 保留标题结构有助于切分 |
| HTML | BeautifulSoup | 需要清理标签和噪音 |
| Word | python-docx | 注意格式转换 |
| 数据库 | SQL 查询 | 需要把结构化数据转为文本 |
| API | HTTP 请求 | 注意频率限制和认证 |
第二步:文本切分 (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_size | 200-1000 字符 | 取决于文档类型和查询粒度 |
| chunk_overlap | chunk_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@K | Top 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 微调。
相关文章
评论
加载中...
评论
加载中...