
RAG,全称 Retrieval-Augmented Generation(检索增强生成),一句话就能说清楚:在 AI 回答问题之前,把你自己的数据作为上下文喂给它。开卷考试,而不是闭卷背诵。
你现在其实已经可以停下来了——检索相关文档,把它们作为上下文,让模型从你的数据里回答,而不是靠训练时的记忆。这就是 RAG 的全部。
但理解 RAG 和真正让它跑起来,是两件事。 作者 Daniil Shykhov 花了很长时间才搞清楚的,不是概念,而是”坑在哪里”。这篇指南,就是他当时最想看到的那种。
RAG 的三步结构
没有更多了,就是三步:
- Retrieve(检索):在文档里搜索与问题相关的片段
- Augment(增强):把这些片段塞进 prompt,作为上下文
- Generate(生成):LLM 用你的文档而不是训练数据来回答
RAG 存在的原因是:LLM 对你公司的内部文档、产品规格、上周二的会议记录一无所知。它们只在公开互联网数据上训练。RAG 通过在需要时把你的信息喂给模型,弥合了这个Gap。
向量数据库、embedding 模型、分块策略,都是实现细节。重要的细节,但仍然是细节。记住这三步,后面的东西会容易理解得多。
先别急着上 RAG
这是每篇教程都跳过的建议:先不要构建 RAG。
把文档直接粘进上下文窗口。问你的问题。看看结果。
现在的上下文窗口已经很大了:Claude 支持 200K tokens,Gemini 支持 200 万。大约等于 150 到 1500 页文字。对很多使用场景来说,这就够了,根本不需要检索管道。
简单测算:你的文档能放进去吗?
1 token ≈ 0.75 个英文单词
100K tokens ≈ 75,000 词 ≈ 150 页
200K tokens ≈ 300 页
估算一下你的文档总词数。如果在 100K tokens 以内:
- 把文档复制进 Claude 或 ChatGPT 的对话
- 提问你希望 RAG 系统回答的问题
- 检查答案是否准确、是否基于文档
如果这个方法管用,你已经完成了,省了好几周的管道开发时间。
什么时候真正需要 RAG
当你遇到这些情况时,上下文窗口塞不下了:
文档量太大。 一家大型企业的法律档案库可能有 5000 万 tokens。Gemini 的 200 万 token 窗口只能覆盖其中的 4%。
速度有要求。 RAG 管道大约 1 秒出结果。把几十万 tokens 喂进长上下文模型要花 30-60 秒,而且每次查询成本更高。
数据频繁变动。 上下文窗口在单次对话里是静态的。RAG 管道可以从一个随文档变化实时更新的知识库里拉取内容。
需要来源追溯。 RAG 能指出答案来自哪个文档的哪段话。在合规、法律、医疗场景里这是硬需求。
访问权限控制。 不同用户应该看到不同文档。RAG 通过过滤每个用户能检索到的内容来实现。把所有内容都粘进上下文?完全没法做访问控制。
如果上面这些都不适用于你,把这篇文章存起来,等需要的时候再回来看。
决策流程
你的文档能放进上下文窗口吗?(< 100K tokens)
│
├── 能 → 粘进去,测一下,答案对吗?
│ ├── 对 → 你不需要 RAG。停在这里。
│ └── 不对 → 为什么?
│ ├── 答案错 / 不完整 → 考虑 RAG
│ └── 太慢或太贵 → 考虑 RAG
│
└── 不能 → 你需要 RAG。
├── 原型阶段 → 用托管工具(见下节)
└── 生产环境 → 构建管道(但先从托管开始)
第一个 RAG 系统:从简单开始
你确定要用 RAG 了。先压住自己手写一切的冲动。
大多数教程的起点是:装 LangChain、配 Pinecone、选 embedding 模型、写分块管道。这是五个决策叠在一起,还没验证 RAG 能不能解决你的问题。
托管路径
先用现成的工具处理检索基础设施:
- OpenAI 的 file search:最快的路径。把文件上传给 Assistant,它帮你分块、embedding、检索。你只管提问。一小时内就能知道 RAG 风格的检索有没有效果。
- Claude Projects:上传文档,文档在每次对话里都作为上下文。这不是严格意义的 RAG(用的是上下文窗口),但解决的是同一个问题,而且不需要任何工程。
- Cursor 的 @docs:在你写代码时对文档做检索。什么都不用配置,开箱即用。如果你的用例是”帮我理解这个 API 怎么用”,从这里开始。
- LlamaIndex:迈向真正 RAG 代码的第一步。几行 Python 就能加载文档、建索引、查询。比完全托管的工具多一些控制权,比从头构建少得多的工作量。
要点:用这些工具先找出什么有效、什么会出问题。等你能说出具体遇到了什么墙,再去自建。
手写路径:最简单的 RAG 管道
当托管工具不够用时,这是一个最小可用的 RAG 管道。没用任何框架,只有纯 Python,让你看清每一步:
# 1. 加载文档
from pathlib import Path
docs = []
for file in Path("./my_docs").glob("*.txt"):
docs.append(file.read_text())
# 2. 分块(简单的按词分割,带重叠)
def chunk_text(text, size=500, overlap=50):
words = text.split()
chunks = []
for i in range(0, len(words), size - overlap):
chunk = " ".join(words[i:i + size])
chunks.append(chunk)
return chunks
all_chunks = []
for doc in docs:
all_chunks.extend(chunk_text(doc))
# 3. 用 ChromaDB embedding 并存储(本地运行,无需 API key)
import chromadb
client = chromadb.Client()
collection = client.create_collection("my_docs")
collection.add(
documents=all_chunks,
ids=[f"chunk_{i}" for i in range(len(all_chunks))]
)
# 4. 查询——检索最相关的 3 个片段
results = collection.query(
query_texts=["How do I reset my password?"],
n_results=3
)
# 这些就是要作为上下文喂给 LLM 的片段
for doc in results["documents"][0]:
print(doc[:200], "\n---")
大约三十行代码。ChromaDB 默认在本地做 embedding,不需要 API key,不需要云基础设施。这是刻意写得简陋的。你会超出它的边界,但到时候你会清楚地知道要升级哪里、为什么,因为你已经看过每一个活动部件了。
分块:真正出问题的地方
有一件事应该早点告诉你:分块策略比 embedding 模型重要得多。
开发者们花好几个小时比较 embedding 模型——OpenAI 的 text-embedding-3 vs. Cohere vs. sentence-transformers——然后三十秒搞定分块策略,直接从教程里复制 chunk_size=500 就过了。
这是搞反了。分块决定了正确的信息是否可以被检索到。embedding 模型决定了它能被匹配得多好。如果正确的信息从来没被孤立成一个可检索的片段,embedding 模型再好也没用。
原文里有两个真实案例:某电商公司把 13% 的幻觉率追溯到太小的分块——片段里有产品描述的碎片,足以匹配搜索词,但没有足够的上下文来正确回答。模型用半截句子自信地生成答案。另一端,某医疗问答系统在摄取时静默丢失了 21% 的文档——编码不匹配导致整个文件消失,系统没有报错,只是文档变少了,没人注意到,直到答案开始变差。
分块的实用建议
从 300-500 token,10-20% 重叠开始。 大多数文本文档的合理默认值。重叠确保你不会在块边界处截断一个想法。
根据文档类型选择策略。 代码和散文需要不同的分块方式。一个函数被拆到两个块里是没用的;一段话被拆到两个块里,有重叠还好。表格需要特殊处理——大多数通用分块器会把表格弄乱。
在自然边界处分割。 段落断点、章节标题、主题转换。无视内容结构、硬在 500 token 处截断,是最常见的新手错误。
测一测。 没有通用的”最佳”块大小。改一改,问同样的问题,对比结果。二十分钟,比任何教程教的都多。
还有一件没人提的事:每次摄取后都检查文档计数。 如果你加载了 100 个文档,但只有 79 个进了管道,你有一个静默的数据质量问题,它会污染下游的每一个答案。
检索才是关键
当你的 RAG 系统忽视文档、凭空捏造时,第一反应往往是怪模型,说”LLM 在幻觉”。
通常这是错误的诊断。LLM 只知道你喂给它的东西。如果正确的片段没有被检索到,世界上没有任何模型能给你好答案。这个重新定义,改变了你调试一切的方式:别调 prompt,先检查实际到达的片段是什么。
简单的验证方法
问你已经知道答案的问题。
找一个确定在你文档里的事实,用你的 RAG 系统问它,然后检查三件事:
- 包含这个事实的片段有没有被检索到?
- 它在前三名结果里吗?
- 生成的答案有没有正确使用它?
大多数工具支持查看被检索到的片段。ChromaDB 里看 results["documents"];LangChain 里设置 return_source_documents=True;OpenAI 的 file search 里查 annotations。
如果正确的片段没有被检索到,再多的 prompt 工程也救不了你。问题在你的分块、embedding 或搜索查询里,不在生成 prompt 里。
调试检查清单
当 RAG 系统给出错误答案时,从上到下走一遍:
1. 检查检索到的内容
→ 正确的片段回来了吗?
→ 没有 → 分块或 embedding 问题。
调整大小,检查数据质量,确认文档真的摄取了。
2. 检查片段质量
→ 检索到的片段有足够的上下文来回答吗?
→ 没有 → 片段太小或分割方式有问题。
增大尺寸或使用语义分块。
3. 检查查询
→ 用户的问题和文档里的语言匹配吗?
→ 不匹配 → 试着改写查询,或加元数据过滤。
4. 检查排名
→ 最好的片段排在前面吗?
→ 没有 → 加一个 reranker(见下节)。
5. 检查 prompt
→ 有没有告诉 LLM 只使用提供的上下文?
→ 没有 → 加上:"只根据提供的文档回答。
如果答案不在里面,直接说不知道。"
大多数问题在第一步或第二步就能找到。走到第五步的时候,通常早就发现了。
大多数教程跳过的一个改进:Reranking
向量搜索找的是语义上接近你查询的片段。但”语义接近”和”真正能回答这个问题”不是一回事。
描述问题的片段可能排在包含解决方案的片段前面——因为问题描述用了更多和问题相同的词。你搜”怎么修复错误 X”,得到三个关于错误 X 的片段,但包含解决方案的那个排在最后。
Reranker 解决这个问题。它拿到检索到的片段,把每一个和查询一起读,问:“这个片段真的能帮助回答这个问题吗?“然后根据这个更深层的分析重新排序。
这是生产 RAG 系统里 ROI 最高的单项改进。它通常是”基本能用”和”真正可靠”之间的分水岭。Cohere Rerank、sentence-transformers 的 cross-encoder 模型,或者一个小的 LLM 调用都能做到。
如果你的检索大体上能用,但有时把错误的片段排在前面,在重建整个管道之前先试试 reranker。
什么时候不该用 RAG
RAG 真的会带来工程复杂度:摄取管道、分块策略、一个要维护的向量存储、embedding 模型选型、持续的数据质量监控。这些情况下不值得:
文档放得进上下文窗口。 这是最常见的过度工程错误。五十页内部文档?粘进去就行,不需要为这个搭基础设施。
数据量小、变动少。 一个有 200 条的产品 FAQ,一个每季度更新的公司手册。这些是上下文窗口问题,不是检索问题。
“以后可能需要扩展”。 “将来可能需要扩展”不是今天构建基础设施的理由。先做最简单能用的东西,等你能指出一个具体的失败点,再加 RAG。
问题是语气,不是知识。 如果模型有正确的信息,但回答的风格或格式不对——这是微调或 prompting 的问题。RAG 给模型的是信息,不改变它如何表达。
从这里开始
读到这里,接下来该怎么做:
- 选你最小的用例。 一个文档文件夹,不是整个知识库。
- 先试上下文窗口。 把文档粘进 Claude 或 ChatGPT,问你的问题。
- 如果管用,停下来。 认真的。
- 如果不管用,试托管工具。 OpenAI 的 file search 或 LlamaIndex,同样的文档,对比结果。
- 只在能说出遇到的墙时,才自建 RAG。 “我的检索返回了错误的片段,因为……”——到这个时候再构建。“我感觉应该有个向量数据库”——这不是理由。
RAG 理解起来不难,做对很难。
但在开始之前就知道问题在哪里——分块、检索、数据质量——这才是每篇教程都跳过的捷径。