Python后端AI专题33:评估召回与答案质量:Recall@K、MRR、nDCG 和引用正确率

发布时间:2026/9/30 2:31:33

Python后端AI专题33:评估召回与答案质量:Recall@K、MRR、nDCG 和引用正确率 Python后端AI专题33评估召回与答案质量RecallK、MRR、nDCG 和引用正确率回答错了可能是相关 chunk 没召回、召回了却排太后、模型没使用证据或引用了错误来源。只看“答案准确率 80%”无法定位该修分块、Embedding、Rerank 还是 Prompt。评测必须把检索、生成和引用拆开。坏标注测试与判断答案loader 现在完整覆盖重复 id、可回答无 relevant、不可回答却有 relevant、空文件评测单测结果8 passed in 0.17s。“退款多久到账”与“七天内申请”是不同事实。expected_facts七天会诱导模型把申请期限当到账时间应把该样本标为不可回答或加入真正说明到账时间的证据和正确事实。评测集本身错误时模型回答正确也会被判错所以失败样本必须回看原文。RecallK相关证据有没有进候选RecallK Top K 命中的相关项数 / 全部相关项数排名[a,b,c,d]相关{b,d}K3只命中 b所以 Recall31/2。Recall 高不代表顺序好把正确文档放第 50 也可能在 K100 时满分但上下文根本放不下。MRR第一个正确结果有多早RR 1 / 第一个相关项名次 MRR 所有问题 RR 的平均上例第一个相关 b 在第 2RR0.5。MRR 特别适合用户通常只看第一条、或 RAG 很依赖头部证据的场景它不关心第二个相关项是否找到。nDCG多个相关项的整体位置二元相关时DCGK Σ relevant(rank) / log2(rank1) nDCG DCG / 理想排序的 DCG上例 K3只有 rank2 的 b1/log2(3)0.63093。两个相关项的理想 DCG 是1/log2(2)1/log2(3)1.63093所以 nDCG≈0.38685。完整指标模块from__future__importannotationsimportmathfromcollections.abcimportSequence,Setdefrecall_at_k(ranked_ids:Sequence[str],relevant_ids:Set[str],*,k:int)-float:ifnotrelevant_ids:return0.0returnlen(set(ranked_ids[:k])set(relevant_ids))/len(relevant_ids)defmean_reciprocal_rank(ranked_ids:Sequence[str],relevant_ids:Set[str])-float:forrank,item_idinenumerate(ranked_ids,start1):ifitem_idinrelevant_ids:return1.0/rankreturn0.0defndcg_at_k(ranked_ids:Sequence[str],relevant_ids:Set[str],*,k:int)-float:ifnotrelevant_ids:return0.0dcgsum(1.0/math.log2(rank1)forrank,item_idinenumerate(ranked_ids[:k],start1)ifitem_idinrelevant_ids)ideal_hitsmin(len(relevant_ids),k)idealsum(1.0/math.log2(rank1)forrankinrange(1,ideal_hits1))returndcg/idealifidealelse0.0defcitation_precision(cited_numbers:Sequence[int],supported_numbers:Set[int])-float:ifnotcited_numbers:return0.0returnsum(numberinsupported_numbersfornumberincited_numbers)/len(cited_numbers)citation_precision([1,3,9], {1,3})2/3。还可增加 citation recall应引用的事实有多少带了出处当前项目先实现精度和“至少一个有效引用”的在线闸门。Runner 怎样处理不可回答样本不可回答且无召回结果时检索指标按 1.0 记录该样本行为但汇总检索分数优先只平均 answerable 样本避免不可回答比例改变掩盖召回退化。答案正确则查拒答关键词。当前字符串包含法简单可解释但无法理解同义表达。真实评测可加入结构化答案、规则归一化、人工评审或 judge modelJudge 也会偏差需要标注集校准不能当真理。Runner 输出每条明细不只总分{id:refund-deadline,ranked_ids:[refund-policy],recall_at_k:1.0,mrr:1.0,ndcg_at_k:1.0,citation_precision:1.0,answer_correct:true,latency_seconds:0.0000165}EvaluationRunner.run()在调用每个evaluate(case)前后使用perf_counter()计时逐条保存latency_seconds再用 nearest-rank 方法计算p95_seconds。这样下一篇的发布门禁所需 P95 来自真实评测执行不再是手工塞进示例字典的孤立字段。同时记录 config_hash、时间、K 和样本数。没有配置身份的两个 0.85 无法比较。计时覆盖本项目传入的完整evaluate函数若真实环境还包含网关排队或响应序列化就必须把计时边界放到包含这些步骤的调用处不能把局部耗时冒充端到端 P95。当前离线接线结果.\.venv\Scripts\python scripts\run_evaluation.py --output..\reports\rag-evaluation.json{summary: {recall_at_k: 1.0, mrr: 1.0, ndcg_at_k: 1.0, citation_precision: 1.0, answer_accuracy: 1.0, p95_seconds: 1.650000922381878e-05}, release_gate_failures: []}这是本次 Fake 运行的真实输出P95 会随机器调度变化不应照抄成性能结论。Fake 根据标注构造完美结果只证明报表链不证明语义真实模型报告必须另存并注明未运行时绝不伪造。需要比较候选版本时保存上一份报告并传入.\.venv\Scripts\python.exe scripts\run_evaluation.py --baseline..\reports\rag-evaluation-baseline.json --output..\reports\rag-evaluation-candidate.jsonCLI 会把候选summary交给下一篇的check_release_gate()。所有硬门槛通过时退出码为 0存在失败原因时打印完整release_gate_failures并以退出码 2 结束所以 CI 不会把“生成了报告”误当成“允许发布”。指标之间怎样做决策Recall 降、MRR 降先查解析/分块/召回Recall 不变、MRR 降融合或重排退化检索不变、答案准确降Prompt/模型/上下文组织答案准确不变、引用精度降引用协议或校验质量上升但 P95/成本翻倍需要产品权衡不是自动接受。本篇最终完整模块metrics.py前面的代码片段用于解释本次改动下面是本篇结束时可直接核对和替换的磁盘完整版本。from__future__importannotationsimportmathfromcollections.abcimportSequence,Setdefrecall_at_k(ranked_ids:Sequence[str],relevant_ids:Set[str],*,k:int)-float:ifnotrelevant_ids:return0.0returnlen(set(ranked_ids[:k])set(relevant_ids))/len(relevant_ids)defmean_reciprocal_rank(ranked_ids:Sequence[str],relevant_ids:Set[str])-float:forrank,item_idinenumerate(ranked_ids,start1):ifitem_idinrelevant_ids:return1.0/rankreturn0.0defndcg_at_k(ranked_ids:Sequence[str],relevant_ids:Set[str],*,k:int)-float:ifnotrelevant_ids:return0.0dcgsum(1.0/math.log2(rank1)forrank,item_idinenumerate(ranked_ids[:k],start1)ifitem_idinrelevant_ids)ideal_hitsmin(len(relevant_ids),k)idealsum(1.0/math.log2(rank1)forrankinrange(1,ideal_hits1))returndcg/idealifidealelse0.0defcitation_precision(cited_numbers:Sequence[int],supported_numbers:Set[int])-float:ifnotcited_numbers:return0.0returnsum(numberinsupported_numbersfornumberincited_numbers)/len(cited_numbers)本篇最终完整模块runner.py前面的代码片段用于解释本次改动下面是本篇结束时可直接核对和替换的磁盘完整版本。from__future__importannotationsfromcollections.abcimportAwaitable,Callable,Sequencefromdatetimeimportdatetime,timezonefrommathimportceilfromstatisticsimportfmeanfromtimeimportperf_counterfromapp.services.evaluation.datasetimportEvaluationCasefromapp.services.evaluation.metricsimportcitation_precision,mean_reciprocal_rank,ndcg_at_k,recall_at_k EvaluationFunctionCallable[[EvaluationCase],Awaitable[tuple[list[str],str,list[int],set[int]]]]classEvaluationRunner:def__init__(self,*,k:int5)-None:self.kkasyncdefrun(self,cases:Sequence[EvaluationCase],*,evaluate:EvaluationFunction,config_hash:str,)-dict[str,object]:results:list[dict[str,object]][]forcaseincases:startedperf_counter()ranked_ids,answer,cited,supportedawaitevaluate(case)latency_secondsperf_counter()-started relevantset(case.relevant_ids)recall1.0ifcase.unanswerableandnotranked_idselserecall_at_k(ranked_ids,relevant,kself.k)mrr1.0ifcase.unanswerableandnotranked_idselsemean_reciprocal_rank(ranked_ids,relevant)ndcg1.0ifcase.unanswerableandnotranked_idselsendcg_at_k(ranked_ids,relevant,kself.k)ifcase.unanswerable:answer_correctany(terminanswerfortermin(没有足够证据,无法回答,证据不足))else:answer_correctall(factinanswerforfactincase.expected_facts)precisioncitation_precision(cited,supported)ifcitedelse(1.0ifcase.unanswerableelse0.0)results.append({id:case.id,question:case.question,ranked_ids:ranked_ids,answer:answer,recall_at_k:recall,mrr:mrr,ndcg_at_k:ndcg,citation_precision:precision,answer_correct:answer_correct,latency_seconds:latency_seconds,})answerable[itemforitem,caseinzip(results,cases,strictTrue)ifnotcase.unanswerable]retrieval_populationanswerableorresults ordered_latenciessorted(float(item[latency_seconds])foriteminresults)p95_indexmax(0,ceil(len(ordered_latencies)*0.95)-1)return{generated_at:datetime.now(timezone.utc).isoformat(),config_hash:config_hash,k:self.k,case_count:len(results),summary:{recall_at_k:fmean(float(item[recall_at_k])foriteminretrieval_population),mrr:fmean(float(item[mrr])foriteminretrieval_population),ndcg_at_k:fmean(float(item[ndcg_at_k])foriteminretrieval_population),citation_precision:fmean(float(item[citation_precision])foriteminresults),answer_accuracy:fmean(bool(item[answer_correct])foriteminresults),p95_seconds:ordered_latencies[p95_index],},cases:results,}本篇最终完整模块run_evaluation.py前面的代码片段用于解释本次改动下面是本篇结束时可直接核对和替换的磁盘完整版本。from__future__importannotationsimportargparseimportasyncioimporthashlibimportjsonimportsysfrompathlibimportPath# Direct script execution puts only scripts/ on sys.path. Add the project root# explicitly so the README command behaves the same before and after install.sys.path.insert(0,str(Path(__file__).resolve().parents[1]))fromapp.services.evaluation.datasetimportEvaluationCase,load_casesfromapp.services.evaluation.gatesimportcheck_release_gatefromapp.services.evaluation.runnerimportEvaluationRunnerasyncdeffake_evaluate(case:EvaluationCase)-tuple[list[str],str,list[int],set[int]]:ifcase.unanswerable:return[],当前知识库没有足够证据回答。,[],set()answer.join(case.expected_facts)。[1]returnlist(case.relevant_ids),answer,[1],{1}defrelease_decision(baseline_path:Path,candidate_summary:dict[str,float])-list[str]:Compare the new run with a saved baseline using the projects hard gates.payloadjson.loads(baseline_path.read_text(encodingutf-8))baselinepayload.get(summary)ifnotisinstance(baseline,dict):raiseValueError(baseline report must contain a summary object)returncheck_release_gate(baseline,candidate_summary)asyncdefmain()-None:parserargparse.ArgumentParser()parser.add_argument(--dataset,typePath,defaultPath(evals/rag_cases.jsonl))parser.add_argument(--output,typePath,defaultPath(../reports/rag-evaluation.json))parser.add_argument(--baseline,typePath,defaultNone,helpoptional prior report; exit 2 when the candidate violates a hard gate,)parser.add_argument(--provider,choices[fake],defaultfake)argsparser.parse_args()casesload_cases(args.dataset)config_hashhashlib.sha256(args.dataset.read_bytes()args.provider.encode()).hexdigest()[:16]reportawaitEvaluationRunner(k5).run(cases,evaluatefake_evaluate,config_hashconfig_hash)args.output.parent.mkdir(parentsTrue,exist_okTrue)args.output.write_text(json.dumps(report,ensure_asciiFalse,indent2),encodingutf-8)summaryreport[summary]ifnotisinstance(summary,dict):raiseTypeError(evaluation report summary must be an object)reasonsrelease_decision(args.baseline,summary)ifargs.baselineelse[]print(json.dumps({summary:summary,release_gate_failures:reasons},ensure_asciiFalse,))ifreasons:raiseSystemExit(2)if__name____main__:asyncio.run(main())本篇练习做一次版本门禁判定基线Recall .90、MRR .76、nDCG .72、引用精度 .98、答案准确 .84、P95 2.0s。候选.92/.74/.75/.96/.87/2.8s。门槛Recall 不下降MRR 最多降 .01引用精度不低于 .97答案准确不下降P95 增幅不超过 20%。逐项计算并给最终是否发布。再写 Python 函数返回所有失败原因而不是遇到第一个就停止。下一篇给出完整门禁函数然后从一次慢请求开始把 embedding/vector/keyword/rerank/model 分段耗时、结构化日志和 Prometheus 指标串起来。
延伸阅读

