# RAG 教程：一步步搭出检索增强生成管线

> 检索增强生成（RAG）的做法是：问题一来，先从你自己的文档里找出相关段落，再放进模型的提示词。最小的管线有六个环节：加载、分块、嵌入、存储、检索和生成。本教程用一个 Python 脚本把六个环节都搭出来，并让 Claude 标明它用了哪些段落。

Mindket 编辑部 · 发布于 2026-11-04 · 更新于 2026-11-05 · https://mindket.com/cn/learn/rag/rag-tutorial

## 要点

- 用一个 Python 脚本搭出 RAG 管线的六个环节。
- 分清索引时和查询时，知道每个设置属于哪一边。
- 拿到带引用的回答，引用能精确到文本块，指向你自己的来源 ID。
- 在两步式 RAG 和由智能体决定何时检索之间做出选择。

## 开始之前

你需要 Python 3、一个 [Voyage AI 密钥](https://dash.voyageai.com/)和一个 [Anthropic 密钥](https://platform.claude.com/settings/keys)。Anthropic 自己没有嵌入模型，它的[嵌入指南](https://platform.claude.com/docs/en/build-with-claude/embeddings)推荐的是 Voyage AI，本课也照着用。任何嵌入服务都能接进这条管线，第 2 课会比较几家的模型。

先确认你真的需要 RAG。Anthropic 的 [Contextual Retrieval 文章](https://www.anthropic.com/research/contextual-retrieval)说，小于 20 万 token（大约 500 页）的知识库可以直接放进提示词，而提示词缓存让这种做法更快、更省成本。语料装不下的时候，检索多出来的这些环节才值得。

## 搭出这条管线

下面的脚本就是完整的管线。切分调用来自 LangChain 的[语义搜索教程](https://docs.langchain.com/oss/python/langchain/knowledge-base)，两次嵌入调用和点积来自 Voyage AI 的[快速入门](https://www.mongodb.com/docs/voyageai/quickstart)，`search_result` 块来自 Anthropic 的[搜索结果指南](https://platform.claude.com/docs/en/build-with-claude/search-results)。把它们连起来的代码、文件名和问题是我们为本课写的示例，请换成你自己的。

````python
import numpy as np
import voyageai
import anthropic
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter

vo = voyageai.Client()          # reads VOYAGE_API_KEY
claude = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

# 1. Load: one Document per file, with a source that never changes
docs = [
    Document(
        page_content=open("refund-policy.txt", encoding="utf-8").read(),
        metadata={"source": "kb://refund-policy", "title": "Refund policy"},
    ),
    Document(
        page_content=open("shipping.txt", encoding="utf-8").read(),
        metadata={"source": "kb://shipping", "title": "Shipping"},
    ),
]

# 2. Chunk
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200, add_start_index=True
)
chunks = splitter.split_documents(docs)

# 3. Embed the chunks (index time)
chunk_embeddings = vo.embed(
    texts=[c.page_content for c in chunks],
    model="voyage-4-large",
    input_type="document",
).embeddings

# 4. Store: a plain list in memory is enough for a first run

# 5. Retrieve (query time)
question = "How long do I have to return an item?"
query_embedding = vo.embed(
    texts=[question],
    model="voyage-4-large",
    input_type="query",
).embeddings[0]
similarities = np.dot(chunk_embeddings, query_embedding)
top = np.argsort(-similarities)[:4]

# 6. Generate, with citations on
results = [
    {
        "type": "search_result",
        "source": chunks[i].metadata["source"],
        "title": chunks[i].metadata["title"],
        "content": [{"type": "text", "text": chunks[i].page_content}],
        "citations": {"enabled": True},
    }
    for i in top
]
response = claude.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": [*results, {"type": "text", "text": question}]}
    ],
)

for block in response.content:
    if block.type == "text":
        print(block.text)
        for c in block.citations or []:
            print(f"  [{c.title}] {c.source}")
````

