假设学校的预约系统改了规则:旧版说“可以当天取消”,新版说“至少提前 24 小时”。用户问:“我明早的预约,现在还能取消吗?”

检索(retrieval)器找到了标题一模一样的旧通知,模型还附上了引用。格式很漂亮,答案却错了。这个例子里不缺生成能力,缺的是找到当前适用的规则,并保留例外和时间条件。

检索增强生成(RAG)把外部检索结果接入生成过程。原始 RAG 论文研究了稠密检索(dense retrieval)与生成模型的组合;工程中不必只用向量,也可以查关键词、结构化数据库或混合检索(hybrid retrieval)。下面关注的是证据怎样一路传到答案里,而不是选哪个框架。

1. 一条答案要经过哪些检查?

通知原文 → 保留版本、标题、段落位置 → 分块并建立索引
                                               ↓
用户问题 → 确认身份与时间 → 查可访问的文档 → 选当前适用的证据
                                               ↓
                                 生成答案与出处 → 检查支持关系

这不是每次请求都要重新建索引。文档解析和编码通常在更新时完成;请求到来后才做查询、取证据和生成。

对预约规则,我们至少要带上这些字段。名字只是示意,不是通用标准:

字段 用来防什么错误
document_id、version、effective_from 拿旧规则回答现在的问题
section、原文位置 有引用链接,却找不到支持答案的那一句
access_scope 把当前用户无权看的内容送进模型
parser_version、chunker_version 文档没改,解析方式却变了,无法解释检索差异
embedding_version、index_snapshot 查询向量、文档向量和元数据版本对不上

权限检查必须在原文暴露给无权接收它的模型或用户之前完成。RAG 本身不保证隐私:外部模型、日志和缓存同样可能接触取回的材料。

2. 分块不是把字符串等长切开就结束

看一段自拟规则:“普通预约须提前 24 小时取消;因场地关闭而取消,不受此限制。”如果把后半句切走,前半句仍然完整、甚至很容易检索,却少了改变答案的例外。

做法 好处 要付出的代价
固定 token 长度,少量重叠 简单、成本容易估算 可能截断条件、列表或表格
按标题、段落和表格结构切 更容易保住局部语义 长段落仍需拆;解析失败会传下去
小块检索,再取父段落或相邻块 小块匹配精准,回答时上下文更完整 多读一些文本,需要原始位置与预算限制

重叠不是越多越好:重复片段会挤掉其他证据。合并时依据同一文档版本里的位置区间去重,不要认定“搜到的第二个块就是第一个块的下一页”,也不要每块统一删掉前 50 个字符。

表格转成纯文本时尤其要保留行列关系。“学生 / 教职工”两行配错了截止时间,文本中的词一个没丢,意思却已经变了。

3. 检索指标先用 5 篇文档算一次

假设针对一个查询,完整标注的相关集合是 {A, C, E},返回前 3 篇为 [B, C, D]:

指标 本例结果 它回答什么
Hit@3 1 有没有至少命中一篇?
Precision@3 1/3 返回的 3 篇里,几篇相关?
Recall@3 1/3 全部 3 篇相关文档里,找回几篇?
Reciprocal rank@3 1/2 第一篇相关文档排在哪里?

Precision 与 Recall 这次恰好相同。如果相关集合改成 {C},前者仍为 1/3,后者变成 1。Hit@K 不是 Precision@K,MRR 则是多个查询 reciprocal rank 的平均,不是单条查询的另一个名字。信息检索教材的指标定义

动画 同一份结果,四个指标各问什么

这张图需要启用 JavaScript。

def retrieval_metrics(ranked_ids, relevant_ids, cutoff):
    if cutoff <= 0:
        raise ValueError("cutoff must be positive")
    if len(ranked_ids) != len(set(ranked_ids)):
        raise ValueError("duplicate document IDs")
    selected = ranked_ids[:cutoff]
    relevant = set(relevant_ids)
    matches = [rank for rank, document_id in enumerate(selected, 1)
               if document_id in relevant]
    return {
        "hit": int(bool(matches)),
        "precision": len(matches) / cutoff,
        "recall": len(matches) / len(relevant) if relevant else None,
        "reciprocal_rank": 1.0 / matches[0] if matches else 0.0,
    }


