第四章:RAG构建私有知识库

第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是更好的选择。

理由很简单:

  1. 知识更新成本:RAG更新知识只需要往向量数据库里塞新文档。微调呢?得重新训练,或者做增量微调,成本高得多
  2. 可解释性:RAG能告诉你答案来自哪份文档。微调是个黑盒,你不知道它从哪学的
  3. 防止幻觉: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是个好选择,原因:

  1. 开源,可以私有部署
  2. 中文效果接近OpenAI的付费API
  3. 社区活跃,有问题能找到答案

选模型时有个坑:嵌入模型的向量维度不是越高越好。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++库,速度快到离谱,但它是个库不是数据库,没有服务端,得自己封装。

我个人的选择标准:

  1. 能不能私有部署(数据不出境是硬要求)
  2. 社区活不活跃(出问题有人能问)
  3. 性能是不是够用(别过度设计)

代码示例------用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算法)反而更合适。

混合检索就是把这个两个结合起来

实现方式不难:

  1. 用BM25做关键词检索,返回Top-K
  2. 用向量搜索做语义检索,返回Top-K
  3. 把两个结果融合,重新排序

融合算法用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)**架构。它把查询和文档拼在一起,一起输入模型,模型直接输出一个相关性分数。精确,但慢。

所以实际用的是两阶段策略:

  1. 用嵌入模型(快)检索Top-100
  2. 用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可能生成:

  1. "大模型部署方法和工具"
  2. "vLLM部署大模型教程"
  3. "大模型推理服务部署"

分别对这三个查询检索,能覆盖更多相关文档。

4.3.4 多路召回

单一检索方法总有局限。实际生产环境里,我通常会配置多个检索通道:

  1. 向量检索(语义相似)
  2. BM25检索(关键词匹配)
  3. 元数据过滤(比如只搜某个部门的文档)
  4. 知识图谱检索(如果建了知识图谱的话)

然后把所有结果融合。融合算法除了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),能在检索时做很多有用的事:

  1. 过滤:只检索某个时间范围、某个部门的文档
  2. 排序:给某些来源的文档加权(比如官方文档比论坛帖子权重高)
  3. 可追溯:用户问"这个答案从哪来的",你能告诉他

常用的元数据字段:

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可以自动计算几个关键指标:

  1. Faithfulness(真实性):答案是否真的基于检索到的文档(检测幻觉)
  2. Answer Relevancy(答案相关性):答案是否回答了用户的问题
  3. Context Precision(上下文精确度):检索到的文档里,有多少是真正相关的
  4. 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个结果都是同一份文档的不同段落,信息冗余。

解决方案:

  1. **MMR(最大边际相关性)**算法:在相关性和多样性之间权衡
  2. 去重:如果多个结果来自同一份文档的相邻段落,只保留分数最高的那个

坑4:上下文太长,超过LLM输入限制

检索了10个相关文档,每个500字,加起来5000字。再加上系统提示和用户问题,可能超过某些模型的上下文窗口(虽然现在128k上下文的模型越来越多了)。

解决方案:

  1. 用长上下文模型(Claude 3支持200k)
  2. 对检索结果做压缩:用另一个小模型提取关键句子,再把关键句子喂给生成模型
  3. 限制检索数量:不是检索越多越好,通常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可见,财务的文档只对财务可见。

实现方式:

  1. 检索时过滤 :在查询向量数据库时,加上where={"allowed_roles": {"$in": [user.role]}}这样的过滤条件
  2. 文档级权限 :给每个文档块打上allowed_usersallowed_roles标签

但这样有个性能问题:过滤是在向量搜索之后做的还是之前做的?如果在之后做,可能过滤完就没几个结果了。最好是在向量索引里就支持元数据过滤(Milvus和Weaviate都支持)。

4.5.4 成本优化

RAG系统的成本主要来自三部分:

  1. 嵌入API调用:如果用OpenAI的text-embedding-3-small,每百万token约0.02美元。百万文档,每篇500字,大概50美元。不多,但如果是实时更新,长期累积也不少。

    • 优化:换开源模型(BGE),一次部署,长期免费
  2. 向量数据库托管:Pinecone按向量数收费。百万向量,每月大概70-100美元。

    • 优化:自托管开源方案(Milvus/Weaviate),只需要服务器成本
  3. LLM推理成本:每次查询都要调用一次LLM生成答案。如果用GPT-4,每次查询可能花0.01-0.05美元。

    • 优化:用更便宜的模型做生成(GPT-3.5或开源模型),把GPT-4留给真正复杂的查询

本章小结

核心要点

  1. RAG解决了大模型的两个硬伤:知识截止和领域知识缺失。它是企业落地大模型的首选技术。

  2. RAG的流程分三步:索引(把文档向量化存起来)→ 检索(根据用户查询找相关文档)→ 生成(基于检索到的文档回答)

  3. 检索优化是提升RAG效果的关键

    • 混合检索(BM25 + 向量搜索)提升召回率
    • Reranker提升精确度
    • 查询改写/扩展优化用户输入
  4. 工程化时注意分块策略、元数据设计、评估体系:这些决定了RAG系统在实际场景里的表现。

  5. 企业级部署要考虑:分布式向量数据库、实时更新、权限控制、成本优化。

思考题

题目1:你负责设计一个支持全公司1万名员工的RAG知识库系统。公司文档库有500万份文档(技术文档、HR政策、会议纪要等),每天新增约2000份。请设计系统架构,并说明:

  • 选用哪种向量数据库,为什么?
  • 如何处理权限控制(不同部门看到不同文档)?
  • 如何保证检索延迟在100ms以内?

思路提示

  1. 向量数据库选Milvus(分布式、支持元数据过滤、社区成熟)
  2. 权限控制:检索时根据用户角色过滤元数据,或者在文档向量里加入角色标签
  3. 延迟优化:HNSW索引、热点查询缓存、按部门分片(减少每次搜索的向量总数)

题目2:你们公司的RAG系统上线后,有用户反馈"检索到的文档和内容对不上"。比如用户搜"年假政策",检索到的是"报销政策"的文档。请分析可能的原因,并提出改进方案。
思路提示

可能原因

  1. 嵌入模型效果不好(中文场景用了英文模型)
  2. 分块太大,一块里包含太多不相关内容
  3. 没有用Reranker,检索结果精确度低

改进方案

  1. 换用中文优化的嵌入模型(BGE)
  2. 调整分块策略,按语义分块而不是硬切
  3. 加入Reranker对检索结果重排序
  4. 加入混合检索(BM25 + 向量搜索)

本章已生成完毕。请回复【继续生成第5章】或提出您对当前章节的疑问。