AI Agent 学习笔记(四):RAG 检索增强生成,给 Agent 一座图书馆

AI Agent 学习笔记(四):RAG 检索增强生成,给 Agent 一座图书馆

上一篇给 Agent 装上了“手”,这篇给它开一座“图书馆”——RAG(Retrieval-Augmented Generation,检索增强生成),让它基于真实资料回答问题,而不是瞎编。

一. 为什么需要 RAG

大模型有两个让人头疼的问题:

  1. 幻觉:不知道的事它会一本正经地编;
  2. 知识过时:训练数据是过去的数据,不知道现在的事,更不知道你公司内部的文档。

解决方案有三种思路,先看对比:

方案 做法 优点 缺点
直接问模型 啥也不做 简单 幻觉、过时
微调(Fine-tune) 用数据再训练模型 改变模型风格/能力 贵、慢、知识更新难
RAG 先把资料检索出来,塞进 Prompt 便宜、实时、可追溯、不用训练 检索质量决定上限

RAG 的思路特别朴素:模型不知道答案?没关系,我们先把答案相关的资料找出来,喂给它,让它看着资料回答。 就像开卷考试。

二. RAG 的整体流程(先看全景)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
【离线阶段:建图书馆】(做一次,资料更新时重做)
原始文档(PDF/Markdown/网页/数据库)
│ ① 清洗、解析

纯文本
│ ② 分块(Chunking)

若干文本块
│ ③ Embedding 向量化

向量数据库(存 文本+向量+元数据)

【在线阶段:查资料回答问题】(每次提问都做)
用户问题
│ ④ 问题向量化

⑤ 向量数据库检索最相似的 K 个文本块


⑥ 把 问题 + 检索到的资料 拼成 Prompt


⑦ LLM 基于资料生成回答(附来源)

三. 离线阶段:给文档建档

3.1 文档解析与清洗

PDF、Word、网页要先转成纯文本。常用的工具:

格式 工具
PDF pypdf、PyMuPDF
Word python-docx
HTML BeautifulSoup
Markdown 直接读(博客、文档站最友好)
1
2
3
# 示例:读取 Markdown 文件(本项目博客就是 Markdown)
with open("notes.md", "r", encoding="utf-8") as f:
raw_text = f.read()

3.2 分块(Chunking):RAG 质量的第一道关

整个文档太长不能直接塞进模型,要切成语义完整的小块

分块策略对比:

策略 做法 适合
固定大小 每 N 个字符一块,块间重叠 M 字符 快速起步
按段落/标题 按 Markdown 的 ## 标题切 结构化文档(手册、博客)
按句子/语义 语义完整处切分 长文本、混合内容
递归分割 先按段落,太长再按句子 通用默认
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def chunk_by_size(text: str, size: int = 500, overlap: int = 50) -> list:
"""固定大小分块,带重叠,避免把语义拦腰切断"""
chunks = []
start = 0
while start < len(text):
chunk = text[start:start + size]
# 尽量在句号/换行处切断(简单实现:找最后一个句号)
if len(chunk) == size:
cut = max(chunk.rfind("。"), chunk.rfind("\n"))
if cut > size // 2:
chunk = chunk[:cut + 1]
chunks.append(chunk)
start += len(chunk) - overlap
return chunks

分块太小(<100 字)上下文不全,太大(>2000 字)噪音太多。500800 字 + 50100 字重叠是常见起点,具体按文档调。

3.3 Embedding:把文本变成向量

(上一篇提过)Embedding 把文本变成一串数字,语义相近的文本向量距离近。常用模型:

模型 维度 特点
text-embedding-3-small 1536 OpenAI,便宜好用
bge-large-zh / bge-m3 1024 中文效果好,可本地部署
text2vec-large-chinese 1024 中文场景常见
jina-embeddings-v3 1024 多语言

3.4 存入向量数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import chromadb
from openai import OpenAI

client = OpenAI()

def embed(text: str) -> list:
resp = client.embeddings.create(model="text-embedding-3-small", input=text)
return resp.data[0].embedding

# 1. 建库(本地 Chroma 无需服务器)
chroma = chromadb.PersistentClient(path="./rag_db")
collection = chroma.get_or_create_collection("my_notes")

# 2. 准备数据:texts 是分块结果,ids 唯一,metadatas 记来源
texts = ["AI Agent 是能自主完成任务的程序……", "RAG 是检索增强生成……", "MCP 是模型上下文协议……"]
ids = [f"chunk-{i}" for i in range(len(texts))]
metadatas = [{"source": "notes.md", "page": 1} for _ in texts]

# 3. 写入(Chroma 内置默认 embedding 也可,这里显式传入)
collection.add(
ids=ids,
documents=texts,
metadatas=metadatas,
embeddings=[embed(t) for t in texts],
)
print("索引完成,共", collection.count(), "个文本块")

