检索增强生成(RAG)是让 LLM 在你自己的数据上回答问题的办法——私有文档、知识库、上周的工单——而无需重新训练它。思路很简单:在调用模型之前,先检索出最相关的文本塞进 prompt,让答案基于真实来源,而不是模型那份冻结又模糊的记忆。但"简单"背后藏着一个真正的分布式系统:一条把文档变成可搜索向量索引的离线管线,以及一条在线管线——每个查询都要向量化、检索、重排、拼 prompt、生成带引用的答案——还要又快、又新、又便宜。本文把两端都设计一遍,以及面试官会追问的取舍。

⚡ 快速要点
  • RAG 是两条管线: 离线索引管线(加载 → 分块 → 向量化 → 入索引)和在线查询管线(向量化 → 检索 → 重排 → 生成)。分开设计。
  • 分块是最高杠杆的旋钮。 太大检索噪声多;太小丢上下文。按结构切、加重叠,并保留指回原文的指针。
  • 向量索引用近似最近邻(ANN) —— HNSW 或 IVF —— 用一点召回率换取在百万级向量上的次线性搜索。
  • 混合检索胜过纯向量。 把语义(向量)和关键词(BM25)结合、融合分数;再加元数据过滤做租户与新鲜度。
  • 对候选做重排。 用 cross-encoder 把 top ~100 重排成真正塞进 prompt 的 top ~5 —— 最便宜的大质量提升。
  • 生成必须有据且带引用。 指示模型只用检索到的上下文作答并引用切片,这样能展示来源、也能抓幻觉。
  • 质量靠测量,不靠假设。 持续评测检索(recall@k)和答案的忠实度/相关性 —— "RAG 三元组"。
tldr

离线:加载文档、切成带重叠的块、给每块向量化、把向量连同元数据 upsert 进 ANN 索引。在线:把查询向量化,做混合(向量 + 关键词)检索 + 元数据过滤,用 cross-encoder 重排候选,把 top 块拼成有据 prompt,让 LLM 带引用作答。难点全在分块、混合 + 重排质量、新鲜度/增量索引,以及评测"检索到底有没有帮上忙"。

索引管线 · 离线 查询管线 · 在线 来源(PDF·网页·DB·文档) 加载与解析 分块按结构切 · 带重叠 向量化模型(批量)块 → 向量 Upsert 向量 + 元数据 向量库 ANN 索引(HNSW / IVF) + 元数据过滤存储 + BM25 关键词索引 用户查询 查询向量化 混合检索(top-k)向量 + BM25 · 过滤 重排(cross-encoder) 拼 prompt(+ 块) LLM → 有据答案带引用 upsert top-k
RAG = 共享一个索引的两条管线 —— 离线把文档分块、向量化、upsert;在线把查询向量化、混合检索、重排,并带引用有据作答

第 1 步 —— 收敛需求

界定范围:我们是在一个语料(支持文档、内部 wiki、合同)上做问答助手吗?语料多大、要多新、是否多租户?一个聚焦集合:

功能需求

非功能需求

面试提示

开篇就定调:RAG 不改模型,改的是 prompt。 每个设计决定都是为了把对的块塞进有限的上下文窗口——所以你真正在工程化的是检索质量,而不是 LLM。

第 2 步 —— 两条管线

最清晰的心智模型——也是面试官想要的结构——是两条在向量索引处交汇的独立管线。索引管线离线(并增量)运行,把文档变成可搜索的向量。查询管线在线、每个请求运行一次。把它们解耦,意味着你可以重建索引、换向量化模型、重新分块,而不碰服务路径;也能按各自的特征扩缩(索引是吞吐受限、批量的;查询是延迟受限的)。

第 3 步 —— 摄入与分块

索引管线先把异构来源加载并解析成干净文本(以及结构——标题、表格、页码——作为元数据保留)。接着就是 RAG 里最关键的一个选择:分块

为什么分块决定质量

