第4章 RAG构建私有知识库
4.1 RAG核心原理与价值
4.1.1 RAG是什么,以及为什么你需要它
大模型有个硬伤:它的知识停在训练截止日。GPT-4的知识截止到2023年底,Claude、Llama同理。你问它昨天发布的iPhone 16规格,它只能猜。
企业场景里这个问题更严重。公司内部的API文档、产品手册、会议纪要、代码规范------这些东西大模型根本没见过。你让模型回答"我们公司的报销流程是什么",它只能瞎编。
RAG(Retrieval-Augmented Generation,检索增强生成)就是用来解决这个问题的。核心思路特别简单:回答问题时,先去知识库里查资料,再把查到的资料交给大模型,让它基于资料来回答。
类比一下:传统大模型像个闭卷考试的学生,只能靠记忆答题。RAG像个开卷考试的学生,可以翻书,但翻书的速度和准确性决定了成绩。
我用RAG这个词,但它本质上就是**"搜索 + 生成"**两件事的拼接。这个想法最早是Meta在2020年的一篇论文里提出的,但真正火起来是2023年,因为大家发现:用RAG给大模型外挂知识库,比微调模型便宜太多,效果还好。
4.1.2 RAG vs 微调:你应该选哪个
这是企业落地大模型时最常见的问题。我直接给结论:
绝大多数场景,RAG是更好的选择。
理由很简单:
- 知识更新成本:RAG更新知识只需要往向量数据库里塞新文档。微调呢?得重新训练,或者做增量微调,成本高得多
- 可解释性:RAG能告诉你答案来自哪份文档。微调是个黑盒,你不知道它从哪学的
- 防止幻觉:RAG强制模型基于检索到的文档回答,大幅减少胡编乱造。微调没法保证这点
但微调也不是没用。如果你的需求是改变模型的说话风格 (比如让它回答更像你们的客服话术),或者优化特定任务的推理能力(比如医疗诊断),那微调是必要的。
我见过的成功案例,基本都是RAG + 微调组合:
- RAG负责注入知识
- 微调负责调整回答风格和格式
4.1.3 RAG的三个阶段:索引、检索、生成
RAG的流程可以拆成三个阶段。我用一个具体例子来说明。
假设你建了一个技术文档知识库,用户问:"怎么用vLLM部署大模型?"
阶段1:索引(Indexing)--- 把文档存起来
这一步是离线做的,不是每次查询都跑。
流程:
原始文档 → 分块 → 向量化 → 存入向量数据库
分块很重要。如果你把整个100页的PDF作为一个块,检索时要么全匹配要么全不匹配,太粗糙。通常的做法是切成256-512个token的块,块之间留一点重叠(比如20个token),避免切断语义。
向量化用的是嵌入模型(Embedding Model)。它会把文本变成一串数字(向量),语义相近的文本,向量也在高维空间里离得近。
存入向量数据库后,每个文档块长这样:
python
{
"id": "doc_123",
"content": "vLLM支持PagedAttention技术,可以显著提升GPU利用率...",
"embedding": [0.123, -0.456, 0.789, ...], # 1536维向量
"metadata": {
"source": "vllm_deployment_guide.md",
"created_at": "2024-03-15"
}
}
阶段2:检索(Retrieval)--- 找相关文档
用户提问后:
"怎么用vLLM部署大模型?"
↓
用同一个嵌入模型把问题变成向量
↓
在向量数据库里找和最相似的K个文档块(余弦相似度)
↓
返回Top-K结果
余弦相似度的公式:
similarity = A ⋅ B ∥ A ∥ ∥ B ∥ \text{similarity} = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\| \|\mathbf{B}\|} similarity=∥A∥∥B∥A⋅B
值越接近1,说明两个向量越相似。
阶段3:生成(Generation)--- 基于上下文回答
把检索到的文档块拼进Prompt,交给大模型:
系统提示:请基于参考文档回答问题。如果文档里没有,就说不知道。
参考文档:
1. vLLM支持PagedAttention技术,可以显著提升GPU利用率...
2. 部署vLLM需要Python 3.8+和CUDA 11.8+...
...
用户问题:怎么用vLLM部署大模型?
回答:
这个阶段的坑是上下文窗口限制。如果你检索了10个文档块,每个500字,加起来可能超过大模型的输入限制(虽然现在128k上下文的模型越来越多了)。
4.1.4 企业里RAG能干什么
我列几个我看过的实际案例:
智能客服:某电商把售后政策、产品FAQ、历史工单全部向量化。客服机器人回答准确率从52%提升到87%,人工介入率降了60%。
内部知识库:某互联网公司的内网文档、技术博客、会议纪要全部接入RAG。新员工入职后可以直接问"咱们的代码规范是什么",不用再到处找文档。
代码辅助:把公司的私有代码库、API文档、最佳实践案例做成知识库。开发者写代码时,IDE插件可以实时推荐公司内部的最佳实践。
合规审查:金融机构用RAG比对合同文本和监管条款。系统自动标注哪些条款可能不合规,审查时间从每单2小时缩短到15分钟。
这些场景有个共同点:知识在持续更新,而且需要回答时有依据。这就是RAG的用武之地。
4.2 向量数据库与嵌入模型
4.2.1 嵌入模型怎么选
嵌入模型决定了RAG系统的检索精度。它把文本变成向量,向量质量直接决定了"找得到找不到"。
我实测过十几个嵌入模型,给你一个实用建议:
中文场景,直接用BGE或者M3E。 别纠结了。
| 模型 | 维度 | 适合场景 | 备注 |
|---|---|---|---|
| BGE-large-zh-v1.5 | 1024 | 中文通用 | 智源发布,开源,效果接近OpenAI |
| M3E-base | 768 | 中文轻量 | 速度快,资源占用小 |
| text-embedding-3-small | 1536 | 快速验证 | OpenAI的,要API key,有出海合规风险 |
| GTE-large-zh | 1024 | 中文通用 | 阿里达摩院发布,效果不错 |
BGE是个好选择,原因:
- 开源,可以私有部署
- 中文效果接近OpenAI的付费API
- 社区活跃,有问题能找到答案
选模型时有个坑:嵌入模型的向量维度不是越高越好。1536维比768维精度高,但存储和检索成本翻倍。对企业知识库来说,1024维通常够用了。
代码示例------用BGE模型生成向量:
python
"""
用BGE模型把文本转成向量
"""
from sentence_transformers import SentenceTransformer
import numpy as np
# 加载模型(第一次会下载,大概400MB)
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
# 准备文本
texts = [
"大模型部署推荐使用vLLM框架",
"vLLM支持PagedAttention技术",
"今天天气不错"
]
# 编码(返回numpy数组)
embeddings = model.encode(texts, normalize_embeddings=True)
print(f"向量形状: {embeddings.shape}")
# 输出: (3, 1024)
# 计算相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
print(f"前两个文本的相似度: {similarity:.4f}")
# 应该接近1,因为都是说vLLM的
normalize_embeddings=True这个参数很重要。归一化后,向量的点积等于余弦相似度,计算更快。
4.2.2 向量数据库选型
向量数据库就是专门存向量、做相似度搜索的数据库。传统MySQL也能存向量,但搜索时要暴力遍历所有行,百万级数据就慢得不行。
我按使用场景给建议:
如果你要快速验证想法 → 用Chroma。它轻量,pip install就能用,数据存本地文件。缺点是规模大了性能下降,适合百万向量以下的场景。
如果你要上线生产环境 → 用Milvus或者Weaviate。
- Milvus是国产的,性能好,支持分布式,某公司用它管了20亿条向量
- Weaviate功能全,支持多模态(图片+文本),社区活跃
如果你在海外,不介意数据出境 → 用Pinecone。完全托管的SaaS,不用管运维,但按向量数收费,大规模用挺贵的。
如果你追求极致性能,而且数据量不大(千万级以下) → 用FAISS。这是Facebook发的C++库,速度快到离谱,但它是个库不是数据库,没有服务端,得自己封装。
我个人的选择标准:
- 能不能私有部署(数据不出境是硬要求)
- 社区活不活跃(出问题有人能问)
- 性能是不是够用(别过度设计)
代码示例------用Chroma建个本地向量库:
python
"""
用Chroma构建本地向量数据库
适合快速原型和中小规模应用
"""
import chromadb
from sentence_transformers import SentenceTransformer
# 初始化Chroma(数据持久化到本地)
client = chromadb.PersistentClient(path="./vector_db")
collection = client.get_or_create_collection("enterprise_knowledge")
# 嵌入模型
embed_model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
# 准备知识库文档
documents = [
"公司年假政策:入职满1年有10天年假,满3年有15天",
"技术栈:后端用Python Django,前端用React,数据库PostgreSQL",
"报销流程:费用发生后30天内提交,附发票原件",
"vLLM部署要求:Python 3.8+,CUDA 11.8+,最少16GB显存",
"深度学习框架推荐PyTorch,动态图调试方便"
]
# 生成向量并存入
embeddings = embed_model.encode(documents, normalize_embeddings=True)
collection.add(
embeddings=embeddings.tolist(),
documents=documents,
metadatas=[{"source": f"doc_{i}"} for i in range(len(documents))],
ids=[f"id_{i}" for i in range(len(documents))]
)
print(f"存入了 {collection.count()} 条文档")
# 搜索
query = "怎么部署大模型"
query_embedding = embed_model.encode([query], normalize_embeddings=True)
results = collection.query(
query_embeddings=query_embedding.tolist(),
n_results=2
)
print(f"\n查询: {query}")
for i, doc in enumerate(results['documents'][0]):
print(f"{i+1}. {doc}")
4.2.3 向量索引和搜索算法
当向量数量到百万级,暴力搜索(把查询向量和所有库向量都比一遍)就太慢了。这时候需要向量索引。
最常用的算法是HNSW(Hierarchical Navigable Small World)。
它的思路类似于"六度分隔"理论:如果所有人都在一个六度分隔的网络里,你可以很快找到任意一个人。HNSW构建了一个多层的图,上层是"高速公路",底层是精细连接。搜索时先在上层快速定位大致区域,再在下层精细查找。
优点:速度快,召回率高(能找到真正最相似的向量)
缺点:建索引慢,占内存
FAISS库里HNSW的实现,我在本地机器上测过:100万条向量,查询延迟大概2-3毫秒。如果是暴力搜索,可能要200-300毫秒。
代码示例------用FAISS建索引:
python
"""
用FAISS构建高效的向量索引
适合对性能要求高的场景
"""
import faiss
import numpy as np
# 生成测试数据:100万条128维向量
dimension = 128
n_vectors = 1000000
np.random.seed(42)
vectors = np.random.randn(n_vectors, dimension).astype('float32')
# 归一化
faiss.normalize_L2(vectors)
# 构建HNSW索引
index = faiss.IndexHNSWFlat(dimension, 32) # 32是M参数,控制精度和速度的平衡
index.add(vectors)
print(f"索引了 {index.ntotal} 条向量")
# 搜索测试
query = np.random.randn(1, dimension).astype('float32')
faiss.normalize_L2(query)
import time
start = time.time()
distances, indices = index.search(query, k=5)
end = time.time()
print(f"搜索耗时: {(end-start)*1000:.2f} ms")
print(f"最相似的5个向量索引: {indices[0]}")
4.2.4 向量数据库的部署
生产环境部署向量数据库,有几个事得考虑:
高可用:别让向量数据库成了单点故障。Milvus和Weaviate都支持集群部署,至少部署两个副本。
监控:盯住查询延迟(P95、P99)、内存使用、磁盘占用。向量数据库很吃内存,一个1536维的向量占6KB,1亿条就是600GB。
备份:向量可以重新生成(虽然慢),但文档的元数据可能丢了就找不回来。定期备份元数据。
4.3 高级检索技术
4.3.1 混合检索:关键词 + 语义
纯粹用向量搜索有个问题:它太"智能"了。
比如用户搜"BMW X5",向量搜索可能返回"宝马X5"、"宝马SUV"相关的结果。但如果用户就是想精确匹配"BMW X5"这个型号名,关键词搜索(BM25算法)反而更合适。
混合检索就是把这个两个结合起来。
实现方式不难:
- 用BM25做关键词检索,返回Top-K
- 用向量搜索做语义检索,返回Top-K
- 把两个结果融合,重新排序
融合算法用RRF(Reciprocal Rank Fusion) :
score ( d ) = ∑ r ∈ { bm25 , vector } 1 k + rank r ( d ) \text{score}(d) = \sum_{r \in \{\text{bm25}, \text{vector}\}} \frac{1}{k + \text{rank}_r(d)} score(d)=r∈{bm25,vector}∑k+rankr(d)1
k k k是个常数,通常取60。思路是:一个文档在两个检索结果里都排名靠前,那它的最终排名就高。
我实际测过,混合检索的召回率比单纯向量搜索高15-20%,尤其是专有名词、产品型号这类查询。
代码示例------实现混合检索:
python
"""
混合检索:结合BM25关键词检索和向量语义检索
"""
from rank_bm25 import BM25Okapi
import numpy as np
from sentence_transformers import SentenceTransformer
class HybridRetriever:
def __init__(self, documents):
self.documents = documents
# 初始化BM25(关键词检索)
tokenized_docs = [list(doc) for doc in documents] # 简化版分词
self.bm25 = BM25Okapi(tokenized_docs)
# 初始化向量检索
self.embed_model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
self.doc_embeddings = self.embed_model.encode(documents, normalize_embeddings=True)
def search(self, query, top_k=5, alpha=0.5):
"""
alpha: 权重,0=只用BM25,1=只用向量检索
"""
# BM25搜索
tokenized_query = list(query)
bm25_scores = self.bm25.get_scores(tokenized_query)
bm25_top_k = np.argsort(bm25_scores)[-top_k:][::-1]
# 向量搜索
query_embedding = self.embed_model.encode([query], normalize_embeddings=True)
similarities = np.dot(self.doc_embeddings, query_embedding[0])
vector_top_k = np.argsort(similarities)[-top_k:][::-1]
# 融合(简化版:按文档ID去重,按相似度加权)
results = {}
for idx in bm25_top_k:
results[idx] = results.get(idx, 0) + (1 - alpha) * bm25_scores[idx]
for idx in vector_top_k:
results[idx] = results.get(idx, 0) + alpha * similarities[idx] * 100 # 缩放一下
# 排序
sorted_results = sorted(results.items(), key=lambda x: x[1], reverse=True)
return [(idx, score, self.documents[idx]) for idx, score in sorted_results[:top_k]]
# 测试
docs = [
"BMW X5的油耗是百公里10L",
"宝马X5是一款中大型SUV",
"奔驰GLE的竞品包括宝马X5",
"电动车特斯拉Model Y的续航是500km"
]
retriever = HybridRetriever(docs)
results = retriever.search("BMW X5", top_k=3)
print("混合检索结果:")
for idx, score, doc in results:
print(f" 分数: {score:.2f} - {doc}")
4.3.2 Reranker:对检索结果重新排序
向量搜索用的嵌入模型,为了速度,模型比较小,精度有限。检索返回的前5个结果,不一定真的和查询最相关。
Reranker就是用来解决这个问题。
和嵌入模型不同,Reranker用的是**交叉编码器(Cross-Encoder)**架构。它把查询和文档拼在一起,一起输入模型,模型直接输出一个相关性分数。精确,但慢。
所以实际用的是两阶段策略:
- 用嵌入模型(快)检索Top-100
- 用Reranker(精确)对这100个重新排序,取Top-5
我测过,加Reranker能把检索精度(Recall@5)从82%提升到91%。代价是每次查询多花200-300ms(取决于模型大小)。
推荐的中文Reranker模型:BAAI/bge-reranker-large
代码示例------用Reranker重排序:
python
"""
用Reranker对检索结果重新排序
"""
from sentence_transformers import CrossEncoder
class Reranker:
def __init__(self, model_name='BAAI/bge-reranker-large'):
print(f"加载Reranker模型: {model_name}")
self.model = CrossEncoder(model_name)
def rerank(self, query, documents, top_k=5):
"""
对文档列表重新排序
"""
# 构建query-document对
pairs = [[query, doc] for doc in documents]
# 预测相关性分数
scores = self.model.predict(pairs)
# 按分数排序
scored_docs = list(zip(documents, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
return scored_docs[:top_k]
# 测试
reranker = Reranker()
query = "如何部署大模型"
candidate_docs = [
"大模型训练需要大量GPU", # 相关度低
"vLLM部署大模型教程", # 高度相关
"Python编程入门", # 不相关
"vLLM支持PagedAttention" # 高度相关
]
reranked = reranker.rerank(query, candidate_docs, top_k=3)
print(f"查询: {query}")
print("Reranker重排序结果:")
for doc, score in reranked:
print(f" 分数: {score:.4f} - {doc}")
输出里,"vLLM部署大模型教程"和"vLLM支持PagedAttention"会排到前两位,即使它们在原始列表里不是最靠前的。
4.3.3 查询改写和查询扩展
用户查询经常很短、很模糊。比如用户搜"大模型",他可能是想了解大模型的基本概念,也可能是想找大模型部署方案。
查询改写就是用LLM把用户的短查询改写成更完整、更清晰的查询。
查询扩展是生成多个相关查询,分别检索,然后合并结果。这样能提高召回率。
我在一个项目里实测过,加查询改写能把"用户满意率"从76%提升到84%。代价是每个查询多调用一次LLM(多花200-500ms和一点API费用)。
代码示例------查询改写:
python
"""
用LLM改写用户查询,提升检索效果
"""
import openai
def rewrite_query(original_query, context=None):
"""
把用户的简短查询改写成更适合检索的版本
"""
prompt = f"""用户查询: {original_query}
请把这个查询改写成一个更完整、更清晰的版本,适合用来搜索技术文档。
只输出改写后的查询,不要解释。"""
response = openai.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
rewritten = response.choices[0].message.content.strip()
return rewritten
# 测试
original = "大模型"
rewritten = rewrite_query(original)
print(f"原始: {original}")
print(f"改写后: {rewritten}")
# 可能输出: "大语言模型(LLM)的基本原理、应用场景和部署方法"
查询扩展的思路类似:让LLM生成3-5个相关查询,分别检索,然后合并结果、去重。
比如查询"如何部署大模型",LLM可能生成:
- "大模型部署方法和工具"
- "vLLM部署大模型教程"
- "大模型推理服务部署"
分别对这三个查询检索,能覆盖更多相关文档。
4.3.4 多路召回
单一检索方法总有局限。实际生产环境里,我通常会配置多个检索通道:
- 向量检索(语义相似)
- BM25检索(关键词匹配)
- 元数据过滤(比如只搜某个部门的文档)
- 知识图谱检索(如果建了知识图谱的话)
然后把所有结果融合。融合算法除了RRF,还可以用学习排序(Learning to Rank)------用历史点击数据训练一个排序模型,效果比固定规则好。
4.4 RAG工程化实战
4.4.1 文档分块策略
分块(Chunking)是RAG系统里最容易被忽视、但对效果影响最大的环节之一。
块太大:检索不精确(块里包含太多不相关内容)
块太小:语义不完整(一个句子被切成两半)
我的经验值:
- 通用文档:每块300-500字(约200-400个token)
- 技术文档(含代码):每块500-800字,保持代码块完整
- FAQ:每个Q&A作为一个块
块之间留重叠(Overlap)。比如块大小是500字,重叠50字。这样避免一个完整的句子被切成两半后,前半句在块A、后半句在块B,检索时只命中一个块,信息不完整。
代码实现------按语义分块(不是硬切):
python
"""
智能文档分块:保持语义完整
"""
import re
def chunk_by_semantics(text, max_chunk_size=500, overlap=50):
"""
按段落和句子边界分块,而不是硬切
"""
# 先按双换行分割段落
paragraphs = re.split(r'\n\s*\n', text)
chunks = []
current_chunk = ""
for para in paragraphs:
para = para.strip()
if not para:
continue
# 如果当前块+这个新段落不超过限制,合并
if len(current_chunk) + len(para) <= max_chunk_size:
current_chunk += para + "\n\n"
else:
# 保存当前块
if current_chunk.strip():
chunks.append(current_chunk.strip())
# 开始新块(带重叠)
# 取当前块的最后overlap个字符作为新块的开头
if len(current_chunk) > overlap:
current_chunk = current_chunk[-overlap:] + para + "\n\n"
else:
current_chunk = para + "\n\n"
# 保存最后一个块
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks
# 测试
doc = """# vLLM部署指南
## 环境要求
Python 3.8+, CUDA 11.8+, 最少16GB显存。
## 安装步骤
pip install vllm
## 启动推理服务
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2-7B-Instruct \
--port 8000
"""
chunks = chunk_by_semantics(doc, max_chunk_size=200, overlap=30)
print(f"分成了 {len(chunks)} 个块")
for i, chunk in enumerate(chunks):
print(f"\n块 {i+1} (长度: {len(chunk)}字):")
print(chunk[:100] + "...")
4.4.2 元数据增强
给文档块加元数据(Metadata),能在检索时做很多有用的事:
- 过滤:只检索某个时间范围、某个部门的文档
- 排序:给某些来源的文档加权(比如官方文档比论坛帖子权重高)
- 可追溯:用户问"这个答案从哪来的",你能告诉他
常用的元数据字段:
python
{
"source": "vllm_docs.md", # 来源文件名
"created_at": "2024-03-15", # 创建时间
"department": "技术部", # 所属部门
"tags": ["部署", "vLLM", "推理"], # 标签
"version": "1.2" # 文档版本
}
在检索时用元数据过滤:
python
# Chroma支持的过滤语法
results = collection.query(
query_texts=["vLLM部署"],
n_results=5,
where={"department": "技术部"} # 只检索技术部的文档
)
4.4.3 RAG评估
怎么知道你的RAG系统效果好不好?不能只靠"感觉"。
开源评估框架RAGAS可以自动计算几个关键指标:
- Faithfulness(真实性):答案是否真的基于检索到的文档(检测幻觉)
- Answer Relevancy(答案相关性):答案是否回答了用户的问题
- Context Precision(上下文精确度):检索到的文档里,有多少是真正相关的
- Context Recall(上下文召回率):应该检索到的文档,有多少被检索到了
用RAGAS评估:
python
"""
用RAGAS框架评估RAG系统
"""
# pip install ragas
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
# 准备评估数据
eval_data = {
"question": ["什么是RAG", "怎么部署大模型"],
"answer": [
"RAG是检索增强生成,通过检索外部知识增强大模型",
"可以用vLLM部署大模型,支持高吞吐量推理"
],
"contexts": [
["RAG(Retrieval-Augmented Generation)是一种结合检索和生成的技术..."],
["vLLM是高效的大模型部署框架..."]
]
}
# 评估
results = evaluate(eval_data, metrics=[faithfulness, answer_relevancy, context_precision])
print(results)
但评估框架只能给你参考。真正重要的指标是用户满意度------用户是不是真的找到了想要的答。
我建议做A/B测试:一部分用户用旧系统,一部分用新系统,比较满意度调查得分。
4.4.4 避坑指南
我踩过的坑,你别再踩了。
坑1:分块大小一刀切
不同文档类型适合不同的分块大小。技术文档(含代码)需要大一点的块(500-800字),因为代码被切断了就没法理解了。FAQ适合小一点的块(200-300字),因为每个Q&A是自包含的。
解决方案:根据文档类型动态调整分块参数。或者,用递归分块策略------先按章节切,章节太长再按段落切,段落还太长再按句子切。
坑2:嵌入模型和领域不匹配
通用嵌入模型(哪怕是OpenAI的)在法律、医疗等专业领域效果会下降。因为专业术语和表达方式在训练数据里不多。
解决方案:用领域预训练的嵌入模型。比如法律领域用Legal-BERT的嵌入版本,医疗用BioBERT。或者,收集你领域的语料,对通用嵌入模型做继续预训练(这个成本高,不到万不得已别做)。
坑3:检索结果缺乏多样性
有时候Top-5结果都来自同一个文档。用户问"大模型部署方法",返回的5个结果都是同一份文档的不同段落,信息冗余。
解决方案:
- **MMR(最大边际相关性)**算法:在相关性和多样性之间权衡
- 去重:如果多个结果来自同一份文档的相邻段落,只保留分数最高的那个
坑4:上下文太长,超过LLM输入限制
检索了10个相关文档,每个500字,加起来5000字。再加上系统提示和用户问题,可能超过某些模型的上下文窗口(虽然现在128k上下文的模型越来越多了)。
解决方案:
- 用长上下文模型(Claude 3支持200k)
- 对检索结果做压缩:用另一个小模型提取关键句子,再把关键句子喂给生成模型
- 限制检索数量:不是检索越多越好,通常Top-3到Top-5就够了
4.5 企业级RAG最佳实践
4.5.1 大规模文档库的索引优化
当文档库到千万级,一个向量数据库实例可能扛不住。需要分布式部署。
Milvus和Weaviate都支持分布式。核心思路是数据分片(Sharding):把向量数据分散到多个节点上,查询时并行搜索多个节点,然后合并结果。
分片策略:
- 按文档ID哈希分片:简单,但可能导致数据倾斜
- 按时间分片:新文档放新节点,旧文档放旧节点。适合有时效性的场景(用户更可能搜新文档)
- 按部门/类别分片:元数据过滤时可以快速定位到特定分片
4.5.2 实时更新
企业知识库每天都在更新。你不能每天半夜重新建索引(虽然可以,但慢)。
解决方案:增量更新。
向量数据库都支持增量添加。当有新文档时,只对新文档做分块、向量化,然后插入向量数据库。删除文档时,根据文档ID从向量数据库里删对应的向量。
但有个问题:文档更新了怎么办?比如一份技术文档v1.0更新到v1.1。你得先删旧的向量,再插新的向量。建议在元数据里加version字段,更新时先查source + version,删旧的,插新的。
4.5.3 权限控制
不是所有文档都应该对所有人可见。HR的文档只对HR可见,财务的文档只对财务可见。
实现方式:
- 检索时过滤 :在查询向量数据库时,加上
where={"allowed_roles": {"$in": [user.role]}}这样的过滤条件 - 文档级权限 :给每个文档块打上
allowed_users或allowed_roles标签
但这样有个性能问题:过滤是在向量搜索之后做的还是之前做的?如果在之后做,可能过滤完就没几个结果了。最好是在向量索引里就支持元数据过滤(Milvus和Weaviate都支持)。
4.5.4 成本优化
RAG系统的成本主要来自三部分:
-
嵌入API调用:如果用OpenAI的text-embedding-3-small,每百万token约0.02美元。百万文档,每篇500字,大概50美元。不多,但如果是实时更新,长期累积也不少。
- 优化:换开源模型(BGE),一次部署,长期免费
-
向量数据库托管:Pinecone按向量数收费。百万向量,每月大概70-100美元。
- 优化:自托管开源方案(Milvus/Weaviate),只需要服务器成本
-
LLM推理成本:每次查询都要调用一次LLM生成答案。如果用GPT-4,每次查询可能花0.01-0.05美元。
- 优化:用更便宜的模型做生成(GPT-3.5或开源模型),把GPT-4留给真正复杂的查询
本章小结
核心要点
-
RAG解决了大模型的两个硬伤:知识截止和领域知识缺失。它是企业落地大模型的首选技术。
-
RAG的流程分三步:索引(把文档向量化存起来)→ 检索(根据用户查询找相关文档)→ 生成(基于检索到的文档回答)
-
检索优化是提升RAG效果的关键:
- 混合检索(BM25 + 向量搜索)提升召回率
- Reranker提升精确度
- 查询改写/扩展优化用户输入
-
工程化时注意分块策略、元数据设计、评估体系:这些决定了RAG系统在实际场景里的表现。
-
企业级部署要考虑:分布式向量数据库、实时更新、权限控制、成本优化。
思考题
题目1:你负责设计一个支持全公司1万名员工的RAG知识库系统。公司文档库有500万份文档(技术文档、HR政策、会议纪要等),每天新增约2000份。请设计系统架构,并说明:
- 选用哪种向量数据库,为什么?
- 如何处理权限控制(不同部门看到不同文档)?
- 如何保证检索延迟在100ms以内?
思路提示
- 向量数据库选Milvus(分布式、支持元数据过滤、社区成熟)
- 权限控制:检索时根据用户角色过滤元数据,或者在文档向量里加入角色标签
- 延迟优化:HNSW索引、热点查询缓存、按部门分片(减少每次搜索的向量总数)
题目2:你们公司的RAG系统上线后,有用户反馈"检索到的文档和内容对不上"。比如用户搜"年假政策",检索到的是"报销政策"的文档。请分析可能的原因,并提出改进方案。
思路提示
可能原因:
- 嵌入模型效果不好(中文场景用了英文模型)
- 分块太大,一块里包含太多不相关内容
- 没有用Reranker,检索结果精确度低
改进方案:
- 换用中文优化的嵌入模型(BGE)
- 调整分块策略,按语义分块而不是硬切
- 加入Reranker对检索结果重排序
- 加入混合检索(BM25 + 向量搜索)
本章已生成完毕。请回复【继续生成第5章】或提出您对当前章节的疑问。