更多相关文章

2026/9/30 2:31:33

SAP单SQL语句导致HANA内存溢出:从告警到修复的完整排查实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 2:31:33

【更新至2025年】2000-2025年上市公司管理层治理能力指标数据

【更新至2025年】2000-2025年上市公司管理层治理能力指标数据 1、时间:2000-2025年 2、来源:上市公司年报 3、指标:证券代码、证券简称、统计截止日期、行业代码、行业名称、行业代码1、行业名称1、产权性质、实际控制人拥有上市公司所有权…

2026/9/30 2:31:33

eVTOL低空经济无人机AI图像处理系统:从方案到工程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 3:36:36

Vue3 + Nginx 在 Windows 部署的三大核心冲突与配置原理

1. 为什么Vue3项目在Windows上用Nginx部署,不是“能用就行”,而是“必须这样搭”你肯定试过:npm run build打包完,把dist文件夹拖进 Nginx 的html目录,双击nginx.exe启动,浏览器打开http://localhost—— 页…

2026/9/30 3:36:36

Windows安装小龙虾姿态估计工具:从环境配置到跑通全流程

实不相瞒,我第一次听到“Windows 安装小龙虾”这个说法的时候,以为是朋友发来的一道脑筋急转弯。后来才知道,他说的是GitHub上一个开源姿态估计工具,中文社区都叫它“小龙虾”。这个工具专门用来检测视频和图片里小龙虾的关键点&a…

