开篇:大模型为什么需要 RAG?

你问 ChatGPT “计算机一班有多少人”,它只能瞎编。

不是因为它不够聪明,而是因为它没见过你的数据。大模型的训练数据截止于某个时间点,对私有文档、企业内部数据、个人笔记一无所知。

RAG(Retrieval-Augmented Generation,检索增强生成) 就是解决这个问题的标准方案。

1
2
3
4
5
6
7
8
9
10
传统方式:                          RAG 方式:
用户提问 用户提问
↓ ↓
LLM 直接回答 ① 先检索知识库
(可能胡编) ↓
② 找到相关文档片段

③ 把片段 + 问题一起给 LLM

LLM 基于真实数据回答 ✅

核心思想:让 LLM 带着参考答案作答,而不是凭空想象。


一、RAG 核心流程

一个完整的 RAG 系统分两个阶段:

阶段一:离线入库(Indexing)

1
2
3
4
5
6
7
8
9
原始文档(Word/PDF/TXT)
↓ ① 文档解析
纯文本
↓ ② 智能切割
N 个 Chunk(文本块)
↓ ③ 向量嵌入(Embedding)
N 个 向量(float 数组)
↓ ④ 存入向量数据库
pgvector / Milvus / Pinecone

阶段二:在线查询(Querying)

1
2
3
4
5
6
7
8
9
用户提问
↓ ① 问题向量化
查询向量
↓ ② 向量检索 + 关键词检索
Top-K 个相关 Chunk
↓ ③ 组装增强 Prompt
带上下文的完整提问
↓ ④ 发送给 LLM
基于真实数据的回答

入库慢无所谓,查询一定要快。 入库通常走异步任务(@Async),查询是同步的,毫秒级响应。


二、文档切割——Chunk 策略

为什么切割?

  1. 上下文窗口有限——LLM 一次只能看几千字,整个文档塞不下
  2. 检索精度——小块更容易精准命中,大块噪音多
  3. Token 成本——多余的无关内容都是钱

切割策略

策略 做法 适用场景
固定长度 每 500 字切一块 通用场景
语义切割 按段落/标题/标点分割 结构清晰的文档
滑动窗口 块间重叠 10-20%,防止信息断裂 长文档、小说
层级切割 保留标题-段落层级关系 知识库、手册
父子检索 (Small-to-Big) 小块检索 + 大块返回 全面覆盖(推荐)

进阶技巧:父子文档检索(Small-to-Big)

这是目前 RAG 公认的最佳切分实践之一:

1
2
3
4
5
6
7
8
原始文档
↓ 切成大块(父文档,1000~2000 tokens)
父块 A 父块 B 父块 C
↓ 再切成小块(子文档,200~500 tokens)
子块A1 A2 A3 子块B1 B2 B3 子块C1 C2 C3

检索阶段: 用"小块"做向量匹配 → 命中率高、噪音少
返回阶段: 返回小块对应的"父块" → LLM 看到的上下文完整

小块负责检索精度,大块负责上下文完整度。

切分实战

1
2
3
4
5
6
7
8
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
chunk_size=512, # 每块最大 token 数
chunk_overlap=50, # 相邻块的重叠 token 数,防止信息在边界处丢失
separators=["\n\n", "\n", "。", ".", " ", ""] # 优先按段落、句子切分
)
chunks = splitter.split_text(document_text)

经验: 小文档(<5000 字)用固定长度即可;大文档务必用语义切割 + 滑动窗口 + 父子检索,否则检索时大概率丢上下文。


三、向量检索基础

什么是文本向量?

把一段文字变成一个固定长度的数字数组

1
2
3
"计算机一班有多少人" → [0.03, -0.12, 0.45, 0.08, ..., -0.11]
↑ ↑ ↑ ↑ ↑
第0维 第1维 第2维 第3维 ... 第511维

这个 512 维的数组编码了文本的语义特征

向量索引算法:ANN

数据量到百万级后,暴力遍历比对太慢。向量数据库用近似最近邻(ANN) 算法加速:

索引类型 原理 特点 适用场景
Flat(暴力) 逐一比对 100% 精确,极慢 <10 万,精度优先
IVF_Flat K-Means 聚类后搜簇 速度快,精度略降 中大规模
IVF_PQ 聚类 + 乘积量化压缩 极省内存 超大规模,内存受限
HNSW 多层跳跃图网络 速度最快,精度高(推荐) 最常用首选
ScaNN Google 出品 优化吞吐量 高并发生产环境