1. 执行 `pip install numpy voyageai anthropic langchain-core langchain-text-splitters` 安装依赖，再设置 `VOYAGE_API_KEY` 和 `ANTHROPIC_API_KEY`，两个客户端都从环境变量里读取密钥。
2. 加载：把每个文件包成 LangChain 的 `Document`。给它一个永远不变的 `source`，比如 `kb://refund-policy`，再加一个 `title`。两者都会出现在每条引用里。
3. 分块：用 `RecursiveCharacterTextSplitter`，`chunk_size=1000`、`chunk_overlap=200`，这是 LangChain 教程里用的数值。`add_start_index=True` 会把每个块的字符偏移量保存在元数据里。
4. 嵌入：用 `input_type="document"` 发送各个块的文本。模型是 `voyage-4-large`，也就是 Voyage 快速入门里用的那个。
5. 存储：向量暂时放在一个 Python 列表里，脚本结束它们就没了。LangChain 提醒，生产环境要把存储持久化。
6. 检索：用 `input_type="query"` 嵌入问题，和每个块的向量求点积，取得分最高的四个。Voyage 的嵌入向量已归一化为长度 1，所以点积等于余弦相似度。
7. 生成：把这四个块作为开启了 `citations` 的 `search_result` 块传入，再在同一条用户消息里放上问题的文本块。
8. 运行它。凡是用到某个块的文本块都带着引用，最后那个循环会把它们打印出来。

## 索引时和查询时

第 1 到第 4 步在文档变化时运行，第 5 步和第 6 步在每次提问时运行。这个划分决定了修改的代价：改动分块方式或嵌入模型，就得把整个语料重新嵌入一遍；改动提示词或块的数量则不用。

