返回 AICAP-180
B5 · Day 49tool/context engineering + 指标树

指标仪表盘雏形

Day 43 我们画了指标树(NSM=task 自主完成率 + 叶子指标),Day 46/48 我们(待 key 后)跑出了真实的 A/B 报告。

阶段: B5 · tool/context engineering + 指标树(Day 41-50) 标签: #dashboard #live-data #metric-tree #evals-ops

今日导引(由浅入深)

Day 43 我们画了指标树(NSM=task 自主完成率 + 叶子指标),Day 46/48 我们(待 key 后)跑出了真实的 A/B 报告。

今天(Day 49)在 B1→B18 曲线上是把这两条线缝合的一步:写一个吃活数据的聚合脚本,从 agent-evals/reports/ 的真实跑批报告里读数,映射回指标树的 NSM + 叶子,输出 JSON + 简图。

关键纪律是「live,不硬编码」——仪表盘要么显示真实数据,要么诚实降级显示「还没有报告」,绝不编造好看的 demo 数字。

明天(Day 50)的 Block 收口会把这个仪表盘连同 Δ、context-budget 表一起定稿。

今天的最小可判定产出:聚合脚本跑通,对缺数据诚实降级出占位 JSON/chart(脚本待建,活数据待 key)。

1. 机理精读

仪表盘的第一性原理:它是数据源的视图,不是装饰。 一个仪表盘只有在它如实反映底层 eval 报告时才有价值。 最常见的反模式是「先做个好看的图,里面填几个 demo 数字」——这种仪表盘在评审时一戳就破,因为数字无法溯源到任何真实跑批。 本项目的硬约束是:仪表盘必须从 agent-evals/reports/ 下的真实报告 JSON 读数,每个展示值都能追到某次具体跑批。 Anthropic《Demystifying evals》(2026-01) 把 eval 报告本身当作数据源(eval 报告即 data source),仪表盘只是它的聚合呈现层。

诚实降级(graceful degradation)优于编造。reports/ 为空(还没跑过真实 eval,比如缺 key 时),正确行为不是填占位假数据,而是显式标注「无数据」。 本仓 buildDashboard([]) 在 reports 为空时返回 nsm.value = null 并带 note: 'no eval reports yet — run pnpm eval:agent (needs an API key) to populate live data'。 这把「缺数据」这个事实本身变成仪表盘的一部分,而不是用假数据掩盖它——这正是这套笔记的诚信底线在工程上的体现。

指标树到仪表盘的映射关系。 Day 43 的指标树是抽象骨架,仪表盘是它的实例化:

  • 根节点 NSM「task 自主完成率」对应 completionRate 的均值;
  • 叶子指标对应 partialCreditMean(部分得分均值)、codePassRate(代码检查通过率)、unknownRate(判 unknown 比例)、totalCostUsd(单次运行成本)、judgeHumanKappa(判分一致性,连回 Day 47)。 每个叶子都映射到 eval 报告里一个真实存在的字段,不引入任何报告里没有的派生臆造量。

纯函数核 + fs 边界分离。 仪表盘逻辑被刻意拆成两层:纯聚合(src/agent/eval/dashboard.tsbuildDashboard,只吃 reports 数组、不碰文件系统)和 IO 读取(scripts/build-dashboard.ts,负责 readdir/读 JSON)。 这样纯逻辑可单测、可在浏览器/bundler 环境复用,而文件读取留在脚本侧。 这是本仓一贯的「pure core / impure shell」分层,保证仪表盘逻辑本身离线可测。

边界:仪表盘不产生新结论,只聚合既有报告。 仪表盘的 NSM/叶子值全是对 reports 的算术聚合(均值/求和),它不做统计推断、不算 CI——那是 abCompare/cohensKappa 的职责。 仪表盘的价值在「一眼看全」,不在「证明什么」。 把推断和展示分清,避免仪表盘里出现没有 CI 的裸数字被误当结论。

与「指标树」的承接关系。 Day 43 的指标树回答「该测什么」(NSM + 叶子的语义结构),今天的仪表盘回答「测出来的数怎么活着呈现」。 两者是定义层与呈现层的关系:指标树一旦改了(比如新增一个叶子「平均步数」),仪表盘的 leaves 数组就要同步加一行,且必须能在报告 JSON 里找到对应字段——否则就是「定义了却测不到」的空指标。 这也是为什么 EvalReportLike 的字段集要和指标树叶子一一对齐:每个展示叶子背后都有一个被 runTaskEval 真实写出的报告字段。这种「指标树 ↔ 报告 schema ↔ 仪表盘叶子」的三方对齐,是 evals-ops 不流于装饰的关键约束。

