难度
进阶
从文档清洗、切块、向量检索、重排序到回答生成与评测闭环,完整搭建一个可落地的 RAG 系统。
难度
进阶
阅读时长
约 125 分钟
更新日期
2026/03/24
主题
RAG / 检索增强 / 向量检索 / 重排序
读完这篇教程后,你应该能:
如果你已经读过 RAG 综述,这篇文章会把论文里的方法图谱落到可执行的工程步骤上。
当团队第一次想把大模型接进业务时,最常见的诉求并不是“让模型变成一个更强的通才”,而是:
这正是 RAG 最适合介入的场景。它的核心思想不是把知识“塞进参数”,而是让模型在回答前先检索外部知识,再基于这些证据生成答案。
这会带来几个非常现实的好处:
但 RAG 也不是银弹。它解决的是“让模型拿到更相关外部信息”的问题,不直接解决推理能力不足、工具调用失败或业务流程编排等其他问题。
一个最小可用的 RAG 系统,通常可以拆成 7 个步骤:
如果你把这七步都建成清晰的模块,后续做性能和质量优化时就会容易很多。因为 RAG 系统里“答不好”的原因往往不止一个,而是不同环节叠加出来的。
先看一张分层图,会更容易把“离线建库”和“在线问答”区分开:
<text x="48" y="64" font-size="22" font-weight="700">离线知识库构建</text>
<text x="48" y="226" font-size="22" font-weight="700">在线问答执行</text>
<g>
<rect x="46" y="84" width="170" height="58" rx="16" fill="#e8f1ff" stroke="#98b7e1" />
<text x="131" y="118" text-anchor="middle" font-size="19" font-weight="700">文档采集</text>
</g>
<g>
<rect x="238" y="84" width="170" height="58" rx="16" fill="#eef6e8" stroke="#a8c48e" />
<text x="323" y="112" text-anchor="middle" font-size="19" font-weight="700">清洗标准化</text>
<text x="323" y="133" text-anchor="middle" font-size="13" fill="#4b5563">去噪 / 去重 / 标题层级</text>
</g>
<g>
<rect x="430" y="84" width="170" height="58" rx="16" fill="#fff4dc" stroke="#e2c36f" />
<text x="515" y="112" text-anchor="middle" font-size="19" font-weight="700">切块与元数据</text>
<text x="515" y="133" text-anchor="middle" font-size="13" fill="#4b5563">chunk / overlap / source</text>
</g>
<g>
<rect x="622" y="84" width="312" height="58" rx="16" fill="#f4ebff" stroke="#c7afe8" />
<text x="778" y="112" text-anchor="middle" font-size="19" font-weight="700">向量化与索引构建</text>
<text x="778" y="133" text-anchor="middle" font-size="13" fill="#4b5563">embedding model + vector index</text>
</g>
<line x1="216" y1="113" x2="238" y2="113" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<line x1="408" y1="113" x2="430" y2="113" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<line x1="600" y1="113" x2="622" y2="113" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<g>
<rect x="36" y="256" width="130" height="64" rx="16" fill="#e8f1ff" stroke="#98b7e1" />
<text x="101" y="293" text-anchor="middle" font-size="18" font-weight="700">用户问题</text>
</g>
<g>
<rect x="180" y="256" width="130" height="64" rx="16" fill="#fef3c7" stroke="#e2c36f" />
<text x="245" y="285" text-anchor="middle" font-size="18" font-weight="700">Query 改写</text>
<text x="245" y="306" text-anchor="middle" font-size="12" fill="#4b5563">可选,但常有效</text>
</g>
<g>
<rect x="324" y="256" width="130" height="64" rx="16" fill="#e0f2e5" stroke="#8bc09b" />
<text x="389" y="293" text-anchor="middle" font-size="18" font-weight="700">召回</text>
</g>
<g>
<rect x="468" y="256" width="130" height="64" rx="16" fill="#fce7ef" stroke="#e2a8bd" />
<text x="533" y="293" text-anchor="middle" font-size="18" font-weight="700">重排序</text>
</g>
<g>
<rect x="612" y="256" width="150" height="64" rx="16" fill="#ece8ff" stroke="#b5abef" />
<text x="687" y="285" text-anchor="middle" font-size="18" font-weight="700">上下文组装</text>
<text x="687" y="306" text-anchor="middle" font-size="12" fill="#4b5563">去重 / 排序 / 截断</text>
</g>
<g>
<rect x="776" y="256" width="168" height="64" rx="16" fill="#dff4f0" stroke="#8dc7bd" />
<text x="860" y="285" text-anchor="middle" font-size="18" font-weight="700">生成回答</text>
<text x="860" y="306" text-anchor="middle" font-size="12" fill="#4b5563">引用证据 / 输出答案</text>
</g>
<line x1="166" y1="288" x2="180" y2="288" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<line x1="310" y1="288" x2="324" y2="288" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<line x1="454" y1="288" x2="468" y2="288" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<line x1="598" y1="288" x2="612" y2="288" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<line x1="762" y1="288" x2="776" y2="288" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<line x1="778" y1="142" x2="389" y2="256" stroke="#5b6b7f" stroke-width="3" marker-end="url(#rag-arrow)" />
<text x="670" y="214" font-size="14" fill="#5b6b7f">索引提供候选片段</text>
</g>
很多 RAG 原型失败,是因为第一步就太粗糙。真实文档通常包含:
如果这些噪声直接进入索引,就会带来两个问题:
因此,文档进入知识库前至少要做:
RAG 的第一层质量控制,不是在召回阶段,而是在数据入口。
切块是 RAG 系统里最容易被低估的一步。过大的 chunk 会把不相关信息绑在一起,过小的 chunk 又会让语义断裂。
一个实用的判断框架是:
实际设计时,你通常要同时决定:
chunk_sizechunk_overlap经验上,最稳定的方式不是“固定按字数硬切”,而是“先尽量保持语义边界,再做长度约束”。
RAG 常被理解成“文本向量检索”,但实际高质量系统几乎都会依赖元数据过滤。常见元数据包括:
元数据的作用不只是给前端展示来源,它还会直接影响检索策略:
如果没有元数据,很多检索错误你几乎无从修正。
第一次做 RAG 时,不必把精力全部花在数据库选型上,但你至少要知道不同方案的侧重点:
例如 FAISS 一类方案,优点是简单、快、无外部依赖,特别适合先验证召回逻辑。
例如 pgvector 这类直接融入已有数据库生态的方案,适合中小规模系统、简化运维。
例如专门的向量数据库或检索服务,更适合高并发、多集合、多租户或复杂过滤需求。
真正的选择标准通常不是“谁最流行”,而是:
很多人第一次做 RAG,会直接把问题编码成向量,然后做相似度搜索。这当然可以作为起点,但它并不总是最优。
常见召回策略大致有四类:
适合语义匹配、问法改写、同义表达等场景。
适合术语、编号、命令、产品名、法规条款等高度词面敏感的查询。
把关键词召回和向量召回结合,通常会更稳,尤其适合企业知识库这类查询分布复杂的场景。
先让模型改写问题、补充同义词或拆成多个子查询,再做召回。这种方式对复杂问题尤其有效,但也会增加延迟和系统复杂度。
RAG 的召回层最值得记住的一句话是:
不同问题的最佳召回方式不一样,统一一种检索很容易把系统做窄。
召回拿回来的 top-k 文档,并不等于最终最适合进入模型上下文的 top-k 文档。因为召回阶段往往更偏高覆盖率,而不是最终排序精度。
这时重排序器的价值就出来了。它通常会对“问题 + 候选 chunk”做更细的相关性判断,把真正最相关的片段排到前面。
重排序能解决的典型问题包括:
对 RAG 系统来说,重排序往往是比“换一个更大模型”更划算的质量提升点。
很多 RAG 初版系统都有一个习惯动作:检索出前 5 或前 10 个 chunk,然后直接拼接给模型。这样做的问题是:
这和 Lost in the Middle 讨论的现象高度相关。长上下文不等于高利用率,证据排布方式会显著影响模型是否真的用到它。
更合理的上下文组装通常会考虑:
RAG 系统做得好不好,常常取决于“给模型看什么、按什么顺序看”,而不是只看检索分数。
下面给一份足够说明流程的最小 Python 风格示例。它不追求工业级完备,但能帮助你把“切块 -> 向量化 -> 检索 -> 组装 prompt”串起来。
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
chunks = [
{"id": "doc1#0", "text": "FlashAttention 通过分块和在线 softmax 降低 attention 的 IO 开销。", "source": "paper-a"},
{"id": "doc2#0", "text": "PagedAttention 通过分页式管理 KV Cache 减少显存碎片。", "source": "paper-b"},
{"id": "doc3#0", "text": "RAG 系统通常包含召回、重排序和上下文组装三个关键环节。", "source": "internal-note"},
]
embedder = SentenceTransformer("BAAI/bge-small-zh-v1.5")
embeddings = embedder.encode(
[item["text"] for item in chunks],
normalize_embeddings=True,
).astype("float32")
index = faiss.IndexFlatIP(embeddings.shape[1])
index.add(embeddings)
def retrieve(query, top_k=3):
q = embedder.encode([query], normalize_embeddings=True).astype("float32")
scores, ids = index.search(q, top_k)
return [chunks[i] for i in ids[0]]
query = "什么是 RAG 里的重排序,它解决什么问题?"
results = retrieve(query, top_k=2)
context = "\n\n".join(
f"[来源: {item['source']}]\n{item['text']}"
for item in results
)
prompt = f"""请基于给定资料回答问题。
资料:
{context}
问题:{query}
要求:
1. 只基于资料回答
2. 若资料不足,请明确说明
3. 输出时保留来源说明
"""
这段代码的重点不是具体模型名,而是你已经能看见完整主线:
RAG 的提示词通常比普通问答更强调边界和证据。一个更稳的模板,通常要明确写出:
你可以把它理解成:RAG prompt 的目标不是“让模型更会说”,而是“让模型更少乱补”。
如果你准备进一步系统化这部分提示词设计,可以继续看 Prompt Engineering 系统指南。
RAG 评测不能只看最终答案质量,因为错误可能来自不同层。一个更完整的评测框架通常至少包含:
如果你把这些问题全混成“用户感觉好不好”,你很难知道优化应该落在哪一层。
当 RAG 效果不佳时,建议按下面顺序排查:
这个顺序很重要,因为很多团队会直接把问题归咎于“模型不够大”,而实际上召回层就已经错了。
如果问题需要复杂规划、工具操作或多步决策,单纯检索文档并不能解决全部问题。
企业场景里,权限、时间和文档类型往往同样关键。
检索更多文档不一定更准,反而可能让模型更难抓住关键证据。
没有固定问题集和失败样本库,你很难判断一次调整到底是真的改进,还是只是偶然。
先看原理,再到模拟器里调参验证,学习效果更稳定。