Skip to content
Go back

RAG 入门指南:在正确的时机用正确的方式构建检索增强生成系统

RAG 入门指南封面

RAG,全称 Retrieval-Augmented Generation(检索增强生成),一句话就能说清楚:在 AI 回答问题之前,把你自己的数据作为上下文喂给它。开卷考试,而不是闭卷背诵。

你现在其实已经可以停下来了——检索相关文档,把它们作为上下文,让模型从你的数据里回答,而不是靠训练时的记忆。这就是 RAG 的全部。

但理解 RAG 和真正让它跑起来,是两件事。 作者 Daniil Shykhov 花了很长时间才搞清楚的,不是概念,而是”坑在哪里”。这篇指南,就是他当时最想看到的那种。

RAG 的三步结构

没有更多了,就是三步:

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 以内:

  1. 把文档复制进 Claude 或 ChatGPT 的对话
  2. 提问你希望 RAG 系统回答的问题
  3. 检查答案是否准确、是否基于文档

如果这个方法管用,你已经完成了,省了好几周的管道开发时间。

什么时候真正需要 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 能不能解决你的问题。

托管路径

先用现成的工具处理检索基础设施:

要点:用这些工具先找出什么有效、什么会出问题。等你能说出具体遇到了什么墙,再去自建。

手写路径:最简单的 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 系统问它,然后检查三件事:

  1. 包含这个事实的片段有没有被检索到?
  2. 它在前三名结果里吗?
  3. 生成的答案有没有正确使用它?

大多数工具支持查看被检索到的片段。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 给模型的是信息,不改变它如何表达。

从这里开始

读到这里,接下来该怎么做:

  1. 选你最小的用例。 一个文档文件夹,不是整个知识库。
  2. 先试上下文窗口。 把文档粘进 Claude 或 ChatGPT,问你的问题。
  3. 如果管用,停下来。 认真的。
  4. 如果不管用,试托管工具。 OpenAI 的 file search 或 LlamaIndex,同样的文档,对比结果。
  5. 只在能说出遇到的墙时,才自建 RAG。 “我的检索返回了错误的片段,因为……”——到这个时候再构建。“我感觉应该有个向量数据库”——这不是理由。

RAG 理解起来不难,做对很难。

但在开始之前就知道问题在哪里——分块、检索、数据质量——这才是每篇教程都跳过的捷径。

参考


Tags


Previous

用 GitHub Copilot SDK 在 C# 中构建多智能体代码分析系统

Next

GitHub Spec Kit:用规格说明驱动 AI 编程的开源工具包