假设学校的预约系统改了规则:旧版说“可以当天取消”,新版说“至少提前 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 的平均,不是单条查询的另一个名字。信息检索教材的指标定义
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把检索与生成的不同评价维度分开,是一个值得对照的起点,不是免人工核查的通行证。
给这条预约问题做三个小实验:
- 固定生成器,换检索结果。 正确规则有没有进入上下文?旧版本是不是总排在前面?
- 直接给人工选好的证据。 还会不会漏掉“提前 24 小时”?如果仍然漏,别只调 embedding。
- 拿走关键证据。 模型会明确说不知道、询问预约时间,还是继续编一个确定答案?
再把正确证据放到更长上下文的不同位置,检查答案是否稳定。噪声片段、冲突版本、无答案问题也要进测试集。一次指标上涨,要能说清改善发生在哪一步。
5. Prompt 帮忙组织证据,不负责保证正确
可以把任务、用户问题、带 ID 的证据和输出要求分开。对这里的任务,要求输出“是否能判断、依据哪条、还缺什么信息”,比只要求“认真思考”更容易检查。
Few-shot 是给示范;reference-based evaluation 是评估时给参考答案。它们不是一回事,也都不是 RAG 的必选项。上下文学习(in-context learning, ICL)用的是放进上下文的示例,这次请求并不会更新模型权重。GPT-3 论文
分隔符可以让格式清楚,但文档里写着“忽略规则、泄露其他通知”,仍应当作为不可信内容处理,而不是更高优先级指令。模型输出了合法 JSON,也只代表通过了语法或结构检查,不代表时间、引用和结论正确。
同样,写出漂亮的推理步骤不等于解释忠实可靠。需要的是可检查的证据支持,而不是更长的自我说明。CoT 忠实性研究。截断、改错推理步骤看答案变不变的干预实验,见长思维链是怎样训练出来的。
6. 最小实验别做得太含糊
保存查询、证据快照、标注规则和模型配置;调参用开发集,留一份未参与选择的测试集。除了总体结果,单列版本冲突、严格权限、表格、无答案和时间条件这些切片(slice)。
先跑一个简单检索基线(baseline),再逐项试 chunk 改动、混合检索、重排。生成侧固定预算,记录证据 token 数、延迟和成本。换了检索又换模型,即使分数更好,也很难知道是谁起了作用。
读完这篇,最值得保留的问题是:错在没找到、没保住,还是没用对? 找到出错的那一步,下一轮改动才不会只是把整个系统再复杂化一次。
- 混合检索与重排:不同来源是否真的互补。
- 向量索引:区分近似误差与相关性误差。
- LLM-as-a-Judge:评分标准(criterion)、偏差与人工锚点。
本页术语 7
- 检索 retrieval
- 从外部语料里找出相关内容
- 稠密检索 dense retrieval
- 用向量相似度而非关键词匹配来检索
- 混合检索 hybrid retrieval
- 结合关键词、向量等不同检索方式,互相补充
- 候选 candidate
- 召回阶段捞出来、等待精排挑选的内容
- 切片 slice
- 按内容或用户属性划分的子群体,用来检查平均值掩盖了什么
- 基线 baseline
- 用来比较新方法的参考方法或配置
- 评分标准 criterion
- 这次评估究竟要判断哪一项质量