11b - RAG 流水线详解
本文是《AI Agent 实战手册》第 11 章第 2 节。 上一节:11a-RAG概念与架构 | 下一节:11c-向量数据库对比
概述
RAG 流水线是将原始文档转化为可检索知识、并在查询时生成有据可查回答的完整数据处理链路。本节将深入拆解流水线的每个阶段——从文档摄入与解析、分块策略、嵌入模型选择、向量存储,到检索方法、检索后处理(重排序与压缩)、最终的生成与引用——每个阶段均包含工具推荐、代码示例和实现指导,帮助你构建生产级 RAG 系统。
1. 文档摄入与解析(Document Ingestion & Parsing)
文档摄入是 RAG 流水线的第一步,也是最容易被低估的环节。“垃圾进,垃圾出”——如果解析质量差,后续所有环节都会受影响。
核心挑战
| 挑战 | 说明 |
|---|---|
| 格式多样性 | PDF、Word、HTML、Markdown、PPT、Excel、图片、扫描件等 |
| 结构保留 | 表格、标题层级、列表、代码块等结构信息容易丢失 |
| 多模态内容 | 文档中嵌入的图表、流程图、公式需要特殊处理 |
| 元数据提取 | 作者、日期、章节标题等元数据对后续过滤检索至关重要 |
| 编码与语言 | 多语言文档、特殊字符、编码问题 |
工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| Unstructured | 通用文档解析,支持 30+ 格式 | 免费(开源);API 版 $0.01/页起 | 企业级多格式文档处理 |
| LlamaParse | LLM 驱动的智能文档解析 | 免费 1000 页/天;付费 $0.003/页起 | 复杂 PDF(表格、图表、公式) |
| Docling(IBM) | 开源文档转换工具 | 免费(MIT 开源) | 本地部署、隐私敏感场景 |
| Apache Tika | Java 生态文档解析 | 免费(Apache 2.0) | Java 项目集成 |
| PyMuPDF (fitz) | 高性能 PDF 解析 | 免费(AGPL) | 纯 PDF 处理,速度优先 |
| Marker | PDF 转 Markdown | 免费(开源) | PDF 转结构化 Markdown |
操作步骤
步骤 1:选择解析工具
根据文档类型和需求选择合适的解析工具:
简单文本(MD/TXT/HTML)──▶ 直接读取或轻量解析
结构化 PDF ──────────────▶ PyMuPDF / Docling
复杂 PDF(表格/图表)────▶ LlamaParse / Unstructured
扫描件/图片 ─────────────▶ LlamaParse(OCR)/ Unstructured
Office 文档 ─────────────▶ Unstructured / Apache Tika
代码文件 ────────────────▶ 直接读取 + 语法感知分块步骤 2:使用 Unstructured 解析多格式文档
from unstructured.partition.auto import partition
# 自动检测格式并解析
elements = partition(filename="report.pdf")
# 按元素类型分类处理
for element in elements:
print(f"类型: {type(element).__name__}")
print(f"内容: {element.text[:100]}...")
print(f"元数据: {element.metadata}")
print("---")步骤 3:使用 LlamaParse 解析复杂 PDF
from llama_parse import LlamaParse
parser = LlamaParse(
result_type="markdown", # 输出 Markdown 格式
num_workers=4, # 并行处理
language="zh", # 中文文档
)
documents = parser.load_data("complex_report.pdf")
for doc in documents:
print(doc.text[:500])步骤 4:使用 Docling 本地解析(隐私优先)
from docling.document_converter import DocumentConverter
converter = DocumentConverter()
result = converter.convert("internal_doc.pdf")
# 导出为 Markdown
markdown_output = result.document.export_to_markdown()
print(markdown_output[:500])提示词模板
请分析以下文档解析结果的质量,并提出改进建议:
## 文档信息
- 文件名:[文件名]
- 格式:[PDF/Word/HTML/...]
- 页数:[页数]
## 解析结果样本
[粘贴解析后的前 500 字]
## 请评估
1. 文本完整性:是否有内容丢失?
2. 结构保留:标题层级、表格、列表是否正确保留?
3. 元数据质量:是否提取了有用的元数据?
4. 建议:推荐使用哪种解析工具或参数调整?2. 分块策略(Chunking Strategies)
分块是 RAG 流水线中对检索质量影响最大的环节之一。分块策略直接决定了检索的粒度和语义完整性。
分块策略全景对比
| 策略 | 原理 | 优势 | 劣势 | 推荐场景 |
|---|---|---|---|---|
| 固定大小分块 | 按固定 token/字符数切分 | 实现简单、速度快 | 可能截断语义 | 快速原型、均匀文本 |
| 递归分块 | 按分隔符层级递归切分 | 平衡语义和大小 | 需要调参 | 通用推荐(生产首选) |
| 语义分块 | 基于嵌入相似度检测语义边界 | 语义完整性最好 | 计算成本高(3-5x) | 高质量需求、预算充足 |
| 文档结构分块 | 按标题/章节/段落切分 | 保留文档结构 | 依赖文档格式规范 | 结构化文档(技术文档、法律文件) |
| 代码感知分块 | 按函数/类/模块切分 | 保留代码语义完整 | 仅适用于代码 | 代码库 RAG |
| Late Chunking | 先用长上下文模型嵌入整文档,再分块 | 保留全局上下文 | 需要长上下文嵌入模型 | 上下文依赖强的文档 |
| 父子分块 | 小块检索,大块返回 | 精确检索 + 完整上下文 | 实现复杂 | 需要精确定位又需完整上下文 |
关键参数指南
┌─────────────────────────────────────────────────┐
│ 分块参数调优指南 │
├─────────────────────────────────────────────────┤
│ │
│ 块大小(Chunk Size) │
│ ├── 256-512 tokens:高精度检索,适合 FAQ/短文档 │
│ ├── 512-1024 tokens:通用推荐,平衡精度和上下文 │
│ └── 1024-2048 tokens:长上下文需求,技术文档 │
│ │
│ 重叠(Overlap) │
│ ├── 推荐:块大小的 10-20% │
│ ├── 过小:上下文断裂 │
│ └── 过大:冗余增加、存储浪费 │
│ │
│ 经验法则 │
│ ├── 递归分块 + 400-800 tokens + 20% 重叠 │
│ │ = 大多数场景的最佳起点 │
│ └── 根据评估指标迭代调优 │
│ │
└─────────────────────────────────────────────────┘操作步骤
步骤 1:固定大小分块(基线方案)
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=160, # 20% 重叠
separators=["\n\n", "\n", "。", ".", " ", ""],
length_function=len,
)
chunks = splitter.split_text(document_text)
print(f"生成 {len(chunks)} 个块")步骤 2:递归分块(生产推荐)
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 中文文档优化的分隔符层级
splitter = RecursiveCharacterTextSplitter(
chunk_size=600,
chunk_overlap=120,
separators=[
"\n\n\n", # 章节分隔
"\n\n", # 段落分隔
"\n", # 行分隔
"。", # 中文句号
";", # 中文分号
",", # 中文逗号
".", # 英文句号
" ", # 空格
"", # 字符级(最后手段)
],
)
chunks = splitter.split_documents(documents)步骤 3:语义分块(高质量需求)
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
semantic_splitter = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95, # 相似度低于 95 分位时切分
)
semantic_chunks = semantic_splitter.split_text(document_text)步骤 4:代码感知分块
from langchain.text_splitter import Language, RecursiveCharacterTextSplitter
# Python 代码分块
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=1000,
chunk_overlap=100,
)
# TypeScript 代码分块
ts_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.TS,
chunk_size=1000,
chunk_overlap=100,
)
code_chunks = python_splitter.split_text(python_source_code)步骤 5:父子分块(小块检索,大块返回)
from llama_index.core.node_parser import (
SentenceSplitter,
HierarchicalNodeParser,
)
from llama_index.core.retrievers import AutoMergingRetriever
# 创建层级分块器:大块 2048、中块 512、小块 128
node_parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128],
)
nodes = node_parser.get_nodes_from_documents(documents)
# 检索时用小块匹配,返回时自动合并为父块
retriever = AutoMergingRetriever(
vector_retriever,
storage_context,
simple_ratio_thresh=0.5, # 超过 50% 子块命中则返回父块
)提示词模板
请为以下文档推荐最佳分块策略:
## 文档特征
- 类型:[技术文档/法律合同/代码库/FAQ/论文/...]
- 平均长度:[页数或字数]
- 语言:[中文/英文/混合]
- 结构化程度:[高(有清晰标题层级)/ 中 / 低(纯文本流)]
- 查询类型:[精确事实查找 / 概念理解 / 代码搜索 / ...]
## 请推荐
1. 分块策略(固定/递归/语义/结构/代码感知)
2. 建议的块大小和重叠比例
3. 是否需要父子分块
4. 需要注意的特殊处理3. 嵌入模型选择(Embedding Model Selection)
嵌入模型将文本转换为高维数值向量,是 RAG 检索质量的核心决定因素。选错嵌入模型,后续优化都是徒劳。
2025-2026 主流嵌入模型对比
| 模型 | 提供商 | 维度 | 最大 Tokens | 价格 | MTEB 排名 | 特点 |
|---|---|---|---|---|---|---|
| text-embedding-3-large | OpenAI | 3072 | 8191 | $0.13/百万 tokens | 高 | 高质量通用嵌入,支持维度缩减 |
| text-embedding-3-small | OpenAI | 1536 | 8191 | $0.02/百万 tokens | 中高 | 性价比之选,适合大多数场景 |
| embed-v4 | Cohere | 1024 | 512 | 免费层可用;生产版按量计费 | 高 | 多语言支持优秀,支持搜索/分类/聚类 |
| voyage-3-large | Voyage AI | 1024 | 32000 | $0.18/百万 tokens | 最高 | MTEB 多领域第一,代码和技术文档优化 |
| voyage-3 | Voyage AI | 1024 | 32000 | $0.06/百万 tokens | 高 | 性价比优秀,长上下文支持 |
| mistral-embed | Mistral | 1024 | 8192 | $0.10/百万 tokens | 最高 | 准确率领先(77.8%),欧洲数据合规 |
| BGE-M3 | BAAI(开源) | 1024 | 8192 | 免费 | 高 | 多语言、多粒度、多功能,可自托管 |
| Jina Embeddings v3 | Jina AI(开源) | 1024 | 8192 | 免费(自托管);API $0.02/百万 tokens | 高 | 任务自适应嵌入,支持 Matryoshka |
| GTE-Qwen2 | 阿里(开源) | 1024 | 32000 | 免费 | 高 | 中文优化,长上下文 |
| NV-Embed-v2 | NVIDIA(开源) | 4096 | 32768 | 免费 | 最高 | 高维度高精度,需要 GPU |
选择决策树
你的场景是什么?
│
├── 快速原型 / 预算有限
│ └── ✅ text-embedding-3-small($0.02/百万 tokens)
│
├── 生产级通用场景
│ ├── 英文为主 ──▶ ✅ voyage-3(性价比最优)
│ ├── 中文为主 ──▶ ✅ BGE-M3 或 GTE-Qwen2(中文优化)
│ └── 多语言 ──▶ ✅ Cohere embed-v4(多语言领先)
│
├── 最高精度需求
│ └── ✅ voyage-3-large 或 mistral-embed
│
├── 代码/技术文档
│ └── ✅ voyage-3-large(代码领域优化)
│
├── 隐私/自托管需求
│ └── ✅ BGE-M3 / Jina v3 / GTE-Qwen2(开源自托管)
│
└── 超长文档(>8K tokens)
└── ✅ voyage-3(32K)/ GTE-Qwen2(32K)/ NV-Embed-v2(32K)操作步骤
步骤 1:使用 OpenAI 嵌入(快速上手)
from openai import OpenAI
client = OpenAI()
def get_embeddings(texts: list[str], model="text-embedding-3-small"):
"""批量获取文本嵌入向量"""
response = client.embeddings.create(
input=texts,
model=model,
)
return [item.embedding for item in response.data]
# 单条嵌入
embedding = get_embeddings(["RAG 流水线是什么?"])[0]
print(f"向量维度: {len(embedding)}") # 1536
# 批量嵌入(推荐,减少 API 调用)
texts = ["文档片段1", "文档片段2", "文档片段3"]
embeddings = get_embeddings(texts)步骤 2:使用 OpenAI 维度缩减(降低存储成本)
# text-embedding-3 系列支持 Matryoshka 维度缩减
response = client.embeddings.create(
input=["RAG 流水线详解"],
model="text-embedding-3-large",
dimensions=256, # 从 3072 缩减到 256,存储减少 12x
)
# 精度损失约 2-5%,但存储和检索速度大幅提升步骤 3:使用开源模型本地嵌入(隐私优先)
from sentence_transformers import SentenceTransformer
# 加载 BGE-M3 多语言模型
model = SentenceTransformer("BAAI/bge-m3")
texts = [
"RAG 系统的核心是检索增强生成",
"向量数据库存储文档的嵌入表示",
]
# 本地推理,数据不出服务器
embeddings = model.encode(texts, normalize_embeddings=True)
print(f"向量维度: {embeddings.shape[1]}") # 1024步骤 4:使用 Voyage AI 嵌入(最高精度)
import voyageai
client = voyageai.Client()
result = client.embed(
texts=["RAG 流水线的检索阶段"],
model="voyage-3-large",
input_type="document", # 文档嵌入用 "document",查询用 "query"
)
embedding = result.embeddings[0]
print(f"向量维度: {len(embedding)}") # 1024提示词模板
请帮我选择最适合的嵌入模型:
## 项目需求
- 文档语言:[中文/英文/多语言]
- 文档类型:[技术文档/法律文件/代码/通用]
- 文档规模:[千级/万级/百万级]
- 查询类型:[语义搜索/精确匹配/代码搜索]
- 部署方式:[云 API/自托管/混合]
- 月预算:[金额]
- 延迟要求:[实时 <100ms / 准实时 <500ms / 批处理]
## 请推荐
1. 首选模型及理由
2. 备选模型
3. 预估月成本
4. 需要注意的限制4. 向量存储(Vector Storage)
向量存储是 RAG 系统的持久化层,负责高效存储和检索嵌入向量。选择合适的向量数据库直接影响检索延迟、可扩展性和运维成本。
详细的向量数据库对比见 11c-向量数据库对比。
工具推荐
| 工具 | 类型 | 价格 | 适用场景 |
|---|---|---|---|
| Pinecone | 全托管云服务 | 免费层可用;标准版 $70/月起 | 零运维、快速上线 |
| Weaviate | 开源 + 云服务 | 免费(自托管);云版 $25/月起 | 混合搜索、GraphQL API |
| Qdrant | 开源 + 云服务 | 免费(自托管);云版 $25/月起 | 高性能、Rust 实现 |
| ChromaDB | 开源嵌入式 | 免费 | 本地开发、原型验证 |
| pgvector | PostgreSQL 扩展 | 免费(随 PostgreSQL) | 已有 PostgreSQL 基础设施 |
| Milvus | 开源分布式 | 免费(自托管);Zilliz Cloud 按量计费 | 大规模(亿级向量) |
操作步骤
步骤 1:使用 ChromaDB(本地开发)
import chromadb
# 创建持久化客户端
client = chromadb.PersistentClient(path="./chroma_db")
# 创建或获取集合
collection = client.get_or_create_collection(
name="my_documents",
metadata={"hnsw:space": "cosine"}, # 使用余弦相似度
)
# 添加文档
collection.add(
documents=["RAG 是检索增强生成", "向量数据库存储嵌入"],
metadatas=[{"source": "doc1"}, {"source": "doc2"}],
ids=["id1", "id2"],
)
# 查询
results = collection.query(
query_texts=["什么是 RAG?"],
n_results=3,
)
print(results["documents"])步骤 2:使用 Qdrant(生产部署)
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# 连接 Qdrant(本地或云端)
client = QdrantClient(url="http://localhost:6333")
# 创建集合
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(
size=1536, # 向量维度
distance=Distance.COSINE,
),
)
# 插入向量
client.upsert(
collection_name="documents",
points=[
PointStruct(
id=1,
vector=embedding_vector,
payload={"text": "文档内容", "source": "report.pdf", "page": 5},
),
],
)
# 搜索
results = client.search(
collection_name="documents",
query_vector=query_embedding,
limit=5,
)步骤 3:使用 pgvector(PostgreSQL 生态)
-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 创建文档表
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
metadata JSONB,
embedding vector(1536), -- 1536 维向量
created_at TIMESTAMP DEFAULT NOW()
);
-- 创建 HNSW 索引(推荐,比 IVFFlat 更快)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 相似度搜索
SELECT id, content, metadata,
1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 5;5. 检索方法(Retrieval Methods)
检索是 RAG 流水线的核心环节,直接决定了 LLM 能获得什么样的上下文信息。
检索方法全景对比
| 方法 | 原理 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 稠密检索(Dense) | 基于向量余弦/点积相似度 | 语义理解强,能匹配同义表达 | 对精确关键词匹配弱 | 语义搜索、概念查找 |
| 稀疏检索(Sparse/BM25) | 基于词频统计的关键词匹配 | 精确匹配强,可解释性好 | 缺乏语义理解 | 精确术语、产品编号 |
| 混合检索(Hybrid) | 结合稠密和稀疏检索 | 兼顾语义和精确匹配 | 需要调节融合权重 | 生产推荐(大多数场景) |
| 多路召回(Multi-path) | 从多个索引/数据源并行检索 | 覆盖面广 | 结果融合复杂 | 多数据源场景 |
| 知识图谱检索 | 基于实体关系的结构化检索 | 关系推理强 | 构建成本高 | 需要关系推理的场景 |
| ColBERT | 延迟交互的 token 级匹配 | 精度高、速度快 | 索引存储大 | 高精度检索需求 |
操作步骤
步骤 1:稠密检索(向量搜索)
from llama_index.core import VectorStoreIndex
# 构建向量索引
index = VectorStoreIndex.from_documents(documents)
# 创建检索器
retriever = index.as_retriever(
similarity_top_k=10, # 返回 Top-10 结果
)
# 检索
nodes = retriever.retrieve("RAG 系统如何处理中文文档?")
for node in nodes:
print(f"相关度: {node.score:.4f} | {node.text[:100]}...")步骤 2:稀疏检索(BM25)
from llama_index.retrievers.bm25 import BM25Retriever
# 创建 BM25 检索器
bm25_retriever = BM25Retriever.from_defaults(
nodes=all_nodes,
similarity_top_k=10,
)
# BM25 擅长精确关键词匹配
results = bm25_retriever.retrieve("pgvector HNSW 索引配置")步骤 3:混合检索(生产推荐)
from llama_index.core.retrievers import QueryFusionRetriever
# 融合向量检索和 BM25 检索
hybrid_retriever = QueryFusionRetriever(
retrievers=[vector_retriever, bm25_retriever],
similarity_top_k=10,
num_queries=1, # 不做查询扩展
mode="reciprocal_rerank", # 使用 RRF 融合排序
)
results = hybrid_retriever.retrieve("如何优化 RAG 检索质量?")步骤 4:使用 Qdrant 原生混合搜索
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
# Qdrant 原生支持混合搜索(稠密 + 稀疏)
results = client.query_points(
collection_name="documents",
prefetch=[
# 稠密检索
models.Prefetch(
query=dense_embedding,
using="dense",
limit=20,
),
# 稀疏检索
models.Prefetch(
query=models.SparseVector(
indices=sparse_indices,
values=sparse_values,
),
using="sparse",
limit=20,
),
],
# RRF 融合
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=10,
)混合检索融合算法
混合检索的关键在于如何融合不同检索器的结果。主流融合算法:
| 算法 | 原理 | 优势 | 使用场景 |
|---|---|---|---|
| RRF(Reciprocal Rank Fusion) | 基于排名倒数加权融合 | 无需归一化分数,鲁棒性强 | 推荐默认选择 |
| 线性加权 | score = α × dense + (1-α) × sparse | 简单直观,可调权重 | 需要精细控制融合比例 |
| 分布式分数融合(DBSF) | 基于分数分布归一化后融合 | 处理不同分数尺度 | 多路召回分数差异大 |
RRF 公式:
RRF_score(d) = Σ 1 / (k + rank_i(d))
其中:
- d = 文档
- k = 常数(通常 60)
- rank_i(d) = 文档 d 在第 i 个检索器中的排名提示词模板
请帮我设计 RAG 系统的检索策略:
## 系统信息
- 知识库规模:[文档数量和总大小]
- 文档类型:[技术文档/FAQ/代码/混合]
- 查询特征:[自然语言/关键词/混合]
- 延迟要求:[<100ms / <500ms / <1s]
## 典型查询示例
1. [示例查询 1]
2. [示例查询 2]
3. [示例查询 3]
## 请推荐
1. 检索方法(稠密/稀疏/混合/多路)
2. 融合算法和权重建议
3. Top-K 参数建议
4. 是否需要查询转换(重写/分解/HyDE)6. 检索后处理(Post-Retrieval Processing)
检索后处理是 Advanced RAG 的核心优化环节,通过重排序和上下文压缩显著提升最终生成质量。
6.1 重排序(Reranking)
重排序使用更精确的模型对检索结果进行二次排序,通常能将回答准确率提升 10-30%。
工具推荐
| 工具 | 类型 | 价格 | 特点 |
|---|---|---|---|
| Cohere Rerank | API 服务 | 免费层 100 次/分钟;$1/千次查询 | 多语言支持,即插即用 |
| Jina Reranker v2 | API + 开源 | 免费(自托管);API $0.02/千次 | 开源可自托管 |
| BGE-Reranker-v2 | 开源 | 免费 | 中文优化,可本地部署 |
| FlashRank | 开源轻量 | 免费 | 超轻量(<100MB),CPU 友好 |
| LLM-as-Reranker | 使用 LLM 重排序 | 按 LLM 调用计费 | 最灵活,但成本最高 |
操作步骤
步骤 1:使用 Cohere Rerank
import cohere
co = cohere.Client("your-api-key")
# 对检索结果重排序
rerank_results = co.rerank(
model="rerank-v3.5",
query="RAG 系统如何处理中文文档?",
documents=[node.text for node in retrieved_nodes],
top_n=5, # 只保留 Top-5
)
for result in rerank_results.results:
print(f"排名: {result.index} | 相关度: {result.relevance_score:.4f}")步骤 2:使用开源 BGE-Reranker(本地部署)
from sentence_transformers import CrossEncoder
# 加载交叉编码器重排序模型
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)
# 计算查询-文档对的相关度分数
query = "RAG 系统如何处理中文文档?"
pairs = [(query, node.text) for node in retrieved_nodes]
scores = reranker.predict(pairs)
# 按分数排序
ranked_indices = scores.argsort()[::-1]
top_nodes = [retrieved_nodes[i] for i in ranked_indices[:5]]步骤 3:在 LlamaIndex 中集成重排序
from llama_index.core.postprocessor import SentenceTransformerRerank
# 创建重排序后处理器
reranker = SentenceTransformerRerank(
model="BAAI/bge-reranker-v2-m3",
top_n=5,
)
# 在查询引擎中使用
query_engine = index.as_query_engine(
similarity_top_k=20, # 先召回 20 个
node_postprocessors=[reranker], # 重排序后保留 5 个
)
response = query_engine.query("RAG 流水线有哪些阶段?")6.2 上下文压缩(Context Compression)
上下文压缩去除检索结果中的冗余和无关内容,让 LLM 聚焦于最相关的信息。
from llama_index.core.postprocessor import LongContextReorder
# 长上下文重排序:将最相关的内容放在开头和结尾
# (研究表明 LLM 对中间内容的注意力较弱——"Lost in the Middle")
reorder = LongContextReorder()
query_engine = index.as_query_engine(
similarity_top_k=10,
node_postprocessors=[reranker, reorder], # 先重排序,再重排列
)from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI
# 使用 LLM 提取每个文档中与查询最相关的部分
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
# 压缩后的文档只包含与查询直接相关的句子
compressed_docs = compressor.compress_documents(
documents=retrieved_docs,
query="RAG 系统的分块策略有哪些?",
)完整检索后处理流水线
检索结果(Top-20)
│
▼
┌──────────────┐
│ 去重(Dedup) │ ← 去除重复或高度相似的文档
└──────┬───────┘
│
▼
┌──────────────┐
│ 重排序(Rerank)│ ← 交叉编码器精排,保留 Top-10
└──────┬───────┘
│
▼
┌──────────────────┐
│ 上下文压缩 │ ← 提取每个文档中最相关的部分
│(Context Compress)│
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 长上下文重排列 │ ← 最相关内容放开头和结尾
│(Lost-in-Middle) │
└──────┬───────────┘
│
▼
送入 LLM 生成7. 生成与引用(Generation with Citations)
生成阶段是 RAG 流水线的最后一环,将检索到的上下文与用户查询组装成 Prompt,由 LLM 生成有据可查的回答。
工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| GPT-4o | 高质量生成,支持长上下文 | $2.50/$10 每百万 tokens(输入/输出) | 通用生产场景 |
| GPT-4o-mini | 性价比生成 | $0.15/$0.60 每百万 tokens | 成本敏感场景 |
| Claude 3.5 Sonnet | 长上下文生成,200K 窗口 | $3/$15 每百万 tokens | 大量上下文的 RAG |
| Claude 3.5 Haiku | 快速低成本生成 | $0.25/$1.25 每百万 tokens | 低延迟需求 |
| Gemini 2.5 Flash | 超长上下文,1M tokens | 免费层可用;$0.15/$0.60 每百万 tokens | 超大上下文 RAG |
| Llama 3.1 70B | 开源大模型 | 免费(自托管);API 按量计费 | 自托管、隐私需求 |
操作步骤
步骤 1:基础 RAG Prompt 组装
def build_rag_prompt(query: str, context_nodes: list) -> str:
"""组装 RAG Prompt"""
context_parts = []
for i, node in enumerate(context_nodes, 1):
source = node.metadata.get("file_name", "未知来源")
context_parts.append(f"[来源 {i}: {source}]\n{node.text}")
context_str = "\n\n---\n\n".join(context_parts)
return f"""你是一个基于知识库的问答助手。请严格根据以下检索到的上下文信息回答用户问题。
## 规则
1. 只使用提供的上下文信息回答问题
2. 在回答中用 [来源 N] 标注信息出处
3. 如果上下文中没有相关信息,明确说明
4. 不要编造上下文中不存在的信息
## 上下文信息
{context_str}
## 用户问题
{query}
## 请回答(附带来源引用):"""步骤 2:带引用的生成
from openai import OpenAI
client = OpenAI()
def generate_with_citations(query: str, context_nodes: list) -> str:
"""生成带引用的回答"""
prompt = build_rag_prompt(query, context_nodes)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是一个严谨的知识库问答助手,回答时必须标注来源。"},
{"role": "user", "content": prompt},
],
temperature=0.1, # 低温度,减少幻觉
max_tokens=1024,
)
return response.choices[0].message.content
# 使用
answer = generate_with_citations(
query="RAG 系统的分块策略有哪些?",
context_nodes=reranked_nodes,
)
print(answer)步骤 3:结构化引用输出
from pydantic import BaseModel
class Citation(BaseModel):
source: str
page: int | None = None
quote: str
class RAGResponse(BaseModel):
answer: str
citations: list[Citation]
confidence: float # 0-1
# 使用 OpenAI 结构化输出
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "基于上下文回答问题,返回结构化引用。"},
{"role": "user", "content": prompt},
],
response_format=RAGResponse,
)
rag_response = response.choices[0].message.parsed
print(f"回答: {rag_response.answer}")
print(f"置信度: {rag_response.confidence}")
for cite in rag_response.citations:
print(f" 来源: {cite.source} | 引用: {cite.quote}")步骤 4:流式生成(低延迟体验)
def stream_rag_response(query: str, context_nodes: list):
"""流式生成 RAG 回答"""
prompt = build_rag_prompt(query, context_nodes)
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是知识库问答助手。"},
{"role": "user", "content": prompt},
],
temperature=0.1,
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
stream_rag_response("RAG 流水线有哪些阶段?", reranked_nodes)提示词模板
RAG 系统 Prompt(带引用和置信度)
你是一个基于知识库的专业问答助手。
## 核心规则
1. **严格基于上下文**:只使用提供的上下文信息回答,不要使用你的训练知识
2. **标注来源**:每个关键信息点用 [来源 N] 标注出处
3. **诚实不确定性**:如果上下文信息不足以完整回答,明确说明哪些部分无法确认
4. **不编造**:绝对不要编造上下文中不存在的信息
## 上下文信息
[检索到的文档片段,每个片段标注来源编号]
## 用户问题
[用户查询]
## 回答格式
1. 直接回答问题
2. 关键信息标注 [来源 N]
3. 如有不确定之处,在末尾说明
4. 最后给出置信度评估(高/中/低)实战案例:构建完整的生产级 RAG 流水线
场景
为一家技术公司构建内部知识库问答系统,支持对技术文档、API 文档和代码仓库的智能检索与问答。
完整实现
"""
生产级 RAG 流水线完整示例
技术栈:LlamaIndex + Qdrant + OpenAI + Cohere Rerank
"""
from llama_index.core import (
VectorStoreIndex,
SimpleDirectoryReader,
Settings,
StorageContext,
)
from llama_index.core.node_parser import SentenceSplitter
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.postprocessor.cohere_rerank import CohereRerank
from qdrant_client import QdrantClient
# ============================================
# 阶段 1:配置全局设置
# ============================================
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(
model="text-embedding-3-small",
dimensions=512, # 维度缩减,节省存储
)
# ============================================
# 阶段 2:文档摄入与解析
# ============================================
documents = SimpleDirectoryReader(
input_dir="./knowledge_base",
recursive=True,
required_exts=[".md", ".txt", ".pdf", ".py", ".ts"],
filename_as_id=True,
).load_data()
print(f"加载了 {len(documents)} 个文档")
# ============================================
# 阶段 3:分块
# ============================================
splitter = SentenceSplitter(
chunk_size=512,
chunk_overlap=100,
paragraph_separator="\n\n",
)
# ============================================
# 阶段 4:向量存储(Qdrant)
# ============================================
qdrant_client = QdrantClient(url="http://localhost:6333")
vector_store = QdrantVectorStore(
client=qdrant_client,
collection_name="tech_knowledge_base",
)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# ============================================
# 阶段 5:构建索引(嵌入 + 存储)
# ============================================
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
transformations=[splitter],
show_progress=True,
)
# ============================================
# 阶段 6:配置检索 + 重排序 + 生成
# ============================================
cohere_rerank = CohereRerank(
model="rerank-v3.5",
top_n=5,
)
query_engine = index.as_query_engine(
similarity_top_k=20, # 先召回 20 个
node_postprocessors=[cohere_rerank], # 重排序后保留 5 个
response_mode="tree_summarize", # 树状摘要模式
)
# ============================================
# 阶段 7:查询
# ============================================
response = query_engine.query(
"我们的 API 认证机制是如何实现的?请详细说明。"
)
print("回答:", response.response)
print("\n来源:")
for node in response.source_nodes:
print(f" - {node.metadata.get('file_name', '未知')} "
f"(相关度: {node.score:.4f})")案例分析
- 维度缩减:使用
dimensions=512将 text-embedding-3-small 的 1536 维缩减到 512 维,存储减少 3x,检索速度提升,精度损失约 2-3% - 过召回 + 重排序:先用向量搜索召回 20 个候选(高召回率),再用 Cohere Rerank 精排到 5 个(高精度),这是生产系统的标准模式
- 树状摘要:使用
tree_summarize模式处理多个上下文片段,比简单拼接更能生成连贯的长回答 - 元数据追踪:通过
filename_as_id=True保留文件来源信息,支持回答溯源
避坑指南
❌ 常见错误
-
跳过文档解析质量检查,直接进入分块
- 问题:PDF 解析后表格变成乱码、标题层级丢失、图片中的文字未提取,导致后续检索到的内容不可用
- 正确做法:在流水线中加入解析质量检查步骤——抽样检查解析结果,验证表格、标题、列表等结构是否正确保留。对复杂 PDF 使用 LlamaParse 等 LLM 驱动的解析工具
-
使用同一个嵌入模型处理查询和文档,但不区分 input_type
- 问题:部分嵌入模型(如 Voyage AI、Cohere)对文档嵌入和查询嵌入使用不同的编码策略,混用会降低检索质量
- 正确做法:文档嵌入时使用
input_type="document",查询嵌入时使用input_type="query"。始终查阅嵌入模型的官方文档确认是否需要区分
-
只用向量检索,忽略混合搜索
- 问题:用户查询包含专有名词、错误代码、产品编号等精确信息时,纯向量搜索可能找不到精确匹配
- 正确做法:生产系统默认使用混合检索(向量 + BM25),通过 RRF 融合排序。对于技术文档和代码库场景,混合搜索通常比纯向量搜索提升 15-25% 的检索准确率
-
重排序模型的 max_length 设置过小
- 问题:交叉编码器重排序模型有最大输入长度限制(通常 512 tokens),超长文档块会被截断,导致重排序不准确
- 正确做法:确保分块大小与重排序模型的 max_length 匹配。如果块较大,考虑使用支持更长输入的重排序模型,或在重排序前先做上下文压缩
-
生成阶段温度设置过高
- 问题:RAG 系统的目标是基于事实生成回答,高温度(>0.5)会增加创造性但也增加幻觉风险
- 正确做法:RAG 生成阶段使用低温度(0-0.2),确保回答紧贴检索到的上下文。只有在需要创意性总结时才适当提高温度
-
没有对流水线各阶段进行独立评估
- 问题:RAG 系统回答质量差时,无法定位是检索问题还是生成问题,盲目调参浪费时间
- 正确做法:分别评估检索质量(context precision、context recall)和生成质量(faithfulness、answer relevance),针对性优化瓶颈环节。详见 11f-RAG评估与优化
✅ 最佳实践
- 流水线各阶段独立可测试——每个阶段(解析、分块、嵌入、检索、重排序、生成)都应该可以独立运行和评估
- 从简单开始,逐步优化——先用递归分块 + 向量检索 + 直接生成跑通,再逐步加入混合搜索、重排序、上下文压缩
- 元数据是免费的性能提升——为每个文档块添加丰富的元数据(来源、日期、章节、作者),支持过滤检索
- 批量嵌入而非逐条调用——嵌入 API 支持批量输入,批量处理可减少 API 调用次数和延迟
- 定期重建索引——知识库更新后及时重建索引,避免检索到过时信息
相关资源与延伸阅读
- LlamaIndex 官方文档 — Ingestion Pipeline :LlamaIndex 数据摄入管线的完整文档,包含分块、嵌入、存储的详细配置
- LangChain — Text Splitters 文档 :LangChain 所有分块策略的详细说明和代码示例
- Firecrawl — Best Chunking Strategies for RAG in 2025 :2025 年分块策略的全面对比和基准测试
- Pinecone — Choosing an Embedding Model :嵌入模型选择的实用指南,包含 MTEB 基准分析
- Qdrant — Hybrid Search with Reranking Tutorial :Qdrant 混合搜索和 ColBERT 重排序的实战教程
- Cohere — Rerank API 文档 :Cohere 重排序 API 的官方文档和最佳实践
- Agenta — The Ultimate Guide to RAG Chunking Strategies :分块策略的深度指南,包含性能对比数据
- Production-Ready RAG Systems: End to End Guide :生产级 RAG 系统的端到端构建指南
参考来源
- The Complete Guide to RAG: Building Retrieval-Augmented Generation Systems 2026 — NerdLevelTech (2025-12)
- Building Production RAG Systems: Best Practices for 2026 — Michael John Peña (2025-12)
- Best Chunking Strategies for RAG in 2025 — Firecrawl (2025-06)
- RAG Chunking Strategies For Better Retrieval — CustomGPT (2025-06)
- Hybrid Search and Re-ranking — DasRoot (2025-12)
- Embedding Models Comparison 2026 — Reintech (2026-01)
- Production-Ready RAG Systems: End to End Guide — Saumil Srivastava (2025-05)
- Advanced RAG Techniques — DataRoot Labs (2025-10)
- RAG Implementation Guide 2025 — TensorBlue (2025-08)
- Hybrid Search with Re-ranking — dbi services (2025-06)
📖 返回 总览与导航 | 上一节:11a-RAG概念与架构 | 下一节:11c-向量数据库对比