HNSW 是目前最主流的 ANN 算法——它构建多层图结构(顶层稀疏、底层密集),查询时像”跳格游戏”一样逐层下探,大幅减少需要比较的节点数,时间复杂度近似 O(log n)。

三种相似度计算方法

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np

def cosine_similarity(a, b):
"""余弦相似度:衡量方向,最常用于文本"""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def euclidean_distance(a, b):
"""欧氏距离:衡量绝对位置差异"""
return np.linalg.norm(a - b)

def dot_product(a, b):
"""点积:结合方向和长度,推荐系统常用"""
return np.dot(a, b)
方法 结果范围 适用场景
余弦相似度 -1 ~ 1 文本语义搜索(最常用)
欧氏距离 0 ~ ∞ 图像检索、地理空间
点积 -∞ ~ ∞ 推荐系统(归一化后等价余弦)

向量数据库选型决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
你的情况是什么?

├── 已有 PostgreSQL,且数据量 < 500 万
│ └──> pgvector,无缝集成,零额外运维

├── 做 AI/LLM 应用原型,快速验证
│ └──> Chroma,几行代码搞定

├── 需要生产级部署,性能优先,500 万 ~ 1 亿
│ └──> Qdrant,Rust 实现,性能强

├── 超大规模(> 1 亿),有 K8s 运维能力
│ └──> Milvus,分布式,功能最全

└── 团队没有运维能力,愿意付费
└──> Pinecone / Zilliz Cloud,开箱即用

Chroma 快速上手

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
import chromadb
from chromadb.utils import embedding_functions

client = chromadb.PersistentClient(path="./my_vector_db")

# 使用 OpenAI 嵌入模型(也可换成本地模型)
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key="your-api-key",
model_name="text-embedding-3-small"
)

collection = client.get_or_create_collection(
name="my_documents",
embedding_function=openai_ef,
metadata={"hnsw:space": "cosine"}
)

# 插入文档 → Chroma 自动向量化
collection.add(
documents=["Python 是一种面向对象的编程语言", "机器学习是 AI 的子领域"],
ids=["doc_0", "doc_1"]
)

# 语义搜索
results = collection.query(query_texts=["如何用 Python 做 AI"], n_results=3)

四、Embedding 模型深入

从词袋到 Transformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第一代:词袋模型(BoW / TF-IDF)
"我爱北京天安门" → {"我":1, "爱":1, "北京":1, "天安门":1}
❌ 没有语序信息,不擅长近义词

第二代:Word2Vec / GloVe
每个词一个固定向量,句子 = 词向量平均
⚠️ 上下文无关,"苹果"(水果)和"苹果"(公司)向量一样

第三代:BERT / Sentence-BERT
整个句子过 Transformer 网络,输出句子级向量
✅ 理解上下文,理解词序,理解同义词

第四代:BGE / E5 / Jina
专门为检索优化的嵌入模型,加入指令微调
✅ 区分 query 和 document,检索精度再提升

主流模型对比

模型 维度 大小 中文效果 推理速度 适用场景
bge-small-zh-v1.5 512 90MB ⭐⭐⭐⭐⭐ 中文 RAG 首选
bge-large-zh-v1.5 1024 1.3GB ⭐⭐⭐⭐⭐ 高精度中文
bge-m3 1024 2.2GB ⭐⭐⭐⭐⭐ 中英文多语言
text-embedding-3-small 1536 API ⭐⭐⭐⭐ API 多语言,性价比高
text-embedding-3-large 3072 API ⭐⭐⭐⭐⭐ API 精度最高,成本较高
paraphrase-multilingual-MiniLM 384 120MB ⭐⭐⭐ 最快 50+语言通用
all-MiniLM-L6-v2 384 90MB ⭐⭐ 最快 纯英文轻量

嵌入模型的选择原则

需求 推荐模型
中英文文本(高质量) OpenAI text-embedding-3-small
中文文本(本地离线) BAAI/bge-large-zh-v1.5
中文文本(轻量离线) BAAI/bge-small-zh-v1.5
多语言通用 paraphrase-multilingual-MiniLM-L12-v2
图文多模态 OpenAI CLIP 系列

