RAG实战:基于向量检索构建企业级知识库问答系统

2026-04-05 · 阅读约14分钟

大模型的知识有截止日期,且无法直接获取你的私有数据。RAG(Retrieval-Augmented Generation,检索增强生成)解决的就是这个问题:让大模型基于你提供的知识库来回答问题,而不是凭"记忆"。

这篇文章从工程实践角度,完整拆解RAG系统的每一个环节:文档处理、向量化、检索、重排序、生成。附核心代码和踩坑经验。

一、RAG系统架构全景

一个完整的RAG系统包含两条链路:

【离线索引链路】
原始文档 → 文档解析 → 文本分块 → Embedding向量化 → 存入向量数据库

【在线查询链路】
用户提问 → 问题Embedding → 向量相似度检索 → Rerank重排序 → 构造Prompt → 大模型生成回答

二、文档解析与预处理

企业知识库的文档格式五花八门:PDF、Word、Excel、PPT、HTML、Markdown。第一步是把它们统一转为纯文本。

常用解析工具

格式推荐工具注意事项
PDFPyMuPDF / pdfplumber扫描件PDF需要OCR预处理
Wordpython-docx注意表格和嵌入图片的处理
HTMLBeautifulSoup / trafilatura去除导航、页脚等噪音
Markdown直接处理保留标题层级作为元数据
import fitz  # PyMuPDF

def parse_pdf(file_path):
    """解析PDF文件,提取文本和元数据"""
    doc = fitz.open(file_path)
    pages = []
    for i, page in enumerate(doc):
        text = page.get_text("text")
        if text.strip():
            pages.append({
                "content": text.strip(),
                "metadata": {
                    "source": file_path,
                    "page": i + 1,
                    "total_pages": len(doc)
                }
            })
    return pages

三、文本分块(Chunking)

分块是RAG效果的关键影响因素。太大的块包含太多无关信息,太小的块丢失上下文。

分块策略对比

策略原理适用场景
固定长度按字符/Token数切割结构不明确的长文本
递归分割按段落→句子→字符逐级切割通用场景(推荐默认)
语义分块用Embedding检测语义断点对精度要求高的场景
按文档结构按标题/章节切割结构化文档(手册、规范)

推荐参数

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 每块约500字符(中文约250字)
    chunk_overlap=80,      # 块间重叠80字符,保留上下文连续性
    separators=["\n\n", "\n", "。", ";", ",", " "],  # 中文优先分割符
    length_function=len
)

chunks = splitter.split_documents(documents)
chunk_overlap非常重要。如果没有重叠,一句话被切成两半,两个块各拿到半句,检索时都不完整。80-100字符的overlap是一个比较好的平衡点。

四、Embedding向量化

将文本转为高维向量,使语义相近的文本在向量空间中距离更近。

Embedding模型选型

模型维度中文效果特点
BGE-large-zh1024优秀开源、中文专优化、MTEB排行榜前列
BGE-M31024优秀多语言、支持稀疏+稠密混合检索
通义千问Embedding1536优秀API调用、无需本地部署
GTE-large-zh1024良好阿里开源、推理速度快
from sentence_transformers import SentenceTransformer

# 加载BGE模型
model = SentenceTransformer("BAAI/bge-large-zh-v1.5")

# 对查询需要加前缀(BGE特有)
query_embedding = model.encode("为用户解答:什么是RAG?")
doc_embeddings = model.encode(["文档块1的内容", "文档块2的内容"])
BGE模型在编码查询时需要添加"为用户解答:"前缀,而编码文档时不需要。这是训练时的设计,不加前缀会降低检索效果。不同模型的前缀要求不同,使用前一定要查文档。

五、向量数据库选型与使用

主流向量数据库对比

数据库类型适用规模特点
Milvus独立部署百万-十亿级高性能、支持多种索引、国产
Chroma嵌入式万-百万级极简API、开发调试方便
FAISS百万级Meta开源、纯内存、速度极快
Qdrant独立部署百万级Rust编写、过滤能力强

Milvus实战

from pymilvus import Collection, CollectionSchema, FieldSchema, DataType, connections

# 连接Milvus
connections.connect(host="localhost", port="19530")

# 定义Schema
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=2000),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1024),
    FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=256),
]
schema = CollectionSchema(fields, description="知识库文档集合")
collection = Collection("knowledge_base", schema)

# 创建索引(IVF_FLAT适合百万级数据)
index_params = {
    "index_type": "IVF_FLAT",
    "metric_type": "COSINE",
    "params": {"nlist": 128}
}
collection.create_index("embedding", index_params)

# 插入数据
collection.insert([contents, embeddings, sources])

# 检索
collection.load()
results = collection.search(
    data=[query_embedding],
    anns_field="embedding",
    param={"metric_type": "COSINE", "params": {"nprobe": 16}},
    limit=10,
    output_fields=["content", "source"]
)

六、Rerank重排序

向量检索返回的Top-K结果,排序并不总是准确。Rerank用一个交叉编码器对"查询-文档"对进行精排,显著提升结果质量。

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-large")

# 对检索结果重新打分
pairs = [(query, doc["content"]) for doc in search_results]
scores = reranker.predict(pairs)

# 按分数重排
reranked = sorted(zip(search_results, scores), key=lambda x: x[1], reverse=True)
top_docs = [doc for doc, score in reranked[:5]]
Rerank的计算成本比Embedding高很多(交叉编码器需要对每个Query-Doc对做完整推理),所以通常是先用向量检索召回Top-20,再用Rerank精排到Top-5。不要对全库做Rerank。

七、Prompt构造与生成

最终把检索到的文档作为上下文,构造Prompt让大模型生成回答:

def build_rag_prompt(query, docs):
    context = "\n\n---\n\n".join([
        f"【来源:{doc['source']}】\n{doc['content']}"
        for doc in docs
    ])

    return f"""基于以下参考资料回答用户的问题。如果参考资料中没有相关信息,请明确说明"根据现有资料无法回答",不要编造。

## 参考资料
{context}

## 用户问题
{query}

## 回答要求
- 基于参考资料回答,引用具体来源
- 如果多个资料有矛盾,指出差异
- 语言简洁,重点突出"""

调用大模型

import dashscope
from dashscope import Generation

response = Generation.call(
    model="qwen-plus",
    messages=[
        {"role": "system", "content": "你是一个企业知识库助手。"},
        {"role": "user", "content": build_rag_prompt(query, top_docs)}
    ],
    temperature=0.1,  # RAG场景用低温度,减少发散
    max_tokens=2000
)
answer = response.output.text

八、效果优化的关键指标

九、常见踩坑与优化

  1. 中文分块不要用英文分隔符:默认的RecursiveCharacterTextSplitter的分隔符是英文标点,中文文本要自定义
  2. PDF表格数据会丢失结构:表格内容被解析为平铺文本后语义丢失,建议用专门的表格解析工具
  3. 重复文档要去重:同一份文档被多次导入会导致检索结果重复,浪费上下文窗口
  4. Embedding模型和Reranker模型建议配套:BGE-large-zh + BGE-reranker-large是验证过的组合
  5. 生产环境Milvus建议用IVF_PQ索引:比IVF_FLAT省内存,大规模数据下性价比更高
← 返回文章列表