27届大模型岗面试准备(十二):RAG 从原理到落地——分块、向量检索、重排与评估的完整链路

如果只允许我给 27 届候选人押一个"必考且必须答好"的工程题,我会押 RAG。原因很简单:它是目前大模型落地渗透率最高的架构,几乎每个做大模型应用的团队都绕不开;同时它链路长、环节多,面试官可以从任何一环切入追问,非常适合区分"背过概念"和"真做过系统"的人。

这一篇把 RAG 的完整链路拆开讲:为什么需要 RAG、每个环节的技术选型与坑、可运行的向量检索代码,以及面试官最爱追问的评估问题。

一、RAG 解决什么问题:三个无法绕过的痛点

大模型有三个结构性缺陷,且都无法靠"训练一个更大的模型"解决:

  1. 知识截止:训练数据有截止日期,模型不知道昨天发生了什么;
  2. 私有知识缺失:企业内部文档、个人笔记从未出现在训练语料里;
  3. 幻觉:模型在"不知道"的时候依然会流畅地编造。

三条路线的对比是面试第一问的标准素材:

方案 知识更新成本 私有知识 幻觉抑制 可溯源 适用场景
继续预训练/SFT 极高(重新训练) 可注入但易遗忘 领域风格与能力迁移
长上下文塞文档 可以 勉强 文档量小(几十页内)
RAG 低(更新索引即可) 原生支持 强(有据可依) 是(附引用) 文档量大、更新频繁

要点是"微调改变的是模型的行为方式,RAG 改变的是模型的信息环境"——想教模型"怎么说话"用微调,想让模型"知道新东西"用 RAG。答到这一层,比背"RAG 是检索增强生成的缩写"高出一截。

二、离线链路:文档进库前的三道工序

2.1 解析与清洗

PDF 表格错乱、双栏 PDF 阅读顺序错误、HTML 标签噪声——真实项目里 60% 的效果问题源自解析质量而非模型。面试聊到这里可以主动提一句"我们发现脏数据进库后,后面每一环都在为它买单",非常加分。

2.2 分块(Chunking)

分块是 RAG 里性价比最高的调优点。核心矛盾:块太小则语义不完整,块太大则检索精度下降且挤占上下文窗口

分块策略 做法 优点 缺点
固定长度 每 N 字符/token 一切 简单可控 粗暴切断语义
递归字符分块 按段落→句子→字符逐级回退 尊重自然边界,工程默认选择 对结构化文档不敏感
结构感知分块 按标题/章节/表格切 语义完整 依赖解析质量
语义分块 按相邻句向量相似度突变点切 语义最连贯 计算成本高
父子分块 检索用小块,返回父块大上下文 兼顾精度与完整性 索引结构复杂

经验参数:中文技术文档 256–512 token 一块、10–20% 重叠是常见起点。但更重要的答法是:分块参数没有万能值,必须配合评估集调——这句话直接把话题引向你熟悉的评估环节。

2.3 向量化与索引

Embedding 模型选型看 MTEB/C-MTEB 榜单(中文常用 BGE 系列),但要注意榜单任务与自己场景的匹配度。索引层面,百万级以下向量用 HNSW 图索引基本是默认解,十亿级才需要考虑 IVF-PQ 这类量化压缩索引。

一个高频追问:为什么生产系统几乎都用近似最近邻(ANN)而不是精确检索? 因为精确检索是 O(N) 的暴力扫描,百万向量、768 维时单查询要几百毫秒到秒级;HNSW 用多层跳表式的图结构把复杂度降到近似 O(logN),以 1–2 个点的召回率损失换两个数量级的速度。

三、在线链路:从 query 到答案

完整流程是:query 改写 → 混合检索 → 重排 → 组装 prompt → 生成。逐环说要点:

  • query 改写:用户的口语化问题往往不适合直接检索。常用手段有同义改写、子问题分解、HyDE(让模型先生成一段假设性答案再拿去检索——因为"答案和答案更相似")。
  • 混合检索:向量检索抓语义,BM25 抓精确关键词(型号、报错码、人名)。两路结果用 RRF(Reciprocal Rank Fusion)融合,公式简单到可以现场手写:每个文档得分 = Σ 1/(k + rank_i),k 常取 60。
  • 重排(Rerank):双塔 embedding 为了速度牺牲了精度(query 和文档独立编码,交互信息丢失);cross-encoder 重排器把 query 和文档拼在一起过模型,精度显著更高但只能对 top 几十条做。"粗排求快、精排求准"的两段式漏斗——这个和推荐系统一样的架构直觉,是面试官判断你有没有工程 sense 的信号。
  • 生成:prompt 里明确要求"仅基于以下资料回答,资料不足时明确说不知道,并标注引用来源",这是幻觉抑制在 prompt 层的最后一道闸。

四、可运行代码:迷你向量检索 + RRF 融合