嵌入模型是不能悄悄更换的部分。[LlamaIndex 的文档](https://developers.llamaindex.ai/python/framework/optimizing/basic_strategies/basic_strategies/)说，换了嵌入模型就必须重新建索引，而且文档和查询必须用同一个模型来嵌入。Voyage AI 写明了一个例外：它的[模型页面](https://www.mongodb.com/docs/voyageai/models/text-embeddings)说，4 系列生成的所有嵌入彼此兼容，第 2 课会用上这一点。

| 环节 | 何时运行 | 需要了解的设置 | 在哪一课讲 |
| --- | --- | --- | --- |
| 加载 | 文档变化时 | 加载方式，以及你附上的 source 和 title | 本课 |
| 分块 | 文档变化时 | chunk_size、chunk_overlap、切分器类型 | 第 3 课 |
| 嵌入 | 文档变化时 | model、input_type、输出维度 | 第 2 课 |
| 存储 | 文档变化时 | 索引类型及其搜索设置 | 第 2 课 |
| 检索 | 每次提问时 | 块的数量、混合搜索、重排序 | 第 3、4 课 |
| 生成 | 每次提问时 | 提示词、引用设置 | 本课 |

## 一条引用里有什么

如果旧教程还在发送 `search-results-2025-06-09` 这个测试版请求头，把它删掉就行。从 2025 年 8 月 8 日起，在 Claude API 上使用搜索结果块已经不需要测试版请求头了。

引用默认是关闭的，而且同一次请求里的所有搜索结果必须用同一个设置。开启之后不需要任何特殊的提示词。回复里凡是用到你内容的文本块，都会带一条 `search_result_location` 类型的引用，字段如下：

- `source` 和 `title`：原样取自你发送的搜索结果。
- `cited_text`：被引用的那个或那几个块的完整文本，不计入输出 token。
- `search_result_index`：这条结果在请求里所有 `search_result` 块中的位置，从 0 开始。
- `start_block_index` 和 `end_block_index`：这条结果 `content` 数组里的一段切片，结束位置不含在内。

**Claude 给你自己的内容加引用，功能是怎样一步步来的**

1. **2025年1月23日** API 支持引用。Claude 可以把它说的话对应到你发送的纯文本文档和自定义内容文档。
2. **2025年7月3日** 搜索结果块推出测试版。你自己的工具可以返回让 Claude 引用的搜索结果，需要带 search-results-2025-06-09 请求头。
3. **2025年8月8日** 搜索结果块结束测试。在 Claude API 和 Vertex AI 上不再需要测试版请求头，所以上面的脚本没有发送它。
4. **2025年9月3日** 工具结果里的文档也能被引用。客户端工具可以返回让 Claude 引用的文档，智能体式 RAG 用的就是这种形态。
5. **2025年11月21日** 搜索结果块登陆 Amazon Bedrock。在那里同样不需要测试版请求头。

来源：[Claude Docs: Release notes](https://platform.claude.com/docs/en/release-notes/overview) (2026-11-05 查阅)

> 文本块是 Claude 能引用的最小单位，它引用整块，不会引用块里的某个片段。想要句子级的引用，就把每个句子放进 `content` 里各自独立的块。如果提示词只要求输出 JSON，引用会很少，甚至没有。

## 两步式 RAG，还是让智能体自己决定

你搭的是两步式 RAG，这是 LangChain 对“检索总是发生在生成之前”这种做法的叫法。另一端是智能体式 RAG：把搜索做成一个工具，由模型决定要不要调用、用什么查询词。Anthropic 的搜索结果指南两种形态都讲了。结果可以像上面那样放在用户消息里，也可以从工具调用里返回，还可以用 `tool_choice` 强制调用，让检索每次都执行。

我们建议先从两步式开始。它的延迟更好预估，因为模型调用的次数是固定的，答错了也只需要查两个地方。当单个问题需要好几次查找时，再换成智能体。把检索函数包装成 MCP 服务器，是把它交给智能体的一种办法。

| 架构 | 谁决定检索 | 延迟 | 适合 |
| --- | --- | --- | --- |
| 两步式 RAG | 你的代码，每次提问都检索 | 快，可预测 | 常见问题、文档机器人 |
| 智能体式 RAG | 模型，在推理过程中决定 | 不固定 | 带多个工具的研究助手 |
| 混合式 | 两者结合，并带验证步骤 | 不固定 | 需要质量把关的专业领域问答 |

## Alex Albert 谈带引用的搜索结果，Hamel Husain 谈长上下文

按 Alex Albert 的 X 简介，他在 Anthropic 做研究。本课用到的 `search_result` 块在 2025 年 7 月上线，他当天[在 X 上](https://x.com/alexalbert__/status/1940823684362781039)发布了这个消息。他在同一串帖子里用一句话讲清了用法：把来源、标题和内容交给 Claude，每条引用都会写明依据的是哪个来源、哪段文字。

放到你的脚本里，第 2 步那两个元数据字段，就成了答案和文件之间唯一的联系。所以要选人看得懂、找得到的值，比如网址，或者永远不变的页面 ID。

Hamel Husain 的 X 简介写着他在 OpenAI 做评估。有人说长上下文窗口会让 RAG 失去意义，他在 2024 年 7 月[在 X 上](https://x.com/HamelHusain/status/1811050329972322708)给出了三条反驳的理由。你经常要按日期这类元数据过滤。检索这一步越来越多地在决定从哪些来源取内容。不管窗口多长，剔除明显无关的文档，结果都会更好。

按“开始之前”里 Anthropic 的大小标准，语料小可以不做检索，但过滤会把检索请回来。一旦用户只能看到自己的工单，或者只该看到今年的政策，你就需要一个带 `WHERE` 条件的检索步骤，第 2 课会在 pgvector 里加上它。

> 不管上下文窗口有多长，剔除明显无关的文档，你都会得到更好的结果。
>
> Hamel Husain, 按他的 X 简介，在 OpenAI 做评估，引文为译文, [X](https://x.com/HamelHusain/status/1811050329972322708) (2026-11-05 查阅)

[![Alex Albert（@alexalbert__）2025 年 7 月 3 日在 X 上宣布：Anthropic API 今天上线了新功能“搜索结果内容块”，让所有 RAG 内容都能带上注明出处的自然引用，把网页搜索级别的引用带到你自己的搜索工具里](https://mindket.com/blog/shots/x-alexalbert-search-result-blocks.webp?v=06e3ef6955ee)](https://x.com/alexalbert__/status/1940823684362781039)

Alex Albert 在 X 上，2025 年 7 月 3 日

[![Hamel Husain（@HamelHusain）2024 年 7 月 10 日在 X 上反驳“长上下文窗口会让 RAG 失去意义”的说法，第一条理由是：你经常要按日期、浏览量等元数据过滤，搜索和信息检索在很多实际场景里都很关键](https://mindket.com/blog/shots/x-hamelhusain-rag-long-context.webp?v=a30397a23dc7)](https://x.com/HamelHusain/status/1811050329972322708)

Hamel Husain 在 X 上，2024 年 7 月 10 日

[Learn RAG From Scratch: Python AI Tutorial from a LangChain Engineer](https://www.youtube.com/watch?v=sVcwVQRHIc8)

freeCodeCamp.org 的 YouTube 视频：标题说明，这是一位 LangChain 工程师用 Python 从零讲解 RAG 的课程。

> X 官方嵌入卡片的截图，拍摄于 2026-11-05，时间为协调世界时（UTC）。Hamel Husain 那条帖子被 X 用“Show more”截断了。点击截图可到 X 上打开原帖。

## 常见错误

- 漏掉 `input_type`。Voyage 的指南说，做检索时不要省略它，它会在查询和文档前面加上检索专用的提示语。
- 一次性嵌入整个语料。Voyage 的 `embed` 每次请求最多接收 1,000 条文本，总 token 数还按模型设有上限，`voyage-4-large` 是 120K。按[文本嵌入页面](https://www.mongodb.com/docs/voyageai/models/text-embeddings)的限制，把各个块分批发送。
- 文档用一个模型嵌入，查询却用一个不相干的模型。只有两边来自同一个模型，或者来自厂商写明共用同一个嵌入空间的模型，向量才能互相比较。
- 觉得多送几个块更保险。Anthropic 的搜索结果指南建议只返回最相关的结果，以免上下文溢出。Contextual Retrieval 文章提醒说，多余的信息可能分散模型的注意力，所以数量是有上限的。请在第 4 课里测一测你自己的数据。
- 把检索到的文本当成可信内容。[LangChain 警告](https://docs.langchain.com/oss/python/deepagents/rag)，RAG 应用容易受到间接提示词注入，因为检索到的块和你的系统提示词共用同一个上下文窗口。没有任何提示词或分隔符方案能完全防住，所以在用户看到输出之前要先校验。
- 丢掉来源。搜索结果的 `source` 可以是任何稳定的字符串，网址或者 `kb://article-1234` 这样的 ID 都行，但它必须存在，引用才有地方可指。

## 自己试一试

- 问一个文档里没有答案的问题，看 Claude 是承认没有，还是凭记忆把空缺补上。
- 打印 `similarities[top]`，找出相关块和噪声块之间得分掉下来的位置。
- 把 `chunk_size` 改成 500，`chunk_overlap` 改成 50，再把四个块增加到八个。LlamaIndex 建议块变小时调大 top-k。
- 把一个块拆成句子大小的块，放进同一个 `search_result`，比较前后的 `cited_text`。

## 下一课

[第 2 课](https://mindket.com/cn/learn/rag/embeddings-explained)展开嵌入和存储两个环节：向量到底是什么，怎样挑模型和维度，怎样把内存里的列表搬进带 pgvector 的 Postgres。四个块的上限，背后是提示词的容量，[上下文窗口那一课](https://mindket.com/cn/learn/prompt-engineering/context-window)讲的就是它，也讲了怎样查看一次请求用掉多少 token。

## 自测一下

1. 为检索嵌入用户的问题时，input_type 应该设成什么？

   A. document
   B. query
   C. search
   D. 不设，用默认值

   答案：B。 Voyage 的指南说，问题用 query，要建索引的文本用 document，做检索时不要省略这个参数。

2. 你的整个知识库是 15 万 token，Contextual Retrieval 文章建议怎么做？

   A. 先建一个向量索引
   B. 切成小块再重排序
   C. 全部放进提示词并缓存
   D. 在上面微调一个模型

   答案：C。 文章说，小于 20 万 token 的知识库可以直接放进提示词，提示词缓存让这种做法更快、更省成本。

3. 你把每个块当作搜索结果里的一个文本块传入，Claude 最细能引用到哪里？

   A. 整个块
   B. 块里的一个句子
   C. 一个字符范围
   D. 只有文档标题

   答案：A。 文本块是最小的可引用单位，Claude 引用整块。想要更细的引用，就把一个块拆成更小的块。

4. 你从 OpenAI 的 text-embedding-3-small 换到 voyage-4-large，已经存好的向量该怎么处理？

   A. 保留，向量可以沿用
   B. 只重新嵌入新文档
   C. 把旧向量重新归一化
   D. 用新模型重新嵌入每一个块

   答案：D。 LlamaIndex 说，换了嵌入模型就要重新建索引。只有厂商写明共用同一个嵌入空间的模型，比如 Voyage 4 系列，才不用这一步。

## 常见问题

### RAG 简单说是什么？

你的代码先在自己的文档里搜索，把最匹配的内容交给模型，让它依据最新的、私有的数据回答，而不只靠记忆。LangChain 的检索概览点出它解决的两个限制：上下文有限，知识是静态的。

### 搭 RAG 一定要用向量数据库吗？

不一定。上面的管线把向量放在 Python 列表里，用 NumPy 的点积排序。需要持久保存向量、语料变大或者要做过滤时，数据库才派上用场。第 2 课会把向量搬进带 pgvector 的 Postgres。

### RAG 比直接把文档放进提示词更好吗？

要等语料超出上下文窗口，或者用户需要按日期、权限这类条件过滤的时候才是。在那之前，把全部内容配上提示词缓存一起发送，搭起来和排查问题都更简单。

### 做 RAG 需要 LangChain 或 LlamaIndex 吗？

不需要。这里 LangChain 只提供切分器，其余都是普通的 API 调用。LangChain 自己的文档把 RecursiveCharacterTextSplitter 称为通用文本的推荐切分器。

### 应该给模型送多少个块？

先用四个，再去测量。Anthropic 在 Contextual Retrieval 文章里试了 5、10 和 20 个块，在它的数据上数量最多的那一档最有效，所以合适的数量取决于你的文档。

### Claude 怎样给 RAG 的来源加引用？

通过开启了引用的 search_result 块。回复里每个被引用的文本块都带着来源、标题，以及它用到的内容块范围。

### RAG 能杜绝幻觉吗？

不能。它给模型提供了依据，模型仍然可能说出依据之外的话。第 4 课用忠实度分数来衡量这个差距。

## 资料来源

- [LangChain Docs: Retrieval Augmented Generation (RAG) with Deep Agents](https://docs.langchain.com/oss/python/deepagents/rag) (2026-11-04 查阅)
- [LangChain Docs: Retrieval](https://docs.langchain.com/oss/python/langchain/retrieval) (2026-11-04 查阅)
- [LangChain Docs: Build a semantic search engine](https://docs.langchain.com/oss/python/langchain/knowledge-base) (2026-11-04 查阅)
- [Claude Docs: Embeddings](https://platform.claude.com/docs/en/build-with-claude/embeddings) (2026-11-04 查阅)
- [Claude Docs: Search results](https://platform.claude.com/docs/en/build-with-claude/search-results) (2026-11-05 查阅)
- [Claude Docs: Release notes](https://platform.claude.com/docs/en/release-notes/overview) (2026-11-05 查阅)
- [Anthropic: Contextual Retrieval in AI systems](https://www.anthropic.com/research/contextual-retrieval) (2026-11-04 查阅)
- [Voyage AI Docs: Quick Start](https://www.mongodb.com/docs/voyageai/quickstart) (2026-11-05 查阅)
- [Voyage AI Docs: Text Embeddings](https://www.mongodb.com/docs/voyageai/models/text-embeddings) (2026-11-05 查阅)
- [LlamaIndex Docs: Basic strategies](https://developers.llamaindex.ai/python/framework/optimizing/basic_strategies/basic_strategies/) (2026-11-04 查阅)
- [Alex Albert on X: Search result content blocks (3 July 2025)](https://x.com/alexalbert__/status/1940823684362781039) (2026-11-05 查阅)
- [Alex Albert on X: how search result blocks work (3 July 2025)](https://x.com/alexalbert__/status/1940823696236859855) (2026-11-05 查阅)
- [Hamel Husain on X: RAG and long context windows (10 July 2024)](https://x.com/HamelHusain/status/1811050329972322708) (2026-11-05 查阅)
- [freeCodeCamp.org on YouTube: Learn RAG From Scratch](https://www.youtube.com/watch?v=sVcwVQRHIc8) (2026-11-05 查阅)

[浏览 MCP 服务器](https://mindket.com/cn/mcp-servers)