你向量化并检索的是而不是整篇文档,因为一个 embedding 把一段文本压成一个向量——段越长,向量越模糊。块太大,查询会匹配到一个泛泛相关的页面、把噪声拖进来;块太小,块就失去成其意义所需的上下文。最佳点通常是几百个 token,按自然结构切(段落、标题、小节)而不是盲目定长,并在块之间留一点重叠,这样跨边界的句子不会成为孤儿。

分块 → 向量化 → upsert(索引)
for doc in source.stream():
    text   = parse(doc)
    chunks = split(text, size=400, overlap=60, on="headings")
    vecs   = embedder.embed(chunks)          # 在 GPU/加速器上批量
    index.upsert([{
        "id": hash(doc.id, i), "vector": v,
        "text": c, "doc_id": doc.id, "tenant": doc.tenant,
        "updated_at": doc.updated_at        # 元数据 = 过滤 + 引用
    } for i,(c,v) in enumerate(zip(chunks, vecs))])

注意每个向量旁边存了什么:原始文本(以便塞进 prompt 并引用)和元数据(租户、文档 id、时间戳)用于过滤和新鲜度。向量化模型的选择也很重要——选定一个就别动,因为换模型意味着对整个语料重新向量化(向量必须在同一空间才能比较)。

第 4 步 —— 向量索引

检索就是 embedding 空间里的最近邻搜索:找到与查询向量(按余弦相似度)最近的块向量。对上亿向量做精确搜索太慢,所以生产系统用近似最近邻(ANN)索引,用一丝召回率换取次线性查询时间。

ANN 索引取舍
HNSW(图)召回/延迟极佳、查询快;内存更高、构建更慢。常见默认。
IVF / IVF-PQ先聚类再搜几个簇;PQ 压缩向量以省内存、代价是一点召回。适合超大语料。
Flat(精确)完美召回、暴力搜——中小规模可以,扛不住 1 亿+。

无论你用专门的向量库(Pinecone、Weaviate、Qdrant、Milvus)还是 Postgres 上的 pgvector,扩缩关注点一样:向量很大、索引常常常驻内存,所以要按文档/租户分片到多节点、并做副本以保证可用性和读吞吐。一次查询扇出到各分片再合并 top 结果。估算经验:一个 768 维 float32 向量约 3KB,所以 1 亿块 ≈ 索引开销之前就有约 300GB 原始向量——这正是大规模下量化(PQ、int8)重要的原因。

第 5 步 —— 检索:走混合

纯向量搜索擅长语义匹配("怎么重置密码"能找到"账号恢复步骤"),但对精确词项很弱——错误码、SKU、生僻名——这些恰恰是 embedding 会糊掉的关键 token。解法是混合检索:并行跑向量搜索经典关键词搜索(BM25),再融合排名(如 Reciprocal Rank Fusion)。也在这里施加元数据过滤——租户与访问控制(为正确性,绝不能省)、加上日期或文档类型约束。

查询 向量搜索ANN · 语义 关键词(BM25)精确词项 融合(RRF)约 100 候选 重排→ top 5
混合检索 —— 向量(语义)与 BM25(精确词)结果融合成约 100 个候选,再由 cross-encoder 重排成进入 prompt 的约 5 个块

第 6 步 —— 重排

检索为召回优化:撒大网,便宜地拉回约 100 个候选块。但你只能负担把少数几个放进 prompt,而这几个的顺序/精度决定答案质量。所以加一道重排:用 cross-encoder 模型对每个(查询,块)对联合打分——比一阶段检索用的 bi-encoder embedding 准得多,但贵到没法跑全语料,这正是它只在约 100 个候选上跑的原因。这种"宽检索、窄重排"的两段式,是 RAG 里最便宜的大质量提升,也是面试里值得主动提的点。

第 7 步 —— 有据生成

现在拼 prompt:用户的问题、重排后的 top 块(每块标上来源 id),以及一条指令——只用提供的上下文作答并引用用到的块,如果上下文不足就说不知道。这种"有据"把 LLM 从自信的瞎猜变成有出处的助手,而引用让用户能核实、也让你能度量忠实度。