2. 代码走读:dashboard.ts + build-dashboard.ts

  • buildDashboard(reports)src/agent/eval/dashboard.ts,纯函数、无 key):入参 EvalReportLike[],每份含 n / completionRate / partialCreditMean / unknownRate / codePassRate / totalCostUsd / judgeHumanKappa
  • 空报告降级if (reports.length === 0) 直接返回 runs:0, totalTasks:0, nsm.value:null,并写 note: 'no eval reports yet — run pnpm eval:agent (needs an API key) to populate live data'——这就是 seed 说「诚实降级而非编造」的代码落点。
  • 类型安全的取数:内部 num(sel)reports.map(sel) 过滤成纯 number 数组(filter((x): x is number => typeof x === 'number')),跳过缺失字段而不报错。
  • NSMnsm: { name: 'task autonomous completion', value: avg(num(r => r.completionRate)), unit: 'rate' }——根节点取所有报告 completionRate 的均值。
  • totalTasksnum(r => r.n).reduce((s,x)=>s+x, 0) 把每份报告的任务数求和。
  • 叶子leaves 数组依次给出 partial-credit mean、code-check pass(rate)、unknown rate、cost (USD/run)、judge-human kappa(从 r.judgeHumanKappa?.kappa 收集后取均值)——精确对应指标树叶子。
  • avg(xs):空数组返回 null(不是 0),让「无数据」和「真值为 0」可区分。
  • IO 侧 scripts/build-dashboard.tsreaddirSync(dir) 过滤 .json、逐个 JSON.parse(解析失败的报告 return null 后过滤掉,容错),喂给 buildDashboard,写 agent-evals/dashboard.json 并打印;if (snap.runs === 0) 时额外提示「no reports yet — run pnpm eval:agent first」。

两种输出对照:降级 vs 活数据

空 reports(缺 key)时 buildDashboard([]) 的输出形态

{
  "runs": 0,
  "totalTasks": 0,
  "nsm": { "name": "task autonomous completion", "value": null, "unit": "rate" },
  "leaves": [],
  "note": "no eval reports yet — run `pnpm eval:agent` (needs an API key) to populate live data"
}

注意 nsm.valuenull 不是 0leaves 是空数组——仪表盘据此渲染「暂无数据」而非一条 0% 的假曲线。

喂入 N 份真实报告后(待 key),同一函数会把 nsm.value 填成各报告 completionRate 的均值、leaves 填齐 5 个叶子。关键是:这两种输出走的是同一段纯函数代码,只因输入不同而分叉——没有任何「演示模式」开关去伪造数据。这正是「live-data,不硬编码」在结构上的保证:你想造假都没有专门的造假入口。

3. 今日实战

  1. 实现/确认聚合脚本 scripts/build-dashboard.tsagent-evals/reports/*.json → 调 buildDashboard → 写 agent-evals/dashboard.json(本仓已具雏形,本日按指标树补齐叶子映射)。
  2. 离线先验证降级路径:reports/ 为空时跑 pnpm dashboard,确认输出 nsm.value=null + 「no reports yet」提示。
  3. 用离线 fixture 报告(含 completionRate/partialCreditMean 等字段)跑一遍,确认 NSM + 5 个叶子都正确聚合、chart/JSON 成形。
  4. 可叠加现有真实 token 基线(tokenizer 的 1760 tokens)作输入层占位指标,标注其为离线测量、非跑批活数据。
  5. 待 Day46/48 真实跑批产出 reports 后,重跑脚本把占位替换为活数据。

4. 今日实测 / 产出

  • 脚本 「待建」(本仓有雏形,本日按指标树对齐叶子映射并补降级路径)。
  • 输出依赖 Day46/48 真实跑批,故 「待跑(需 OPENROUTER_API_KEY 先产报告)」
  • 可先用现有 tokenizer 真实数 (1760 tokens)+ 离线 fixture 报告 跑通脚本出占位 chart/JSON,待 key 后替换为活数据。
  • 本日把占位 demo 数字写成实测,把待建/待跑升级成已完成。

5. 常见误区 / 陷阱

  • 硬编码 demo 数字冒充实测:seed 点名的头号反模式;缺数据要降级显示「无报告」。
  • 空数组聚合返回 0 而非 null:会让「无数据」和「真值 0」无法区分;avg([]) 必须返回 null。
  • 在仪表盘里塞没有 CI 的裸 Δ 当结论:展示层不做推断,CI 归 abCompare
  • 解析失败的报告让脚本崩:IO 侧要 try/catch 跳过坏 JSON(本仓已 return null 容错)。

6. 学习资源(每条带 YYYY-MM)

  • Anthropic, Demystifying evals — 2026-01(eval 报告即数据源、live-data 实践)。
  • Langfuse 文档(evals/observability 仪表盘实践)— 2026。
  • Braintrust 文档(eval 报告聚合与可视化)— 2026。
  • 本仓代码:src/agent/eval/dashboard.tsscripts/build-dashboard.ts — 2026-06。

SOTA检查 (2026-06 更新)

  • 当前主流:吃活数据(live-data)的 evals-ops 仪表盘(Langfuse / Braintrust 类)是 2026-06 当前实践,仪表盘从真实跑批报告读数而非硬编码。
  • 是否仍 SOTA:是。pure core + 报告即数据源 + 诚实降级的组合无过时风险。
  • 过时黑名单:硬编码 demo 数字冒充实测;空数据用假数填充;在展示层混入无 CI 的推断结论。
  • 下次复查点:Day46/48 真实报告就绪后,重跑脚本把占位换活数据;关注是否需接 Langfuse/OTel 做持续聚合。

衔接

  • 昨天:Day 48 — 跨模型稳健性,产出 Qwen3 复核报告。
  • 今天:写吃活数据的聚合脚本,把指标树 NSM + 叶子映射到真实跑批报告,诚实降级。
  • 明天:Day 50 — Block 收口与归因,汇总 Δ + budget 表 + 仪表盘定稿 tool/context 三件套。