主题
第三篇:Embedding、向量数据库与 RAG 实战【进阶选读】
⏱ 推荐分享时长:60 分钟
设计思路:这是整个系列的进阶选读篇。第一篇 1.8 已经讲清了 Embedding 和 RAG 的基础概念,本篇面向需要深入落地 RAG 的同事——知识库问答、智能客服、文档搜索、代码检索,只要涉及"让 AI 基于私有数据回答问题",就需要这篇的内容。
整体逻辑:真实问题引入 → Embedding 深入 → 模型选型 → 向量数据库 → 分块策略 → RAG 架构 → 进阶优化 → PDF 问答实战
3.1 为什么需要这篇——从一个真实场景说起
开场演示
现在就来做个实验。把一份公司内部的技术文档丢给 ChatGPT:
你:"我们的用户服务(user-service)怎么部署到 K8s 集群?需要配置哪些环境变量?"
ChatGPT:"抱歉,我没有关于贵公司 user-service 的具体部署文档。
不过,一般来说,部署到 K8s 需要以下步骤:
1. 编写 Dockerfile...
2. 创建 Deployment YAML..."
问题:它给的是通用答案,不是你公司的答案。为什么 ChatGPT 答不了?
- 知识截止:模型训练数据有截止日期,没有最新信息
- 私有数据:你的内部文档不在互联网上,模型从没见过
- 窗口限制:即使你想粘贴进去,几十页文档也超出上下文窗口
三种思路对比
方案一:直接粘贴进对话框
┌────────────────────────┐
│ 把50页文档粘贴进去... │ ❌ 文档太长,超出上下文窗口
│ │ ❌ 即使能放进去,"Lost in the Middle"
│ │ ❌ 每次对话都要重新粘贴
└────────────────────────┘
方案二:微调模型
┌────────────────────────┐
│ 用内部文档微调模型 │ ❌ 成本高(需要 GPU + 数据标注)
│ │ ❌ 文档更新了就要重新训练
│ │ ❌ 微调适合"风格"不适合"知识"
└────────────────────────┘
方案三:RAG(检索增强生成)✅
┌────────────────────────┐
│ 文档变成可搜索的知识库 │ ✅ 只检索相关片段,不超窗口
│ 用户提问时检索相关内容 │ ✅ 文档更新只需重新索引
│ 把检索结果注入 Prompt │ ✅ 答案有据可查,可引用溯源
└────────────────────────┘RAG 的核心链路预览
离线阶段(建索引,只做一次) 在线阶段(回答问题,每次请求)
┌─────────────────────────────┐ ┌──────────────────────────────────┐
│ │ │ │
│ 📄 文档 → ✂️ 分块 │ │ ❓ 用户提问 │
│ ↓ │ │ ↓ │
│ 🔢 向量化 │ │ 🔢 向量化 │
│ (Embedding) ↓ │ │ ↓ │
│ 💾 存入向量数据库 │ │ 🔍 向量检索 → 返回相关片段 │
│ │ │ ↓ │
└─────────────────────────────┘ │ 📝 拼入 Prompt → LLM 生成回答 │
│ ↓ │
│ ✅ 回答 + 引用来源 │
└──────────────────────────────────┘带着问题往下学
- "向量化"是怎么把文字变成可搜索的? → 3.2 Embedding
- Embedding 模型这么多,该选哪个? → 3.3 模型选型
- 向量存在哪里?怎么快速查? → 3.4 向量数据库
- 文档太长怎么切分才合理? → 3.5 分块策略
- RAG 完整流程到底怎么走? → 3.6 RAG Pipeline
- 检索到了但答案不准怎么办? → 3.7 进阶优化
3.2 什么是 Embedding
一句话理解
把文本变成一组数字(向量),语义相似的文本在向量空间中距离相近。
为什么需要这么做?因为计算机不理解"React 性能优化"和"前端渲染速度提升"是类似的意思,但它能计算两组数字之间的距离。Embedding 就是这个"翻译"过程——把人类语言翻译成机器能比较的数学表示。
直观演示:3 行代码理解语义搜索
python
from openai import OpenAI
client = OpenAI()
# 把三句话变成向量
texts = [
"React 列表渲染性能优化",
"前端页面渲染速度如何提升",
"Go 语言微服务架构设计",
]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts,
)
# 取出向量
vectors = [item.embedding for item in response.data]
# 计算相似度(余弦相似度)
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"React优化 vs 前端渲染: {cosine_similarity(vectors[0], vectors[1]):.3f}")
# → 0.891(非常相似!虽然没有共同关键词)
print(f"React优化 vs Go微服务: {cosine_similarity(vectors[0], vectors[2]):.3f}")
# → 0.234(不太相关)
print(f"前端渲染 vs Go微服务: {cosine_similarity(vectors[1], vectors[2]):.3f}")
# → 0.198(不太相关)关键发现:
- "React 列表渲染性能优化"和"前端页面渲染速度如何提升"没有任何共同关键词,但相似度高达 0.891
- 传统关键词搜索找不到这个关联,语义搜索可以
- 这就是 Embedding 的力量——它理解的是意思,不是字面
Embedding 的维度
每个文本被编码成一个固定长度的数字列表,这个长度就是"维度":
"React 性能优化" → [0.12, -0.34, 0.56, ..., 0.78]
└──────── 1536 个数字 ────────┘
(维度 = 1536)| 模型 | 维度 | 精度 | 存储 | 速度 |
|---|---|---|---|---|
| text-embedding-3-small | 1536 | 标准 | 6 KB/向量 | 快 |
| text-embedding-3-large | 3072 | 高 | 12 KB/向量 | 中 |
| BGE-M3 | 1024 | 高 | 4 KB/向量 | 快 |
类比:
- 3072 维像超高清照片(4K)——细节丰富,文件大
- 1536 维像高清照片(1080P)——日常够用
- 256 维像缩略图——粗略预览,体积小
维度越高,语义表达能力越强,但存储和计算成本也越高。大多数场景 1024~1536 维是最佳平衡点。
不只是文本:多模态 Embedding
现代 Embedding 模型不仅能处理文字,还能处理图片、音频、视频:
同一个向量空间中:
"一只猫在沙发上睡觉" → [0.45, 0.12, -0.33, ...]
🖼️ [猫睡觉的照片] → [0.43, 0.14, -0.31, ...] ← 距离很近!
🖼️ [汽车在公路上] → [-0.22, 0.67, 0.15, ...] ← 距离很远
→ 你可以用文字搜索图片,或者用图片搜索文字!应用场景:
- 电商:用文字描述搜索商品图片("红色碎花连衣裙")
- 设计:用草图搜索相似设计稿
- 视频:用文字描述搜索视频片段
MRL(Matryoshka 套娃表示)
MRL 是一种实用技巧——高维向量可以截断到低维仍然可用:
原始向量(3072 维):[0.12, -0.34, 0.56, 0.78, 0.23, ..., 0.45]
└────────────── 3072 个数字 ──────────────┘
截断到 1024 维: [0.12, -0.34, 0.56, 0.78, 0.23, ..., 0.67]
└────────── 1024 个数字 ──────────┘
精度损失约 5%,存储节省 67%
截断到 256 维: [0.12, -0.34, 0.56, ..., 0.91]
└──── 256 个数字 ────┘
精度损失约 15%,存储节省 92%为什么叫"套娃"? 就像俄罗斯套娃一样,大向量里面"嵌套"着一个可用的小向量。不需要重新计算,直接截断前 N 维就行。
实际意义:
- 百万级文档时,3072 维 → 256 维可以从 12GB 存储降到 1GB
- 先用低维做粗筛(快),再用高维做精排(准)
- OpenAI text-embedding-3 系列和多数新模型都支持 MRL
3.3 Embedding 模型选型
2026 年主流模型全景
| 模型 | 厂商 | 模态 | 维度 | 核心优势 | 定价 |
|---|---|---|---|---|---|
| Gemini Embedding 2 | 文本/图像/视频/音频/PDF | 768 | 五模态,跨语言综合第一 | $0.006/M tokens | |
| Jina Embeddings v4 | Jina AI | 文本/图像 | 2048 | 三个 LoRA 适配器切换场景,MRL 优秀 | $0.018/M tokens |
| Voyage Multimodal 3.5 | Voyage AI | 文本/图像/视频 | 1024 | 均衡无短板,MRL 压缩第一 | $0.02/M tokens |
| text-embedding-3-large | OpenAI | 文本 | 3072 | API 生态最成熟,MRL 支持 | $0.13/M tokens |
| text-embedding-3-small | OpenAI | 文本 | 1536 | 便宜,快速验证首选 | $0.02/M tokens |
| Cohere Embed v4 | Cohere | 文本 | 1024 | MTEB 榜单常客,自带 Reranker | $0.1/M tokens |
| Qwen3-VL-Embedding | 阿里 | 文本/图像 | 2048 | 开源多模态,2B 跨模态超闭源 | 免费(自部署) |
| BGE-M3 | 智源 | 文本 | 1024 | 开源中文标杆,100+ 语言 | 免费(自部署) |
| mxbai-embed-large | MixedBread | 文本 | 1024 | 轻量级(335M),资源受限场景 | 免费(自部署) |
MTEB 排行榜解读
MTEB(Massive Text Embedding Benchmark)是 Embedding 模型最权威的评测榜单。
怎么看榜单:
MTEB 榜单列很多,但你只需关注这几个:
1. Retrieval Average(检索平均分)← 做 RAG 最重要的指标
2. Classification(分类)← 做文本分类时关注
3. Clustering(聚类)← 做主题聚类时关注
4. Overall Average(综合平均)← 整体能力参考
做 RAG 选模型 → 只看 Retrieval Average 列就够榜单地址:MTEB Leaderboard
选型核心维度
| 维度 | 说明 | 测试方法 |
|---|---|---|
| 跨语言能力 | 中文 query 能否准确检索英文文档 | 准备中英文混合测试集 |
| 长文档能力 | 文本超过 4K Token 时精度是否衰减 | 用长短文本对比检索效果 |
| MRL 维度压缩 | 截断到低维后精度损失多少 | 同一测试集比较不同维度 |
| 多模态需求 | 是否需要处理图文混合文档 | 根据业务场景判断 |
选型决策图
你的场景是什么?
│
├── 快速验证 / Demo
│ └── → OpenAI text-embedding-3-small(便宜、API 简单、$0.02/M tokens)
│
├── 正式项目 · 纯文本
│ ├── 中文为主 → BGE-M3(开源免费,中文能力强)
│ ├── 英文为主 → Cohere Embed v4(MTEB 榜单前列)
│ └── 中英混合 → text-embedding-3-large 或 BGE-M3
│
├── 正式项目 · 图文混合
│ ├── 预算充足 → Gemini Embedding 2(五模态全覆盖)
│ ├── 需要本地部署 → Qwen3-VL-Embedding(开源可商用)
│ └── 均衡选择 → Jina Embeddings v4
│
├── 资源受限(边缘端 / 低配服务器)
│ └── → mxbai-embed-large (335M) 或 nomic-embed-text (137M)
│
└── 🔑 无论选哪个:先用自己的业务数据跑测试!
公开 Benchmark ≠ 你的业务效果3.4 相似度计算与向量数据库
相似度度量
拿到两个向量后,如何判断它们有多"相似"?
余弦相似度(最常用)
A · B 向量A和B的点积
cos(θ) = ────────── = ─────────────────────
|A| × |B| A的长度 × B的长度
范围:-1 到 1
1 = 完全相同方向(语义一致)
0 = 完全正交(不相关)
-1 = 完全相反方向(语义相反)类比:把两个向量想象成从原点出发的两支箭头。余弦相似度衡量的是箭头方向的夹角,不关心箭头长短。方向越接近,相似度越高。
python
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 语义相似的文本
sim1 = cosine_similarity(embed("React hooks 教程"), embed("React 钩子使用指南"))
# → 0.92
# 语义不相关的文本
sim2 = cosine_similarity(embed("React hooks 教程"), embed("今天北京天气不错"))
# → 0.15其他度量方式
| 度量 | 特点 | 适用场景 |
|---|---|---|
| 余弦相似度 | 衡量方向,对向量长度不敏感 | 大多数场景首选 |
| 欧几里得距离(L2) | 衡量绝对距离 | 归一化后的向量 |
| 点积(Inner Product) | 归一化后等价于余弦相似度 | 部分场景计算更快 |
简单结论
选余弦相似度就对了。除非你有特殊需求且充分了解不同度量的差异,否则余弦相似度是最安全的选择。
为什么需要专门的向量数据库
场景:你有 100 万条文档,每条变成 1536 维向量
用户提问 → 变成向量 → 找到最相似的 Top 5
暴力计算:
逐个比较 100万 次余弦相似度
100万 × 1536 × 4字节 ≈ 6 GB 内存
每次查询需要遍历全部 → 延迟 > 1 秒
❌ 慢!用户等不了
向量数据库(使用 ANN 索引):
预先建立索引结构(HNSW / IVF)
查询时只需比较少量候选(~1%)
延迟 < 10 毫秒,准确率 > 95%
✅ 快!用户无感核心概念:ANN 近似最近邻
ANN(Approximate Nearest Neighbor)的核心思想:不追求 100% 精确的最近邻,而是用极小的精度损失换取数量级的速度提升。
两种主流索引类型:
| 索引类型 | 原理 | 类比 | 特点 |
|---|---|---|---|
| HNSW | 多层图结构,层层缩小搜索范围 | 坐电梯——先到大致楼层,再走楼梯精确定位 | 查询快、内存占用大 |
| IVF | 先聚类分桶,只在相关的桶里搜索 | 图书馆分区——先定位"科技类"书架,再在书架上找 | 内存省、需要训练 |
HNSW 工作原理(简化):
Layer 2: A ────── B ────── C (稀疏层,快速定位大区域)
│ │
Layer 1: A ── D ── B ── E ── C (中间层,缩小范围)
│ │ │ │ │
Layer 0: A─F─D─G─B─H─E─I─C─J (底层,精确搜索)
查询过程:
1. 从 Layer 2 开始,找到离目标最近的节点(比如 B)
2. 下降到 Layer 1,在 B 附近找更近的节点
3. 下降到 Layer 0,精确搜索
→ 只遍历了少量节点,而不是全部 100 万个主流向量数据库对比
| 数据库 | 语言 | 定位 | 数据规模 | 特色 | 部署方式 |
|---|---|---|---|---|---|
| ChromaDB | Python | 轻量嵌入式 | < 100 万 | pip install 即用 | 嵌入进程 / Docker |
| Qdrant | Rust | 高性能 | 百万~千万 | 性能优秀,SDK 完善 | Docker / Cloud |
| Milvus | Go/C++ | 分布式 | 亿级 | 分布式,大规模生产 | K8s / Cloud |
| Weaviate | Go | 全功能 | 百万~千万 | 混合检索内置 | Docker / Cloud |
| Pinecone | - | 全托管 SaaS | 亿级 | 零运维 | Cloud only |
| pgvector | C | PG 插件 | < 百万 | 复用现有 PG | 随 PG 部署 |
选型快速对比:
开发体验 性能 规模 运维成本
ChromaDB ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
pgvector ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
Qdrant ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Milvus ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
Weaviate ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Pinecone ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐团队落地建议
你的场景是什么?
│
├── Demo / 快速验证 / 小项目(< 10 万条)
│ └── → ChromaDB(pip install chromadb,零配置)
│
├── 前端 Node.js 项目
│ ├── 小规模 → ChromaDB(JS SDK)
│ └── 中规模 → Qdrant(Node.js SDK 成熟)
│
├── 后端 Go 项目
│ ├── 中规模 → Qdrant(Go SDK 完善)
│ └── 大规模 → Milvus(Go SDK + 分布式能力)
│
├── 已有 PostgreSQL
│ └── → 先试 pgvector,够用就不引入新组件
│
└── 起步路径:
ChromaDB 跑通 Demo → 验证效果 → 数据量大了迁移到 Qdrant / Milvuspython
# ChromaDB 最小示例(5 行代码跑通向量存储和检索)
import chromadb
client = chromadb.Client()
collection = client.create_collection("docs")
# 存入文档(自动计算 Embedding)
collection.add(
documents=[
"React 虚拟列表可以解决大数据量渲染卡顿问题",
"Go 的 goroutine 实现了轻量级并发",
"Kubernetes 通过 Pod 实现容器编排",
],
ids=["doc1", "doc2", "doc3"],
)
# 语义检索
results = collection.query(
query_texts=["前端列表渲染性能优化怎么做"],
n_results=2,
)
print(results["documents"])
# → [["React 虚拟列表可以解决大数据量渲染卡顿问题", ...]]3.5 文本分块策略(Chunking)
为什么要分块
问题 1:Embedding 模型有上下文窗口限制
text-embedding-3-small 最大 8191 Token
一篇 50 页的技术文档远超这个限制 → 必须切分
问题 2:大块文本检索精度低
一个 5000 字的 chunk 里可能只有一句话和问题相关
→ 检索回来一大段"废话",LLM 找不到关键信息
所以:分块 = 让每一块都"小而精",既不超窗口,又能精准匹配4 种常见分块方法
我们用同一段文档来对比不同切法的效果:
markdown
原始文档:
# React 性能优化指南
## 虚拟列表
当列表数据量超过 1000 条时,直接渲染会导致页面卡顿。
推荐使用 react-window 或 react-virtuoso 实现虚拟滚动。
虚拟列表的核心原理是只渲染可视区域内的 DOM 元素。
## React.memo
React.memo 是一个高阶组件,用于避免不必要的重渲染。
当组件的 props 没有变化时,React.memo 会跳过渲染。
注意:React.memo 只做浅比较,对象和数组需要保持引用稳定。方法 1:固定大小分块
每 200 字符切一刀,不管内容:
Chunk 1: "# React 性能优化指南\n\n## 虚拟列表\n当列表数据量超过 1000 条时,直接渲染会导致页面卡顿。\n推荐使用 react-window 或 react-virtuoso 实现虚拟滚动。\n虚拟列表的核心原理"
Chunk 2: "是只渲染可视区域内的 DOM 元素。\n\n## React.memo\nReact.memo 是一个高阶组件,用于避免不必要的重渲染。\n当组件的 props 没有变化时,React.memo 会跳过"
Chunk 3: "渲染。\n注意:React.memo 只做浅比较,对象和数组需要保持引用稳定。"
❌ 问题:"虚拟列表的核心原理"和"是只渲染可视区域内的DOM元素"被切断了!方法 2:递归字符分块(推荐 ✅)
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50, # 50字符重叠
separators=["\n\n", "\n", "。", ",", " ", ""], # 优先按段落切
)
chunks = splitter.split_text(document)切分逻辑:先按 "\n\n"(段落)切 → 段落太长就按 "\n"(行)切 → 还太长按句号切
Chunk 1: "# React 性能优化指南\n\n## 虚拟列表\n当列表数据量超过 1000 条时,直接渲染会导致页面卡顿。推荐使用 react-window 或 react-virtuoso 实现虚拟滚动。虚拟列表的核心原理是只渲染可视区域内的 DOM 元素。"
Chunk 2: "## React.memo\nReact.memo 是一个高阶组件,用于避免不必要的重渲染。当组件的 props 没有变化时,React.memo 会跳过渲染。注意:React.memo 只做浅比较,对象和数组需要保持引用稳定。"
✅ 按段落自然切分,语义完整方法 3:语义分块(Semantic Chunking)
python
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
)切分逻辑:
1. 先把文档拆成句子
2. 计算相邻句子的 Embedding 相似度
3. 相似度突然下降的地方就是"语义断点"
4. 在断点处切分
效果:同一个主题的内容被放在一起,不会把"虚拟列表"和"React.memo"混在一个chunk
✅ 语义最完整
❌ 需要调用 Embedding API(额外成本)
❌ 分块速度较慢方法 4:按文档结构分块
python
from langchain_text_splitters import MarkdownHeaderTextSplitter
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "title"),
("##", "section"),
]
)切分逻辑:利用 Markdown 标题作为天然分隔符
Chunk 1: {title: "React 性能优化指南", section: "虚拟列表", content: "当列表..."}
Chunk 2: {title: "React 性能优化指南", section: "React.memo", content: "React.memo是..."}
✅ 保留了文档层级结构(检索时知道"这段属于哪个章节")
✅ 特别适合 Markdown、HTML 等有明确结构的文档Chunk Size 与 Overlap 调优
Chunk Size 太大 Chunk Size 太小
┌──────────────────────┐ ┌──────────────────────┐
│ 一个 chunk 2000 字 │ │ 一个 chunk 50 字 │
│ 其中只有一句和问题相关 │ │ "推荐使用react-window" │
│ → 检索回来一大段"废话" │ │ → 缺少上下文 │
│ → LLM 被干扰 │ │ → "为什么推荐?"不知道 │
└──────────────────────┘ └──────────────────────┘
最佳平衡点
┌──────────────────────┐
│ Chunk Size: 500~1000 字符│
│ Overlap: 100~200 字符 │
│ → 每块信息完整 │
│ → 重叠部分保持上下文连贯│
└──────────────────────┘通用经验值:
| 文档类型 | 推荐 Chunk Size | 推荐 Overlap |
|---|---|---|
| 技术文档 | 800~1200 | 200 |
| 对话记录 | 300~500 | 50 |
| 法律合同 | 1000~1500 | 300 |
| 代码文件 | 按函数/类切 | 0(结构化切分) |
最佳实践
- 先用 RecursiveCharacterTextSplitter(LangChain 默认推荐),大多数场景够用
- 如果文档有明确结构(Markdown/HTML),用结构化分块
- 追求极致效果时,试试语义分块
- 一定要根据业务场景 A/B 测试——同一个参数在不同数据集上表现可能完全不同
🔗 回到 Mini-OpenClaw
第二篇的 Mini-OpenClaw 中,memory_indexer.py 对 MEMORY.md 使用的是 LlamaIndex 的默认分块策略(类似递归字符分块),chunk_size 约 1024 Token,overlap 约 200 Token。对于 Markdown 格式的记忆文件来说,这个配置是合理的。
3.6 RAG 检索增强生成
为什么需要 RAG
| 问题 | 说明 | RAG 如何解决 |
|---|---|---|
| 知识截止 | 模型训练数据有截止日期 | 知识库可实时更新 |
| 幻觉 | 没有依据时模型会编造 | 答案基于检索到的真实文档 |
| 私有数据 | 企业数据不在训练集中 | 把私有数据变成可检索的知识库 |
| Token 限制 | 不能把所有文档塞进上下文 | 只检索最相关的几个片段 |
RAG vs 微调(Fine-tuning)
| 维度 | RAG(开卷考试) | 微调(考前突击) |
|---|---|---|
| 类比 | 带着参考书去考试 | 提前把知识背到脑子里 |
| 知识更新 | 更新文档即可,秒级生效 | 需要重新训练,小时~天级 |
| 成本 | 向量数据库 + Embedding API | GPU 训练成本 |
| 准确性 | 答案有引用来源,可追溯 | 可能产生幻觉 |
| 适用 | 知识查询、文档问答 | 风格调整、格式定制 |
| 门槛 | 低(只需 API 调用) | 高(需要标注数据 + GPU) |
关键结论
90% 的场景 RAG 优先。只有当 RAG 满足不了(比如需要模型输出特定风格、严格遵守复杂格式)时,才考虑微调。两者不互斥,可以组合使用。
经典 RAG Pipeline
完整的数据流分为三个阶段:
═══════════════════════════════════════════════════════════════
阶段一:Indexing(离线建索引,只做一次或定期更新)
═══════════════════════════════════════════════════════════════
📄 原始文档 💾 向量数据库
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ PDF │ │ Chunk 1 │ │ [0.12,…] │ │ │
│ Markdown │ → │ Chunk 2 │ → │ [0.34,…] │ → │ ChromaDB │
│ HTML │ │ Chunk 3 │ │ [0.56,…] │ │ Qdrant │
│ Word │ │ ... │ │ ... │ │ pgvector │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
文档加载 分块 Embedding 存储
(Document Loader) (Splitter) (Embedding Model) (Vector Store)
═══════════════════════════════════════════════════════════════
阶段二:Retrieval(在线检索,每次请求都执行)
═══════════════════════════════════════════════════════════════
❓ 用户提问 🔍 向量检索
┌──────────────┐ ┌──────────┐ ┌──────────────────┐
│ "user-service │ │ [0.15,…] │ │ Chunk 7 (0.94) │
│ 怎么部署?" │ → │ │ → │ Chunk 12 (0.89) │
└──────────────┘ └──────────┘ │ Chunk 3 (0.85) │
用户问题 Embedding └──────────────────┘
Top-K 相关片段
═══════════════════════════════════════════════════════════════
阶段三:Generation(在线生成,每次请求都执行)
═══════════════════════════════════════════════════════════════
┌─────────────────────────────────────┐
│ System Prompt: │
│ "根据以下参考资料回答用户问题。 │
│ 如果资料中没有相关信息,请说不知道。 │
│ │
│ 参考资料: │
│ [Chunk 7] user-service 部署需要... │
│ [Chunk 12] 环境变量配置包括... │ ┌────────────────┐
│ [Chunk 3] K8s YAML 示例..." │ → │ ✅ 准确的回答 │
│ │ │ + 引用来源 │
│ 用户问题:user-service 怎么部署? │ └────────────────┘
└─────────────────────────────────────┘
LLM 生成回答代码实现:完整 RAG Pipeline
python
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# ═══ 阶段一:Indexing ═══
# 1. 加载文档
loader = PyPDFLoader("internal-docs.pdf")
documents = loader.load()
# 2. 分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
chunks = splitter.split_documents(documents)
# 3. Embedding + 存储
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db", # 持久化到磁盘
)
# ═══ 阶段二 + 三:Retrieval + Generation ═══
# 4. 构建检索器
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5}, # 返回 Top 5
)
# 5. RAG Prompt 模板
RAG_PROMPT = ChatPromptTemplate.from_template("""
根据以下参考资料回答用户的问题。
要求:
1. 只基于参考资料回答,不要编造
2. 如果参考资料中没有相关信息,请明确说"根据已有资料无法回答"
3. 在回答末尾标注引用来源
参考资料:
{context}
用户问题:{question}
""")
# 6. 构建 RAG Chain
llm = ChatOpenAI(model="gpt-4o", temperature=0)
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| RAG_PROMPT
| llm
| StrOutputParser()
)
# 7. 提问
answer = rag_chain.invoke("user-service 怎么部署到 K8s?")
print(answer)🔗 回到 Mini-OpenClaw
Mini-OpenClaw 的 RAG 模式就是这个经典 Pipeline 的实现:
- Indexing:
memory_indexer.py对MEMORY.md做分块 + 向量索引 - Retrieval:
search_knowledge_baseTool 执行向量检索 - Generation:Agent 将检索结果注入上下文后生成回答
区别在于:Mini-OpenClaw 是 Agentic RAG——Agent 自主决定何时调用检索(而不是每次都检索),下一节详细讲。
3.7 进阶检索与优化策略
从"能用"到"好用"的关键——当基础 RAG 效果不够好时,这些策略可以帮你大幅提升。
混合检索(Hybrid Search)
问题:纯向量检索擅长"语义匹配",但遇到精确关键词时可能失灵:
文档:"user-service 使用 gRPC 协议通信,端口号为 50051"
查询:"user-service 的 gRPC 端口是多少?"
纯向量检索:可能返回 ✅(语义相关)
纯关键词检索:一定返回 ✅(精确匹配 "gRPC 端口")
但如果查询是:
"微服务之间怎么通信?"
纯向量检索:返回 ✅(理解"通信"和"gRPC协议"的语义关联)
纯关键词检索:可能返回 ❌(没有"通信"这个关键词)解决方案:两种检索互补——向量检索覆盖"意思相近",BM25 关键词检索覆盖"精确匹配"。
python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# 向量检索器
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# BM25 关键词检索器
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# 混合检索(各占 50% 权重)
hybrid_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.5, 0.5],
)
results = hybrid_retriever.invoke("user-service 的 gRPC 端口")效果对比:
查询:"user-service gRPC 端口配置"
纯向量检索 Top 3:
1. gRPC 服务配置说明(✅ 相关)
2. 微服务通信架构概述(⚠️ 大方向对但不精确)
3. HTTP API 端口配置(❌ 不是 gRPC)
混合检索 Top 3:
1. user-service gRPC 端口号 50051(✅ 精确命中)
2. gRPC 服务配置说明(✅ 相关)
3. user-service 部署配置(✅ 相关)重排序(Re-ranking)
问题:向量检索返回的 Top-K 排序不一定准——第 3 名可能比第 1 名更相关。
解决方案:先用向量检索召回较多候选(Top-20),再用更精确的 Cross-Encoder 模型重新排序,最终取 Top-5。
向量检索(快但粗):
召回 Top-20 候选 → 毫秒级
Cross-Encoder 重排序(慢但精):
对 20 个候选逐一精排 → 百毫秒级
重新排序后取 Top-5
为什么 Cross-Encoder 更准?
向量检索:query 和 doc 分别编码,只比较向量距离(Bi-Encoder)
重排序:query 和 doc 拼在一起输入模型,联合编码(Cross-Encoder)
→ 能捕捉更细粒度的语义关系python
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
# 基础检索器(召回 Top-20)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
# Cohere Reranker(精排 → Top-5)
reranker = CohereRerank(model="rerank-v3.5", top_n=5)
# 组合
reranking_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever,
)
results = reranking_retriever.invoke("user-service 怎么部署?")效果提升:根据实测数据,加上 Re-ranking 后 RAG 的准确率通常能提升 10~25%。这是性价比最高的优化手段之一。
查询改写(Query Transformation)
问题:用户的提问方式可能和文档的表述方式不一样。
用户问:"服务部署不上去怎么办?"
文档写的是:"deployment 失败的排查步骤"
→ 虽然说的是同一件事,但用词差异大,向量检索可能不够精准HyDE(Hypothetical Document Embeddings)
核心思想:先让 LLM 生成一个"假设性答案",用答案的 Embedding 去检索(比问题本身的 Embedding 更接近目标文档)。
传统方式:
用户问题 "服务部署不上去怎么办?"
→ Embedding → 检索
→ 可能匹配不到"deployment 失败排查"
HyDE 方式:
用户问题 "服务部署不上去怎么办?"
→ LLM 生成假设性答案:
"当 deployment 失败时,常见原因包括:
1. 镜像拉取失败
2. 资源配额不足
3. 配置文件错误..."
→ 用这个假设性答案的 Embedding 去检索
→ 更容易匹配到"deployment 失败的排查步骤"!python
from langchain.chains import HypotheticalDocumentEmbedder
hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
llm=ChatOpenAI(model="gpt-4o-mini"),
base_embeddings=OpenAIEmbeddings(),
prompt_key="web_search",
)
# 用 HyDE embeddings 替换普通 embeddings
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=hyde_embeddings,
)Multi-Query
一个问题改写成多个角度,分别检索后合并去重:
原始问题:
"React 列表渲染卡顿怎么优化?"
改写为多个查询:
1. "React 虚拟列表实现方案"
2. "前端大数据量渲染性能优化"
3. "react-window react-virtuoso 使用教程"
→ 三个查询分别检索 → 合并结果 → 去重 → 覆盖面更广Step-back Prompting
先问一个更宏观的问题,获取背景知识:
原始问题:
"为什么 user-service 在 K8s 中 OOM 重启?"
Step-back 问题:
"K8s 中 Pod OOM 的常见原因有哪些?"
→ 先检索 OOM 的通用知识
→ 再结合 user-service 的具体文档回答Agentic RAG(2026 趋势)
传统 RAG 是固定流水线——每次提问都走 "检索 → 生成" 的固定路径。
Agentic RAG 让 Agent 自主决策:
传统 RAG(固定 Pipeline):
用户提问 → 检索 → 生成
每次都检索,即使是 "你好" 这种不需要检索的问题
Agentic RAG(Agent 决策):
用户提问 → Agent 判断:
├── "你好" → 直接回答(不需要检索)
├── "user-service怎么部署" → 触发检索 → 生成
├── 检索结果不够 → 换个查询词重新检索
└── 需要多个数据源 → 分别检索后综合回答python
# Agentic RAG 的核心:把检索包装成 Tool,让 Agent 自主决定是否调用
from langchain_core.tools import tool
@tool
def search_docs(query: str) -> str:
"""搜索内部技术文档。当用户询问公司内部的技术细节、
部署配置、服务文档等信息时使用此工具。"""
results = retriever.invoke(query)
return "\n\n".join(doc.page_content for doc in results)
# Agent 自己决定是否调用 search_docs
agent = create_react_agent(
model=llm,
tools=[search_docs, web_search, calculator],
)Mini-OpenClaw 的 search_knowledge_base 就是 Agentic RAG 的雏形——Agent 根据问题判断是否需要查知识库。
RAG 质量评估
| 指标 | 衡量什么 | 怎么理解 |
|---|---|---|
| Faithfulness | 答案是否忠于检索结果 | "回答的内容是不是都来自文档"(防止幻觉) |
| Relevance | 检索结果是否和问题相关 | "找到的文档有没有用" |
| Answer Correctness | 答案是否正确 | "最终回答对不对" |
python
# 使用 Ragas 评估 RAG 质量
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
results = evaluate(
dataset=test_dataset, # 准备好的测试集
metrics=[faithfulness, answer_relevancy, context_precision],
)
print(results)
# faithfulness: 0.92(92% 的回答忠于文档)
# answer_relevancy: 0.88(88% 的回答和问题相关)
# context_precision: 0.85(85% 的检索结果精确)常见问题诊断
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 检索不到相关文档 | 分块太大 / Embedding 质量差 / 查询表述和文档差异大 | 调小 chunk_size + 换 Embedding 模型 + 混合检索 |
| 检索到了但答案不对 | 检索结果不够精确,排在前面的不是最相关的 | 加 Re-ranking + 增大召回量(k=20 → rerank → top 5) |
| 答非所问 | LLM 没好好利用检索结果 | 优化 RAG Prompt 模板(强调"只基于参考资料回答") |
| 回答太笼统 | 检索到的片段缺乏细节 | 调小 chunk_size + 增大 k 值 |
| 多个主题混淆 | 检索结果来自不同文档,互相矛盾 | 增加元数据过滤(按文档来源分类) |
RAG 效果不好时的排查顺序:
Step 1:检查分块 → 看看 chunk 是不是太大/太小/切断了关键信息
Step 2:检查检索 → 打印 Top-K 结果,看是不是检索到了相关内容
Step 3:检查 Prompt → 看 LLM 拿到的上下文是不是合理
Step 4:试试混合检索 + Re-ranking → 往往能大幅改善
Step 5:换 Embedding 模型 → 用自己的数据做对比测试3.8 实战:基于 LangChain 实现 PDF 智能问答
把全篇知识串成一个完整的、可运行的项目。
项目目标
上传 PDF → 解析 → 分块 → 向量化 → 对话式问答(带引用溯源)
效果演示:
📄 上传:React-Performance-Guide.pdf(30页技术文档)
❓ 用户:"虚拟列表的原理是什么?推荐用哪个库?"
✅ 回答:
"虚拟列表的核心原理是只渲染可视区域内的 DOM 元素,
而不是渲染全部数据。当用户滚动时,动态替换可视区域
的内容。
推荐两个库:
- react-window:轻量级,适合简单列表
- react-virtuoso:功能丰富,支持动态高度
📖 引用来源:
- 第 12 页 "虚拟列表优化方案"
- 第 15 页 "推荐工具库对比""技术选型
┌──────────────────────────────────────────────┐
│ PDF 智能问答系统 │
├───────────────┬──────────────────────────────┤
│ 核心框架 │ LangChain │
│ PDF 解析 │ PyPDFLoader │
│ Embedding │ OpenAI text-embedding-3-small │
│ 向量存储 │ ChromaDB │
│ LLM │ GPT-4o(或 DeepSeek) │
│ 后端 │ FastAPI │
│ 前端 │ Gradio(快速 UI) │
└───────────────┴──────────────────────────────┘完整实现
Step 1:安装依赖
bash
pip install langchain langchain-openai langchain-chroma langchain-community
pip install pypdf chromadb gradioStep 2:PDF 加载与分块
python
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
def load_and_split_pdf(pdf_path: str):
# 加载 PDF(每页作为一个 Document,保留页码元数据)
loader = PyPDFLoader(pdf_path)
pages = loader.load()
# 分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", "。", ",", " ", ""],
)
chunks = splitter.split_documents(pages)
print(f"PDF 共 {len(pages)} 页,分成 {len(chunks)} 个 chunks")
return chunksStep 3:Embedding + 向量存储
python
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
def create_vectorstore(chunks, persist_dir="./chroma_db"):
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=persist_dir,
)
print(f"已将 {len(chunks)} 个 chunks 存入向量数据库")
return vectorstoreStep 4:构建 RAG Chain(带引用溯源)
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
def create_rag_chain(vectorstore):
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5},
)
prompt = ChatPromptTemplate.from_template("""
你是一个专业的文档问答助手。请根据以下参考资料回答用户的问题。
要求:
1. 只基于参考资料回答,不要编造信息
2. 如果参考资料中没有相关信息,请明确说"根据文档内容无法回答此问题"
3. 回答末尾附上引用来源(标注页码)
4. 使用清晰的结构化格式回答
参考资料:
{context}
用户问题:{question}
请回答:""")
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# 格式化检索结果(保留页码信息)
def format_docs(docs):
formatted = []
for doc in docs:
page_num = doc.metadata.get("page", "未知")
formatted.append(
f"[第 {page_num + 1} 页]\n{doc.page_content}"
)
return "\n\n---\n\n".join(formatted)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
return rag_chain, retrieverStep 5:支持多轮对话
python
from langchain_core.messages import HumanMessage, AIMessage
class ChatWithPDF:
def __init__(self, vectorstore):
self.rag_chain, self.retriever = create_rag_chain(vectorstore)
self.chat_history = []
def ask(self, question: str) -> str:
# 如果有历史对话,把历史上下文拼入问题
if self.chat_history:
history_text = "\n".join(
f"{'用户' if isinstance(m, HumanMessage) else 'AI'}: {m.content}"
for m in self.chat_history[-6:] # 保留最近 3 轮
)
contextualized_question = (
f"对话历史:\n{history_text}\n\n当前问题:{question}"
)
else:
contextualized_question = question
# 检索 + 生成
answer = self.rag_chain.invoke(contextualized_question)
# 记录对话历史
self.chat_history.append(HumanMessage(content=question))
self.chat_history.append(AIMessage(content=answer))
return answerStep 6:Gradio UI
python
import gradio as gr
# 全局状态
chat_instance = None
def upload_pdf(file):
global chat_instance
chunks = load_and_split_pdf(file.name)
vectorstore = create_vectorstore(chunks)
chat_instance = ChatWithPDF(vectorstore)
return f"✅ PDF 已加载!共 {len(chunks)} 个文档片段,可以开始提问了。"
def ask_question(question, history):
if chat_instance is None:
return history + [[question, "❌ 请先上传 PDF 文件"]]
answer = chat_instance.ask(question)
return history + [[question, answer]]
with gr.Blocks(title="PDF 智能问答") as app:
gr.Markdown("# 📄 PDF 智能问答系统")
gr.Markdown("上传 PDF 文档,然后用自然语言提问")
with gr.Row():
pdf_upload = gr.File(label="上传 PDF", file_types=[".pdf"])
upload_status = gr.Textbox(label="状态", interactive=False)
pdf_upload.change(upload_pdf, inputs=pdf_upload, outputs=upload_status)
chatbot = gr.Chatbot(label="对话", height=500)
question_input = gr.Textbox(
label="提问",
placeholder="请输入你的问题...",
)
question_input.submit(
ask_question,
inputs=[question_input, chatbot],
outputs=chatbot,
)
app.launch()进阶优化
在基础版本跑通后,可以逐步加上这些优化:
加入 Re-ranking
python
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
# 在 create_rag_chain 中替换 retriever
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
reranker = CohereRerank(model="rerank-v3.5", top_n=5)
retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever,
)混合检索
python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
bm25 = BM25Retriever.from_documents(chunks)
bm25.k = 10
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
hybrid = EnsembleRetriever(
retrievers=[vector_retriever, bm25],
weights=[0.6, 0.4], # 向量检索权重略高
)更换 Embedding 模型对比效果
python
# 对比不同 Embedding 模型的效果
configs = [
("text-embedding-3-small", 1536),
("text-embedding-3-large", 3072),
]
for model_name, dim in configs:
embeddings = OpenAIEmbeddings(model=model_name)
vs = Chroma.from_documents(chunks, embeddings)
retriever = vs.as_retriever(search_kwargs={"k": 5})
# 用测试集评估
for q, expected in test_cases:
results = retriever.invoke(q)
score = evaluate_retrieval(results, expected)
print(f"{model_name}: {q} → score={score:.2f}")与 Mini-OpenClaw 对照
| 维度 | 本实战(经典 RAG) | Mini-OpenClaw(Agentic RAG) |
|---|---|---|
| 检索触发 | 每次提问都检索 | Agent 自主判断是否检索 |
| Pipeline | 固定:检索 → 生成 | 灵活:Agent 决定流程 |
| 数据源 | 单一 PDF | MEMORY.md + 可扩展 |
| 适用场景 | 文档问答(结构化) | 通用 Agent(开放式) |
两种模式各有优势:
- 经典 RAG:简单可靠、延迟可控、适合"文档问答"这类明确的检索场景
- Agentic RAG:灵活智能、适合复杂场景,但需要更好的 Agent 能力和护栏机制
串联回顾
这个项目用到了第三篇的哪些知识点?
| 知识点 | 在项目中的体现 |
|---|---|
| 3.2 Embedding | OpenAIEmbeddings 把文档片段变成向量 |
| 3.3 模型选型 | 选择 text-embedding-3-small 做快速验证 |
| 3.4 向量数据库 | ChromaDB 存储和检索向量 |
| 3.5 分块策略 | RecursiveCharacterTextSplitter,chunk_size=1000,overlap=200 |
| 3.6 RAG Pipeline | 完整的 Indexing → Retrieval → Generation 流程 |
| 3.7 进阶优化 | Re-ranking + 混合检索 + 效果对比 |
总结
第三篇到这里就结束了。回顾我们学到的内容:
✅ 为什么需要 RAG(3.1):私有数据 + 知识截止 + 幻觉 → RAG 是最佳方案
✅ Embedding 深入(3.2):语义搜索的原理、维度含义、多模态、MRL 套娃
✅ 模型选型(3.3):2026 年主流模型全景、MTEB 榜单、选型决策图
✅ 向量数据库(3.4):相似度度量、ANN 索引、6 款数据库对比
✅ 分块策略(3.5):4 种切法对比、Chunk Size/Overlap 调优
✅ RAG Pipeline(3.6):Indexing → Retrieval → Generation 完整流程
✅ 进阶优化(3.7):混合检索、Re-ranking、HyDE、Agentic RAG、质量评估
✅ PDF 问答实战(3.8):完整可运行的项目 + 进阶优化 + 与 Mini-OpenClaw 对比至此,AI 基础知识分享系列全部三篇完成:
- 第一篇:大模型全景——认知框架 ✅
- 第二篇:AI 开发工具链与 Agent——开发实战 ✅
- 第三篇:Embedding、向量数据库与 RAG 实战——进阶选读 ✅
从核心概念到 Agent 开发再到 RAG 落地,一条线讲下来,你已经具备了在业务中应用 AI 的完整知识体系。🎉