生成端的最后一道闸:引用编号、校验,和两道拒答
很多人把 RAG 的成败全押在"检索得准不准"上。但检索做得再好,最后一步没约束住,就是白做——模型可以无视检索结果,自己编一段通顺但错误的话出来。这篇讲我在生成端加的三道约束。
一、引用:既要编号,又要校验
给模型加提示词约束只是第一层:
1. 每个结论后标注来源编号,如 [1][2] 2. 只使用提供的文档,不得夹带文档之外的知识 3. 若文档不足以回答,直接输出 NO_ANSWER但只有这三句是不够的。编号只是让"根据文档所述"变得可验证;校验才是让编号不是装饰。
没有校验时,"带引用"就是纯粹的装饰——模型完全可以编一个[3]出来,而[3]在原文里根本不存在。所以我加了一个extract_cited步骤:把答案里出现的所有[n]抽出来,逐个比对检索回来的文档,看这个编号是不是真被引用了、对应内容是不是真在那个文档里。
引用校验是唯一能被自动化发现的幻觉信号。模型编一段假话你很难自动判断,但模型编一个不存在的
[3]是可以被代码抓住的。
实测:瞎标率 0/29。这不是说模型不会幻觉了,是说"乱编引用"这一种幻觉被堵住了——剩下的幻觉要靠 LLM-as-judge 去抓,那是下一周的事。
二、两道拒答闸门,是被数据逼出来的
"答不上来就拒答"是 RAG 的基本功。但拿什么判断"该拒答",我一开始想当然用了向量相似度,结果被实测数据打脸:
| 组 | 向量相似度 p50 |
|---|---|
| 命中组(有答案) | 0.775 |
| 未命中组(没答案) | 0.749 |
几乎一样。因为 API 文档语料高度同质化,一个域外问题和域内问题,向量分都落在 0.75 上下,余弦根本区分不了"像不像"和"能不能回答"。
所以我把拒答拆成了两道闸门:
- 闸门①(检索侧,阈值 0.40):只挡"跑题"的。域外问题向量分落在 0.236~0.397,全拦下,而且这一步成本是 0——不用调 LLM;
- 闸门②(生成侧):兜住"域内但答非所问"的——域内问题向量分不低,但文档里其实没有答案,交给模型自己输出 NO_ANSWER。
两道都要有,不是设计癖好,是被数据逼出来的。余弦这道闸只能挡明显的域外问题,挡不住"看着相关但答案不在文档里"的,后者只能靠模型。
三、为什么拒答阈值用向量余弦,而不是融合分
既然有了混合检索,为什么不用融合分(RRF)做阈值?因为融合分是1/(60+名次)的累加,取值范围只有0.016~0.033——这么窄的区间根本选不出有意义的阈值,而且换个 k 值全部失效。
向量余弦是 0~1,语义直观:“低于 0.40 就不答”,谁都听得懂、也好调。
四、三道闸实测的拒答率
50 条 golden set + 18 条拒答集,三种模式的拒答率:
| 模式 | abstain_rate |
|---|---|
| 纯向量 | 0.16 |
| 纯 BM25 | 0.28 |
| 混合 | 0.20 |
拒答率不是越高越好,也不是越低越好——它是准确拒答的比例。纯 BM25 拒答率最高(0.28),但这不代表它最好,因为拒答率要跟"该拒的有没有拒、不该拒的有没有误拒"一起看。这正是评测要回答的问题,下一篇讲。
小结
| 结论 | 数字 / 事实 |
|---|---|
| 引用要编号 + 校验 | 编号可验证,校验是唯一自动幻觉信号,瞎标率 0/29 |
| 两道拒答闸门 | 余弦挡域外(成本 0)+ 模型兜域内 |
| 余弦分不了"像不像"和"能不能答" | 命中 0.775 vs 未命中 0.749,几乎一样 |
| 拒答阈值用余弦不用融合分 | 融合分 0.016~0.033 太窄 |
| 闸门①阈值 | 0.40,域外问题 0.236~0.397 全拦 |
上一篇:混合检索为什么用 RRF。下一篇:给 RAG 建 baseline。