BGE 模型的特殊设计:指令感知

BGE 系列在训练时用了对比学习 + 指令微调

1
2
3
4
5
6
# 训练时:query 带指令,document 不带
query = "为这个句子生成表示以用于检索相关文章:" + "计算机一班有多少人"
doc = "学生总数: 2人,作业总数: 2份"

# 训练目标:拉近 query 和正确答案的向量,拉开和错误答案的向量
loss = contrastive_loss(query_embedding, pos_doc_embedding, neg_doc_embeddings)

所以使用时也要严格区分:

1
2
3
4
5
6
7
// ✅ Query:加指令前缀
embeddingService.embedQuery("计算机一班有多少人");
// 实际传入: "为这个句子生成表示以用于检索相关文章:计算机一班有多少人"

// ✅ Document:不加前缀
embeddingService.embedDocument("学生总数: 2人");
// 直接传入原文本

五、混合检索——为什么单路不够?

语义向量的盲区

盲区类型 例子 为什么失效
数字/代码 i%5==0, int a[5] 模型没见过这些 token 组合
缩写 “CS2”, “JDK” 低频 token,模型理解弱
精确匹配 人名、地名、产品型号 语义相近但不相等

关键词检索的盲区

1
2
3
4
5
用户: "怎么交换两个变量的值?"
文档: "t=x;x=y;y=t;"

关键词检索: ❌ 没命中(没有"交换"、"变量"这些词)
语义检索: ✅ 理解语义 → 命中

混合检索 = 取长补短

1
2
3
4
5
6
7
8
9
查询
├── 语义路:理解"交换变量"≈"变量互换"≈"swap values"
│ └── 结果: chunk_A, chunk_B, chunk_C (按余弦距离排序)

├── 关键词路:精确命中 "int a[5]", "i%5==0"
│ └── 结果: chunk_D, chunk_C, chunk_E (按关键词命中数排序)

└── RRF 融合
└── 最终: chunk_C(双路命中🥇), chunk_A, chunk_D, ...

RRF(Reciprocal Rank Fusion)原理

核心公式:

1
2
3
4
5
RRF_score(chunk) = Σ ─────────
k + rank_i

k = 60 (标准常数,Elasticsearch / Weaviate 都在用)
rank_i = 该 chunk 在第 i 路检索中的排名

为什么用排名而不是原始分数?

1
2
3
4
5
语义分数: 0.85 (chunk_A), 0.12 (chunk_B)
关键词分数: 5 (chunk_C), 1 (chunk_A)

直接相加: 0.85+1 = 1.85 vs 0.12+? = 无意义!
排名融合: rank=1 和 rank=5 → 1/61 + 1/65 = 0.0318 ✅ 量纲统一

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// RRF 融合 - 核心逻辑
Set<String> allChunkIds = new LinkedHashSet<>();
allChunkIds.addAll(semanticRanks.keySet());
allChunkIds.addAll(keywordRanks.keySet());

return allChunkIds.stream()
.map(id -> {
double rrfScore = 0.0;
Double semRank = semanticRanks.get(id);
Double kwRank = keywordRanks.get(id);
if (semRank != null) rrfScore += 1.0 / (60 + semRank);
if (kwRank != null) rrfScore += 1.0 / (60 + kwRank);
return new RrfResult(id, rrfScore);
})
.sorted((a, b) -> Double.compare(b.rrfScore, a.rrfScore))
.limit(topK)
.toList();

Elasticsearch 8.x 内置了 RRF,开箱即用:

1
2
3
4
5
6
7
8
9
10
11
12
{
"retriever": {
"rrf": {
"retrievers": [
{ "standard": { "query": { "match": { "content": "..." } } } },
{ "knn": { "field": "embedding", "query_vector": [...], "k": 20 } }
],
"rank_window_size": 20,
"rank_constant": 60
}
}
}

六、Advanced RAG —— 进阶架构

基础架构(Naive RAG)常面临检索不准确、冗余信息多等问题。Advanced RAG 通过 预检索优化 → 检索融合 → 后检索优化 三段式架构予以解决。

1. 预检索:查询优化

查询改写(Query Rewriting)

用户的原始问题往往表达不够精确,用 LLM 改写为规范化的检索词:

1
2
3
原始提问: "那个一班的作业怎么样了"
↓ LLM 改写
改写后: "计算机一班 学生作业 完成情况 成绩统计"

