RAG 基础 - 让 Agent 拥有"知识" / Retrieval-Augmented Generation Fundamentals for Agent Knowledge
📅 创建时间:2026-05-08 🏷️ 标签:#RAG #向量数据库 #Embedding #检索增强 📚 前置知识:[[02-framework-evolution]]
📋 本章目标
- 理解为什么 LLM 需要 RAG
- 掌握 RAG 的三阶段:检索 → 增强 → 生成
- 理解 Embedding 和向量相似度的概念
- 了解主流向量数据库的特点
- 能够从零构建一个简单的 RAG 流程
- 理解 RAG 的局限性
第0部分:先搞清"文本变成向量"到底是怎么发生的
RAG 的整个流程中,最让人困惑的一步就是"把文本变成向量"。很多人第一次看到 embeddings.embed_query("年假政策") 这行代码时,不知道背后发生了什么——它是一个本地计算?还是又一个 API 调用?返回的 [0.12, -0.34, 0.56, ...] 这个数组到底是什么意思?
0.1 Embedding 模型是一个独立的 API
Embedding 模型和聊天模型(GPT-4o、Claude)是两种不同的 API 端点:
聊天模型(Chat Completion):
POST /v1/chat/completions
输入: messages (对话历史)
输出: {"content": "回答文本"}
Embedding 模型:
POST /v1/embeddings
输入: input (一段文本)
输出: {"data": [{"embedding": [0.12, -0.34, 0.56, ...]}]}Embedding 模型不做对话、不回答问题、不调用工具。它只做一件事:接收一段文本,返回一个浮点数数组。这个数组就是"向量"。
一个真实的 Embedding API 调用长这样:
POST https://api.openai.com/v1/embeddings
{
"model": "text-embedding-3-small",
"input": "公司年假政策:工作满1年享受5天年假"
}
响应:
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [
0.0123, -0.0456, 0.0789, -0.0234, 0.0567,
... // 共 1536 个浮点数(text-embedding-3-small 的维度)
]
}
]
}所以 embeddings.embed_query("...") 本质上也是一个 HTTP POST 请求,和你调用聊天模型没有本质区别——只是换了端点和返回格式。
0.2 向量是什么——当成"语义坐标"来理解
不要一上来就看余弦相似度公式。先建立直觉。
把 1536 维向量想象成空间中的一个点(虽然人脑想象不了 1536 维,但 2 维的直觉完全适用)。Embedding 模型做的事就是:把语义相似的文本映射到空间中相近的位置。
文本 模型输出的向量(只展示前2维做示意)
──── ──────────────────────────
"年假有多少天" → [0.82, 0.31, ...]
"带薪休假怎么算" → [0.78, 0.35, ...] ← 和上面很接近
"请假需要什么流程" → [0.40, 0.60, ...] ← 有点远
"今天天气真好" → [-0.70, -0.15, ...] ← 完全不同的方向它不是关键词匹配。"年假"和"带薪休假"两个词完全不同,但它们的向量很接近——因为 Embedding 模型在训练时从海量文本中学到了这两个概念在语境中经常互换使用。
0.3 怎么判断两个向量"接近"——不用背公式
你有两个向量:
A = [0.82, 0.31]
B = [0.78, 0.35]判断它们"接近"最直观的方法是看它们指向的方向是否一致。因为在实际应用中,向量的长度(模)会被归一化到 1,所以唯一有意义的就是方向。两个向量方向越一致 → 它们代表的文本语义越相似。
这就是余弦相似度的直觉——衡量两个箭头指向同一个方向的程度:
- 值 = 1:完全同一个方向(语义完全相同)
- 值 = 0:互相垂直(语义无关)
- 值 = -1:完全相反(语义对立,实际中很少见)
代码里你不需要自己算——向量数据库替你做了这个计算。但知道原理后,"检索"这一步就不会觉得神秘了。
0.4 为什么不是直接调用 LLM 来搜索?
你可能想:既然 LLM 已经能理解语义,为什么不直接把所有文档发给 LLM 让它找最相关的?
两个原因:
- 成本:每次查询都把全部文档发给 LLM → token 消耗 = 全部文档的长度 × 每次查询。有 1000 篇文档的话,每次查询花几十美元。
- 速度:LLM 生成回答需要几秒,而向量数据库的最近邻搜索(ANN)在毫秒级完成。
Embedding + 向量数据库的本质是:用极低成本预先算好所有文档的"语义坐标",查询时只需在这个坐标空间中找最近的点——不需要每次都让 LLM 读一遍全部文档。
0.5 整条数据流
把 Embedding 放到 RAG 的完整数据流中,每个环节的数据格式就清楚了:
索引阶段(上线前执行一次):
文档.txt
→ 切成小块: ["块1文本", "块2文本", ...]
→ 每块调一次 Embedding API
→ 得到向量: [[0.12, -0.34, ...], [0.45, 0.67, ...], ...]
→ 存入向量数据库: {id: "块1", vector: [0.12, -0.34, ...], text: "块1原始文本"}
查询阶段(每次用户提问):
用户问题: "年假有多少天?"
→ 调 Embedding API
→ 得到查询向量: [0.11, -0.30, ...]
→ 在向量数据库中做 ANN 搜索(找最近的 K 个向量)
→ 返回: [{text: "公司年假政策...", score: 0.95}, {text: "请假流程...", score: 0.72}]
→ 把检索到的文本 + 用户问题拼成 prompt
→ 调 Chat Completion API
→ 返回最终回答带着这个数据流模型,下面的细节就好理解了。
第1部分:为什么需要 RAG?
1.1 LLM 的知识困境
┌─────────────────────────────────────────────────────────────┐
│ LLM 的知识困境 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 困境1:知识有截止日期 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GPT-4 的知识截止到 2023年12月 │ │
│ │ 问它:"今天有什么新闻?" → 无法回答 │ │
│ │ 问它:"2024年的新政策" → 可能编造(幻觉) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 困境2:私有知识不可用 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 公司的内部文档、财务数据 │ │
│ │ • 用户的个人笔记、聊天记录 │ │
│ │ • 最新的产品手册、技术文档 │ │
│ │ LLM 没有权限访问这些数据 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 困境3:上下文窗口有限 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 把整个知识库塞进 Prompt? │ │
│ │ • 超出上下文限制 │ │
│ │ • Token 成本爆炸 │ │
│ │ • 模型处理长文本效果下降 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.2 RAG 的解决思路
RAG = 检索增强生成(Retrieval-Augmented Generation)
┌─────────────────────────────────────────────────────────────┐
│ RAG 核心思想 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 不把知识"塞进"模型 │
│ 而是在需要时"找到"相关知识 │
│ │
│ 当用户提问时: │
│ 1. 先去"知识库"里找相关的资料 │
│ 2. 把找到的资料 + 用户问题一起发给 LLM │
│ 3. LLM 基于真实资料回答 │
│ │
└─────────────────────────────────────────────────────────────┘1.3 RAG vs 微调(Fine-tuning)
┌─────────────────────────────────────────────────────────────┐
│ RAG vs 微调对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 维度 │ RAG │ 微调 │
│ ├──────────────┼──────────────────┼──────────────────────┤
│ │ 知识更新 │ 实时(改知识库) │ 需要重新训练 │
│ │ 成本 │ 低(无需训练) │ 高(GPU + 时间) │
│ │ 幻觉减少 │ 效果好 │ 有一定效果 │
│ │ 适用场景 │ 知识问答 │ 风格/行为调整 │
│ │ 可解释性 │ 高(可查原文) │ 低(知识隐含在权重) │
│ │ 适用知识量 │ 大规模知识库 │ 小而精的知识 │
│ │
│ 结论: │
│ • 知识问答、信息检索 → RAG │
│ • 改变说话风格、专有任务 → 微调 │
│ • 两者可以结合使用 │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:RAG 三阶段详解
2.1 阶段概览
┌─────────────────────────────────────────────────────────────┐
│ RAG 三阶段 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 阶段1:索引(Index)← 只执行一次 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 文档 → 分块(Chunk) → Embedding → 存入向量数据库 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 阶段2:检索(Retrieve)← 每次查询执行 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户问题 → Embedding → 在向量数据库中找相似块 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 阶段3:生成(Generate)← 每次查询执行 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户问题 + 检索结果 → LLM → 最终回答 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.2 阶段1:索引(Indexing)
目标:将文档处理后存入向量数据库
Step 1:文档加载(Load)
┌─────────────────────────────────────────────────────────────┐
│ 支持的格式:PDF、TXT、Markdown、HTML、Word、网页... │
│ └─────────────────────────────────────────────────────┐ │
│ from langchain.document_loaders import DirectoryLoader │ │
│ loader = DirectoryLoader("./docs", glob="*.pdf") │ │
│ docs = loader.load() │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
Step 2:文档分割(Chunk)
┌─────────────────────────────────────────────────────────────┐
│ 把长文档切成小块 │
│ └─────────────────────────────────────────────────────┐ │
│ from langchain.text_splitter import RecursiveCharacterText │ │
│ splitter = RecursiveCharacterTextSplitter( │ │
│ chunk_size=500, │ │
│ chunk_overlap=50 │ │
│ ) │ │
│ chunks = splitter.split_documents(docs) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
Step 3:向量化(Embed + Store)
┌─────────────────────────────────────────────────────────────┐
│ 把每个块变成向量,存入数据库 │
│ └─────────────────────────────────────────────────────┐ │
│ from langchain_openai import OpenAIEmbeddings │ │
│ from langchain.vectorstores import Chroma │ │
│ │ │
│ embeddings = OpenAIEmbeddings() │ │
│ vectorstore = Chroma.from_documents( │ │
│ documents=chunks, │ │
│ embedding=embeddings │ │
│ ) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘2.3 阶段2:检索(Retrieve)
目标:根据用户问题,找到最相关的知识块
┌─────────────────────────────────────────────────────────────┐
│ 检索流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户问题:"公司的年假政策是什么?" │
│ ↓ │
│ Step 1:把问题变成向量(Embedding) │
│ ↓ │
│ Step 2:在向量数据库中找相似向量 │
│ ↓ │
│ Step 3:返回最相似的 Top-K 个块 │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 向量相似度检索原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 语义相似 → 向量距离近 │
│ │
│ "年假政策" │
│ ↑ │
│ │ 语义上接近 │
│ ↓ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 年假天数 │ │ 请假流程 │ │ 团建安排 │ │
│ │ 向量A │ │ 向量B │ │ 向量C │ │
│ │ 距离: 0.2 │ │ 距离: 0.7 │ │ 距离: 1.5 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ 最相似 ↑ 中等相似 ↑ 不太相关 │
│ │
└─────────────────────────────────────────────────────────────┘检索代码示例:
# 方式1:相似度搜索
results = vectorstore.similarity_search(
query="公司的年假政策是什么?",
k=3 # 返回最相关的 3 个块
)
# 方式2:带分数的相似度搜索
results_with_scores = vectorstore.similarity_search_with_score(
query="年假有多少天?",
k=5
)
# 过滤低分结果
for doc, score in results_with_scores:
if score < 0.7: # 阈值过滤
print(f"内容: {doc.page_content}")2.4 阶段3:生成(Generate)
目标:用检索到的内容 + 用户问题,生成最终回答
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
llm = ChatOpenAI(model="gpt-4o")
# 方式1:用 RetrievalQA 链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 把检索结果"塞进"一次调用
retriever=vectorstore.as_retriever(search_kwargs={"k": 3})
)
result = qa_chain.invoke({"query": "公司年假政策是什么?"})
print(result["result"])第3部分:Embedding 核心原理
3.1 什么是 Embedding?
Embedding = 把文字变成一串数字(向量),语义相似的文字有相似的向量
┌─────────────────────────────────────────────────────────────┐
│ Embedding 示意 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 文字:"公司的年假政策" │
│ ↓ │
│ 模型处理 │
│ ↓ │
│ 向量:[0.12, -0.34, 0.56, 0.89, -0.21, ..., 0.45] │
│ ↑ │
│ 1536 维(GPT-4 的 embedding 维度) │
│ │
│ 语义相近 → 向量距离近: │
│ "年假有多少天" → [0.11, -0.30, 0.58, ...] 距离: 0.15 │
│ "请假流程" → [0.20, -0.50, 0.30, ...] 距离: 0.65 │
│ "今天天气怎么样" → [-0.80, 0.10, -0.20, ...] 距离: 1.82 │
│ │
└─────────────────────────────────────────────────────────────┘3.2 向量相似度计算
┌─────────────────────────────────────────────────────────────┐
│ 两种常用相似度度量 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 余弦相似度(Cosine Similarity) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ A · B │ │
│ │ cos = ───────── │ │
│ │ |A| × |B| │ │
│ │ │ │
│ │ 衡量方向是否相似,取值 -1 到 1 │ │
│ │ 1 = 完全相同方向 │ │
│ │ 0 = 垂直(无关联) │ │
│ │ -1 = 完全相反 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 2. 欧氏距离(Euclidean Distance) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ d = √[(A₁-B₁)² + (A₂-B₂)² + ... + (Aₙ-Bₙ)²] │ │
│ │ │ │
│ │ 衡量绝对距离,取值 0 到 +∞ │ │
│ │ 0 = 完全相同 │ │
│ │ 越小越相似 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 选择建议: │
│ • 语义相似性(语义角度)→ 余弦相似度 │
│ • 数值上有差异→欧氏距离(但通常差异不大) │
│ │
└─────────────────────────────────────────────────────────────┘3.3 Embedding 模型
from langchain_openai import OpenAIEmbeddings
# OpenAI 的 Embedding 模型
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small" # 最新模型,更小更快
)
# 或者
embeddings = OpenAIEmbeddings(
model="text-embedding-3-large" # 更大,效果更好
)
# 将文本转为向量
vector = embeddings.embed_query("公司的年假政策是什么?")
print(f"向量维度: {len(vector)}")第4部分:向量数据库
4.1 为什么需要向量数据库?
普通数据库 vs 向量数据库:
┌─────────────────────────────────────────────────────────────┐
│ 普通数据库的局限 │
├─────────────────────────────────────────────────────────────┤
│ │
│ SELECT * FROM docs WHERE content LIKE '%年假%' │
│ │
│ 问题: │
│ • 只能精确匹配关键词 │
│ • "年假"搜不到"带薪休假" │
│ • "请假政策"搜不到"年假安排" │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 向量数据库的优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ results = vectorstore.similarity_search("假期安排") │
│ │
│ 能找到语义相关的结果,即使没有相同关键词: │
│ ✓ "年假政策" │
│ ✓ "带薪休假的计算方式" │
│ ✓ "法定节假日与年假区别" │
│ │
└─────────────────────────────────────────────────────────────┘4.2 主流向量数据库对比
┌─────────────────────────────────────────────────────────────┐
│ 向量数据库对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 数据库 │ 类型 │ 适用场景 │
│ ├─────────────┼────────────┼───────────────────────────────┤
│ │ ChromaDB │ 轻量/本地 │ 开发测试、快速原型 │
│ │ FAISS │ 库/本地 │ 需要快速搜索、大数据量 │
│ │ Milvus │ 服务/云原生│ 企业级、生产环境 │
│ │ Pinecone │ 服务/云 │ 全托管、免运维 │
│ │ Weaviate │ 服务/云原生│ 混合搜索(向量+关键词) │
│ │ Qdrant │ 服务/云原生│ 高性能、注重准确率 │
│ │
│ 选型建议: │
│ • 学习/原型 → ChromaDB(最简单) │
│ • 大数据量 → FAISS(Meta 出品,免费高效) │
│ • 生产环境 → Milvus / Qdrant / Pinecone │
│ │
└─────────────────────────────────────────────────────────────┘4.3 ChromaDB 快速上手
# 安装:pip install chromadb
import chromadb
# 创建客户端(默认是本地文件存储)
client = chromadb.PersistentClient(path="./chroma_db")
# 创建集合(类似表)
collection = client.create_collection(
name="company_docs",
metadata={"description": "公司文档知识库"}
)
# 添加文档
collection.add(
documents=[
"公司年假政策:工作满1年享受5天年假,满3年享受10天...",
"请假流程:员工登录OA系统,提交请假申请..."
],
ids=["doc_1", "doc_2"],
metadatas=[{"source": "hr_policy"}, {"source": "oa_guide"}]
)
# 检索
results = collection.query(
query_texts=["年假有多少天?"],
n_results=2
)
print(results["documents"])第5部分:完整 RAG 示例
from langchain_openai import OpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader
# ========== 阶段1:索引 ==========
# 1. 加载文档
loader = DirectoryLoader("./company_docs", glob="*.txt")
docs = loader.load()
# 2. 分割文档
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)
# 3. 向量化并存入数据库
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./vector_db"
)
# ========== 阶段2+3:检索+生成 ==========
llm = ChatOpenAI(model="gpt-4o")
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 构建 QA 链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True # 返回来源文档
)
# ========== 问答 ==========
question = "我在公司工作3年了,每年有多少天年假?"
result = qa_chain.invoke({"query": question})
print(f"回答:{result['result']}")
print(f"参考来源:{[doc.metadata for doc in result['source_documents']]}")核心总结
总结1:RAG 三阶段
索引(一次性):文档 → 分块 → Embedding → 向量数据库
检索(每次查询):用户问题 → Embedding → 相似度搜索
生成(每次查询):用户问题 + 检索结果 → LLM → 回答总结2:Embedding 的作用
文字 → 向量(高维数字)
语义相似 → 向量距离近
→ 检索本质:找距离最近的向量总结3:向量数据库选型
| 场景 | 推荐 |
|---|---|
| 学习/原型 | ChromaDB |
| 大数据量 | FAISS |
| 生产环境 | Milvus / Pinecone |
章节测试
测试1:RAG 的作用
RAG 主要解决 LLM 的哪两个问题?
测试2:RAG 三阶段
请按顺序写出 RAG 的三个阶段及其作用。
测试3:Embedding 概念
什么是 Embedding?它解决了什么问题?
测试4:向量数据库
ChromaDB 和 FAISS 分别适合什么场景?
测试5:RAG vs 微调
什么情况下应该用 RAG,什么情况下应该用微调?
参考答案
测试1答案
答案:
- 知识有截止日期,无法获取最新信息
- 无法访问私有数据(公司文档、个人笔记等)
测试2答案
答案:
- 索引(Index):将文档加载、分块、向量化后存入向量数据库(一次性操作)
- 检索(Retrieve):根据用户问题,在向量数据库中找到最相关的文档块
- 生成(Generate):将检索结果和用户问题一起发给 LLM,生成最终回答
测试3答案
答案:Embedding 是将文字转换为高维向量的技术。语义相似的文字会产生"距离相近"的向量,从而实现"语义检索"——即使没有相同关键词,也能找到相关内容。
测试4答案
答案:
- ChromaDB:轻量级、本地存储,适合学习、快速原型、开发测试
- FAISS:Meta 出品,支持超大数据量检索,适合需要快速搜索海量向量的场景
测试5答案
答案:
- RAG:知识问答、需要实时更新知识、减少幻觉、需要可解释性 → RAG
- 微调:改变模型说话风格、专有任务模式、小而精的知识注入 → 微调
- 两者可以结合使用
相关笔记
- [[00-agent-overview]] - Agent 整体框架中 RAG 的位置
- [[07-rag-advanced]] - RAG 进阶技术:混合检索、重排序等
- [[05-agent-workflow]] - RAG 在 Agent 工作流中的应用
下一步学习
- [ ] 阅读 04 - 记忆管理
学习状态:🟡 开始学习