前言

你有没有遇到过这种情况:大模型一本正经地胡说八道,引用了根本不存在的论文,或者把三年前的过时信息当成最新答案输出?

这就是幻觉(Hallucination)——大模型最让人头疼的问题之一。而 RAG(Retrieval-Augmented Generation,检索增强生成)正是目前业界解决这个问题最主流、最实用的方案。

但 RAG 远不只是「搜一下然后喂给模型」这么简单。一个生产级的 RAG 系统,涉及文档解析、向量化、索引构建、混合检索、重排序、上下文压缩等一系列工程问题。本文就来拆解这些环节,聊聊怎么把 RAG 从 demo 做到能扛住真实业务。

RAG系统架构全景图


一、RAG 的核心原理:先查后答

RAG 的思路其实很朴素——别让模型凭记忆回答,先去知识库里查资料,再基于查到的内容生成答案

用户提问 → 检索相关文档片段 → 拼接成 Prompt → 大模型生成回答

听起来简单?但魔鬼全在细节里。

1.1 为什么不能只靠模型的参数知识?

问题 参数知识(纯LLM) RAG
知识时效性 训练数据截止,过时 实时检索,随时更新
幻觉风险 高(编造不存在的事实) 低(有据可查)
可溯源性 黑盒,无法追溯 可标注来源文档
领域知识 通用,专业深度有限 接入专业知识库后深度定制
成本 每次推理都消耗模型参数 检索+小模型也能出好结果

一句话总结:RAG 让模型从「靠记忆答题」变成了「开卷考试」


二、文档处理:垃圾进,垃圾出

RAG 系统的效果,70% 取决于文档处理的质量。这一步做不好,后面再怎么优化检索和模型都是白搭。

2.1 文档解析

真实业务中的文档格式五花八门:PDF、Word、PPT、Excel、Markdown、网页、甚至扫描件。每种格式的解析都有坑:

# 文档解析的典型流程
from unstructured.partition.auto import partition

# 自动识别文档类型并解析
elements = partition(filename="technical_report.pdf")

# 关键:保留文档结构信息
for elem in elements:
    print(f"类型: {elem.category}")  # Title, NarrativeText, Table, Image...
    print(f"内容: {elem.text}")
    print(f"元数据: {elem.metadata}")

常见坑:

  • PDF 中的表格被解析成乱码 → 用 camelotpdfplumber 专门处理表格
  • 扫描件 PDF 没有文字层 → 先 OCR 再解析
  • PPT 中的图表信息丢失 → 截图 + 多模态模型理解

2.2 文档切分(Chunking)

这是最容易被低估、也最容易做错的环节。切分粒度直接决定检索质量:

切分策略 优点 缺点 适用场景
固定长度(如512 token) 实现简单 可能切断语义 快速原型
按段落/章节 保留语义完整性 块大小不均匀 结构化文档
递归字符切分 兼顾长度和语义 需要调参 通用场景
语义切分 语义边界最准确 计算成本高 高质量要求
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 推荐:递归切分 + 重叠窗口
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 每块500字符
    chunk_overlap=50,      # 前后重叠50字符,避免语义断裂
    separators=["\n\n", "\n", "。", "!", "?", ";", " "]
)

chunks = splitter.split_text(document_text)

经验法则: chunk_size 设为 300~800 字符,overlap 设为 chunk_size 的 10%~20%。太小会丢失上下文,太大会稀释相关性。


三、向量化:把文字变成数学

文档切好了,下一步是把文本块转换成向量(Embedding),这样就能用数学方法计算相似度。

3.1 Embedding 模型选择

2026 年主流的 Embedding 模型对比:

模型 维度 特点 推荐场景
OpenAI text-embedding-3-large 3072 综合能力强 通用场景
BGE-M3 1024 多语言,开源 中文为主
Cohere embed-v4 1024 多语言,检索优化 多语言场景
Jina-embeddings-v3 1024 长文本支持好 长文档
from sentence_transformers import SentenceTransformer

# 使用 BGE-M3(中文场景推荐)
model = SentenceTransformer('BAAI/bge-m3')

# 批量编码
embeddings = model.encode(
    chunks,
    batch_size=32,
    show_progress_bar=True,
    normalize_embeddings=True  # 归一化,方便余弦相似度计算
)

print(f"向量维度: {embeddings.shape}")  # (n_chunks, 1024)

3.2 向量数据库选型

数据库 特点 适用规模
Milvus 分布式,功能全面 大规模生产
Qdrant Rust 实现,性能优秀 中大规模
Chroma 轻量,适合原型 小规模/开发
Weaviate 内置向量化,GraphQL API 中规模
FAISS Meta 开源,纯库 嵌入到应用中

我的建议: 原型阶段用 Chroma 或 FAISS,生产环境用 Milvus 或 Qdrant。别一上来就搞分布式,先把检索效果跑通再说。

向量数据库与知识检索


四、检索策略:混合召回才是王道

只用向量检索?那你可能错过了 30% 的相关文档。生产级 RAG 通常采用混合检索(Hybrid Search)

4.1 向量检索 vs 关键词检索