改写后的查询包含更多精确的关键词,检索命中率大幅提升。

HyDE(假设文档嵌入)

这是目前最巧妙的预检索技术之一:

1
2
3
4
5
用户提问: "计算机一班有多少人"
↓ Step 1: LLM 盲猜一个答案(不需要是真的)
假设答案: "计算机一班共有30名学生,其中男生15人,女生15人"
↓ Step 2: 用假设答案生成向量去检索(而不是用原问题)
向量搜索 → 召回真实文档

为什么有效? LLM 生成的假设答案往往比原问题包含更多专业术语和完整的句式结构,与知识库中真实文档的向量分布更接近,检索命中率显著提升。

2. 检索融合:混合检索

已在第五章详述。将语义检索 + 关键词检索的结果通过 RRF 融合,在专有名词、代码片段等场景尤其重要。

3. 后检索:重排序(Reranking)

向量检索虽然快,但打分不够精确。重排序引入 Cross-Encoder 模型进行精排:

1
2
3
4
5
粗排阶段(快):
bge-small-zh → Top-50 候选

精排阶段(精准):
bge-reranker-v2-m3 → 50 个候选重新交叉打分 → Top-5

Cross-Encoder 将「问题」和「文档」成对输入模型进行联合推理打分,精度远高于向量距离,但运算量大,只负责精选 Top-N。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

# 1. 粗排:向量检索极速召回 Top-50
candidates = vector_store.similarity_search(query, k=50)

# 2. 精排:构建 [问题, 文档] 对进行精确打分
pairs = [[query, doc.page_content] for doc in candidates]
scores = reranker.predict(pairs)

# 3. 筛选最终传入 LLM 的 Top-5
ranked_docs = sorted(zip(scores, candidates), reverse=True)
final_docs = [doc for _, doc in ranked_docs[:5]]

4. CRAG:修正式 RAG

CRAG(Corrective RAG) 在拿到检索结果后,先由 LLM 充当”评委”打分:

1
2
3
4
5
检索结果
↓ LLM 评估质量
├── 高质量 → 直接回答 ✅
├── 模糊 → 补充 Web Search 🔍
└── 完全不相关 → 纯 Web Search 回答 🌐

如果本地知识库查无此文或质量极低,系统会自动触发 Web Search(如 Google API)作为补充,大幅降低幻觉。

5. GraphRAG:知识图谱 + 检索融合

传统 RAG 将知识库当作独立的文本碎片,无法回答跨文档、多跳推理的复杂问题。GraphRAG 引入知识图谱,将实体和关系显式建模:

1
2
3
4
5
6
7
8
9
传统 RAG:
Q: "找到所有市值超千亿且由创始人担任 CEO 的公司"
→ 片段的文本检索无法完成多条件关联推理 → ❌

GraphRAG:
Q: 同上
├── 向量检索: 召回相关公司描述
├── 图遍历: Neo4j 中查询 (公司)-[CEO]->(创始人) 路径
└── 融合生成: LLM 拿到文本片段 + 图结构 → 精准回答 ✅

核心步骤:

  • 知识构建:离线阶段用 LLM 从文档提取三元组(主体、关系、客体),写入 Neo4j 等图数据库
  • 双路检索:向量检索 + 图遍历
  • 图文融合:文本片段 + 关系路径 → LLM

七、ONNX 模型部署

为什么需要 ONNX?

问题 说明
部署重 PyTorch 运行时 ≈ 2GB+,不适合容器
依赖多 Python + CUDA + PyTorch,环境地狱
跨平台难 Java 调 Python 要么走 HTTP 服务,延迟高

ONNX(Open Neural Network Exchange) 解决这些:

1
2
3
4
5
PyTorch 模型
↓ torch.onnx.export()
model.onnx (单一文件,~90MB)
↓ ONNX Runtime (任何语言都能调)
Java / C++ / Rust / Go / Node.js

八、评估与优化

RAGAS 评估框架

RAG 系统不能凭直觉评估,业界标准是 RAGAS 框架,从检索和生成两个维度自动化量化:

指标 衡量 说明
Context Recall 检索 标准答案中的信息有多少比例被检索到?越高越好
Context Precision 检索 检索到的文档中有多少比例是真正相关的?越高越好
Faithfulness 生成 生成的答案是否都有检索文档支撑?(检测幻觉)
Answer Relevance 生成 答案是否真正回答了用户问题?(防止答非所问)

