做 RAG 优化时,最容易出现一种假象:参数改了不少,Demo 里的几个问题也答得更顺了,于是大家默认“效果有提升”。
比如把 Chunk Size 从 500 调到 800,换一版 Embedding,再加上 BM25 和 Rerank。改完以后,挑几个熟悉的问题试一遍,答案确实比之前像样。可一旦问“到底提升了多少”“哪些问题变好了”“有没有问题反而变差”,往往就说不清了。
我在项目里越来越不相信这种“手感”。原因很简单:随手测试的问题可能刚好是系统最擅长的;错误码查询变好了,长问题却可能退化;答案看着没错,也可能只是模型凭自身知识答对,检索结果根本没有提供有效证据。
所以,RAG 优化真正的起点不是换模型,也不是继续调 Top K,而是先建立一把稳定的尺子。至少要回答四件事:
- 正确资料有没有被找到?
- 找到的内容里有多少真正有用?
- 模型有没有基于这些资料回答?
- 质量提升是否值得新增的延迟和成本?
这篇文章就从这四个问题出发,搭一套可以重复运行的 RAG 评测流程。
一、先别急着看最终答案
假设用户问:
Windows 客户端出现 ERR_CONN_RESET_1007,应该怎么处理?
系统回答:
请关闭系统代理、清理客户端缓存,然后重新登录。
答案没有问题,但它背后的链路可能完全不同:
- 检索到了 4.2 版操作手册,模型按资料作答;
- 检索到了旧版文档,模型靠参数知识碰巧答对;
- 新旧文档同时被召回,模型这一次恰好选对了。
从用户眼前这一句话看,三种情况没有区别;从系统可靠性看,只有第一种值得信任。后两种换个问法、换个模型,甚至只是提高一点 Temperature,就可能露出问题。
因此,一次完整评测至少要拆成三层:
检索层看正确证据是否进入候选集、排名是否足够靠前,常用 Hit Rate、Recall、MRR 和 nDCG。
生成层看回答是否忠于上下文、是否真正回答问题、是否覆盖参考答案,常用 Faithfulness、Answer Relevancy 和 Answer Correctness。
业务层才看问题解决率、转人工率、追问率、P95 延迟和单次成本等线上结果。
这三层解决的是不同问题。检索层回答“找没找到”,生成层回答“用没用对”,业务层回答“值不值得上线”。只保留一个总分,定位问题时几乎一定会抓瞎。
二、评测集才是优化的地基
评测集不需要一开始就做得很大,但必须让不同方案在同一批问题、同一套标注下比较。一条实用的样本可以长这样:
{ "id": "support-001", "question": "Windows 客户端出现 1007 错误怎么办?", "reference_answer": "关闭系统代理,清理客户端缓存后重新登录。", "relevant_doc_ids": ["error-guide-4.2"], "relevant_chunk_ids": ["error-guide-4.2#section-6#chunk-2"], "answerable": true, "filters": { "platform": "windows", "status": "active" }, "tags": ["error_code", "short_query"] }这里有三类信息尤其容易被省略:文档与 Chunk 的相关 ID、问题是否可回答,以及用于分桶分析的标签。
文档 ID 和 Chunk ID 都要留
系统命中一份 80 页的产品手册,不代表它找到了答案。如果真正包含处理步骤的 Chunk 没进上下文,生成模型仍然无从作答。
relevant_doc_ids用来判断方向是否正确,relevant_chunk_ids则判断证据是否真的到位。只做文档级标注,结果通常会比实际效果乐观。
别让所有问题都有答案
如果评测集里每个问题都能在知识库中找到答案,你测出来的只是“回答能力”,而不是“知道什么时候不该回答”。
第一版可以放入 10%~20% 的无答案样本,例如:知识库尚未覆盖的新功能、缺少必要条件的问题、无权限访问的资料、带有错误前提的问题。对这些样本,合格的行为可能是说明证据不足、追问必要信息或转人工。
用标签暴露平均分背后的问题
建议至少按查询类型、文档类型、难度和风险打标签:
query_type:语义问题 / 关键词 / 错误码 / 多跳 / 多轮 document_type:FAQ / PDF / 表格 / API 文档 / 制度 difficulty:简单 / 中等 / 困难 risk:普通 / 财务 / 法务 / 医疗 / 权限 language:中文 / 英文 / 中英混合总体 Recall@5 从 0.82 涨到 0.86,看起来不错。但如果分桶后发现普通问答提升 8%,表格问题下降 12%,结论就完全不同。平均分适合看趋势,分桶结果才适合找原因。
问题从哪里来
真实业务数据的优先级最高:线上搜索与失败日志、客服工单、人工答疑、点踩和追问,都比开发者凭空编出的“标准问题”更有价值。
系统还没上线时,可以从文档生成问题,但要专门补充真实用户的表达变体:
| 类型 | 标准问法 | 更接近真实输入的问法 |
|---|---|---|
| 口语化 | 如何重置登录密码? | 密码忘了咋整 |
| 缩写 | 如何配置单点登录? | SSO 怎么开 |
| 错别字 | 发票如何申请? | 发漂在哪开 |
| 精确实体 | ERR_CONN_RESET_1007 如何处理? | 1007 报错 |
| 多轮指代 | Mac 客户端如何清缓存? | 那 Mac 呢? |
| 版本冲突 | 4.2 版如何配置代理? | 新版代理设置在哪 |
| 无答案 | 是否支持离线导出? | 没网能不能批量导出 |
第一版做 100~300 条就够了。200 条经过业务人员确认、证据清晰的样本,远比 5000 条未经审核的合成数据有用。高风险样本最好双人复核,有争议的样本单独标记,不要为了凑数据强行写出一个“标准答案”。
三、检索指标不必全上,关键是选对
假设某个问题有两个相关文档A和C,系统返回前 5 条:
排名:1 2 3 4 5 结果:B A D E C 相关:否 是 否 否 是Hit Rate@K最直观:前 K 条里只要出现一个相关结果就算命中。它适合回答“有没有找到”,但不关心找全没有,也不关心答案排第一还是排第五。
Recall@K看应该找到的证据找回了多少:
Recall@K = 前 K 条中的相关结果数 / 全部相关结果数上例中Recall@3 = 0.5,Recall@5 = 1.0。多文档总结和多跳问答尤其依赖Recall,因为漏掉一条关键证据,答案就可能不完整。
Precision@K看送进上下文的内容有多少真正有用:
Precision@K = 前 K 条中的相关结果数 / K上例的Precision@5 = 0.4。证据虽然找全了,但上下文中仍有 60% 的噪声。这类问题通常要从候选数量、过滤规则和 Rerank 入手。
MRR只关心第一个正确结果排得多靠前:
RR = 1 / 第一个相关结果的排名 MRR = 所有问题 RR 的平均值上例第一个相关结果排第二,因此 RR 为 0.5。FAQ、客服等通常只需要一个主要答案的场景,MRR 很实用。
nDCG@K适合存在多级相关性的情况。比如直接且完整的证据记 3 分,只覆盖部分答案记 2 分,主题相关但无法回答记 1 分。它既看相关性,也看排序位置,比简单命中更接近真实的检索体验。
实际项目不必把所有指标塞进仪表盘。单答案 FAQ 可以先看 Hit Rate 和 MRR;多文档问答重点看 Recall 和 nDCG;进入最终上下文的内容则重点看 Precision。指标越多不等于判断越准,能对应到具体优化动作才有价值。
四、生成质量到底在评什么
检索正确,不代表答案一定正确。生成阶段至少要分清下面几件事。
Faithfulness:有没有脱离资料发挥
上下文只说“支持 Windows 和 macOS”,回答却补了一句“也支持 Linux”,即使现实中碰巧如此,对当前 RAG 链路来说仍然是不忠实的。Faithfulness 检查的是回答中的事实能否得到上下文支持。
Answer Relevancy:有没有正面回答
用户问“年假有多少天”,模型完整复述了休假申请流程,却没有给出天数。这段回答可能句句有出处,但仍然没有解决问题。
Answer Correctness:和参考答案是否一致
这个指标通常需要reference_answer。参考答案应包含必须出现的事实,不必写成长篇范文,更不要要求生成答案逐字匹配。
Context Precision 与 Context Recall
Context Precision 关心检索上下文中是否混入太多噪声,Context Recall 关心回答需要的事实是否都被上下文覆盖。它们和基于文档 ID 的 Precision、Recall 视角不同,最好同时保留。
如果产品要求附带来源,还应单独检查 Citation Correctness:引用是否存在、版本是否有效、片段是否真的支撑对应结论、用户是否有查看权限。答案末尾挂几个链接,不等于引用可靠。
五、用 RAGAS 把评测跑起来
RAGAS 把 Faithfulness、Answer Relevancy、Context Precision、Context Recall 等指标封装成了可批量执行的流程。它适合扩大评测覆盖面,但不是“客观裁判”:Judge 模型、Prompt、参考答案质量和模型版本变化都会影响得分。
下面基于LangChain 1.1.0、langchain-openai 1.1.0 和 RAGAS 0.3.9验证。RAGAS 的 API 迭代较快,langchain-community的模块结构也发生过变化,因此这里锁定完整依赖组合,而不是只限制大版本范围。
pip install \ "langchain==1.1.0" \ "langchain-openai==1.1.0" \ "langchain-community==0.4.1" \ "ragas==0.3.9" \ "pandas>=2,<3"这里特意将langchain-community固定为 0.4.1。RAGAS 0.3.9 仍会导入langchain_community.chat_models.vertexai,而该模块在 0.4.2 中已经移除;如果不锁定版本,安装可能成功,运行时却会出现ModuleNotFoundError。
运行前还需要设置 OpenAI API Key。不要把密钥直接写进代码或提交到仓库:
# Windows PowerShell $env:OPENAI_API_KEY="your-api-key"# macOS / Linux export OPENAI_API_KEY="your-api-key"准备两条样本:
samples = [ { "user_input": "Windows 客户端出现 1007 错误怎么办?", "response": "关闭系统代理,清理客户端缓存后重新登录。", "retrieved_contexts": [ ( "Windows 客户端出现 ERR_CONN_RESET_1007 时," "先关闭系统代理,清理客户端缓存,然后重新登录。" ), "网络连接失败时,可检查防火墙、DNS 和代理配置。", ], "reference": "关闭系统代理,清理客户端缓存后重新登录。", }, { "user_input": "客户端支持 Linux 离线导出吗?", "response": "现有资料中没有说明 Linux 离线导出能力。", "retrieved_contexts": ["桌面客户端当前支持 Windows 和 macOS。"], "reference": "知识库信息不足,应明确说明无法确认。", }, ]这里使用的是新版字段user_input、response、retrieved_contexts和reference。旧教程里的question、answer、contexts、ground_truth不要混进来。
接着配置 Judge:
from langchain_openai import ChatOpenAI, OpenAIEmbeddings from ragas.llms import LangchainLLMWrapper from ragas.embeddings import LangchainEmbeddingsWrapper judge_llm = LangchainLLMWrapper( ChatOpenAI(model="gpt-4.1-mini", temperature=0) ) judge_embeddings = LangchainEmbeddingsWrapper( OpenAIEmbeddings(model="text-embedding-3-small") )生成模型和 Judge 不必相同。重要实验最好不要让同一个模型既当选手又当裁判,并先用 30~50 条人工确认的样本校准 Judge。
需要说明的是,RAGAS 0.3.9 已经将LangchainLLMWrapper和LangchainEmbeddingsWrapper标记为弃用,但这两个类在上述锁定版本中仍能正常使用。本文保留它们,是为了与 LangChain 1.1.0 组成一套经过验证、可以复现的示例;后续升级 RAGAS 时,应按照对应版本文档迁移到llm_factory和新的 Embedding Provider。
最后执行四个指标:
import asyncio from ragas.dataset_schema import SingleTurnSample from ragas.metrics import ( AnswerRelevancy, Faithfulness, LLMContextPrecisionWithReference, LLMContextRecall, ) faithfulness = Faithfulness(llm=judge_llm) answer_relevancy = AnswerRelevancy( llm=judge_llm, embeddings=judge_embeddings, ) context_precision = LLMContextPrecisionWithReference(llm=judge_llm) context_recall = LLMContextRecall(llm=judge_llm) async def evaluate_sample(item): sample = SingleTurnSample(**item) return { "faithfulness": await faithfulness.single_turn_ascore(sample), "answer_relevancy": await answer_relevancy.single_turn_ascore(sample), "context_precision": await context_precision.single_turn_ascore(sample), "context_recall": await context_recall.single_turn_ascore(sample), } async def main(): results = [] for item in samples: results.append(await evaluate_sample(item)) print(results) asyncio.run(main())上面的asyncio.run(main())适合保存为 Python 脚本后直接运行。如果在 Jupyter Notebook 中执行,由于 Notebook 已经启动事件循环,最后一行应改为:
await main()样本量上来以后,还要补并发限制、超时、重试、结果缓存、单样本错误隔离和费用统计。低分样本最好保存 Judge 的判断理由,否则最后只剩一个分数,很难排查。
RAGAS 不能替代确定性指标
只要评测集标注了相关文档 ID,Hit Rate、Recall 和 MRR 就可以直接计算,不必调用 LLM:
def hit_rate_at_k(retrieved_ids, relevant_ids, k): return float(bool(set(retrieved_ids[:k]) & set(relevant_ids))) def recall_at_k(retrieved_ids, relevant_ids, k): relevant = set(relevant_ids) if not relevant: return 0.0 return len(set(retrieved_ids[:k]) & relevant) / len(relevant) def reciprocal_rank(retrieved_ids, relevant_ids): relevant = set(relevant_ids) for rank, doc_id in enumerate(retrieved_ids, start=1): if doc_id in relevant: return 1.0 / rank return 0.0这些指标便宜、稳定,而且能直接告诉你检索器有没有命中标注结果。完整报告应该同时包含确定性检索指标、RAGAS 指标、延迟和成本。
六、怎么公平比较不同方案
假设要比较三套链路:
方案 A:纯向量检索 方案 B:向量检索 + BM25 + RRF 方案 C:混合检索 + Rerank知识库快照、评测集、生成模型、Prompt、上下文 Token 预算、权限过滤、Judge 和评测 Prompt 都要固定。否则,知识库刚好更新了一批文档,也可能被误判成 Rerank 带来的提升。
| 方案 | Recall@5 | MRR | Faithfulness | P95 延迟 | 单次成本 |
|---|---|---|---|---|---|
| A:纯向量 | 0.78 | 0.69 | 0.82 | 420 ms | ¥0.018 |
| B:混合检索 | 0.86 | 0.76 | 0.85 | 560 ms | ¥0.019 |
| C:混合 + Rerank | 0.88 | 0.84 | 0.91 | 910 ms | ¥0.026 |
表中数字只用于展示报告结构,不代表任何方案的通用性能。
如果业务要求 P95 小于 700 ms,方案 C 即使质量最好,也不适合直接全量上线。更现实的做法可能是:默认走方案 B,只对低置信度或高价值问题启用 Rerank。
评测不是一场单纯的分数竞赛。最终要找的是在质量、延迟、成本和风险之间满足业务约束的方案。
七、让评测真正进入开发流程
一套评测脚本只在上线前跑一次,价值很有限。它应该和单元测试一样,跟着系统持续更新。
可以把数据拆成两部分:
一份固定回归集,用来观察每次改动有没有破坏已有能力;
一份挑战集,专门放表格、多跳、版本冲突、无答案等系统容易失败的问题。线上点踩、转人工和异常引用经过脱敏、复核后,也应补进回归集。
质量门禁不必照抄别人的阈值,可以从自己的业务约束出发。例如:
总体 Recall@5 不得下降超过 1% 错误码分桶 MRR 不得低于 0.85 无答案拒答准确率不得低于 0.90 高风险样本必须通过人工审核 P95 延迟不得超过 800 ms 单次平均成本不得增加超过 20%每次运行还要记录数据集版本、知识库快照、Chunk 策略、Embedding、索引、Retriever、Reranker、Prompt、生成模型、Judge 和代码版本。否则几周后再看到一条Faithfulness = 0.91,很可能已经不知道它是怎么跑出来的。
LLM-as-a-Judge 也要定期抽检。它可能偏爱长答案,可能偏好与自己风格相似的表达,也可能因为模型升级改变评分尺度。自动评测适合做大范围回归和筛查,财务、法务、医疗等高风险结论仍然需要领域人员确认。
八、总结
RAG 难优化,不是因为没有参数可调,而是因为可调的地方太多。Chunk、Embedding、Top K、混合检索、Rerank、Prompt 和生成模型都可能改善一类问题,同时破坏另一类问题。
一套够用的起步方案其实并不复杂:先整理 100~300 条真实问题,标注参考答案和证据;同时计算 Recall、MRR 等确定性指标与 RAGAS 指标;按标签查看退化;最后把质量、延迟和成本放在同一张报告里比较。
不必一开始就搭评测平台。一份版本化的 JSONL、一个可重复运行的脚本、一份能看到 Bad Case 的对比报告,已经比“改完随手问几个问题”可靠得多。
评测不是 RAG 优化的收尾工作,而是每一次“效果变好了”能够成立的前提。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~