四. 在线阶段:回答问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1. 问题向量化
question = "什么是 RAG?"
q_vec = embed(question)

# 2. 检索最相似的 K 个文本块
results = collection.query(
query_embeddings=[q_vec],
n_results=3,
include=["documents", "metadatas", "distances"],
)

# 3. 取出资料
docs = results["documents"][0]
context = "\n\n".join(f"[资料{i + 1}]\n{doc}" for i, doc in enumerate(docs))

# 4. 拼 Prompt 让模型基于资料回答
from openai import OpenAI
llm = OpenAI()

resp = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是知识库问答助手。只能根据提供的资料回答,资料中没有的信息要明确说明。回答末尾列出引用的资料编号。"},
{"role": "user", "content": f"资料:\n{context}\n\n问题:{question}"},
],
)
print(resp.choices[0].message.content)

4.1 检索结果的相似度阈值

向量检索永远会返回 K 条结果,但可能全是无关的。要设置阈值过滤:

1
2
3
4
5
6
7
8
9
# Chroma 的 distance 是 L2 距离,越小越相似(0 = 完全一致)
# 不同 embedding 模型阈值不同,先用测试数据观察分布
MAX_DISTANCE = 1.2
filtered = [
doc for doc, dist in zip(docs, results["distances"][0])
if dist < MAX_DISTANCE
]
if not filtered:
print("知识库中没有相关内容,直接告诉用户不知道。")

五. 进阶:检索质量不够怎么办

RAG 的效果上限由检索决定。检索结果不准,后面生成得再好也没用。常用升级手段:

5.1 混合检索(Hybrid Search)

向量检索懂语义但不懂“精确关键词”,BM25 关键词检索刚好相反,两者结合效果最好:

1
2
3
4
用户问题
├──► 向量检索(语义相似) ──┐
│ ├──► 合并去重 ──► 重排序 ──► Top K
└──► BM25 检索(关键词) ──┘
1
2
3
4
5
6
7
8
9
# 思路:两个检索结果按分数归一化后加权合并
def hybrid_search(vector_hits, keyword_hits, alpha: float = 0.6):
"""alpha 是向量权重;按文档 id 合并分数"""
scores = {}
for doc, s in vector_hits:
scores[doc] = scores.get(doc, 0) + alpha * s
for doc, s in keyword_hits:
scores[doc] = scores.get(doc, 0) + (1 - alpha) * s
return sorted(scores.items(), key=lambda x: x[1], reverse=True)

5.2 重排序(Rerank)

先用便宜方法召回 20 条,再用**重排序模型(如 bge-reranker、Cohere Rerank)**精排成 Top 5。重排序模型会看“问题和文档的匹配程度”,比纯向量相似度准得多。

5.3 查询改写

用户问“它多少钱”这种指代不清的问题,先用 LLM 把问题补全成“这个产品的价格是多少”再检索。

5.4 分块优化

  • 保留章节标题作为元数据,检索后返回“标题+正文”,让模型知道上下文;
  • 父子分块:检索小块(精确),回传大块(完整上下文)。

六. RAG 与 Memory、Tool 怎么配合

它们解决不同问题,可以同时用:

1
2
3
4
5
6
7
8
9
10
用户提问


┌────────────────────────────────────────────┐
│ Agent 编排层 │
│ ① 查长期记忆(用户偏好/历史)→ 个性化 │
│ ② RAG 检索知识库 → 事实依据 │
│ ③ 需要实时数据 → 调用工具(天气/搜索/API) │
│ ④ 全部塞进 Prompt → LLM 生成回答 │
└────────────────────────────────────────────┘

七. RAG 常见坑(初学者避雷)

表现 对策
分块不合理 答案断章取义 按结构分块、加重叠
不设阈值 无关资料被引用 相似度阈值过滤
中文 Embedding 效果差 搜不准 换中文模型(bge 等)
只检索不溯源 无法验证对错 返回来源(文件名/页码)
资料太多 超出上下文窗口 限制 K、截断、重排序
知识库不更新 资料过期 定期重建索引

八. 小结与作业

小结

  1. RAG = 检索 + 生成:先找资料,再让模型看着资料回答;
  2. 离线建索引:解析 → 分块 → Embedding → 向量库;
  3. 在线问答:问题向量化 → 检索 Top K → 拼 Prompt → 生成;
  4. 质量升级三件套:混合检索、重排序、查询改写;
  5. RAG 治“知识”,Memory 记“人”,Tool 干“活”。

作业

  1. 用你自己的一篇博客 Markdown 建一个 Chroma 知识库,问 3 个关于文章内容的问题;
  2. 实验不同分块大小(200 / 500 / 1000 字),对比回答质量;
  3. 故意问一个知识库里没有的问题,确认 Agent 会说“不知道”而不是编造。

下一篇:Skill 技能——把常用的提示词、流程和工具打包成可复用的“技能”。