评测体系:给评测做评测——hollow eval 事故、异步化与「全绿 ≠ 无缺陷」
发布时间:2026/9/27 21:31:53来源:尧图网络
评测体系给评测做评测——hollow eval 事故、异步化与「全绿 ≠ 无缺陷」语言 / Language中文 系列第九章目录 上一章全链路延迟与稳定性调优项目Agentdemo007 —— 电商智能客服 Agent技术栈Java 17 / Spring Boot / 自研 eval 包黄金集 强类型断言 异步作业 Nacos黄金集托管/ Vue3 评测页周期Phase 15评测基座→ 2026-09-17Nacos 双源→ 09-18异步化→ Phase 20 T92hollow eval 修复验证规模默认套件 9 个 stage 63 例全量加载断言GoldenSuiteTesteval 包 38 例单测源码github.com/Gavincui123/Agentdemo007前言评测体系最容易死在两个地方断言是空的测了但什么都没测以及评测本身把系统拖垮同步跑全量真 LLM——这两件我都真实踩过。前者是 hollow eval 事故黄金集字段全绿评审时才发现 JSON 里的扩展字段被宽容反序列化静默丢弃、断言从未执行我在 Phase 20 用 T92内部修复工单编号后文 T76/T88 同类把期望收口成 13 参的编译期契约。后者是全量评测拖到前端 axios 30s 超时后端还在跑且全程无进度修复靠异步作业秒回加进度轮询顺带把黄金集从 classpath 搬进 Nacos 双源改数据不再跟着发布走。还有两笔账要单独记熔断这种跨请求累积状态天生进不了一请求一断言的黄金集我选择诚实登记、改由单测覆盖而 2026-09-19 交付当天 1213 个测试 0 失败人工走查照样翻出一串缺陷。从那份报告起全绿和无缺陷在交付文档里分开陈述——这是本章最想留下的结论。一、黄金集长什么样动笔写评测之前我先回答一个问题断言应该长什么样。直觉方案是把期望写成MapString, Object反序列化完直接比省事——我否决了它。Map 里没有任何机制逼你为某个字段写对照逻辑JSON 写了这个字段和系统真的比了这个字段是两回事而第二节的事故会证明这道裂缝连强类型都未必堵得住。所以评测的基本单位我收口成「stage → case → expected」每个 stage 一个 JSON意图、路由、降级、注入、RAG、HITL、审计……每例声明输入与一个 13 参的强类型 record——字段名就是契约。// eval/EvalExpected.java —— 期望断言强类型收口非 MappublicrecordEvalExpected(Stringscenario,Stringintent,Stringroute,StringselectedModel,Booleandegraded,Stringoutcome,Booleanblocked,BooleanzeroLlm,BooleanshortCircuit,BooleanescalatedToModel,StringqueryEnrichmentContains,StringragFragmentsContain,BooleanhasFragments){/** 既有 10 参构造Phase 15 调用方零改动新字段缺省 null 不参与比较。 */publicEvalExpected(…){…}}运行时执行器把流水线实际产出收口成ActualOutcome含派生字段与期望逐字段对照产出 mismatches 明细。这套设计有意让期望与实际两侧都是强类型新增一个断言维度等于加字段、加对照逻辑、加测试三件事绑在一起编译器逼着你接完。在这套护栏下JSON 里写了个字段但没人读的幻觉理论上不存在——下一节就是幻觉真实发生的故事。二、hollow eval一次「全绿但什么都没测」的事故事故是 Phase 20 检索增强做完后、评审时暴露的发现了让整个评测体系蒙羞的一幕早期的EvalExpected设计里就有standardQueryContains/hasFragments/summaryTriggered这些字段JSON 里也写了——但 record 上没有这些参数。Jackson 反序列化配了FAIL_ON_UNKNOWN_PROPERTIESfalse当初是为了容忍 JSON 里的 context 前置条件字段结果这些期望字段被静默丢弃发现既有EvalExpected的standardQueryContains/hasFragments/summaryTriggered字段被FAIL_ON_UNKNOWN_PROPERTIESfalse静默丢弃——文档字段从未被对照hollow eval。——开发计划 T92 注记也就是说黄金集里写着期望 standardQuery 含’Q3 销售’、“期望有 RAG 片段”评测一直全绿——因为断言从未执行。宽容的反序列化配置 弱类型 JSON 期望共同制造了看起来在测的空转。修复分三步全部围绕同一个原则让文档字段变成编译期契约。EvalExpected从 10 参扩到 13 参新增queryEnrichmentContains/ragFragmentsContain/hasFragments10 参的次级构造原样保留Phase 15 以来的既有调用方零改动。ActualOutcome增 3 个派生字段全部从PipelineContext的实际产出派生。compare增两个对照算子再配 4 例单测把对照逻辑本身钉死// eval/EvalExecutor.java —— Phase 20T92新断言维度的对照算子// Phase 20T92约束改写补全槽 RAG 片段含时效标注checkContains(mismatches,queryEnrichmentContains,expected.queryEnrichmentContains(),actual.queryEnrichmentKeywords());checkAnyContains(mismatches,ragFragmentsContain,expected.ragFragmentsContain(),actual.ragFragments());checkBoolean(mismatches,hasFragments,expected.hasFragments(),actual.hasFragments());/** 期望某精确词在补全槽关键词列表中T88 约束改写补全。 */privatevoidcheckContains(ListStringmismatches,Stringfield,Stringexpected,ListStringactual){if(expected!null!actual.contains(expected)){mismatches.add(field: expected~containsexpected actualactual);}}/** 期望某文本子串在任一 ragFragments 元素中T89 Hybrid 命中 / T90 时效标注。 */privatevoidcheckAnyContains(ListStringmismatches,Stringfield,Stringexpected,ListStringactual){if(expected!nullactual.stream().noneMatch(f-f!nullf.contains(expected))){mismatches.add(field: expected~anyContainsexpected actualactual);}}有一个维度我没救回来只能诚实登记escalatedToModel是否上交小模型仲裁不可从实际产出派生跳过比较——它被记为已知限制而不是假装能测。hollow eval 的教训浓缩成一条评测断言的存在必须在类型系统里可验证JSON 字段 宽容反序列化 自欺欺人的温床。三、同步评测拖爆 HTTP异步化第二件事不用等评审用起来当天就会炸。评测要跑真实流水线每例都是真实 LLM 调用最初实现图省事把全量评测塞进 HTTP 请求里同步跑。结果前端 axios 30s 超时报网络连接失败后端还在跑而且全程无进度——用户不知道系统是死是活。排查结论不复杂跑几十秒量级的批处理被当成了请求处理评测结果的生命周期绑死在 HTTP 连接上两头受气是必然。修复是异步化三件套时序如下eval-runner 守护单线程EvalJobManagerCAS 单作业POST /eval/run评测页前端eval-runner 守护单线程EvalJobManagerCAS 单作业POST /eval/run评测页前端loop[逐 stageNacos 拉数据 → 解析 →逐例跑]loop[前端每 1s]启动评测1start(resources)2running.compareAndSet(false,true)已在跑 → 409 CONFLICT3秒回启动快照runId/startedAt4runner.execute(runAll)5单 stage 失败 → 跳过记 skipped不杀整体6GET /eval/progress7不可变进度快照分环节亮灯 mismatches 明细8// eval/EvalJobManager.java —— 评测异步作业管理器/** 启动评测作业已有作业在跑 → false调用方转 CONFLICT 话术。 */publicbooleanstart(ListStringresources){if(!running.compareAndSet(false,true)){returnfalse;}… runner.execute(()-runAll(resources,runId,startedAtMs));returntrue;}// runAll 内每步降级}catch(Exceptione){// ②每步降级单 stage 数据缺失/解析失败 → 跳过其余照常log.warn(评测 stage 加载/执行失败跳过resource{} reason{},resource,e.getMessage());skipped.add(stageName);}三个工程细节值得单独说。进度快照不可变单写线程整体重建快照HTTP 读线程随意读靠不可变性换来无锁的发布安全性。重复启动收 409 CONFLICT用 CAS 防双跑而不是靠前端禁用按钮。单 stage 失败跳过不杀整体评测是批处理一个 stage 的数据问题不该清空其他 stage 的结果。配套的安全故事也要交代/eval/**在发布态仅限本机 loopback且检查先于 token 鉴权——评测跑真 LLM 全量黄金集比聊天更烧钱必须比聊天更严第六章闸口章的同款裁决。四、黄金集双源改测评数据不重启黄金集最初编译进 classpath代价是每改一个期望值都要重新打包部署——评测数据的迭代节奏被发布节奏绑架。可评测数据是数据不是代码迭代不该跟着发布走所以我给它做了双源Nacos 优先、本地兜底永不抛。// eval/EvalContentResolver.java —— Nacos 优先、本地兜底永不抛publicResolvedresolve(StringclasspathResource){StringstagestageOf(classpathResource);if(nacosEnabled){StringdataIddataIdPrefix-stage.json;try{StringcontentconfigService.getConfig(dataId,group,timeoutMs);if(content!null!content.isBlank()){returnnewResolved(content,SOURCE_NACOS);}log.info(Nacos 无评测数据dataId 不存在或为空回退本地 classpathdataId{},dataId);}catch(Exceptione){log.warn(Nacos 评测数据读取失败回退本地 classpathdataId{} reason{},dataId,e.getMessage());}}returnnewResolved(readClasspath(classpathResource),SOURCE_LOCAL);}每次POST /eval/run实时拉取dataId 形如agentdemo-eval-stage.json改完 Nacos 下一次 run 即生效读取失败、未配置或为空就逐 stage 回退本地 classpath。来源标签NACOS/LOCAL随报告透传到前端徽标评测结果的出处可见排查为什么期望变了时不用猜。两个防御性参数也交代一下拉取超时 3sConfigService构造失败直接降级 local-only不阻塞启动——评测数据源坏了评测照常能跑。五、评测的评测哪些东西测不了要明说一套评测体系可信与否取决于它对自身边界的诚实程度。这一节记三件我明确不测的事每件都有写下来的理由。熔断语义进不了黄金集。熔断 OPEN 是跨请求累积状态而 eval 一请求一断言单用例内无法复现连挂 N 次后快速失败——这是 T76 登记的关键限制。结论是不硬塞不扩eval/tool.json改由单测覆盖含用 AtomicLong 控时验证三态流转并把这条限制写进开发计划。GoldenSuiteTest只验加载与跑通不验通过率。黄金套件在测试环境用 trivial 执行器恒返回 ok冒烟验证的是63 例全部能加载、能执行、能收口通过率依赖真实流水线属部署门禁。这样既避免单测环境烧真 LLM也避免单测绿 业务达标的误读。stage 失败是skipped不是passed。异步评测里数据缺失的 stage 单独记账进度条上它不亮绿灯——把没测和测过在 UI 层就分开。六、全绿 ≠ 无缺陷2026-09-19 拒答 知识库录入交付走查报告专门立了一节「全绿≠无缺陷」缺陷清单。那天 1213 个测试 0 失败人工走查照样翻出一串测试覆盖不到的问题。最要紧的四条录入主链无事务 换版先于索引嵌入失败窗口内旧向量已删、新版未落库该文档在检索侧消失版本号读-改-写无唯一约束并发重灌可产生重复同版本 ACTIVE 行目录快照多副本盲区多实例下新录文档在其他实例不可见fail-closed、PRIVATE 收紧不扩散链路异常被当作知识未命中RagStep 的 catch(Exception) 也置groundingMissstrict 模式下存储宕机会把系统故障包装成知识库暂无资料。第 4 条在第五章展开过。把它们放回评测章是因为这四条撑起了本章的核心论点测试全绿证明测过的行为对走查才证明没测的行为不存在。两者是互补的证据不是替代。七、经验小结这一章的经验都是从上面这些坑里长出来的断言必须活在类型系统里。JSON 期望 宽容反序列化 hollow eval新增断言维度要让编译器强迫你接完对照逻辑。批处理别绑在请求生命周期上。作业 CAS 单实例 秒回 轮询快照是我后来所有跑了很久的按钮的标准答案。评测数据是数据不是代码。放 Nacos 双源 来源徽标迭代节奏与发布节奏解耦。测不了的要写下来不是假装测了。escalatedToModel跳过比较、熔断语义归单测、trivial 套件只验加载——边界写得越清楚评测越可信。全绿与无缺陷分开陈述。评测覆盖行为回归人工走查覆盖没人想到要测的缝交付报告把两者并列是诚实的最低要求。八、已知边界诚实清单通过率是部署门禁单测环境不跑真实流水线评测通过率依赖部署后手动/门禁触发。评测进度无持久化EvalJobManager快照在内存进程重启丢当前作业评测是低频运营动作可接受。对照仍非全量escalatedToModel不可派生跳过ActualOutcome覆盖不了的输出质量问题话术好不好听本就不该由黄金集管——那是人工评测与用户反馈的地盘。本文机制出处eval/EvalExpected / EvalExecutor / EvalJobManager / EvalContentResolver / EvalController黄金集资源src/main/resources/eval/*.json15 个 stage 文件缺陷清单全文见 2026-09-19 拒答录入报告 §7。相关阅读系列目录 · 第六章·eval 封锁与闸口 · 第五章·strict 模式故障误报缺陷 · 下一章前端与流式交互
网站建设高端定制企业官网