RAG(检索增强生成):AI 如何结合外部知识库回答问题
LLM 是"大脑",RAG 是给它配了一个"外挂硬盘"。
一、为什么需要 RAG?
1.1 LLM 与生俱来的三个硬伤
| 硬伤 | 表现 | 举例 |
|---|---|---|
| 知识截止 | 训练数据有截止日期,不知道新信息 | “最新的 iOS 版本是什么?” → 胡编 |
| 幻觉 | 不知道的事会自信地编造 | “请引用《XX 法》第 3 条” → 编了一条不存在的 |
| 无法访问私域数据 | 只学过公开数据,不懂公司内部文档 | “我们的退款政策是什么?” → 编造政策 |
1.2 RAG 的核心思想
不修改模型本身,而是让模型在回答前"先去查资料"。
传统 LLM: 用户提问 → 模型凭记忆回答(可能忘、可能编)RAG: 用户提问 → 先去知识库搜索相关文档 → 把文档和问题一起给模型 → 模型基于文档回答
就像一个开卷考试:书(知识库)是外部提供的,学生(LLM)负责理解和作答。只要书是对的,答案就不会太离谱。
二、RAG 的完整架构
┌─────────────────────────────────────────────────────┐│ 离线阶段(建库) ││ ││ 文档 → 切块 → Embedding → 向量数据库 ││ (PDF/网页/数据库) (chunks) (向量) (存储+检索) ││ │└─────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────┐│ 在线阶段(查询) ││ ││ 用户提问 → 向量化 → 检索 Top-K 相关文档块 ││ ↓ ││ Prompt 拼接: [检索到的文档] + [用户问题] ││ ↓ ││ 发给 LLM → 基于文档生成答案 → 返回给用户 ││ │└─────────────────────────────────────────────────────┘
三、分步详解
3.1 文档预处理与切块(Chunking)
RAG 的第一道关卡:把一个长文档切成小块。
为什么不能直接用整个文档?
- LLM 有上下文长度限制(虽然现在 128K 已很普遍,但不能浪费)
- 文档太长 → 检索精度下降(大海捞针)
- 不相关内容占用 Token → 成本浪费
切块策略——这是 RAG 最重要的工程决策之一:
原始文档(5000 字) ↓策略选择 ↓┌──────────────────────────────────────────────────┐│ 策略 1:固定大小切块 ││ 每块 512 字,无重叠 ││ 简单但容易把一句话切两半,丢失语义 │├──────────────────────────────────────────────────┤│ 策略 2:滑动窗口重叠切块 ││ 每块 512 字,前后各重叠 50 字 ││ 保证连贯性,但增加存储量 │├──────────────────────────────────────────────────┤│ 策略 3:语义切块 ││ 按段落/句子边界切,保证每块是一个完整语义单元 ││ 效果最好,但需要 NLP 分句工具或模型辅助 │├──────────────────────────────────────────────────┤│ 策略 4:层级切块 ││ 小块的 parent 指针指向大块 ││ 检索时先找小块,返回时带上父块上下文 │└──────────────────────────────────────────────────┘
最佳实践:
| 文档类型 | 建议策略 | 块大小 |
|---|---|---|
| 技术文档 | 固定大小 + 语义边界 | 256-512 token |
| 法律合同 | 语义切块 | 200-400 token |
| 项目代码 | 按函数/类边界 | 按需 |
| 长篇论文 | 层级切块 | 小 256 + 大 1024 |
3.2 Embedding:把文本变成向量
不是随便什么向量都行,必须让"语义相近的文本向量距离也近"。
"狗" 和 "猫" 的向量距离:近(都是宠物)"狗" 和 "宪法" 的向量距离:远(完全不相关)
常用 Embedding 模型:
| 模型 | 维度 | 特点 |
|---|---|---|
| OpenAI text-embedding-3-small | 1536 | 性价比高,多语言 |
| OpenAI text-embedding-3-large | 3072 | 精度最高 |
| BGE-M3 | 1024 | 开源,多语言,支持稀疏+稠密混合检索 |
| Jina Embeddings v2 | 768 | 长文本(8K token)支持好 |
| Cohere Embed v3 | 1024 | 多语言,分类/检索双模式 |
生成 Embedding 的注意事项:
- 输入文本不要直接裸传,加上 prefix(如 BGE 模型要求加
"为这个句子生成表示:...") - 中文和多语言场景优先选多语言模型
- 向量维度越高信息越丰富,但检索越慢,存储越大
3.3 向量数据库:存什么、怎么查
向量数据库的核心能力:在百万、亿级向量中,毫秒级找到最相似的几个。
主流选型:
| 数据库 | 类型 | 适用场景 |
|---|---|---|
| Milvus | 分布式向量库 | 亿级以上,企业级 |
| Pinecone | 托管服务 | 不想自建,海外场景 |
| Weaviate | 开源 + 云 | 插件生态丰富 |
| Qdrant | Rust 高性能 | 轻量、数据处理灵活 |
| Chroma | 轻量嵌入式 | 原型开发、本地 RAG |
| pgvector | PostgreSQL 插件 | 已有 PG 的项目,最简单的集成 |
| Elasticsearch | 搜索引擎 + 向量 | 需要混合检索(关键词 + 语义) |
衡量标准:
- QPS(每秒查询数)
- 召回延迟(P99 < 100ms)
- 召回率(Recall@K,K=5 时是否召回正确答案)
3.4 检索:找到最相关的文档块
这步是 RAG 的核心——检索质量直接决定回答质量。
基础检索
用户问题 → Embedding → 向量相似度搜索 → 返回 Top-K 文档
混合检索(Hybrid Search)
纯向量检索有时会"近视"——找出一堆语义相似但不直接相关的文档。混合检索融合关键词匹配(BM25)+ 语义搜索:
用户问题:"Python 3.12 中的 dict 类型有什么新特性?"BM25 匹配到:"Python 3.12 release notes dict changes" ← 精确关键词语义匹配到:"字典合并操作符在最新 Python 版本中的使用指南" ← 语义相关混合排序 → 取出最靠谱的几个
重排序(Re-ranking)
Top-K 初筛后(比如 K=20),用更强的模型重新精排,再取 Top-3 喂给 LLM:
粗排: 向量检索 → 快速,精度一般 → 返回 20 个精排: Cross-Encoder → 慢但精准 → 对 20 个重新打分 → 取 Top 3
常用 Reranker:Cohere Rerank、BGE-Reranker、Cross-Encoder(cross-encoder/ms-marco-MiniLM-L-6-v2)。
3.5 生成:拼接 Prompt 并调用 LLM
检索到文档后,用以下模板拼接:
markdown请仅根据以下提供的参考文档回答问题。如果参考文档中不包含相关信息,请直接说"根据已有资料无法回答"。参考文档:---[文档 1]{retrieved_chunk_1}[文档 2]{retrieved_chunk_2}---问题:{user_question}请遵循以下原则:1. 回答必须有引用标注,格式为 [来源: 文档N]2. 不要编造参考文档中不存在的信息3. 如果信息不完整,明确提出"文档未覆盖的部分"
关键设计点:
- 引用标注:让用户知道答案从哪来的,建立信任
- "不知道"边界:强制模型承认知识盲区,防止幻觉
- 角色约束:如需要可以加 System Prompt 限定专业角色
四、RAG 的高级技巧
4.1 查询重写(Query Rewriting)
用户提问往往不够"检索友好":
用户原问题:"上次那个 bug 修好了吗?" ← "那个"是什么?无法检索重写后:"支付模块订单超时未取消的 bug 修复状态"
可以用 LLM 自动重写:
- 把口语变书面语
- 把指代消解(“那个”→具体是什么)
- 根据对话历史补全上下文
- 拆解复合问题成多个子查询
4.2 父文档检索(Parent Document Retrieval)
存储时: 小块(用于检索,精确命中) 大块(作为上下文补充,防止信息断裂)检索时: 找到小块 → 顺便返回它的父文档 → 给 LLM 更完整的上下文
4.3 多跳检索(Multi-Hop Retrieval)
有些问题需要"查询 A 的结果 → 再查询 B"才能回答。
问:"我们公司 CEO 是谁?他的上一家公司叫什么?"Hop 1: 检索"公司 CEO" → 得到"张三"Hop 2: 检索"张三 上一家公司" → 得到"某某科技"
实现方式:迭代 LLM → 检索 → LLM → 检索,直到信息足够。
4.4 摘要索引(Summary Index)
大块文档先生成摘要,检索时先匹配摘要,命中再取原文:
文档 → 每段生成摘要 → 摘要 Embedding查询 → 匹配摘要 → 命中的取原始文档内容 → 回答
4.5 时间衰减加权
知识有时效性,旧文档权重应降低:
相关性得分 = 语义相似度 × e^(-λ × 距今天数)
五、评估 RAG 系统
光看回答"像不像人话"不够,需要量化指标:
| 指标 | 含义 | 计算方式 |
|---|---|---|
| 命中率(Hit Rate) | 正确答案是否出现在检索结果中 | 检索结果包含答案 ÷ 总查询数 |
| MRR | 正确答案在检索结果中的排名倒数 | 1/排名 的平均值 |
| 忠实度(Faithfulness) | 回答是来自检索文档还是编的 | 用 LLM 检查回答是否有文档支撑 |
| 答案相关性 | 回答是否真的回应了问题 | 人工或 LLM 评分 |
| 上下文相关性 | 检索到的文档是否相关 | 信号噪声比 |
| 延迟 | 检索 + 生成的总时间 | P50/P95/P99 毫秒 |
| Token 消耗 | 每次查询消耗的 Token | 成本核算 |
推荐开源评估工具:
- Ragas:最流行的 RAG 评估框架,一行代码出报告
- DeepEval:支持单元测试风格的 pipeline 评估
- TruLens:可视化追踪 + 评估
六、RAG vs 微调 vs 长上下文
RAG 不是唯一解法,三种方案各有适用场景:
| 方案 | 原理 | 何时用 |
|---|---|---|
| RAG | 检索外部文档注入 Prompt | 知识经常更新;私域数据量大;需要引用来源 |
| 微调 | 用私域数据继续训练模型 | 风格/格式没法学;领域术语需要内化;数据稳定 |
| 长上下文 | 直接塞整个文档进去 | 文档短(< 50K);一次性任务;延迟不敏感 |
三者可以组合:用 RAG 检索相关内容 + 用长上下文模型避免切块丢失 + 用微调保证特定风格。
七、开源 RAG 技术栈速览
数据加载: Unstructured, LlamaIndex Reader, LangChain Document Loader文档切块: LangChain TextSplitter, LlamaIndex NodeParserEmbedding: BGE-M3, Jina, text-embedding-3向量库: Chroma(原型), Qdrant(生产), pgvector(已有 PG)编排框架: LangChain, LlamaIndex, Haystack评估: Ragas, DeepEval一站式: Dify, FastGPT(国内友好)
八、RAG 的常见坑
| 坑 | 原因 | 解法 |
|---|---|---|
| 检索到不相关的文档 | Embedding 不够好 / 切块不当 | 换模型、加混合检索、加 Reranker |
| 检索不到正确的文档 | 同义词、语言差异 | 查询重写、HyDE(假设性文档 Embedding) |
| 答案丢失上下文 | 切块把关键信息打断了 | 父文档检索、加大块尺寸 |
| 答案前后矛盾 | 多个检索结果互相冲突 | 给 LLM 加冲突处理指令 |
| 速度和成本爆炸 | 每步都调 LLM | 缓存 Embedding、预计算、减少搜索范围 |
RAG 本质是做了一件事:让 LLM 从"闭卷考试"变成"开卷考试"。卷子(知识库)是你给的,答案准确性的责任就回到了你这边。
延伸阅读:如果 RAG 是你的主要场景,推荐深入学习 LlamaIndex 或 LangChain 的 RAG 模块。从 Chroma + BGE-M3 开始动手,30 分钟就能跑通第一个原型。





