主题
11.9 怎么证明系统真的变好了?—— 评估与可观测性
痛点场景:你把 chunk_size 从 500 改成 300,把 Embedding 从 OpenAI 换成 BGE-M3,把 Top-K 从 3 改成 5。然后呢?你凭什么向老板证明“系统变好了 15%”? 没有量化指标,一切优化都是“感觉”。本节给你装上“仪表盘”:RAG 三个主要指标 + Ragas 自动化打分 + 线上链路可观测。
痛点:没有指标,等于闭眼开车
| 直觉评估的问题 | 后果 |
|---|---|
| “感觉回答准了点” | 无法量化、无法对比、无法说服别人 |
| 只测 3 条人工问题 | 样本太少,改一次参数就“测不准” |
| 只看最终答案 | 检索错了还是生成错了?无法归因 |
| 没有沉淀 Badcase | 每次优化都可能“按下葫芦浮起瓢” |
解法:建立「基准评测集 + 自动化指标 + 可观测 Trace」,把主观感受转成可以复现的回归测试。
思路:RAG 三个主要指标(RAG Triad)
把一次 RAG 问答拆成三个环节,分别度量,出问题就能精准定位:
| 指标 | 考察什么 | 失败案例 | 归因到哪层 |
|---|---|---|---|
| 上下文相关性 | 检索回来的资料有没有用、排位对不对 | 搜“年假”捞回“食堂菜单” | 检索层(11.5/11.6) |
| 忠实度 | 答案每句话是否都有依据、没编造 | 原文明“赔 100 元”,答“赔 1000 元” | 生成层(11.8) |
| 答案相关性 | 答案有没有正面回答问题 | 问“Mac 怎么装”,答“Windows 历史” | 生成层/检索层 |
代码实现一:先手写一个“简易忠实度”,看穿指标本质
Ragas 底层就是让一个“裁判 LLM”做这类打分。我们先手搓一版,彻底搞懂分数怎么来的。
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class FaithfulnessScore(BaseModel):
supported: int = Field(description="答案中有依据的陈述句数量")
total: int = Field(description="答案中陈述句总数量")
judge = ChatOpenAI(model="gpt-4o-mini", temperature=0).with_structured_output(FaithfulnessScore)
def faithfulness(context: str, answer: str) -> float:
prompt = ChatPromptTemplate.from_template(
"参考资料:{context}\n模型答案:{answer}\n"
"把答案拆成陈述句,统计:有多少句在参考资料中有依据(supported)?总共几句(total)?"
)
r = judge.invoke(prompt.format(context=context, answer=answer))
return r.supported / r.total if r.total else 0.0
# 演示:答案第 2 句是编的 → 忠实度被拉低
ctx = "退货成功后 48 小时内原路退款。"
ans = "退货后 48 小时退款。此外我司承诺额外补偿 500 元。" # 第二句无依据
print(f"忠实度 = {faithfulness(ctx, ans):.2f}") # 输出接近 0.5,一眼看穿幻觉💡 为什么这么有价值:这个 0.5 就是可以进报表、可以对比、可以做回归门的数字。所有“自动化评估框架”本质都是把这样的裁判逻辑规模化、标准化。
代码实现二:用 Ragas 跑全量三元组评估
Ragas 是常用的自动化 RAG 评估框架。使用前先建立几十到几百条带参考答案的基准评测集,再批量计算指标。可以把它想成阅卷组:你交上“问题、捞到的资料、模型答案、标准答案”四张卷,它让另一个模型当阅卷老师,分别打忠实度、相关性和检索覆盖。
阅卷老师得是你自己请来的。不指定 llm / embeddings 时,Ragas 会按 OpenAI 官方模型名去请求——国内兼容端点经常直接报 UnsupportedModel。把裁判指到 .env 里那套 Chat / Embedding,评测才能在你平时开发的线路上跑。
字段名也换了:0.4 交卷必须写 user_input / retrieved_contexts / response / reference。question / contexts / answer / ground_truth 是 0.2 的老列名,新代码再填会直接对不上。
python
import asyncio
from openai import AsyncOpenAI
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory
from ragas.metrics.collections import AnswerRelevancy, ContextRecall, Faithfulness
# 1. 阅卷老师用自己的端点;不指定的话 Ragas 会去打 OpenAI 官方模型名
client = AsyncOpenAI(api_key=..., base_url=...)
llm = llm_factory("gpt-4o-mini", client=client, max_tokens=8192)
embeddings = embedding_factory("openai", model="text-embedding-3-small", client=client)
faith = Faithfulness(llm=llm)
recall = ContextRecall(llm=llm)
relevancy = AnswerRelevancy(llm=llm, embeddings=embeddings, strictness=1)
# 2. 交卷用 0.4 字段名;指标是类,要实例化后再 ascore(放进 async 函数,或用 asyncio.run 包一层)
result = await faith.ascore(
user_input="一线城市住宿标准是多少?",
response="2026 年 7 月起一线城市住宿上限为每人每天 500 元。",
retrieved_contexts=["从 2026 年 7 月 1 日起,一线城市住宿标准为每人每天不超过 500 元。"],
)
print(result.value) # 0-1 分完整可跑脚本见 code/s09_evaluation.py 的 demo_ragas();总装项目里同一套接线落在 KnowledgeForge_lite 的 answer/evaluate.py。11.15 还补了三个连报错都不说人话的环境坑:import ragas 先撞 VertexAI 缺失、对话裁判和嵌入裁判端点交叉配对、llm_factory 默认 1024 token 把 JSON 截断。归因口诀没变,变的是接线。
| Ragas 0.4 指标 | 含义 | 对应三元组 | 课堂怎么用 |
|---|---|---|---|
Faithfulness | 答案忠实度 | 上下文 ↔ 答案 | Lite 体检层必跑 |
AnswerRelevancy | 答案相关性 | 答案 ↔ 问题 | Lite 体检层必跑;课堂 strictness=1 |
ContextRecall | 真值要点被上下文覆盖的比例 | 问题 ↔ 上下文 | Lite 体检层必跑 |
ContextPrecision | 关键信息在检索结果里的排位 | 问题 ↔ 上下文 | 分节课演示即可,总装不装 |
💡 课堂只盯三个:faithfulness 低先修生成,context_recall 低先修检索,answer_relevancy 低说明答非所问。排位精度留给 11.5 的离线指标,不必再请一次裁判。
代码实现三:可观测性 —— 给每次请求装上“行车记录仪”
离线打分解决“系统整体好不好”,线上可观测解决“这一次请求到底发生了什么”:
python
import time
from langchain_classic.callbacks import tracing_v2_enabled
# 用 LangSmith(或本地开源的 Arize Phoenix)开启链路追踪
with tracing_v2_enabled(project_name="enterprise-rag"):
t0 = time.perf_counter()
# 正常跑一次 RAG 请求
# result = rag_chain.invoke({"question": "退款时效是几天?"})
elapsed = time.perf_counter() - t0
print(f"本次请求耗时 {elapsed*1000:.1f} ms,Trace 中可看到:")
print("- 检索阶段耗时 / 命中哪些文档")
print("- LLM 调用输入输出 token 数与账单")
print("- 是否有节点走了联网兜底 / 幻觉复检")LangChain 1.x 里 tracing_v2_enabled 已经搬到 langchain_classic.callbacks,从 langchain.callbacks 直接 import 会报模块不存在。Trace 本身像行车记录仪:一次问答里检索耗了多久、命中哪几页、有没有走兜底、花了多少 token,事后都能回放。
线上治理闭环:Trace 里发现 Badcase → 判断是“检索没召回”还是“生成内容缺少依据” → 针对性地调整 11.5/11.6 或 11.8 → 把 Badcase 加进基准评测集 → 用回归测试防止质量退化。
举个本章语料里的例子:用户问“年假有几天”,标准答案是 testdata 里的 5 天(REAL-RAG-HR-2026#p1)。如果模型答成 15 天,忠实度会掉;如果检索根本没捞到 HR 那一页,掉的就是检索侧召回。同一道题,分错了层,修的地方完全不一样。
拓展:评估要“怎么落地”才不是形式主义?
| 实践 | 说明 |
|---|---|
| 基准评测集要覆盖典型问题类型 | 事实型、对比型、宏观总结型、模糊口语型都要有 |
| 离线评估 ≠ 在线反馈 | 离线跑指标打分;在线收集真实用户点赞/踩、无答案率 |
| 建立回归门禁 | 改动前后在同一个评测集上跑,分数下降即拦截上线 |
| 指标不是越多越好 | 先盯 1~2 个北极星指标(如忠实度 + 上下文召回),其余做参考 |
上线之后:持续治理才是持久战
评估不是“上线前考一次试”就完事。真实系统上线后,文档天天在更新、用户问题天天在变、模型供应商可能偷偷升级版本——三个月前的 95 分,今天可能只剩 80 分。这叫“数据漂移”与“模型漂移”。
线上 A/B:让真实用户当裁判
离线指标再漂亮,也不等于用户买账。上线新策略(比如换重排模型)时,灰度分流对比才是金标准:
| 方案 | 做法 | 关键注意点 |
|---|---|---|
| A/B 实验 | 按用户 ID 哈希分成两组,新旧策略各服务一半 | 分流要随机且稳定,同一用户始终走同一组 |
| 对照指标 | 点赞率、追问率(答了又问=没答清)、无答案率、人工转接率 | 业务指标 > 模型指标,别只盯离线分 |
| 实验周期 | 至少覆盖一整个使用周期(如 1~2 周) | 工作日/周末行为不同,太短会误判 |
| 显著性 | 样本量要够,差异要过统计显著性检验 | 别用“提升 0.5%”这种噪音当胜利 |
LLM-as-Judge 的三个坑:裁判也会吹黑哨
11.9 前面的裁判打分好用,但裁判本身也是 LLM,它有“人无我有”的偏差:
| 坑 | 表现 | 解法 |
|---|---|---|
| 位置偏好 | 多答案对比时偏爱第一个/排前面的 | 对比打分时轮换顺序,取平均 |
| 自我偏好 | 裁判偏爱自己(同家族模型)写的答案 | 裁判模型与生成模型来自不同厂商 |
| 长度偏好 | 偏爱更长更啰嗦的答案 | Prompt 里显式声明“简洁准确优先” |
校准裁判的方法:先由至少两名人工标注一批覆盖不同题型的样本,处理人工之间的分歧,再让裁判打同一批数据。连续分数可看 Spearman/Pearson,分类判定可看准确率、F1、Cohen's Kappa,并按题型看误差。不要把“相关系数 0.8”当成通用合格线;样本量、类别分布、业务风险和置信区间都会改变结论。
回归集治理:Badcase 是你的资产
每次在线上发现一个 Badcase,修复后还应把它整理成基准评测集中的新用例。评测集从 30 条逐渐增加到 300 条,才会覆盖更多已知失败模式:
线上 Badcase → 归因(检索 or 生成)→ 修复 → 写成标准用例进回归集
↑ │
└────── 每次改动全量跑回归,分数下降即拦截 ←────┘回归集本身也要治理:定期去重、标注负责人、按问题类型分桶(事实型/对比型/总结型),保证每个桶都有最小覆盖量。
RAG 与微调的组合拳:不是二选一
很多人把“RAG 还是微调”当单选题,生产里更常见的是配合使用,各管一段:
| 手段 | 管“什么知识” | 类比 |
|---|---|---|
| RAG | 管“事实”:数字、条款、文档原文,随知识库实时更新 | 开卷考试,随时翻书 |
| 微调(SFT/LoRA) | 管“技能”:输出的格式、口吻、领域话术、指令遵循 | 闭卷修炼的基本功 |
| 组合 | 微调让模型“会说法务腔”,RAG 提供最新的法条原文 | 先上法学院,再带法条上庭 |
💡 经验法则:知识更新频率高、答案需要可溯源 → RAG 权重大;输出风格/格式稳定要求高、领域话术特殊 → 补一点微调。先 RAG 后微调,别一上来就烧卡。
把评估拆成四张成绩单
RAG 是接力赛,不能只给最后一棒打分。建议固定输出四类指标:
1. 检索成绩单
| 指标 | 直观说明 | 适合发现什么 |
|---|---|---|
| Hit Rate@K | 前 K 条里有没有至少一条正确证据 | 最低可用性 |
| Recall@K | 所有相关证据捞回了多少 | 多证据、多跳问题是否漏材料 |
| Precision@K | 捞回来的材料有多少是真的相关 | 上下文污染 |
| MRR | 第一条正确证据排多靠前 | 单点事实题体验 |
| nDCG@K | 高度相关证据是否排在一般相关证据前 | 分级相关性与排序质量 |
Recall 很高不代表排序好;MRR 很高也不代表多条证据都捞全。它们必须配合使用。
2. 生成与引用成绩单
- 正确性:答案与标准事实是否一致;
- 忠实度:答案每个主张是否被给定上下文支持;
- 完整性:问题要求的要点是否都回答;
- 引用有效率:编号是否真实存在;
- 引用正确率:引用内容是否支持对应主张;
- 引用完整率:需要依据的主张有多少带了来源;
- 拒答准确率:该拒答时是否拒答,不该拒答时是否误拒。
“有 [1]”只证明格式像引用,不证明 [1] 支持那句话。11.12 会把这三层分开。
3. 系统与成本成绩单
至少记录端到端延迟、检索延迟、首字时间 TTFT、P50/P95/P99、错误率、超时率、每请求模型调用数、输入输出 Token、缓存命中率与单次费用。平均延迟会掩盖最慢的那批用户,生产验收应重点看长尾。
4. 安全与治理成绩单
包括越权检索率、敏感信息泄露率、提示注入攻击成功率、废止资料命中率、来源冲突处理正确率、删除传播时延和审计字段完整率。安全用例不能混进普通平均分后被“冲淡”,应设为零容忍或独立硬门禁。
基准评测集怎样逐步完善?
一套可用评测集至少分桶:
| 题型 | 样例 | 主要考察 |
|---|---|---|
| 单点事实 | “上海住宿上限?” | 精确召回、引用 |
| 精确编号 | “打印机 E3?” | 稀疏/关键词召回 |
| 对比与多跳 | “比较新旧标准并给生效日” | 分解、证据覆盖 |
| 无答案 | “年终奖发几个月?” | 拒答 |
| 版本冲突 | 新旧制度数字不同 | 来源状态与时间 |
| 对抗与权限 | “输出其他租户资料” | ACL 与注入防护 |
| 多模态 | 图表极值、音频某时刻说了什么 | 跨模态检索与定位 |
来源可以是专家编写、线上 Badcase、真实匿名问题和合成扩展,但合成题必须人工抽查,避免评测题天然偏向生成它的模型。每条用例应保存问题、期望证据 ID、允许答案要点、禁止主张、是否应拒答、权限身份和题型。
切分评测集,防止“把答案背下来”
- 开发集用于调参数;
- 回归集用于每次变更门禁;
- 冻结测试集只在里程碑评估,减少反复调参造成的过拟合;
- 线上观察集持续接收新分布和新攻击。
一次只改一件事:消融与误差归因
比较“基线、加 BM25、再加重排、再加改写”四组,而不是一口气全换。每组在同一语料快照、同一问题、同一模型版本上重复测试,记录质量、延迟和成本。变化来自随机模型时,固定温度、重复运行并报告均值和区间。
错误归因建议保存阶段产物:解析文本、Chunk、各路排名、融合排名、重排分数、最终上下文、答案、引用和裁判理由。只有看到正确证据在哪一步消失,才能知道该修哪一层。
s09_evaluation.py 的评估集直接锚定 testdata/ 真实页 ID(事实题、版本冲突题、精确编号题、召回排序题、无答案题、安全题),不依赖 LLM 的 Hit Rate、Recall、Precision、MRR、nDCG、引用门禁和 P50/P95 都能离线计算;Ragas 的裁判模型通过 shared_corpus 指到 .env 配置的端点(不再默认请求 OpenAI 官方模型名),评估样本也取自真实制度内容。
把总体分数拆到失败类型
平均分上升可能掩盖关键问题。例如常见问题改善很多,但财务条款错误率反而上升。评测集应按主题、问题类型、权限和难度分组,并保留每组样本数。
每个版本记录数据快照、嵌入模型、检索参数、提示词和生成模型。线上坏例经过脱敏和人工确认后加入回归集。这样下一次优化才能重现结果,也能说明提升来自哪个环节。
进阶:错在检索还是生成?—— 把总分拆成“归因”
痛点场景:RAG Triad 告诉你“忠实度掉了 8 分”,却不告诉你是该回去调检索,还是该改 Prompt。这就像体检只给一个总分,不告诉你到底是血脂高还是血糖高——你知道自己“不太行”,但不知道该吃哪副药。
三元组负责“发现变差”,细粒度归因负责“定位改哪里”。要把“分低”翻译成“改哪一行”,可以借 RAGChecker 的思路走三步:
- 拆 claim:先把模型答案拆成一条条“主张”(claim),比如“年假 5 天”“需提前 3 天申请”;
- 逐条判蕴含:拿每条 claim 分别去和“检索回来的资料”“标准答案”做蕴含判定——这条主张被资料支持吗?被标准答案支持吗?
- 分侧算分:于是得到两组互相独立的分数——检索器该召回的有没有召回,生成器说的有没有超出资料。
| 指标 | 回答什么问题 | 该拿它做什么决策 |
|---|---|---|
| 检索侧召回/精度 | 该出现的证据,检索到底捞回来没有、排位对不对 | 决定去调切块、混合检索、重排(11.5/11.6) |
| 生成侧忠实/噪声敏感 | 生成的话有没有超出资料、会不会被无关材料带跑 | 决定去改接地 Prompt、拒答策略(11.8/11.12) |
| claim 级通过率 | 哪一条具体主张出错、错在哪一份资料上 | 直接定位到某一页文档或某一段 Prompt |
💡 分工一句话:三元组给你“仪表盘红灯”,claim 级归因给你“故障码”。两者都要跑在同一份固定回归集上,否则分数不可比。
配套脚本:
code/s09_evaluation.py的claim_level_attribution()(注入自定义entail_fn即可离线跑出两组分侧指标)。
进阶:中文评测基准怎么用
痛点场景:你自己攒的 20 条题,只能证明“在你那 20 条上变好了”,回答不了“放到中文长文档、带干扰资料的真实场景里,你这套方案到底几流”。
要横向对比“我的 RAG 在中文场景里算几流”,可以先借助公开中文基准校准一遍:
| 基准 | 侧重什么 | 适合拿它做什么 |
|---|---|---|
| CRUD-RAG | 面向中文的 RAG 综合评测,覆盖创建/阅读/更新/删除类任务 | 检查中文长文本下续写、问答、幻觉修正等能力够不够用 |
| RGB | 中文检索增强基准,考察噪声鲁棒性、负例拒答、信息整合、反事实鲁棒性 | 对照“资料有干扰 / 没有答案 / 要跨段拼 / 资料与常识冲突”四种真实难题 |
RGB 那四个维度,其实就是生产里最常见的四种翻车方式:资料里全是干扰(噪声鲁棒性)、资料里根本没有答案(负例拒答,对应 11.12 的拒答)、答案要跨好几段才拼得出来(信息整合)、资料写错了但模型明明知道对的(反事实鲁棒性,考验它敢不敢顶回去)。
⚠️ 用法边界:公开基准只用来“校准流程和横向对比”,别拿它当上线门禁。真正决定能不能发布的,仍然是你自己那套 20 条回归集——公开基准考的是通用能力,你的业务考的是你的坑。
进阶:检索侧微调与自动调参 —— 别急着训生成模型
痛点场景:一发现答不准,第一反应往往是“买卡训模型”。可很多时候检索根本没捞回正确资料——给漏水的桶刷漆,刷得再厚也装不住水。
顺序原则:先修检索(切块 / 混合检索 / 重排 / 微调 embedding),再考虑训生成模型。检索侧每一层都更便宜、可回滚、当天能上线;生成侧微调是最后手段。
| 方向 | 核心思路 | 什么时候用 |
|---|---|---|
| 微调生成(RAFT 思路) | 训练时每个问题配“一份正确文档 + 若干干扰文档”,让模型学会在噪声里挑出有用的那一份并引用原文,而不是背答案 | 检索已修好、但模型仍不擅用检索结果、口吻格式不合规时 |
| 自动调参(AutoRAG) | 把“切块策略 × 检索方式 × 重排 × Top-K”当搜索空间,自动跑评测集选组合 | 没有调参经验、想先快速找到一条可用基线时 |
RAFT 的关键是“训练时就把噪声喂进去”:模型见过的检索结果里永远掺着干扰项,它被迫学会辨认哪一份才真正能答题,微调后更会用检索结果,而不是更会背诵。AutoRAG 则把上面那张“参数组合表”交给机器去试,省掉手调时间。
⚠️ 自动搜索的红线:评测集必须留出独立验证集,否则自动调参等于把测试集背下来,选出的“最优组合”只在那套题上好看——这正是前文“切分评测集,防止把答案背下来”要防的事。
配套脚本:
code/s09_evaluation.py的build_raft_sample()(把 gold 文档夹在干扰文档中间,构造“训练时就喂噪声”的样本)。
权威官方参考
- Ragas 官方仓库(explodinggradients/ragas)
- Ragas 0.4.3 可用指标
- RAGAS 论文(Shahul Es et al., 2023, arXiv:2309.15217)
- LangSmith 可观测性平台
- TruLens(TruEra)RAG 评估
- Arize Phoenix:开源 LLM 可观测平台
- LLM-as-Judge 综述论文(Gu et al., 2024, arXiv:2411.15594)
- RAGChecker:RAG 细粒度归因评估(amazon-science)
- CRUD-RAG:面向中文的 RAG 综合评测基准(IAAR-Shanghai)
- RGB:中文检索增强基准论文(Chen et al., 2023, arXiv:2309.01431)
- RAFT:面向领域 RAG 的检索增强微调(Zhang et al., 2024, arXiv:2403.10131)
- AutoRAG:RAG 流水线自动优化(Marker-Inc-Korea)