RAG 实战:从向量数据库到 GraphRAG
《大模型知识与部署》系列 · No.26 / 35(应用生态篇开篇)
适合人群:AI 工程师、后端开发、应用架构师
阅读时间:约 28 分钟

写在前面
前 25 篇我们走完了从模型架构到部署运维的完整链路。从这一篇开始,进入应用生态篇(第 26-30 篇)——把模型变成产品。
第一站是 RAG(Retrieval-Augmented Generation,检索增强生成)。
为什么先讲 RAG?因为这是 2026 年大模型应用最普遍的形态:
ChatGPT 类对话 ── 10% 应用
RAG 知识问答 ── 60% 应用 ⭐
Agent 自动化 ── 20% 应用
多模态特定场景 ── 10% 应用
如果你做过相关工作,下面这些问题应该不陌生:
- 我有 1TB 公司文档,怎么让大模型"知道"它们?
- 向量检索老是召回不准,是 embedding 不行还是切片不对?
- Pinecone / Weaviate / Milvus / Qdrant 怎么选?
- GraphRAG 真的比传统 RAG 强吗?什么场景用?
- 用户问的明明文档里有,为什么模型说"我不知道"?
- RAG + 长上下文已经够用了,为什么还要 GraphRAG / Self-RAG?
读完本文你将能:
- 设计完整的 RAG 系统(文档 → 切片 → 索引 → 检索 → 生成)
- 选对 embedding 模型和向量库
- 用 hybrid 检索 + rerank 把召回准确率提到 90%+
- 理解 GraphRAG / Adaptive RAG / Agentic RAG 等进阶方案
- 跑通一个端到端 RAG 服务
我们开始。
一、为什么 RAG 是大模型应用的「地基」
1.1 RAG 解决了什么问题
大模型本身有 4 个天生缺陷:
- 知识有截止日期——训练数据有 cut-off
- 不知道私有知识——你公司内部文档它没见过
- 会幻觉——一本正经地胡说
- 不能追溯——无法说明答案来源
RAG 用「先检索再生成」的方式同时解决这 4 个:
用户问题 → 检索相关文档 → 把文档作为上下文 → LLM 基于上下文回答
效果:
- ✅ 实时知识(文档可以随时更新)
- ✅ 私有知识(你自己的文档库)
- ✅ 减少幻觉(基于事实生成)
- ✅ 可追溯(指明引用来源)
1.2 RAG vs 长上下文 vs 微调
第 5 篇 + 第 15 篇我们已经讨论过 RAG vs 长上下文。再加上微调,这是当下三大"知识注入"路径:
| 方案 | 适用 | 成本 | 时效性 |
|---|---|---|---|
| 微调 | 风格 / 格式 / 推理能力 | 高 | 低(要重训) |
| 长上下文 | 单次 / 跨文档分析 | 中(API 贵) | 高 |
| RAG | 大规模知识 / 高频查询 | 低 | 极高 |
核心认知:
RAG 不是为了"省 token",是为了"管理无限大的知识库"。
公司知识库可能 100 GB+,1M 上下文也装不下——RAG 是唯一选择。
1.3 RAG 在 2026 年的演进
2023 朴素 RAG ── 文档 → 切片 → embedding → 余弦检索 → 喂 LLM
2024 Advanced ── + 多路召回 + Rerank + Query 改写
2025 GraphRAG ── + 知识图谱 + 实体关系
2026 Agentic RAG ── + LLM 主动决策 + 多步检索 + 自反思
每一代都解决了上一代的痛点。这一篇我们从基础到前沿都讲透。
二、RAG 的完整流程
2.1 端到端架构
┌────────────────────────────────────────────────────┐
│ 离线索引阶段 │
│ 原始文档 → 解析 → 切片 → embedding → 写入向量库 │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ 在线查询阶段 │
│ 用户问题 → embedding → 检索 → rerank → LLM 生成 │
└────────────────────────────────────────────────────┘
2.2 关键技术栈
| 环节 | 主流工具 |
|---|---|
| 文档解析 | unstructured, MinerU, Nougat, LlamaParse |
| 切片 | LangChain TextSplitter, LlamaIndex Parser |
| Embedding | BGE, GTE, OpenAI Embedding, Voyage AI |
| 向量库 | Milvus, Qdrant, Weaviate, Chroma, Pinecone |
| Rerank | BGE-Reranker, Cohere Rerank, Voyage Rerank |
| LLM | vLLM 自部署 / Claude / GPT API |
| 编排 | LangChain, LlamaIndex, DSPy |
接下来我们逐个深入。
三、文档处理:90% 的问题在这一步
业内有个共识:RAG 效果差,多半是文档处理没做好。
3.1 文档解析的挑战
不同格式的文档难度天差地别:
| 格式 | 难度 | 主流工具 |
|---|---|---|
| 纯文本 / Markdown | ⭐ | 任何 splitter |
| HTML | ⭐⭐ | trafilatura, BeautifulSoup |
| PDF(扫描版) | ⭐⭐⭐⭐⭐ | MinerU, Nougat, PaddleOCR |
| PDF(原生) | ⭐⭐⭐ | pdfplumber, PyMuPDF |
| Word / PPT | ⭐⭐ | python-docx, unstructured |
| Excel | ⭐⭐⭐ | pandas, openpyxl |
| 表格 / 公式 / 图 | ⭐⭐⭐⭐⭐ | MinerU, Nougat |
PDF 解析的坑
PDF 是企业最常见也最难处理的格式。常见问题:
- 多栏布局 → 文字顺序错乱
- 表格 → 解析出来是凌乱数字
- 图片中文字 → 需要 OCR
- 公式 → 需要 LaTeX 重建
- 页眉页脚 → 污染正文
推荐工具组合:
# 高质量 PDF 解析
from mineru import MinerU # 国产开源,结构化输出
result = MinerU.process("complex.pdf")
# 输出:
# {
# "blocks": [{"type": "text", "content": "..."},
# {"type": "table", "content": "...", "html": "..."},
# {"type": "formula", "latex": "..."}],
# "page_info": [...]
# }
LlamaParse(商业服务)和 MinerU(开源)是 2026 年 PDF 解析的两大主流。
3.2 切片策略
切片(chunking)是 RAG 的核心决策之一。切得太大召回不准、太小语义破碎。
策略 1:固定大小切片(最朴素)
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64, # 重叠保持上下文
separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""],
)
chunks = splitter.split_text(document)
优点:实现简单,性能稳定
缺点:可能把一个完整句子切两半
策略 2:语义切片
按语义边界切(段落、章节):
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers = [("#", "Header 1"), ("##", "Header 2")]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers)
优点:上下文完整
缺点:chunk 大小不均匀,长 chunk 可能召回不准
策略 3:层级切片(推荐)
把文档同时按多个粒度切:
原文档
├── L1: 大段(2000 tokens)── 完整上下文
│ ├── L2: 中段(500 tokens)── 检索单位
│ │ ├── L3: 小句(100 tokens)── 精确召回
检索时用 L3 精确召回,但返回 L2 给 LLM(带上下文)。
LlamaIndex 实现:
from llama_index.core.node_parser import HierarchicalNodeParser
parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128]
)
nodes = parser.get_nodes_from_documents(docs)
策略 4:基于 LLM 的语义切片
让 LLM 自己决定在哪里切——效果最好但成本最高:
def llm_chunk(text):
prompt = f"把下面文本按主题切成不超过 500 字的段落...\n{text}"
return llm(prompt).split("---")
适合关键知识库 + 离线处理。
3.3 切片质量评估
如何判断切片好不好?三个指标:
| 指标 | 怎么测 |
|---|---|
| 覆盖率 | 真实问题 → 答案是否在某个 chunk 内 |
| 召回率 | 检索 top-K chunks 是否包含正确答案 |
| 冗余度 | chunk 之间是否大量重复 |
实操:准备 100 个真实业务 Q&A,跑评估,召回率应 > 85%。
四、Embedding 与向量库
4.1 Embedding 模型选型
主流 Embedding 模型(2026 中):
| 模型 | 维度 | 中文 | 英文 | 价格 | 备注 |
|---|---|---|---|---|---|
| BGE-M3 (智源) | 1024 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 开源 | 中文首选 ⭐ |
| BGE-large-zh | 1024 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 开源 | 通用稳定 |
| GTE-large-zh (阿里) | 1024 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 开源 | 增长快 |
| OpenAI text-embedding-3-large | 3072 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | $0.13/1M | 通用主流 |
| Voyage 3 | 1024 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | $0.12/1M | 闭源最强 |
| Cohere Embed v4 | 1536 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | $0.10/1M | 商业可用 |
| nomic-embed-text | 768 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 开源 | Ollama 内置 |
关键选择维度:
- 语言:中文用 BGE-M3,英文为主用 OpenAI / Voyage
- 维度:高维度 = 高质量但慢,1024 维是平衡点
- 成本:自部署 BGE 系列免费
部署 BGE-M3 服务
# 用 sentence-transformers 自部署
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3", device="cuda")
embeddings = model.encode(
["大模型推理优化", "RAG 检索增强"],
normalize_embeddings=True, # 归一化,便于余弦计算
)
或者用 vLLM / Infinity 服务化:
# Infinity(高性能 embedding 服务)
docker run -p 7997:7997 \
michaelf34/infinity:latest \
--model-id BAAI/bge-m3
4.2 向量库横评
| 向量库 | 类型 | 优势 | 适合 |
|---|---|---|---|
| Milvus | 开源 | 国产、扩展性强 | 中大规模 ⭐ |
| Qdrant | 开源 | Rust 实现快 | 中小规模 ⭐ |
| Weaviate | 开源 | 内置混合检索 | 中型 |
| Chroma | 开源 | 极简易用 | 开发 / 小规模 |
| Pinecone | 商业 SaaS | 托管省心 | 不想运维 |
| pgvector | PostgreSQL 扩展 | 已有 PG | 小规模 |
| Elasticsearch | 老牌全文检索 | 集成方便 | 与 ES 共存 |
推荐选择
- 个人 / Demo:Chroma 或 Qdrant
- 中型业务:Qdrant 或 Weaviate
- 大型业务:Milvus 或 Pinecone
- 已有 ES / PG:直接用现成的
4.3 Milvus 实战
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
# 1. 创建 collection
client.create_collection(
collection_name="docs",
dimension=1024,
metric_type="COSINE",
index_type="HNSW", # 高性能近似检索
index_params={"M": 16, "efConstruction": 256},
)
# 2. 插入数据
client.insert("docs", [
{"id": 1, "vector": [0.1, 0.2, ...], "text": "...", "source": "doc1.pdf"},
{"id": 2, "vector": [...], "text": "...", "source": "doc2.pdf"},
])
# 3. 检索
results = client.search(
collection_name="docs",
data=[query_embedding],
limit=10,
output_fields=["text", "source"],
)
索引算法选择
| 算法 | 特点 | 适合 |
|---|---|---|
| FLAT | 暴力检索,精确 | < 10 万条 |
| IVF_FLAT | 倒排索引 | 中等规模 |
| HNSW | 图索引,高速 | 主流首选 ⭐ |
| IVF_PQ | 量化压缩 | 极大规模 |
| DiskANN | 磁盘索引 | 百亿级 |
HNSW 是 99% 场景的最佳选择——速度 + 质量平衡好。
五、检索策略升级
5.1 朴素 RAG:基线版本
def naive_rag(question, top_k=5):
# 1. 问题 embedding
q_vec = embed(question)
# 2. 向量检索
chunks = vector_db.search(q_vec, top_k=top_k)
# 3. 拼成 prompt
context = "\n\n".join([c.text for c in chunks])
prompt = f"基于以下文档回答问题:\n{context}\n\n问题:{question}"
# 4. LLM 生成
return llm(prompt)
痛点:
- 召回率低(语义匹配不全)
- 缺乏精排
- 问题不清晰时检索差
5.2 Hybrid 检索(向量 + 关键词)
向量检索擅长语义,关键词检索擅长精确。两者互补:
def hybrid_search(question, top_k=10):
# 1. 向量检索
vec_results = vector_db.search(embed(question), top_k=20)
# 2. BM25 关键词检索
bm25_results = elasticsearch.search(question, top_k=20)
# 3. 融合(RRF - Reciprocal Rank Fusion)
fused = reciprocal_rank_fusion([vec_results, bm25_results])
return fused[:top_k]
def reciprocal_rank_fusion(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, doc in enumerate(ranking):
scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])
实测收益:召回率从 75% → 88%。
5.3 Rerank(重排)
向量检索粗排出 50 条,用 cross-encoder 精排出最相关的 5 条:
from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)
def rerank(question, candidates):
pairs = [[question, c.text] for c in candidates]
scores = reranker.compute_score(pairs, normalize=True)
sorted_idx = sorted(range(len(scores)), key=lambda i: -scores[i])
return [candidates[i] for i in sorted_idx[:5]]
为什么 rerank 这么强:
- Embedding:双塔结构(Q 和 D 独立编码,最后算相似度)—— 快但粗
- Rerank:cross-encoder(Q 和 D 拼一起送入)—— 慢但准
典型流程:
向量检索 50 条 → Rerank → top 5 → LLM
实测收益:精度从 75% → 92%。
5.4 Query 改写
用户问题往往不适合直接检索。三种改写:
1. HyDE(Hypothetical Document Embeddings)
def hyde(question):
# 让 LLM 先编一个"假设答案"
hypothetical = llm(f"写一段回答这个问题的文档:{question}")
# 用假设答案做检索(比原问题准)
return vector_db.search(embed(hypothetical))
2. Step-back Prompting
def stepback(question):
# 抽取更通用的问题
general = llm(f"把这个问题改写得更通用:{question}")
# 两路检索 + 合并
return combine(search(question), search(general))
3. Multi-query
def multi_query(question, n=5):
# 让 LLM 生成 n 个改写版本
queries = llm(f"用 {n} 种不同方式重写问题:{question}").split("\n")
# 每个查询独立检索 + 去重融合
return rrf_merge([search(q) for q in queries])
5.5 GraphRAG:当下最热
GraphRAG(Microsoft 2024)解决传统 RAG 的两个问题:
- 跨文档推理弱:传统 RAG 只能召回相关 chunk
- 全局问题答不好:“这家公司有哪些产品” 这种问题需要看遍所有文档
核心思路:
离线阶段:
1. 用 LLM 从文档抽取实体 + 关系
2. 构建知识图谱
3. 用社区检测算法(如 Leiden)聚类
4. 每个社区生成摘要
在线阶段:
- 局部问题:向量检索(传统 RAG)
- 全局问题:检索相关社区 + 摘要回答
实测对比:
| 问题类型 | 传统 RAG | GraphRAG |
|---|---|---|
| 局部查询 | 88% | 89% |
| 跨文档推理 | 52% | 84% ⭐ |
| 全局总结 | 31% | 78% ⭐ |
代价:
- 离线索引成本高(要让 LLM 处理所有文档)
- 实现复杂
适合:知识库较稳定 + 需要复杂推理的场景。
5.6 Agentic RAG / Self-RAG
2025-2026 的新方向——让 LLM 自己决定检索策略。
Self-RAG 流程:
1. LLM 判断「是否需要检索」
2. 如需要 → 检索 → 评估检索质量
3. 质量差 → 重新生成 query → 再检索
4. 质量好 → 基于检索生成答案
5. 自评答案是否引用了检索内容
6. 没引用 → 重生成
Agentic RAG 流程:
LLM Agent 拿到问题
├─ 子问题 1 → 检索 A 库
├─ 子问题 2 → 检索 B 库 + 搜索引擎
├─ 子问题 3 → 调用工具
└─ 合并 → 最终答案
这其实就是 Agent + RAG 的融合,下一篇我们会专门讲 Tool Use。
六、生产实战:端到端 RAG 服务
6.1 完整架构
┌─────────────────────────────────────────────┐
│ 数据采集层 │
│ 爬虫 / 上传 / 同步 → 文档队列 │
├─────────────────────────────────────────────┤
│ 文档处理层 │
│ MinerU / Unstructured → 切片 → 元数据 │
├─────────────────────────────────────────────┤
│ 索引层 │
│ BGE-M3 Embedding + Milvus + ES (BM25) │
├─────────────────────────────────────────────┤
│ 检索层 │
│ Hybrid Search + RRF + BGE Reranker │
├─────────────────────────────────────────────┤
│ 生成层 │
│ vLLM + Qwen3-32B (or Claude) │
├─────────────────────────────────────────────┤
│ 服务层 │
│ FastAPI + 流式输出 + 缓存 │
└─────────────────────────────────────────────┘
6.2 核心代码(用 LlamaIndex)
"""
端到端 RAG 服务(生产级简化版)
依赖:pip install llama-index llama-index-vector-stores-milvus
"""
from llama_index.core import VectorStoreIndex, StorageContext, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.vector_stores.milvus import MilvusVectorStore
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.openai_like import OpenAILike
from llama_index.core.postprocessor import SentenceTransformerRerank
# 1. 全局配置
Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-m3")
Settings.llm = OpenAILike(
model="qwen3-32b",
api_base="http://vllm:8000/v1",
api_key="dummy",
)
Settings.node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=64)
# 2. 向量库
vector_store = MilvusVectorStore(
uri="http://milvus:19530",
collection_name="docs",
dim=1024,
overwrite=False,
)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# 3. 索引(首次构建)
from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader("./docs").load_data()
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
show_progress=True,
)
# 4. 查询引擎 + Rerank
reranker = SentenceTransformerRerank(
model="BAAI/bge-reranker-v2-m3",
top_n=5,
)
query_engine = index.as_query_engine(
similarity_top_k=30, # 召回 30 条
node_postprocessors=[reranker], # rerank 到 5 条
streaming=True,
)
# 5. 查询
response = query_engine.query("我们的产品有哪些主要功能?")
for chunk in response.response_gen:
print(chunk, end="", flush=True)
# 6. 查看来源
for node in response.source_nodes:
print(f"\n来源:{node.node.metadata}")
6.3 工程化要点
1. 异步处理
文档解析慢,要异步:
from celery import Celery
@celery.task
def process_document(doc_path):
text = parse_document(doc_path)
chunks = splitter.split(text)
embeddings = embed_model.encode(chunks)
milvus.insert(...)
2. 增量更新
def upsert_document(doc_id, content):
# 删除旧版本
milvus.delete(filter=f"doc_id == '{doc_id}'")
# 插入新版本
chunks = split(content)
milvus.insert([{
"doc_id": doc_id,
"version": new_version,
"vector": embed(c),
"text": c
} for c in chunks])
3. 多租户隔离
# 用 partition 隔离不同租户
milvus.search(
collection_name="docs",
data=[q_vec],
filter=f"tenant_id == '{user.tenant_id}'",
output_fields=["text"],
)
4. 监控指标
| 指标 | 阈值 |
|---|---|
| 检索延迟 (p99) | < 100ms |
| 召回率 | > 85% |
| 答案引用源覆盖率 | > 90% |
| LLM 拒答率 | < 5% |
5. 评估体系
# 用 RAGAS 评估 RAG 质量
from ragas import evaluate
from ragas.metrics import (
faithfulness, # 答案是否基于检索内容
answer_relevancy, # 答案是否相关
context_precision, # 检索准确率
context_recall, # 检索召回率
)
scores = evaluate(
dataset=test_dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
七、避坑清单 + 下一篇预告
7.1 RAG 5 大常见坑
坑 1:用错 chunk 大小
症状:召回不准 / 答非所问。
对策:
- 一般 500 tokens 是平衡点
- 业务文档(FAQ):300-400
- 技术文档:500-800
- 长篇报告:层级切片
坑 2:embedding 模型语言不匹配
症状:中文检索效果差。
对策:
- 中文为主:BGE-M3
- 中英混合:BGE-M3 / Voyage 3
- 不要用纯英文模型做中文
坑 3:召回不做 rerank
症状:top-5 chunk 不是真正最相关的。
对策:召回 30-50 条 → rerank 到 5 条。
坑 4:单路检索
症状:用户用关键词查询时效果差。
对策:向量 + BM25 hybrid,永远比单路好。
坑 5:不评估
症状:感觉不太对但不知道哪里差。
对策:用 RAGAS / TruLens 等工具量化评估每个环节。
7.2 下一篇预告
- 第 27 篇:Function Calling / Tool Use - 让大模型「动」起来 —— RAG 让模型"知道知识",Tool Use 让模型"做事"。我们会讲清楚标准协议、主流实现、Agent 基础。
- 之后是 Agent 框架(28 篇)、多模态(29 篇)、Prompt 工程(30 篇)。
结语:RAG 是大模型应用的「地基中的地基」
读完本文你应该明白:
- RAG 不是为省 token,是为管理无限知识库
- 80% 的 RAG 问题在文档处理和切片——花时间在这里
- BGE-M3 是中文 embedding 首选
- Hybrid 检索 + Rerank 让召回准确率 75% → 92%
- GraphRAG 适合跨文档推理 + 全局总结
- 生产级 RAG = LlamaIndex / LangChain + Milvus + BGE + vLLM
下一篇我们继续:
- 第 27 篇:Function Calling / Tool Use —— 让模型不只是"会聊",还能"会做"。
我们下篇见。
📮 关于「码海寻道」
这里是一个聚焦 AI 工程化、大模型部署、后端架构实战的技术专栏。
写最一线的踩坑经验,做最务实的技术拆解。如果这篇文章对你有启发,欢迎点赞、转发、关注。我们下篇见。
1万+




被折叠的 条评论
为什么被折叠?