2026/9/30 3:36:36

2004-2025研究生数学建模竞赛试题可检索题库构建与训练

第一次系统整理这套题是2016年前后,起因很简单:手上散落着几十个文件名各异的PDF,有的叫“A题.pdf”,有的叫“2013年试题”,还有的干脆是“final2(1).pdf”,想找某一届的某道题得靠回忆。后来带队伍、做赛前…

2026/9/30 3:36:36

UML图实战指南:软件工程师高效建模的8种核心图型与应用

作为软件工程师,你可能对UML的印象还停留在大学课程和软考备考资料里——一堆矩形框和箭头,画了也没有代码跑得快。工作几年后你会发现,真正让项目失控的,往往不是某段代码写得烂,而是团队对“这个模块到底是什么、之间…

2026/9/30 3:36:36

MySQL慢查询日志:从配置到优化的性能排查指南

排查MySQL性能问题,我习惯做的第一件事就是翻慢查询日志。这个文件会按照我们设定的阈值,把所有执行时间超标的SQL原原本本记录下来,是定位数据库性能瓶颈最直接的入口。不管你是后端开发、运维还是DBA,只要跟MySQL打交道&#xf…

2026/9/30 3:31:36

K8s从入门到生产实践:踩坑总结与核心原理剖析

搞K8s这几年,踩过的坑比写过的yaml都多。之前帮一个朋友排查节点初始化问题,日志停在[init] using kubernetes version: v1.26.0和[preflight] running pre-flight checks半天不动,最后发现是cgroup驱动和容器运行时没对齐,这问题…