维度 向量检索 关键词检索(BM25)
语义理解 ✅ 强(同义词、近义词也能匹配) ❌ 弱(必须包含关键词)
精确匹配 ❌ 弱(可能返回语义相近但不精确的结果) ✅ 强(专有名词、编号)
计算成本 高(需要预先计算向量) 低(倒排索引)

实际场景举例:

  • 用户问「MySQL 的 MVCC 是怎么实现的?」→ 向量检索能找到「多版本并发控制」相关文档
  • 用户问「ERR-1042 这个报错怎么解决?」→ 关键词检索更靠谱,向量检索可能匹配到完全不相关的错误码

4.2 混合检索实现

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma

# 向量检索器
vectorstore = Chroma(embedding_function=embedding_model)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

# BM25 关键词检索器
bm25_retriever = BM25Retriever.from_texts(texts, k=10)

# 混合检索:向量 0.6 + BM25 0.4
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.6, 0.4]
)

results = ensemble_retriever.invoke("MVCC 实现原理")

4.3 重排序(Reranking)

混合召回后,通常会拿到 10~20 个候选文档。但这些文档的质量参差不齐,需要重排序模型来精排:

from sentence_transformers import CrossEncoder

# 使用交叉编码器重排序
reranker = CrossEncoder('BAAI/bge-reranker-v2-m3')

# 计算 query-doc 对的相关性分数
pairs = [(query, doc.page_content) for doc in retrieved_docs]
scores = reranker.predict(pairs)

# 按分数排序,取 top 5
ranked = sorted(zip(retrieved_docs, scores), key=lambda x: x[1], reverse=True)
top_docs = [doc for doc, score in ranked[:5]]

重排序的效果很显著——通常能把最终答案的准确率提升 10%~20%。唯一的代价是多了一次模型推理,延迟增加 50~200ms,但对大多数场景来说完全值得。


五、Prompt 工程:把检索结果喂好

检索到了好文档,但如果 Prompt 写得不好,模型照样可能忽略关键信息。

5.1 RAG Prompt 模板

RAG_PROMPT = """你是一个专业的技术问答助手。请基于以下参考资料回答用户的问题。

## 规则
1. 只基于提供的参考资料回答,不要编造信息
2. 如果参考资料中没有相关内容,直接说"根据现有资料无法回答"
3. 回答时标注信息来源(如:根据[文档1])
4. 保持回答简洁专业

## 参考资料
{context}

## 用户问题
{question}

## 回答
"""

5.2 上下文窗口管理

别把所有检索到的文档都塞进去。要控制总 token 数:

def build_context(docs, max_tokens=3000):
    context = ""
    for i, doc in enumerate(docs):
        chunk = f"[文档{i+1}] {doc.page_content}\n来源: {doc.metadata['source']}\n\n"
        if count_tokens(context + chunk) > max_tokens:
            break
        context += chunk
    return context

六、避坑指南:那些年我踩过的 RAG 的坑

坑 1:切分太碎,丢失上下文

把一篇技术文档按 200 字符切分,结果「分布式事务的 ACID 特性」被切成了两半,前半段只有「分布式事务的」,检索到了也没意义。

解法: chunk_size 不要低于 300 字符,加上 overlap。

坑 2:Embedding 模型和查询不匹配

文档用中文写的,Embedding 模型却用的英文模型。检索效果一塌糊涂。

解法: 中文场景用 BGE 系列或 M3E,别用纯英文模型。

坑 3:只做了向量检索,漏掉了精确匹配

用户搜「ERR-1042 错误码」,向量检索返回了一堆「数据库错误」相关的文档,但就是没有包含「1042」的那篇。

解法: 混合检索,BM25 + 向量,缺一不可。

坑 4:没有做结果验证

检索到了过时的文档,模型基于过时信息生成了错误答案。

解法: 给文档加上时间戳元数据,检索时优先返回最新的文档;或者在 Prompt 中要求模型检查信息时效性。


七、性能优化:让 RAG 跑得更快

当文档量达到百万级,检索延迟就成了瓶颈。几个实用的优化手段:

优化手段 效果 复杂度
HNSW 索引 检索速度提升 10x
量化(PQ/SQ) 内存占用降低 4~8x
分片 + 缓存 热门查询延迟 <50ms
异步检索 并发吞吐量提升 3x
多级索引 先粗筛再精排
# HNSW 索引配置示例(Milvus)
index_params = {
    "metric_type": "COSINE",
    "index_type": "HNSW",
    "params": {
        "M": 16,              # 每个节点的连接数
        "efConstruction": 200  # 构建时的搜索范围
    }
}

写在最后

RAG 不是一个「接上就能用」的银弹。它更像是一个系统工程——从文档处理到向量化,从检索策略到 Prompt 设计,每个环节都需要根据业务场景调优。

但好在,这个领域的工具链已经相当成熟了。LangChain、LlamaIndex、Dify 这些框架把大部分脏活累活都封装好了,你真正需要花心思的,是理解自己的业务场景,然后选择合适的策略组合。

记住一句话:RAG 的效果 = 文档质量 × 检索策略 × Prompt 工程。三者缺一不可,但文档质量永远是第一位的。别急着上花哨的检索算法,先把你的知识库整理干净再说。

有问题欢迎在评论区讨论,尤其是踩过坑的朋友们,分享出来让大家少走弯路。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