1. RAG 上线后最怕被问“到底行不行”
RAG 应用有个很典型的阶段:demo 阶段惊艳,上线之后心里没底。用户问一句,系统答一段,看起来像模像样,但产品经理问“准确率多少”,测试问“这次改动有没有把上次修好的问题弄回去”,老板问“能不能上”,你只能回一句“感觉答得还行”。这句话在工程上等于没说。
问题不在于 RAG 难做,而在于质量没有被量化。RAG 的链路比普通 LLM 应用长:query 改写、向量检索、重排、上下文拼接、生成,任何一环出问题,最终表现都是“答得不对”,但根因可能完全不同。如果只盯着最终回答看,定位问题基本靠猜。
这套评测方案要解决的就是这件事:把 RAG 质量拆成检索层和生成层两层可量化指标,用 ragas 算指标、用 promptfoo 做回归门禁、用 GPT-4o 当 LLM-as-a-Judge 打分,配一份 200 条左右的 golden set,让每次改动都有数字可对比。适合正在做 RAG 应用、需要给质量一个交代的工程同学,也适合想把评测接进 CI 的团队。下面从指标定义讲到端到端跑通,配置和代码都可以直接抄。
2. 指标分层:先分清“没检索到”还是“没答好”
RAG 的错误一半以上出在检索层,但很多人只盯最终回答,导致定位全靠猜。把指标拆两层,出问题先看检索层,再看生成层,定位成本能低一个数量级。
| 层级 | 指标 | 衡量什么 | 怎么算 |
|---|---|---|---|
| 检索层 | Hit Rate@k | 正确答案是否在 top-k 里 | 每个 query 有 golden chunk 标注,命中即 1 |
| 检索层 | MRR | 正确答案排得多靠前 | 1/rank 取平均 |
| 检索层 | Context Precision@k | 捞上来的 chunk 里有用的比例 | 有用 chunk 的 precision 按位置加权 |
| 检索层 | Context Recall@k | 该捞的 chunk 捞上来多少 | 命中 golden chunk 数 / golden 总数 |
| 生成层 | Faithfulness | 回答里的 claim 有多少能被检索上下文支撑 | 逐 claim 判定,支撑数 / 总数 |
| 生成层 | Answer Relevancy | 回答是否切题、没答非所问 | judge 对 question↔answer 相关性打分 |
| 生成层 | Noise Sensitivity | 混入无关上下文后回答漂移多少 | 注入 top-k 外的噪声 chunk,对比回答变化 |
实际经验是:先修检索层再谈生成层。context recall 低于 0.8 时,生成层指标再好看也是假象——答案可能来自模型参数记忆而非检索结果,上线后换个知识库就翻车。检索层达标后,生成层的 faithfulness 才是真正的“有没有忠实于资料”。
注意:golden set 里的 golden chunk 标注要人工确认,别用 LLM 自动生成后直接采信,否则检索层指标本身就是错的。
3. TaoToken 前置:把 judge 模型的调用通道准备好
LLM-as-a-Judge 的稳定性高度依赖 judge 模型本身,方案里固定用 GPT-4o 且 temperature=0。要让 ragas 和 promptfoo 都能稳定调到模型,先把调用通道准备好。
TaoToken 在这里的角色是统一的模型调用入口,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。它提供 OpenAI 兼容的接口,所以 ragas 里用ChatOpenAI、promptfoo 里用openai:gpt-4o都能直接对接,不用改评测代码结构。
操作路径很直接:进控制台创建 API Key,然后在环境变量里配置 base_url 和 key。控制台入口在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。如果你还没决定用哪个模型做 judge,可以先去模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 手动试几条评测样本,感受一下判定风格再定。
环境变量这样配:
export OPENAI_API_KEY="你的 TaoToken API Key" export OPENAI_BASE_URL="https://taotoken.net/api"配好之后,ragas 和 promptfoo 都会读这两个变量,judge 调用就走通了。这一步不做,后面所有评测都跑不起来,所以先把它确认掉。
4. 可复制配置:ragas 指标接入 + promptfoo 回归骨架
4.1 ragas 跑全套指标
先装依赖:
pip install ragas langchain-openai pandas然后写评测脚本。核心是把retrieved_contexts、response、reference对齐到同一次运行快照:
from ragas import EvaluationDataset, evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_precision, context_recall, ) from ragas.llms import LangchainLLMWrapper from langchain_openai import ChatOpenAI judge = LangchainLLMWrapper( ChatOpenAI(model="gpt-4o", temperature=0) ) dataset = EvaluationDataset.from_dict({ "user_input": [ "合同里违约金比例是多少?", "公司2025年营收多少?", ], "retrieved_contexts": [ ["<chunk 1>", "<chunk 2>"], ["<chunk 1>"], ], "response": [ "合同约定违约金为合同总额的10%……", "根据检索资料,未找到相关数据。", ], "reference": [ "违约金为合同总额的10%。", "资料中未披露2025年营收。", ], }) result = evaluate( dataset, metrics=[ faithfulness, answer_relevancy, context_precision, context_recall, ], llm=judge, ) df = result.to_pandas() print(df[["faithfulness", "answer_relevancy", "context_precision", "context_recall"]].describe())三个关键点必须记住。第一,temperature=0写死,否则同一条数据两次跑分能差 0.1。第二,retrieved_contexts和response必须来自同一次运行快照,分开采集会导致指标对不上。第三,golden set 的reference要人工反复校对,因为 faithfulness 的判定基准是检索上下文,reference 本身有幻觉会带偏整个分数。
4.2 judge prompt 模板:拆 claim 再判
judge 最容易犯的错是让 LLM 对“整个回答”打一个分。整体打分有两个毛病:对长回答的局部错误不敏感,还容易受“看起来专业”的措辞影响。正确做法是两步走:先拆原子 claim,再逐条判定是否被上下文支撑。
你是一个严谨的评测员。任务:判断回答中的每个事实性陈述(claim)是否能被“给定上下文”支撑。 第一步:把回答拆成原子 claim,每个 claim 只包含一个可验证的事实。 第二步:对每个 claim 输出 JSON: {"claim": "...", "supported": true/false, "reason": "..."} 判定规则: - 只有上下文明确包含的信息才算 supported,模型自身知识不算。 - 上下文没有提及的 claim 判 false(哪怕它是对的)。 - 上下文与 claim 矛盾判 false。 只输出 JSON 数组,不要额外解释。4.3 promptfoo 回归骨架
离线批跑解决“这个版本行不行”,解决不了“这次改动有没有把之前修好的问题弄回去”。用 promptfoo 把评测固化进 CI:
# promptfooconfig.yaml prompts: - file://prompts/rag_answer.txt providers: - id: openai:gpt-4o config: temperature: 0 defaultTest: options: # 每个 test 先跑检索器取上下文,保证上下文是当前版本的真实输出 setup: file://setup/retrieve.py tests: - vars: question: "合同里违约金比例是多少?" assert: - type: llm-rubric value: "回答必须只基于给定上下文;上下文不含该信息时必须明确说不知道" - type: contains-all value: ["10%"] - vars: question: "公司2025年营收是多少?" assert: - type: llm-rubric value: "若上下文无营收数据,回答必须明确拒绝,禁止编造" - vars: question: "合同有效期多长?" assert: - type: llm-rubric value: "回答必须与上下文一致,不得引入上下文之外的事实"llm-rubric类型的断言就是 LLM-as-a-Judge 的工程化封装,语义化断言比字符串匹配抗“说法变了但意思没变”的情况。CI 里执行:
promptfoo eval --config promptfooconfig.yaml --output report.json配合断言阈值参数,分数跌破阈值直接让流水线失败。这样每次改 prompt、改切分策略、改重排,都会自动跑一遍 golden set。
5. 验证请求:跑一次端到端评测闭环
配置写完,做一次端到端验证,确认整条链路是通的。
第一步,准备一份最小 golden set,先放 5 条,覆盖“能答”“不能答”“边界”三类:
[ { "question": "合同里违约金比例是多少?", "golden_chunks": ["chunk_contract_penalty"], "reference": "违约金为合同总额的10%。" }, { "question": "公司2025年营收是多少?", "golden_chunks": [], "reference": "资料中未披露2025年营收。" } ]第二步,跑检索器,把每条 query 的 top-k 上下文和回答落盘成快照文件,供 ragas 和 promptfoo 共用。这一步是保证指标对齐的关键。
第三步,执行 ragas 脚本,观察输出:
faithfulness answer_relevancy context_precision context_recall count 5.000000 5.000000 5.000000 5.000000 mean 0.842000 0.901000 0.780000 0.860000 std 0.061000 0.045000 0.092000 0.058000 min 0.760000 0.830000 0.650000 0.780000 max 0.910000 0.950000 0.890000 0.930000第四步,执行 promptfoo,看断言通过情况:
promptfoo eval --config promptfooconfig.yaml预期输出里每条 test 的llm-rubric断言显示 PASS,contains-all命中。如果某条 rubric 失败,报告里会给出 judge 的判定理由,直接定位到是哪条 claim 没被支撑。
第五步,把两次结果对齐看:如果 ragas 的 faithfulness 高但 promptfoo 的 rubric 失败,说明 rubric 比指标更严格,需要检查 rubric 措辞是否过严;反过来则说明指标口径偏松。这一步跑通,自动化评测闭环就成立了。
6. 本篇常见错排查
坑 1:judge 的位置偏差和自褒偏差。同一份回答,把上下文顺序调换,faithfulness 能差 0.05~0.1;judge 还倾向给“结构完整、语气自信”的回答加分。对策是关键版本跑两遍(上下文顺序打乱各一次)取均值,rubric 里明确禁止“well-structured”之类的评价维度,只许判事实支撑关系。
坑 2:reference 污染。早期 golden set 的参考答案是让另一个 LLM 生成的,没人工核对,结果 faithfulness 虚高——judge 看到参考答案里也有该 claim,倾向判 supported。后来对 200 条 reference 做了两轮人工校对,faithfulness 整体掉了 0.06,但和人工抽检的一致率从 71% 涨到 84%。指标的可信度比数值好看更重要。
坑 3:噪声敏感度必须主动构造。真实用户 query 里“上下文混入无关内容”是小概率事件,被动采样测不出来。做法是评测时把检索结果第 k+1 到 k+3 的 chunk 强制塞进上下文,看回答漂移。某次改完重排后漂移率从 18% 降到 6%,但只测 faithfulness 完全看不出来——这就是噪声敏感度单独成指标的原因。
坑 4:阈值拍脑袋。一开始定 faithfulness ≥ 0.85 才算过,结果小模型加简单场景根本到不了,大模型加难场景轻松超,阈值形同虚设。改成按场景分档:标准问答场景 ≥ 0.85,长文档综合场景 ≥ 0.75,低于档位禁止上线。阈值要和业务场景绑定,不能全局一个数。
坑 5:judge 调用报 401 或超时。先检查OPENAI_API_KEY和OPENAI_BASE_URL是否都配了,ragas 和 promptfoo 读的是同一组环境变量。如果单条评测超时,把 golden set 分批跑,别一次塞几百条。
7. 工具对比与后续接入
| 工具 | 定位 | 强项 | 适合场景 |
|---|---|---|---|
| ragas | RAG 专项指标库 | 忠实度/检索指标全面,离线批跑 | 版本迭代的指标基线 |
| promptfoo | prompt/agent 测试框架 | llm-rubric 断言、CI 集成、diff 对比 | 回归门禁、prompt 调优 |
| Opik | 评测+追踪一体 | 线上 trace 与离线评测打通 | 线上问题回溯定位 |
| Giskard | 测试+红队 | agent 测试、漏洞扫描 | 安全与边界场景 |
组合建议:离线指标用 ragas 定基线,线上回归用 promptfoo 接 CI,线上观测用 Opik 接 trace。小团队先上 ragas 加 golden set 就够,别一上来铺四个工具,评测链路本身也是要维护的成本。
落地顺序可以这样走:先建 200 条左右 golden set,覆盖各业务场景和边界 case;用 ragas 跑出检索层加生成层基线;修检索层(切分、embedding、重排)到 recall 达标;再调生成层(prompt、模型);最后用 promptfoo 把 golden set 固化进 CI 当回归门禁。评测不是一次性工程,是跟着提示词、切分策略、重排一起迭代的闭环。
如果你在接入过程中卡在 judge 调用或断言配置上,可以对照接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 检查参数;需要长期跑编码类 Agent 评测的,可以看 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ;Claude Code 相关接入参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。进阶方向有三个:用 LLM 合成数据自动扩充 golden set、把 judge 蒸馏成小模型降推理成本、按 diff 增量触发评测而不是全量跑。