2026/9/29 11:07:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 0:01:22

MATLAB+Yalmip+CPLEX实战:综合能源系统优化调度全流程解析

做综合能源系统优化调度这活儿,最痛苦的不是建模本身,而是模型写完之后不知道该怎么求解。看论文里轻飘飘一句“采用Yalmip调用CPLEX求解”,自己上手时却往往卡在环境配置、变量声明、约束写法和求解状态判读上,一耗就是两三天。这…

2026/9/30 0:01:22

I3C比I2C快10倍?RK3576实战:速率、DTS配置与混合总线避坑指南

I3C 比 I2C 快 10 倍?这句话在嵌入式群里传了很久,每次都能吵出一堆截图。前段时间我正好在 RK3576 上调板级 I3C 接口,从控制器寄存器一路摸到 Linux DTS 配置,踩了不少坑,也把这笔速度账彻底算明白了。本文就用 RK35…

2026/9/30 0:01:22

字符串转对象:JSON.parse、new Function与URLSearchParams

“字符串转对象”这几个字,我在技术群里见过的问法至少有十几种:有人拿着一串{a:1,b:2}说 JSON.parse 直接报错,有人要从 URL 里抠出参数,还有人只是想把abc变成能挂属性的东西。js 这门语言里,字符串和对象之间的转换…

2026/9/29 3:53:39

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/29 9:46:12

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/29 6:36:14

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