> For the complete documentation index, see [llms.txt](https://levon.gitbook.io/agent-engineering/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://levon.gitbook.io/agent-engineering/12rag-jian-suo-zeng-qiang-sheng-cheng.md).

# 第 12 课：RAG 检索增强生成——从资料入库到有据可查的回答

用户问客服助手：“我和爸妈能共用几个账号？”助手回答：“最多 3 个。”但产品手册写的是：家庭套餐支持 6 个账号，个人套餐支持 3 个。它找错了资料，回答得倒很干脆。

这是本课使用的假设案例。前面的课程已经让程序能调用工具、保存状态并控制执行；现在换一个任务：让助手依据产品资料回答问题。先不换模型，沿着资料进入系统、被检索、送进模型的过程，找出错误会发生在哪里。

## 1. RAG：检索资料，再生成回答

程序先找相关资料，再把问题和资料交给模型回答，这种方法叫 **Retrieval-Augmented Generation（RAG，检索增强生成）**。\[1] 检索可以使用关键词、向量或两者结合，向量化不是必要前提。

```
用户问题 -> 检索相关资料 -> 取得原文
-> 将问题与原文放入模型输入 -> 生成回答
```

产品手册存在数据库里，不代表模型已经看过；生成了向量，也不等于把产品知识训练进回答模型。只有选中的文字进入本次请求，模型才能据此作答。

这里可以先用固定 Workflow：检索一次，再调用模型回答。如果允许模型根据结果申请继续查、换关键词或询问用户，就加入了 Agent 式决策。Harness 仍检查权限、执行工具和限制次数。RAG 是可供 Agent 使用的能力，不要求整个系统都采用自主循环。

## 2. Chunking：分块保存完整意思

长手册可以按小节切成文本块，称为 **Chunking（文档分块）**。给每块一个编号，连同标题和原文保存，例如数据库中每块一行：

| 块编号 | 标题   | 原文                        |
| --- | ---- | ------------------------- |
| C1  | 家庭套餐 | 家庭套餐最多支持 6 个账号。           |
| C2  | 个人套餐 | 个人套餐最多支持 3 个账号。           |
| C3  | 退款规则 | 套餐购买后 7 天内可退款，但已经使用的套餐除外。 |

分块处理段落边界，分词提取检索用词，两者不同。这里按主题分块，不因为三段都有“套餐”就切在这个词上。

尤其不能把 C3 的退款期限与例外条件拆散。只取到“7 天内可退款”，就会漏掉已经使用的情况。整本手册塞成一块又可能带入太多无关内容，甚至超出上下文窗口。我会先按完整小节切分，保留标题；命中后缺少条件，再补取相邻段落。很短的资料直接提供全文也可以。

每块还要带上来源、产品、版本、生效时间和访问范围等 **Metadata（元数据）**。这些字段描述资料适用于谁、什么时候有效，不是给模型看的装饰。

## 3. Retrieval：倒排索引、向量索引与混合检索

保存好了原文，接下来解决“怎样找到 C1”。常见的两条路线如下：

| 检索方式  | 查询输入     | 查找依据       |
| ----- | -------- | ---------- |
| 关键词检索 | 查询中的词或编号 | 倒排索引中的词项匹配 |
| 向量检索  | 问题生成的向量  | 向量索引中的相似度  |

### 关键词检索：由词找到文本块

提前记录每个词出现在哪些块中，就是 **Inverted Index（倒排索引）**。\[2] 假设分词提取了以下词项，索引中的几条记录可以是：

```
家庭套餐 -> C1
账号     -> C1、C2
套餐     -> C1、C2、C3
退款     -> C3
```

“套餐”是一个词项，不是整个索引。查询“退款”，先找到 C3，再用编号取回原文。原文与索引可以放在同一套数据库里，逻辑上分清职责即可。

用户说“我和爸妈一起用”，资料却写“家庭套餐”，单纯字面匹配可能漏检。可以人工维护说法对应表，或让模型改写查询；这些方法不要求向量索引，但规则需要维护，模型改写也可能理解错。

### 向量检索：由相似表示找到文本块

**Embedding** 把文字转换成一组数字。用于语义检索的模型经过训练，让相关表达的表示更接近，便于比较。\[3] 它没有“家庭套餐这一维固定等于 6”这样的人工规则。

在本例的常见实现中，把 C1 的整段文字交给 Embedding 模型，得到 C1 的向量；C2、C3 分别处理。不是为每个中文词单独建一条检索记录，也不是必须把整本手册变成一个向量。模型内部的 token 编码，不要与应用层分块混为一谈。

```
资料准备：文本块 -> Embedding -> 保存向量、原文及关联
用户查询：问题 -> 配套的查询向量 -> 向量索引
          -> 找到对应文本块 -> 取得原文
```

向量化后原文仍要保留。检索接口若直接返回原文，就不用再查一次数据库；若只返回 C1，则按编号取回。这里比较的是向量，不需要再用它去查前面的词项倒排索引。

“家庭套餐”和“个人套餐”都讨论账号，向量可能觉得它们很像。相似度只帮助找候选，不能证明条款适用。

### Hybrid Search：融合两路结果

关键词适合找准错误码、产品名；向量可以补充表达不同的相关资料。让同一次查询结合两路结果，叫 **Hybrid Search（混合检索，也叫混合搜索）**。\[4]

```
问题 -> 关键词检索 -> 候选列表 A
     -> 向量检索   -> 候选列表 B

A、B -> 合并去重、融合排名 -> 选取原文
```

只建两种索引、每次只用一种，还不是这里说的混合检索。同一块被两路找到，不必重复送给模型；也不要求两路都命中才保留。两路分数可能不在同一尺度，可用排名融合等方法整理，先不展开计算公式。

![同一问题分别经过倒排索引和向量索引，两路候选合并去重后取回对应原文](https://3256184984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FO56Bk11JpENsvGCw6xhb%2Fuploads%2Fgit-blob-37b7a23832007bc13b66cf2c01d178d0778e499f%2F01-two-retrieval-paths.png?alt=media)

关键词检索与向量检索分别找候选，不是前后串联。混合检索融合两路结果，再选取原文交给模型。图中两路都命中 C1 只为展示去重，不要求每份资料都被两路同时找到；排序和适用性检查仍需完成。

## 4. Context Assembly：把适用的原文交给模型

回到开头的问题。检索找到 C1，只完成了定位。下面的代码使用预设命中编号，演示取回原文和组装输入，不运行搜索或调用模型：

```python
chunks = {
    "C1": "家庭套餐最多支持 6 个账号。",
    "C2": "个人套餐最多支持 3 个账号。",
}
question = "我和爸妈能共用几个账号？"
hit_ids = ["C1"]
evidence = []
for chunk_id in hit_ids:
    evidence.append(chunks[chunk_id])
model_input = "问题：" + question + "\n参考资料：\n" + "\n".join(evidence)
print(model_input)
```

输出为：

```
问题：我和爸妈能共用几个账号？
参考资料：
家庭套餐最多支持 6 个账号。
```

模型收到问题和依据后，才有依据回答“家庭套餐最多支持 6 个账号”。实际请求还要包含任务指令；资料作为待核对的数据，不得变成指挥工具执行的命令。若通过工具循环检索，Harness 还须把结果放入对应的 Tool Result，再发起下一次模型请求。只写日志，模型仍看不到。

送入前要确认资料属于当前公司和产品，用户有权访问，时间也适用。权限由程序执行，不让模型自己决定。问“现在”，用当前规则；问“2023 年”，用当时生效的规则，不能把历史资料一律丢弃。

如果用户问退款，却只找到账号数量，答案应说明缺少退款依据。可以在预算内继续检索，仍找不到就暂停确认。搜不到政策，不能推导出“不支持退款”。

## 5. Index Maintenance：原文变化，索引也要跟上

假设 C3 的退款期限从 7 天改成 3 天。原文更新后，要更新受影响的词项索引，并重新生成该块的向量。C1、C2 的向量输入和生成方式没变，就可以复用，不必重算整本手册。

旧向量并非一定搜不到 C3，而是检索依据已经过期。如果仍命中 C3，并取回最新原文，模型可能看到正确的 3 天；若返回的文字副本也没更新，就会继续拿旧政策回答。

删除 C3 时同样要处理索引和缓存副本。在索引只存编号的实现中，只删原文会留下“命中 C3，却取不到内容”的结果。程序应跳过失效候选并记录问题，不能把命中编号当作拿到了证据。

这些检查也解释了开头的错误：先看送给模型的原文。送错了个人套餐，查检索与筛选；家庭套餐原文正确、回答却说 3 个，才继续查输入组装和生成。否则，换了模型也可能只是换个人读错资料。

## 配套实践

先用本课三块资料完成一次纸面排查，不安装数据库：为“我和爸妈能共用几个账号”写出检索候选和最终交给模型的输入；再把问题换成“套餐已经使用，买了 5 天，能退款吗”，核对是否保留完整限制条件。

预期依据分别是 C1 的 6 个账号，以及 C3 的“已使用除外”。这里是给定规则下的阅读练习，不代表真实产品政策，也不证明已经运行过向量检索。完成标准是能区分查找路线、取回原文与生成回答三个动作，并指出证据不足时在哪一步停止。

## 资料

1. [RAG 原始论文：检索与生成的结合](https://arxiv.org/abs/2005.11401)。论文采用稠密检索，不代表所有 RAG 都必须使用向量。
2. [信息检索教材：倒排索引](https://nlp.stanford.edu/IR-book/html/htmledition/an-example-information-retrieval-problem-1.html)。正文用文本块编号简化了教材中的文档清单。
3. [Google：Embedding 的获得与训练](https://developers.google.com/machine-learning/crash-course/embeddings/obtaining-embeddings)。用于理解向量表示如何产生，不用于证明某次检索必然正确。
4. [Elastic：Hybrid Search](https://www.elastic.co/docs/solutions/search/hybrid-search)。用于核对关键词与向量结果融合，不绑定本课的存储选型。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://levon.gitbook.io/agent-engineering/12rag-jian-suo-zeng-qiang-sheng-cheng.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