一个有据 prompt
# system
只用下面的上下文作答。来源用 [n] 引用。
如果上下文里没有答案,就说你不知道。

# context  (重排后的 top 块)
[1] (doc: billing-faq#refunds) "退款在 5–7 天内到账……"
[2] (doc: policy-v3#cancel)     "取消请进 设置 → 账单……"

# user
退款要多久?怎么取消?

这里有两个预算决定:放几个块(更多上下文可能有帮助,但增加 token、延迟,以及模型"迷失在长上下文中间"的风险),以及检索很弱时怎么办——如果 top 分数都很低,返回"没找到好答案"比逼模型瞎编要好。

第 8 步 —— 新鲜度与增量索引

语料从不静止,而每晚全量重建既慢又陈旧。服务路径需要一个持续更新的索引,所以把摄入接到变更事件上:文档创建/更新/删除发出一个事件(来自源库的 CDC 或 webhook),走同一条 分块 → 向量化 → upsert 路径,删除就移除该文档的块。结果是一个几分钟内最终一致的索引。给每个块保留 updated_at,以便过滤到新鲜内容并对账/回收陈旧块。

要点

把索引当成文档的一个物化、可重放的投影——就像搜索索引一样。真相之源是文档;向量索引是派生的,所以你总能重新分块或重新向量化再重建。有了这个心态,升级向量化模型和改分块就是一次重建任务,而不是迁移危机。

第 9 步 —— 评测:RAG 三元组

测不了就改不了,而 RAG 在两个不同的地方会失败——检索和生成——所以两个都要评。常见的框架是 RAG 三元组:

建一个小的黄金集(问题 → 理想来源/答案)做离线回归,并用 LLM-as-judge 大规模给忠实度和相关性打分(见 LLM 应用的评测)。也采集线上信号——点赞/点踩、"引用的来源对不对"——并回灌。检索 bug(取错块)和生成 bug(无视块)需要不同的修法,只有分开的指标才能区分它们。

第 10 步 —— 扩缩与成本

三个成本中心主导,各有杠杆:

成本中心杠杆
向量化(索引)在加速器上批量;只重新向量化变更的块;按内容哈希缓存 embedding。
索引内存量化向量(PQ / int8);跨节点分片;冷数据分层到磁盘 ANN。
生成(每查询)更少更好的块(重排)→ 更短 prompt;重复查询缓存答案;简单题用小模型。

延迟方面,检索 + 重排通常几十毫秒;LLM 调用主导端到端时间,所以聊天助手里那套流式和缓存技巧同样适用。激进缓存:查询 embedding、热门查询的检索结果、完全重复查询的整段答案。

第 11 步 —— 失败模式与取舍

小结

RAG 是一个尾巴上钉了个 LLM 的检索系统——所以把设计精力花在检索上。两条管线(离线索引、在线查询)、结构感知的带重叠分块、一个你分片并量化的 ANN 索引、带元数据过滤的混合(向量 + BM25)检索、一道 cross-encoder 重排,以及有据带引用的生成。然后用 RAG 三元组评测闭环,因为知道检索有没有帮上忙的唯一办法就是去测。

🎯 面试速答

一句话说 RAG 是什么? 检索相关文本塞进 prompt,让 LLM 基于你的数据作答——改的是 prompt,不是模型。
为什么要分块、怎么分? 你向量化/检索的是文本段,越长向量越糊;按结构切(约几百 token)、带重叠,并保留来源元数据。
为什么要混合检索? 向量擅长语义但漏精确词(码、名);加 BM25 并融合——再用 cross-encoder 重排候选。
为什么要重排? 一阶段检索便宜地最大化召回;cross-encoder 精确地把 top ~100 重排成 top ~5——最便宜的大质量提升。
怎么保持新鲜? CDC/webhook 驱动的增量索引,走同一条 分块→向量化→upsert 路径;索引是文档的可重放投影。
怎么评测? RAG 三元组——上下文相关性(recall@k)、忠实度、答案相关性——在黄金集上,大规模用 LLM-as-judge。

← 上一篇
设计 ChatGPT —— AI 聊天助手