Skip to content

11.9 怎么证明系统真的变好了?—— 评估与可观测性 ​

痛点场景:你把 chunk_size 从 500 改成 300,把 Embedding 从 OpenAI 换成 BGE-M3,把 Top-K 从 3 改成 5。然后呢?你凭什么向老板证明“系统变好了 15%”? 没有量化指标,一切优化都是“感觉”。本节给你装上“仪表盘”:RAG 三个主要指标 + Ragas 自动化打分 + 线上链路可观测。


痛点:没有指标,等于闭眼开车 ​

直觉评估的问题后果
“感觉回答准了点”无法量化、无法对比、无法说服别人
只测 3 条人工问题样本太少,改一次参数就“测不准”
只看最终答案检索错了还是生成错了?无法归因
没有沉淀 Badcase每次优化都可能“按下葫芦浮起瓢”

解法:建立「基准评测集 + 自动化指标 + 可观测 Trace」,把主观感受转成可以复现的回归测试。


思路:RAG 三个主要指标(RAG Triad) ​

把一次 RAG 问答拆成三个环节,分别度量,出问题就能精准定位:

💡 思路:RAG 三个主要指标(RAG Triad)

指标考察什么失败案例归因到哪层
上下文相关性检索回来的资料有没有用、排位对不对搜“年假”捞回“食堂菜单”检索层(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 的思路走三步:

  1. 拆 claim:先把模型答案拆成一条条“主张”(claim),比如“年假 5 天”“需提前 3 天申请”;
  2. 逐条判蕴含:拿每条 claim 分别去和“检索回来的资料”“标准答案”做蕴含判定——这条主张被资料支持吗?被标准答案支持吗?
  3. 分侧算分:于是得到两组互相独立的分数——检索器该召回的有没有召回,生成器说的有没有超出资料。
指标回答什么问题该拿它做什么决策
检索侧召回/精度该出现的证据,检索到底捞回来没有、排位对不对决定去调切块、混合检索、重排(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 文档夹在干扰文档中间,构造“训练时就喂噪声”的样本)。


权威官方参考 ​