下面用纯 NumPy 实现一个可运行的最小检索管线(TF-IDF 风格词向量 + 余弦相似度 + BM25 简化版 + RRF 融合),不依赖任何外部服务,帮你把"检索到底在算什么"焊在肌肉记忆里:

import numpy as np
from collections import Counter

docs = [
    "RAG 通过检索外部知识库来增强大模型的回答质量",
    "向量数据库使用 HNSW 索引加速近似最近邻检索",
    "BM25 是基于词频和逆文档频率的经典检索算法",
    "知识蒸馏用教师模型的软标签训练学生模型",
    "重排序模型使用 cross-encoder 提升检索精度",
]

def tokenize(text):
    # 简化:按字切分(生产中文场景应使用 jieba 或模型 tokenizer)
    return [c for c in text if c.strip() and not c.isascii()] + \
           [w.lower() for w in "".join(c if c.isascii() else " " for c in text).split()]

# ---- 构建词表与 TF-IDF 向量 ----
tokenized = [tokenize(d) for d in docs]
vocab = {t: i for i, t in enumerate(sorted(set(t for d in tokenized for t in d)))}
N, V = len(docs), len(vocab)

df = Counter(t for d in tokenized for t in set(d))
idf = np.zeros(V)
for t, i in vocab.items():
    idf[i] = np.log((N + 1) / (df[t] + 1)) + 1

def embed(tokens):
    vec = np.zeros(V)
    for t, c in Counter(tokens).items():
        if t in vocab:
            vec[vocab[t]] = c * idf[vocab[t]]
    n = np.linalg.norm(vec)
    return vec / n if n > 0 else vec

doc_vecs = np.stack([embed(t) for t in tokenized])

def vector_search(query, topk=3):
    scores = doc_vecs @ embed(tokenize(query))    # 余弦相似度(已归一化)
    order = np.argsort(-scores)[:topk]
    return [(int(i), float(scores[i])) for i in order]

def bm25_search(query, topk=3, k1=1.5, b=0.75):
    q_tokens, avgdl = tokenize(query), np.mean([len(t) for t in tokenized])
    scores = []
    for idx, d in enumerate(tokenized):
        tf, dl, s = Counter(d), len(d), 0.0
        for t in q_tokens:
            if t in tf:
                s += idf[vocab[t]] * tf[t] * (k1 + 1) / (
                     tf[t] + k1 * (1 - b + b * dl / avgdl))
        scores.append(s)
    order = np.argsort(-np.array(scores))[:topk]
    return [(int(i), float(scores[i])) for i in order]

def rrf_fuse(rank_lists, k=60, topk=3):
    scores = Counter()
    for ranks in rank_lists:
        for r, (doc_id, _) in enumerate(ranks):
            scores[doc_id] += 1.0 / (k + r + 1)
    return scores.most_common(topk)

query = "如何加速向量检索"
v, b25 = vector_search(query), bm25_search(query)
print("向量检索:", [(i, f"{s:.3f}") for i, s in v])
print("BM25   :", [(i, f"{s:.3f}") for i, s in b25])
print("RRF 融合:", rrf_fuse([v, b25]))
for doc_id, score in rrf_fuse([v, b25]):
    print(f"  [{score:.4f}] {docs[doc_id]}")

这段代码浓缩了在线链路的三个核心算子:向量相似度、BM25、RRF。面试被要求"手写一个简单检索"时,能写出归一化后点积即余弦、IDF 平滑、RRF 的 1/(k+rank) 这几个细节就稳了。

五、评估:没有评估的 RAG 调优是玄学

面试官几乎必问"你的 RAG 效果怎么量化"。分两层回答:

检索层:Recall@K(正确文档是否进了 top-K)、MRR(正确文档排多高)。做法是构造 query→golden 文档对的评估集,几百条就能指导迭代。

生成层:三个经典维度——忠实度(答案是否忠于检索内容,测幻觉)、答案相关性(是否回答了问题)、上下文相关性(检索内容与问题是否相关)。RAGAS 等框架用 LLM-as-a-Judge 自动打分。

有了分层评估,定位问题就有了决策树:忠实度低 → 查 prompt 约束与生成模型;检索 Recall 低 → 查分块与 embedding;Recall 高但答案差 → 查重排与上下文组装。"先定位是检索问题还是生成问题"这句话是 RAG 排障的总纲。

六、答题框架与延伸

推荐的作答结构:三痛点起手 → 微调 vs RAG 边界 → 离线三工序(解析/分块/索引)→ 在线四环(改写/混合检索/重排/生成)→ 分层评估收尾。全程贯穿"漏斗"和"先定位再优化"两个工程直觉。

延伸考点预告:当检索一次不够、需要模型自主决定"查不查、查几轮"时,RAG 就升级成了 Agentic RAG(B10 已详细拆过);而支撑这类系统上线的评估体系,正是今天 B 系列第十一篇的主题。

下一篇 A13 讲长上下文与上下文工程:当模型窗口从 4K 卷到 1M,RAG 会被取代吗?答案比多数人想的更微妙。

更多推荐