性能优化技巧

技巧 做法 效果
批量插入 collection.add(documents=all_docs, ids=all_ids) 10x+ 提升
向量归一化 入库前 L2 归一化,查询时用点积等价余弦 加速计算
合理 n_results RAG 场景 3~10 条足够,别设太大 减少 token 成本
元数据过滤 配合 where={"category":"tech"} 缩小搜索范围 提升精度
定期重建索引 数据量增长后适时重建 HNSW 索引 维持查询性能
1
2
3
4
5
6
# 批量插入(推荐)
collection.add(documents=docs_list, ids=ids_list)

# ❌ 不推荐:循环单条(每次都重新索引,效率极低)
# for doc, id in zip(docs_list, ids_list):
# collection.add(documents=[doc], ids=[id])

常见坑大全

现象 解决
维度不匹配 pgvector expected 384, not 512 ALTER TABLE ... TYPE vector(512)
嵌入模型不统一 插入和查询用了不同模型 在配置文件中固定模型版本
噪音淹没 99 个不相关 chunk 压住 1 个相关 chunk 管好入库质量,删掉不相关文档
Chunk 太小 检索到碎片,LLM 看不明白 增大 chunk + 加 overlap
Chunk 太大 语义被稀释,相似度不准 按段落或固定长度切分
模型未区分 query/doc 检索精度比预期低 BGE 加 query 前缀
文本过长 模型 token 限制(512~8192) 先分块再嵌入
Java Text Block 空格 SQL 拼接 WHEREContent \s 保留行尾空格
LIMIT 类型错误 Pg LIMIT 必需 bigint List<Object> 别用 String.valueOf()
冷启动慢 数据量大时首次加载索引耗时 提前预热,使用持久索引

九、技术栈选择指南

1
2
3
4
5
6
7
8
9
你的需求:
├── 纯中文、小规模 → bge-small-zh + pgvector + RRF ✅
├── 多语言、中等规模 → paraphrase-multilingual + Qdrant + RRF
├── 企业级、大规模 → bge-large-zh + Milvus + 重排序
├── 不想管运维 → Pinecone / Zilliz Cloud + Cohere Rerank + GPT-4o
├── 需要专有名词匹配 → Elasticsearch / Weaviate(内置 BM25+向量混合)
├── 没 GPU、Java 项目 → ONNX Runtime + DJL
├── 有 GPU、Python 项目 → sentence-transformers + PyTorch
└── 需要多跳推理 → Neo4j + GraphRAG

十、我们的实践

以「作业批改系统」为例:

架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
QQ 消息

OpenClaw OneBot 插件(Node.js)
↓ POST /api/onebot/rag
Spring Boot RAG Controller
↓ embedQuery()
EmbeddingService (bge-small-zh-v1.5 / ONNX Runtime)

├── pgvector cosine search (语义路)
├── ILIKE keyword search (关键词路)
└── RRF 融合 → Top-6 chunks
↓ 组装增强 prompt
OpenClaw Agent (deepseek-v4-flash)
↓ 生成回复
QQ 消息

效果对比

阶段 嵌入方式 检索结果
初版 哈希伪向量 + 硬阈值 0.75 hasContext: false,什么都搜不到
中期 哈希伪向量 + RRF hasContext: true,能搜到但精度差
当前 bge-small-zh ONNX + RRF 可精准命中仪表盘数据

总结

RAG 不是一个单一技术,而是一个技术栈

1
文档切割 + 向量嵌入 + 向量数据库 + 混合检索 + Advanced RAG + LLM 生成

每个环节都有多种选择,根据实际场景权衡:

  • 快速起步:pgvector + bge-small-zh ONNX + 简单切割 + RRF
  • 追求精度:父子检索 + HyDE + 重排序 + 混合检索 + 更大的嵌入模型
  • 企业级高可用:Milvus + bge-large-zh + 多路召回 + CRAG + 缓存层
  • 复杂推理:Neo4j + GraphRAG + 文本检索双路融合

核心经验只有一条:知识库的干净程度比模型精度更重要。 100 个不相关的 chunk 足以毁掉最好的嵌入模型——管好入库质量,比死磕调参有效得多。


参考资源