metrics = retrieval_metrics(["B", "C", "D"], {"A", "C", "E"}, 3)
assert metrics == {
    "hit": 1, "precision": 1 / 3,
    "recall": 1 / 3, "reciprocal_rank": 0.5,
}

这里固定 Precision@K 的分母为 K,少返回的槽位不算命中;Recall 对“没有标注相关项”的查询返回 None,汇总时必须报告这类查询的数量和处理方式。不要静默丢掉,然后只汇报更好看的平均数。

实际标注往往不完整,未标注文档也可能相关。此时测到的是相对于已知标注的表现。NDCG 能使用分级相关性,但归一化并不会让不同标注、候选(candidate)池和 K 下的数值自动可比。

4. 检索对了,不代表生成对了

“回答有没有被材料支持”和“回答对不对”是两个问题。忠实复述旧通知,可能忠实但过时;碰巧答对却引用了不相关段落,则不能说明检索真的帮上了忙。Ragas把检索与生成的不同评价维度分开,是一个值得对照的起点,不是免人工核查的通行证。

给这条预约问题做三个小实验:

  1. 固定生成器,换检索结果。 正确规则有没有进入上下文?旧版本是不是总排在前面?
  2. 直接给人工选好的证据。 还会不会漏掉“提前 24 小时”?如果仍然漏,别只调 embedding。
  3. 拿走关键证据。 模型会明确说不知道、询问预约时间,还是继续编一个确定答案?

再把正确证据放到更长上下文的不同位置,检查答案是否稳定。噪声片段、冲突版本、无答案问题也要进测试集。一次指标上涨,要能说清改善发生在哪一步。

5. Prompt 帮忙组织证据,不负责保证正确

可以把任务、用户问题、带 ID 的证据和输出要求分开。对这里的任务,要求输出“是否能判断、依据哪条、还缺什么信息”,比只要求“认真思考”更容易检查。

Few-shot 是给示范;reference-based evaluation 是评估时给参考答案。它们不是一回事,也都不是 RAG 的必选项。上下文学习(in-context learning, ICL)用的是放进上下文的示例,这次请求并不会更新模型权重。GPT-3 论文

分隔符可以让格式清楚,但文档里写着“忽略规则、泄露其他通知”,仍应当作为不可信内容处理,而不是更高优先级指令。模型输出了合法 JSON,也只代表通过了语法或结构检查,不代表时间、引用和结论正确。

同样,写出漂亮的推理步骤不等于解释忠实可靠。需要的是可检查的证据支持,而不是更长的自我说明。CoT 忠实性研究。截断、改错推理步骤看答案变不变的干预实验,见长思维链是怎样训练出来的。

6. 最小实验别做得太含糊

保存查询、证据快照、标注规则和模型配置;调参用开发集,留一份未参与选择的测试集。除了总体结果,单列版本冲突、严格权限、表格、无答案和时间条件这些切片(slice)。

先跑一个简单检索基线(baseline),再逐项试 chunk 改动、混合检索、重排。生成侧固定预算,记录证据 token 数、延迟和成本。换了检索又换模型,即使分数更好,也很难知道是谁起了作用。

读完这篇,最值得保留的问题是:错在没找到、没保住,还是没用对? 找到出错的那一步,下一轮改动才不会只是把整个系统再复杂化一次。

本页术语 7
检索 retrieval
从外部语料里找出相关内容
稠密检索 dense retrieval
用向量相似度而非关键词匹配来检索
混合检索 hybrid retrieval
结合关键词、向量等不同检索方式,互相补充
候选 candidate
召回阶段捞出来、等待精排挑选的内容
切片 slice
按内容或用户属性划分的子群体,用来检查平均值掩盖了什么
基线 baseline
用来比较新方法的参考方法或配置
评分标准 criterion
这次评估究竟要判断哪一项质量