从零搭建你的第一个 RAG 应用:手把手教程 + 深度理解
2026-06-18 11:00:00 · 标签:RAG、LangChain、OpenAI、GPT、教程、AI工程
阅读指南
这篇文章分为两部分:
- 上篇(手把手教程):如果你从没接触过 RAG,跟着走完就能跑通自己的第一个智能文档问答系统。每一步都有完整代码和解释。
- 下篇(深度理解):如果你已经跑通了 Demo,想知道每个环节"为什么这么选""还有哪些可能性",这部分是为你准备的。
你可以先跟着上篇动手,再读下篇;也可以直接跳到感兴趣的部分。
上篇:手把手教程
前提条件
开始之前,确保你的环境满足以下条件:
- Python 3.10+ 已安装(终端输入
python --version确认) - OpenAI API Key(去 platform.openai.com/api-keys 创建,新账号有免费额度)
- 一个放文档的文件夹(PDF、Markdown、TXT 都行——你可以先用几份产品手册、技术文档或任何你想"问"的文件来测试)
没有 OpenAI API Key? 也可以用开源模型替代——参见《从零搭建你的第一个 RAG 应用(开源模型篇):零成本、全本地、可商用》。
第 1 步:安装依赖
打开终端,创建项目文件夹并安装所需库:
mkdir my-rag-app && cd my-rag-app
pip install langchain langchain-community langchain-openai faiss-cpu pypdf
#或者国内镜像
pip install langchain langchain-community langchain-openai faiss-cpu pypdf -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn
其他可选镜像:https://mirrors.aliyun.com/pypi/simple/、https://pypi.mirrors.ustc.edu.cn/simple/
装了什么?
| 包 | 作用 |
|---|---|
langchain | RAG 流程的核心框架 |
langchain-community | 社区贡献的文档加载器、向量库集成 |
langchain-openai | OpenAI 的 LLM 和 Embedding 接口 |
faiss-cpu | Meta 开源的向量检索库(CPU 版,免配) |
pypdf | 读取 PDF 文件 |
第 2 步:准备你的文档
在项目文件夹里新建一个 data/ 目录,把你想"问"的文档丢进去:
mkdir data
# 把你的 PDF、MD、TXT 文件复制到 data/ 里
比如你可以放一份《员工手册》PDF、几篇技术博客的 Markdown 文件,或者产品的 API 文档。先放 2-3 份文件就好,跑通了再加更多。
第 3 步:加载文档 → 切成小块
这是 RAG 的第一步:把原始文件读进来,切成合适大小的"文本块"(chunk)。
为什么不能直接把整份 PDF 丢给 AI?因为大模型的上下文窗口有限,而且把整本书塞进去,它反而抓不住重点。所以我们要先把文档切成几百字的小块,每次只挑最相关的几块送给 AI。
新建 main.py,写入:
import os
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# ========== 第 3 步:加载与分割文档 ==========
def load_and_split(data_dir="./data"):
"""把 data/ 里的所有文档读进来,切成小块"""
# --- 3.1 加载 PDF ---
pdf_loader = DirectoryLoader(
data_dir,
glob="**/*.pdf",
loader_cls=PyPDFLoader
)
documents = pdf_loader.load()
# --- 3.2 加载 TXT 和 Markdown ---
text_loader = DirectoryLoader(
data_dir,
glob="**/*.{txt,md}",
loader_cls=TextLoader
)
documents += text_loader.load()
if not documents:
raise FileNotFoundError(f"data/ 目录下没找到任何文档,请放入 PDF/TXT/MD 文件。")
# --- 3.3 分割 ---
splitter = RecursiveCharacterTextSplitter(
chunk_size=600, # 每块最多 600 个字符
chunk_overlap=100, # 相邻两块重叠 100 个字符
separators=["\n\n", "\n", "。", ".", " ", ""]
)
chunks = splitter.split_documents(documents)
print(f"加载了 {len(documents)} 个文档,切成 {len(chunks)} 个文本块")
# 打印第一个块看看长什么样
print(f"\n第一个文本块预览:\n{chunks[0].page_content[:300]}...")
return chunks
if __name__ == "__main__":
load_and_split()
跑一下看看效果:
python main.py
你应该看到类似这样的输出:
加载了 3 个文档,切成 47 个文本块
第一个文本块预览:
第3章 请假制度
员工每年享有5天带薪年假...
chunk_size 设多大? - 中文文档:400-800 字比较合适 - 英文文档:256-512 tokens - 太小的块语义不完整,太大的块检索不精确 chunk_overlap 为什么重要? 假设关键信息正好卡在第 300 和第 301 个字之间——如果没有重叠,这句话会被切成两半,AI 永远看不到完整的。100 个字符的重叠像个"安全网",确保关键信息至少完整出现在一个块里。
第 4 步:把文本块变成"向量",存进向量库
这是 RAG 最核心的概念,我们用最直觉的方式理解它:
什么是向量? 把一句话变成一个 1000 多个数字的数组。两句话意思越接近,它们在数学空间里的距离就越近。
什么是向量库? 一个专门存"向量 + 原文"的数据库,可以根据"距离远近"快速找到最相似的内容。
流程是这样的:
你问:"年假有几天?"
↓ Embedding 模型把它变成向量 [0.03, -0.12, 0.47, ...]
↓ 向量库在所有文本块中找距离最近的
↓ 找到:"员工每年享有5天带薪年假..."
↓ 把这句话和你的问题一起发给 AI
↓ AI 回答:"根据员工手册,每年享有5天带薪年假。"
在 main.py 中追加:
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
# ========== 第 4 步:构建向量库 ==========
def build_index(chunks, persist_dir="./vector_store"):
"""把文本块转为向量,存入 FAISS 向量库"""
# 使用 OpenAI 的 Embedding 模型
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small", # 性价比最高,1024维
openai_api_key=os.getenv("OPENAI_API_KEY") # 从环境变量读取
)
# 逐块计算向量,构建索引
vector_store = FAISS.from_documents(chunks, embeddings)
# 保存到磁盘(下次就不用重新计算了)
vector_store.save_local(persist_dir)
print(f"向量库已保存到 {persist_dir}/")
return vector_store
为什么要 save_local? Embedding 计算要调 API,如果你的文档很多,每次都重新计算既慢又花钱。保存后下次直接加载就行。
配置 API Key:
在终端里设置环境变量(每次开新终端都要设一次):
# Mac / Linux
export OPENAI_API_KEY="sk-你的key"
# Windows PowerShell
$env:OPENAI_API_KEY="sk-你的key"
# Windows CMD
set OPENAI_API_KEY=sk-你的key
第 5 步:实现最基础的问答
现在把"检索"和"生成"串起来。追加到 main.py:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# ========== 第 5 步:问答 ==========
def ask(question, vector_store):
"""根据向量库中的文档回答问题"""
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
openai_api_key=os.getenv("OPENAI_API_KEY")
)
# 检索最相关的 4 个文本块
docs = vector_store.similarity_search(question, k=4)
# 拼接成上下文
context = "\n\n---\n\n".join(
f"[来源: {d.metadata.get('source', '未知')}]\n{d.page_content}"
for d in docs
)
# 构造提示词
prompt = ChatPromptTemplate.from_template("""
你是一个知识助手。请根据以下参考资料回答问题。
如果资料不足以回答,请诚实地说"根据现有资料无法确定"。
参考资料:
{context}
问题:{question}
回答:
""".strip())
chain = prompt | llm | StrOutputParser()
answer = chain.invoke({"context": context, "question": question})
# 打印结果
print(f"\n检索到 {len(docs)} 个相关片段:")
for i, d in enumerate(docs):
print(f" [{i+1}] {d.metadata.get('source', '?')}: {d.page_content[:80]}...")
print(f"\n回答:\n{answer}")
return answer
第 6 步:跑起来!
把前面几步串成一个完整的交互程序。修改 main.py 最后的 if __name__ == "__main__"::
if __name__ == "__main__":
import os
if not os.getenv("OPENAI_API_KEY"):
print("请先设置 OPENAI_API_KEY 环境变量")
print(" Mac/Linux: export OPENAI_API_KEY='sk-...'")
print(" Windows: $env:OPENAI_API_KEY='sk-...'")
exit(1)
# 如果已有向量库就直接加载,否则先构建
if os.path.exists("./vector_store/index.faiss"):
print("加载已有向量库...")
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
openai_api_key=os.getenv("OPENAI_API_KEY")
)
vector_store = FAISS.load_local(
"./vector_store", embeddings,
allow_dangerous_deserialization=True
)
else:
print("首次运行,构建向量库...")
chunks = load_and_split("./data")
vector_store = build_index(chunks)
# 交互式问答
print("\n" + "="*50)
print(" 你的文档问答助手已就绪!")
print(" 输入问题开始对话,输入 q 退出")
print("="*50 + "\n")
while True:
question = input("你的问题:").strip()
if question.lower() == "q":
print("再见!")
break
if not question:
continue
ask(question, vector_store)
print("\n" + "-"*50)
完整运行:
python main.py
你会进入一个交互式对话界面:
首次运行,构建向量库...
加载了 3 个文档,切成 47 个文本块
向量库已保存到 ./vector_store/
==================================================
你的文档问答助手已就绪!
输入问题开始对话,输入 q 退出
==================================================
你的问题:年假有几天?
检索到 4 个相关片段:
[1] 员工手册.pdf: 第3章 请假制度。员工每年享有5天带薪年假,工作满1年后可申请...
[2] 考勤制度.pdf: 年假申请需提前3个工作日提交OA审批...
...
回答:
根据《员工手册》的规定,员工每年享有5天带薪年假,工作满1年后可申请。
申请需要提前3个工作日通过OA系统提交。
你的问题:q
再见!
恭喜! 🎉 你刚刚从零搭建了一个能"读懂"你的文档、回答你的问题的 AI 助手。这就是 RAG。
第 6.5 步:理解你刚刚做了什么(上篇小结)
停下来想一下整个流程,这对后面深入理解很重要:
你的文档(PDF/MD/TXT)
↓ [加载] 读入程序
↓ [分割] 切成几百字的小块
↓ [向量化] 每块变成一个 1000+ 维的数学向量
↓ [存储] 向量 + 原文存入 FAISS 向量库
════════════════════════ 以上是"准备阶段",只做一次
↓
你的问题
↓ [向量化] 问题也变成向量
↓ [检索] 在向量库中找距离最近的 K 个文本块
↓ [拼接] 把文本块 + 问题组合成提示词
↓ [生成] 发给 GPT/Claude 生成回答
════════════════════════ 以上是"问答阶段",每次提问都走一遍
你的程序做了两件聪明的事:
- 检索(Retrieval):不是全文关键词匹配(Ctrl+F),而是在"语义空间"里找——"年假"和"带薪休假"在向量空间里距离很近,即使字面上完全不同。
- 增强生成(Augmented Generation):AI 不需要"记住"你的文档,而是在你提问的瞬间动态检索最相关的内容,作为参考资料喂给它。
这就是 RAG 这五个字母的全部秘密。
下篇:深度理解与进阶
上篇我们跑通了一个最朴素的 RAG。它已经能用了,但你很快会发现问题:
- 问"去年Q3的营收"返回"Q3的市场策略"——语义接近但答非所问
- 检索回来的4个片段其实是同一段话的不同部分——浪费了上下文
- 用户问"张三写的支付模块设计文档"——朴素检索完全不知道"张三"和"支付模块"是过滤条件
下面我们逐一解决这些问题,探讨 RAG 每个环节的深度设计。
一、文档分割:RAG 中最被低估的环节
为什么分割策略决定了下限?
想象你在图书馆找书。如果每本书都被撕成随机大小的碎片,即使图书管理员(向量检索)再厉害,也很难帮你找到完整的信息。分割策略就是决定"每一片多大""从哪里撕开"的问题。
三种分割方式,什么时候用哪种
方式 1:按字符数分割(最常用、最基础)
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=600,
chunk_overlap=100,
separators=["\n\n", "\n", "。", ".", " ", ""]
)
Recursive 的精妙之处在于 separators 的优先级:它先尝试在段落处断开(\n\n),如果段落太长才退而求其次在句子处断开(。),实在不行才在空格处断。这样能最大程度保持语义完整。
经验值:
- 中文:chunk_size 400-800,chunk_overlap 取 size 的 15-20%
- 英文:chunk_size 256-512 tokens,overlap 同样 15-20%
- 代码:按函数/类边界分割,而非字符数
方式 2:按文档结构分割(有标题时优先用)
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
docs = splitter.split_text(markdown_content)
# 每个块会带上它的标题层级作为 metadata
# 比如 doc.metadata = {"h1": "第三章", "h2": "请假制度"}
这有个巨大的好处:检索时你可以利用 metadata 做过滤——"只要第三章的内容"、"只要关于请假制度的部分"。这是让 RAG 从玩具走向可用的关键一步。
最佳实践:先用 MarkdownHeaderTextSplitter 按结构分割,再对过长的块用 RecursiveCharacterTextSplitter 二次分割。
方式 3:语义分割(最智能但最慢)
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=90
)
原理:计算相邻句子的向量相似度。当相似度突然大幅下降时——说明话题切换了——就在那里断开。比固定长度更合理,但代价是分割阶段就要调 Embedding API。
简单粗暴的选择指南
| 你的文档 | 推荐策略 |
|---|---|
| 纯文本、没什么结构 | RecursiveCharacterTextSplitter |
| Markdown、有标题层级 | MarkdownHeaderTextSplitter + Recursive 二次分割 |
| 话题跳跃大、预算充足 | SemanticChunker |
| 混合格式 | 按文档类型分别处理 |
二、Embedding:从"搜关键词"到"搜意思"
直观理解
- 关键词搜索(Ctrl+F):搜"年假"只能命中包含"年假"这两个字的句子。搜"带薪休假"什么也找不到。
- 向量搜索(Embedding):"年假"和"带薪休假"在向量空间里距离很近——因为它们在大量语料中出现在相似的上下文里。Embedding 模型学会了这一点。
2026 年主流 Embedding 模型对比
| 模型 | 维度 | 最佳场景 | 成本 |
|---|---|---|---|
OpenAI text-embedding-3-small | 512/1536 | 性价比首选,中英文都够用 | $0.02/1M tokens |
OpenAI text-embedding-3-large | 256/1024/3072 | 精度要求高时用 | $0.13/1M tokens |
| BGE-M3(开源·BAAI) | 1024 | 中文场景最优,支持稀疏+稠密混合 | 自部署 |
Cohere embed-v4 | 1024 | 压缩率高,延迟敏感场景 | 有免费额度 |
Jina jina-embeddings-v3 | 1024 | 长文本(8192 token 输入) | 有免费额度 |
选型逻辑(不用记,需要时回来查)
- 先看语言:中文为主 → BGE-M3 或 text-embedding-3-large;英文为主 → 都可以
- 再看规模:< 1 万条 → 随便选,差别不大;> 10 万条 → 关注维度和存储成本
- 最后看预算:想省钱 → text-embedding-3-small 或自部署 BGE-M3
维度高一定更好吗? 不。1024 维在绝大多数场景下已经够用。768 → 1024 的收益递减,1024 → 3072 更是边际效应。更高维度 = 更多存储 + 更慢检索,要权衡。
三、检索策略:从"找到"到"找对"
朴素检索用的是 similarity_search——找向量空间中距离最近的 K 个。这在工作中有几个典型问题:
问题 1:检索结果冗余——4 个片段其实是同一段话
场景:你的文档里,"年假政策"在 5 个地方用不同措辞重复了 5 遍。朴素检索返回的 4 个片段全是关于年假的——第 5 名可能是"病假政策",但根本没被选上。
解法:MMR(最大边际相关性)
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={
"k": 4, # 最终返回 4 个
"fetch_k": 20, # 先拉 20 个候选,再从中挑 4 个多样化的
"lambda_mult": 0.6 # 0=最多样化, 1=最相关, 0.5-0.7 平衡
}
)
MMR 做了件聪明的事:它不只挑"最相关的",还尽量避免挑"跟前一个太像的"。结果就是你会同时拿到"年假政策""病假政策""事假政策"——覆盖更全面。
问题 2:用户的查询自带条件——"张三写的支付设计文档"
场景:用户说"找张三写的支付模块设计文档"。朴素检索只会用整个句子做向量搜索,"张三"和"支付模块"这些结构化信息被淹没在语义噪声里。
解法:自查询检索(Self-Querying)
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
metadata_field_info = [
AttributeInfo(name="author", description="文档作者", type="string"),
AttributeInfo(name="date", description="文档日期", type="string"),
AttributeInfo(name="category", description="文档分类", type="string"),
]
retriever = SelfQueryRetriever.from_llm(
llm, vector_store, "文档库",
metadata_field_info
)
# 用户输入:"张三写的支付模块设计文档"
# LLM 自动拆解:
# - 结构化过滤:author = "张三", category = "支付模块"
# - 语义搜索:"设计文档"
# 两个条件同时生效
这是实际业务中最常用的高级特性——用户查询几乎总是"XX 写的关于 XX 的 XX"这种格式。
问题 3:同一个问题需要从多个角度检索
场景:用户问"怎么提升数据库性能"。你只做一次检索,可能只命中"索引优化"相关的内容,漏掉了"查询改写""连接池配置""缓存策略"这些同样相关的方面。
解法:多查询检索(Multi-Query)
from langchain.retrievers.multi_query import MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(
retriever=base_retriever,
llm=llm
)
# 原始查询:"怎么提升数据库性能"
# LLM 自动生成变体:
# 1. "数据库性能优化方法有哪些"
# 2. "SQL慢查询的排查和解决方案"
# 3. "数据库索引优化的最佳实践"
# 三次检索结果合并去重 → 覆盖更全面
变体数量控制在 3-5 个——太少起不到多角度效果,太多增加延迟且引入噪声。
四、高级范式:当朴素 RAG 不够用
当你把上面的改进都加上了,系统在大多数情况下表现不错。但还有几个"刁钻"场景:
HyDE:问题太短,检索不到
问题:用户输入"那个转账失败"——只有 6 个字。向量搜索用这 6 个字去匹配几百字的文档片段,距离不可靠。
HyDE 的思路很反直觉:先让 LLM 编一个答案,用这个编出来的答案去做检索。
# 第一步:让 LLM 编一个假设性答案
# 用户:"那个转账失败"
# → LLM 编:"转账失败通常是以下原因:1. 账户余额不足 2. 风控规则拦截
# 3. 银行通道异常 4. 收款账户信息错误。排查步骤:先查余额,再查日志..."
# 第二步:用这个充满"专业词汇"的长答案去检索
# → 轻松命中"转账失败排查指南""风控规则配置""银行通道监控"等文档
为什么有效?因为 HyDE 把短查询中缺失的"领域词汇"补全了。用户可能不知道"风控拦截"这个词,但 LLM 生成的假设性回答里自然会有。
hyde_prompt = ChatPromptTemplate.from_template("""
请根据以下问题,写一段简短的科普性回答。
即使不确定,也给出你的最佳猜测,尽量使用专业术语。
问题:{question}
回答:
""")
# 用假设性回答的向量去检索
hypo_text = (hyde_prompt | llm | StrOutputParser()).invoke({"question": question})
docs = vector_store.similarity_search(hypo_text, k=5)
CRAG:检索回来的文档真的有用吗?
朴素 RAG 默认"检索结果一定相关"。但向量检索有时候会抽风——明明不相关的文档因为措辞巧合被排在前面。
CRAG(Corrective RAG)在检索后加了一个自检环节:
class RelevanceCheck(BaseModel):
relevant: bool
reason: str
def crag_retrieve(question, vector_store, llm):
# 1. 检索
docs = vector_store.similarity_search(question, k=5)
# 2. 逐条检查:这篇文档真的相关吗?
verified = []
for doc in docs:
check = llm.with_structured_output(RelevanceCheck).invoke(
f"这篇文档与问题相关吗?\n问题:{question}\n文档:{doc.page_content[:300]}"
)
if check.relevant:
verified.append(doc)
# 3. 如果相关文档太少,向外求助(Web 搜索)
if len(verified) < 2:
print("本地文档不足,触发 Web 搜索...")
web_docs = web_search(question) # 用 Tavily 或类似 API
verified.extend(web_docs)
return verified
CRAG 的核心哲学:不要假设检索结果一定是对的。验证、筛选、补救——三步闭环。
混合检索:稠密 + 稀疏,两条腿走路
向量检索(稠密)擅长语义匹配,但遇到精确查找就拉胯——搜"订单号 20240618-XXXX"时向量检索不懂这是 ID 精确匹配。
关键词检索(稀疏,如 BM25)恰好相反——精确匹配 ID、日期、代码片段是强项,但没有语义理解能力。
2026 年生产环境的标配:两者结合
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
# 稀疏检索:关键词匹配
bm25_retriever = BM25Retriever.from_documents(all_docs)
# 稠密检索:语义匹配
dense_retriever = vector_store.as_retriever(search_kwargs={"k": 5})
# 集成:两个结果加权融合
ensemble = EnsembleRetriever(
retrievers=[dense_retriever, bm25_retriever],
weights=[0.7, 0.3] # 语义为主,关键词为辅
)
五、生产化考量:Demo 和产品的距离
延迟优化
一个典型的 RAG 请求要走:查询改写 → 向量检索 → 相关性判断 → LLM 生成。每一步都有延迟。
| 优化点 | 具体做法 | 节省 |
|---|---|---|
| 查询改写用小模型 | gpt-4o-mini 替代 gpt-4o | ~500ms |
| Embedding 批量计算 | 多条文档一起 Embedding | 50-80% |
| 向量索引预加载 | 服务启动时加载到内存 | ~200ms |
| 流式输出 | Token 级 Streaming | 体感大幅提升 |
| 多路检索并行 | 稠密/稀疏同时跑 | 40-60% |
成本控制:分层模型策略
# 一个生产级 RAG 的模型分工
rewrite_llm = ChatOpenAI(model="gpt-4o-mini") # 查询改写 → 便宜
judge_llm = ChatOpenAI(model="gpt-4o-mini") # 相关性判断 → 便宜
answer_llm = ChatOpenAI(model="gpt-4o") # 最终回答 → 质量优先
# Embedding
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 够用就好
经验法则:检索侧的 LLM 调用全用 mini 模型,只给最终生成用大模型。成本下降 60-80%,回答质量几乎不变。
缓存
相同的问题会反复出现(尤其在客服场景)。缓存两次 LLM 调用和 Embedding 计算会省不少钱:
from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
set_llm_cache(InMemoryCache()) # 进程内缓存
# 生产环境用 Redis:同一问题跨进程共享缓存
通常有 15-30% 的缓存命中率——意味着每省下 3 次 API 调用就有 1 次。
评估:不要盲目调参
没有评估的优化是盲目的。RAGAS 是目前最成熟的 RAG 评估框架:
from ragas import evaluate
from ragas.metrics import (
faithfulness, # 回答是否忠于检索到的文档(有没有胡编)
answer_relevancy, # 回答是否切题
context_precision, # 检索到的文档是否精确相关
context_recall, # 检索是否找到了所有相关信息
)
result = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)
四个指标各盯一个环节:
context_recall低 → 你的检索没找到该找的文档 → 调整 chunk_size 或 Embedding 模型context_precision低 → 检索到了太多不相关的东西 → 引入 MMR 或相关性过滤faithfulness低 → AI 在胡说八道(幻觉) → 优化提示词或考虑 CRAGanswer_relevancy低 → 答非所问 → 检查查询改写是否生效
六、什么时候不该用 RAG
RAG 不是银弹。以下场景请三思:
| 场景 | 为什么不适合 | 替代方案 |
|---|---|---|
| "总结我所有文档的核心观点" | RAG 每次只看 K 个片段,无法把握全局 | Map-Reduce 或长上下文模型 |
| "对比所有产品的价格" | 精确的结构化查询 | SQL 直接查数据库 |
| 实时性要求 < 500ms | 多层 LLM 调用延迟不可控 | 预计算 + 缓存,或不用 RAG |
| 知识更新极度频繁(秒级) | 向量库重建跟不上 | 直接 Fine-tune 或只用搜索 |
七、2026 年的前沿方向
- Agentic RAG:检索策略不再硬编码,而是由 LLM Agent 动态决策——先去哪个数据源?用稠密还是稀疏?结果不够时要不要扩大范围?每一步都是自主判断。
- 长上下文 + RAG 融合:Claude 200K、Gemini 千万级上下文。当窗口大到能装下整本手册时,RAG 的角色从"检索"变为"索引"——快速定位关键段落,再让长上下文模型消化完整章节。
- 多模态 RAG:不只检索文本——产品截图、架构图、数据表格同样可检索。LangChain 的
MultiVectorRetriever已支持为同一内容生成多模态向量。
总结
这篇文章从零走到生产级,核心就三条:
- 找到真正相关的内容——靠分割策略、Embedding 选型、混合检索和查询改写。这决定了系统的召回率(该找的都找到了吗)。
- 不让模型被噪声带偏——靠 MMR 去重、上下文压缩、相关性自检。这决定了系统的精确率(找到的都有用吗)。
- 平衡成本与延迟——靠分层模型、缓存和并行架构。这决定了系统能不能上线。
朴素 RAG 只是一个起点。从 Multi-Query 到 CRAG,从混合检索到 Agentic RAG,每一步改进都在缩小"原型"和"产品"的差距。
最好的 RAG 系统,是用户感受不到"检索"存在的系统——他们只是在提问,然后得到准确的回答。
希望这篇指南让你的 RAG 之路少些弯路。如果只有一句话带走,那就是:先跑通最简单的版本,然后针对你实际遇到的问题逐步加组件——而不是一开始就把所有高级范式堆上去。
参考资料 - LangChain 官方文档:https://python.langchain.com/docs - RAGAS 评估框架:https://docs.ragas.io - Gao, Y. et al. "Retrieval-Augmented Generation for Large Language Models: A Survey" - FAISS: https://github.com/facebookresearch/faiss