RAG 实战第四课:答案带引用出处 + 生成层评测,从能查到能信
生命不息,折腾不止。前三课把「召回 + 精排」做得能查能测了,但用户拿到手的是一段没有出处、不知道靠不靠谱的文字——这课给答案装上「引用出处」和「生成层打分」两样东西,让回答不光有据可依,还能摊开给你看它凭啥这么说。
上一课收尾时留了个口子:检索端你已经能做「召回 → 精排」,但那套 recall / MRR / hit 指标只证明「该捞的捞回来了」,没回答用户真正关心的两件事:
- 这答案哪来的? 大模型吐出的话,用户想问一句「依据呢?」,你答不上来,信任就崩了。
- 这答案编没编? recall 再高,生成那一步照样可能满嘴跑火车——而且 RAG 的失败往往「不报错」,只会给你一段流畅、自信、但错得离谱的文字,没有任何异常日志,你连它错了都不知道。
这就是这一课要补的最后一块:生成层。干两件活——让答案带内联引用 [1][2][3],再用 faithfulness(忠实度)、答案正确性这些指标给生成这一步打分。做完,「召回 → 精排 → 生成」这条链路才算真正闭环。
一、先分清:检索层指标 vs 生成层指标,测的不是一回事
很多人评测 RAG 会犯一个错——拿一个「端到端正确率」的分数,拍脑袋说系统好还是坏。错了。检索和生成是两段,得分段测,否则你根本定位不了 bug 在哪:
| 层 | 测的是 | 指标 | 依赖什么 |
|---|---|---|---|
| 检索层 | 该捞的捞回来没有 | recall、MRR、hit、context precision | 标准 chunk 标注 |
| 生成层 | 编没编、答没答到点上 | faithfulness、answer relevancy、answer correctness | 上下文 + (部分指标要)参考答案 |
一句话记住分工:检索指标查「检索器」的账,生成指标查「生成器」的账。上一课的 recall 低 = 检索没捞到,这课的 faithfulness 高但答案错 = 检索到的上下文本身就是错的。混在一起测,只会误导你朝错误的方向调参。
生成层三个最常用的指标,先给它们一句话定性(后面逐个展开):
- Faithfulness(忠实度):答案里的每个论断,是不是都能从「喂给它的那份上下文」里推出来。不需要参考答案,有「问题 + 答案 + 实际检索到的上下文」就能算。
- Answer Relevancy(答案相关性):答案有没有正面回答用户的问题,是不是顾左右而言他。
- Answer Correctness(答案正确性):答案跟「标准参考答案」比,事实对不对。这个必须要有 ground truth(参考答案)。
二、让答案带内联引用:[1][2][3] 出处可回查
先解决「依据呢」。最省事、也最成熟的方案是内联引用(inline citation):让大模型在正文里用 [n] 标注每条结论来自第几条资料,引用列表不在 prompt 里让模型生成,而是由你的代码动态拼出来(这样来源可控、能加链接、能折叠)。
第一步:检索结果要带元数据。 光有正文没用,每个 chunk 得带上来源信息(文档标题、chunk 编号、URL 等),否则引用了也没法回查:
1 | # 检索结果统一成三元组: (chunk_id, title, text) |
第二步:prompt 里要求标引用,但不许它自己生成引用列表。
1 | from openai import OpenAI |
这里有两个要点:一是让模型只标号、别自己列来源;二是 temperature 调低(0~0.2),引用标注这种结构性输出要它收着点,别即兴发挥。
第三步:用代码解析引用号,动态生成来源列表。 模型正文只回来 [1][3] 这种标号,你拿正则把号抠出来,再回头映射到你手里的 metadata:
1 | import re |
这样前端就能「正文高亮标号 + 底部折叠来源列表」双栏展示,用户点一下直接对到原文。引用的价值不在装样子,而在可回查——这是把「AI 瞎编」跟「有据可查」分开的那道线。
小提示:这一步的生成模型只要 OpenAI 兼容接口就行。多模型、按量付费想省心的话,可以统一接中转站(比如本博客常说的 ai.aklibk.com)一个 key 调多个模型,judge 和生成分开用不同档位的模型。不接也不影响本文跑通。
三、Faithfulness 忠实度:把「编没编」量化成一个数
引用标出来了,不代表它没编。faithfulness 是生成层最有用的一把尺子,专门回答那个要命的问题:模型是不是在它被喂的材料之外,自己发挥(幻觉)了?
它的定义很干净,一句话:faithfulness = 被上下文支持的论断数 ÷ 总论断数。满分 1.0 = 每句话都有据;0.6 = 四成内容是模型自己补的。关键是它不依赖参考答案(reference-free),所以你线上随便一个真实问题都能算,不用人肉写标准答案。
原理分两步,用「LLM 当裁判」(LLM-as-judge)来做:
- 拆 claim:让 judge 把答案拆成一条条独立、可单独验证的陈述;
- 逐条验证:让 judge 判每条陈述「是否被上下文支持」,支持的算 yes,不支持的算 no。
手搓版代码(延续前几课「不靠框架」的思路,逻辑就上面两行):
1 | def faithfulness(answer, context, judge_client): |
这里有个很值钱的细节:judge 用二选一(yes/no)比 1~5 打分靠谱得多。打分制里「3 分和 4 分差在哪」永远吵不清;二选一强制你定义清楚「什么算支持」,聚合出来的数才有明确含义。
跑出来可能是这样:
1 | context: ……ERR_500 是后端进程崩溃,查 nginx-error.log 最后 20 行…… |
第三条「重启上次更新过的服务」上下文里压根没有,模型凭记忆补的——这就是被 faithfulness 逮住的幻觉。它可能现实里碰巧是对的,但不在你喂的材料里,就不该出现在 RAG 的答案里,这正是 faithful 要卡的行为。
不想手搓的话,RAGAS 这个开源评测库把这套东西打包好了(真实可用,API 我对着官方文档核过):
1 | pip install ragas |
1 | from openai import AsyncOpenAI |
两个版本的本质一样,都是「拆 claim + 逐条验」。上了规模、要持续跑 CI 回归,用 RAGAS 省心;想搞清楚原理、要完全可控,手搓 30 行也够了。
四、答案正确性 / 相关性:faithfulness 测不了的,它俩来补
别把 faithfulness 当万金油。 它只管「模型有没有老实照抄上下文」,不管上下文本身对不对。一个更直白的说法:
- Faithfulness:这段话有没有超出它手里的材料(没有 = 高);
- Answer Correctness:这段话到底对不对(跟参考答案比)。
一个极端例子:知识库里存着一条过时的内部记录说「价格是 100 元」,模型照实回答「100 元」,faithfulness = 1.0 满分——但它错了,因为真正价格早改了。faithfulness 高一不等于答案对,所以还得有 answer correctness。
Answer Correctness 必须要 ground truth(标准答案),这也是它和 faithfulness 最本质的区别。它的计算是「事实正确率」和「语义相似度」的加权和,其中事实正确率用 F1 的套路:
1 | TP = 参考答案和生成答案都有的论断 |
还是爱因斯坦那个经典例子:参考答案「爱因斯坦 1879 年生于德国」,生成答案「爱因斯坦 1879 年生于西班牙」。
1 | TP: [爱因斯坦 1879 年生] ← 两边都有 |
这套逻辑同样能用 LLM-as-judge 手搓:让 judge 输出「参考答案 vs 生成答案」的 TP / FP / FN 三张清单,套公式即可。
Answer Relevancy(答案相关性) 则更轻量,它测的是「答案有没有正面回答问题」,和上下文、参考答案都无关——同一批检索结果,模型是「答非所问」还是「直击要害」,这一项就能筛出来。
三者合起来,才构成生成层的完整体检表:
| 指标 | 需要什么 | 能抓到的问题 |
|---|---|---|
| faithfulness | 问题 + 答案 + 上下文 | 幻觉、超出材料发挥 |
| answer relevancy | 问题 + 答案 | 答非所问、跑题 |
| answer correctness | 问题 + 答案 + 参考答案 | 答案事实错误(哪怕有据) |
五、把生成层评测接进上一课的脚本,闭环跑起来
前面单独讲指标容易飘,实际你要的是「同一份脚本,改完参数一键出全套分」。把上一课的检索层评测和这课的生成层评测串起来,就是一条完整链路:
1 | def full_evaluate(test_set, build_index, generator): |
拿这套脚本做对比实验,你就能看懂「问题出在哪一层」:
1 | 改 embedding 前: recall@5 0.71 / faithfulness 0.85 / correctness 0.60 |
看这组数:faithfulness 没怎么动(生成器没换),correctness 涨了——说明这次改进是检索端带动的,上下文更准,答案自然更对。反过来,如果 faithfulness 低而 recall 高,那就是生成器爱加戏,得去压生成 prompt、加「不知道就说不知道」的硬约束。分层指标的真正价值就在这:能定位,而不是一句「系统变差了」。
六、小结 + 这几个坑别踩
到这,「召回 → 精排 → 生成」三段全部闭环了。回顾一下四课走过的路:第一课手搓最短链路,第二课装 FAISS + 混合检索,第三课加 reranker + 检索层评测,今天把生成层的引用和打分补上。一条能生产用的 RAG,骨架不过如此。
收尾前把今天几个坑点一点:
- 引用号是代码发出去的,别让模型自报家门。prompt 里只让它标
[n],来源列表由你的代码从 metadata 映射,才能保证出处可控、可加链接、可折叠。 - faithfulness 一定要拿「实际喂给模型的上下文」去验,而不是检索器返回的原始排序列表——中间可能截断、拼接、去重过,「模型真看见什么」和「检索器返回什么」未必一致。
- judge 用 yes/no 二选一,别用 1~5 打分。二选一强制你定义「什么算支持」,聚合分数才有意义;打分制里「3 分和 4 分」永远扯不清。
- 别用 faithfulness 替代 correctness。faithfulness 高一 ≠ 答案对,知识库内容错了,它照抄也是满分。要抓事实错误,得上需要 ground truth 的 answer correctness。
- ground truth 别偷懒只写一问一答。answer correctness 靠 TP/FP/FN 拆论断,参考答案写得越完整(关键事实都列上),算出来的分越可信。
- judge 和生成别用同一个劣质模型。让一个爱幻觉的小模型去判断另一个模型有没有幻觉,等于让瞎子带路;judge 至少换个靠谱且在角色上稳的模型。
生命不息,折腾不止。四课下来,你的 RAG 已经「能查、能测、能信」,但有一个更贴近生产的高频场景还没碰:多轮对话里的 RAG——用户追问「那它的增长率呢」,你得让系统记得上一轮说的是哪家公司的哪份数据,还要在改写 query 时不把话题带偏。下一篇 RAG 实战第五课:给链路接上多轮记忆 + query 改写(单轮变多轮、追问能接住),让知识库从「一问一答的问答机」升级成「记得住上下文的对话助手」。回见。