niceeval show 的终端输出和 niceeval view 的网页输出。本教程用相同的结果数据编写考试成绩单、代码行数表和质量 × 成本图等自定义报告。
一份报告对应一个报告文件:一棵由内置组件搭起来的树。niceeval show 和 niceeval view 打开结果、选出当前范围(Scope),交给这棵树渲染。把文件路径传给 --report,同一份定义在终端和网页都能渲染。自定义报告同样支持 Attempt 证据链接、--results 指定结果根和静态导出。
show / view 的默认报告也是一份报告文件
niceeval show / view 不传 --report 时渲染的默认报告不是私有实现,而是包里自带的一个普通报告文件:报告、Attempts、追踪三个导航页,加一个不进导航、按 Attempt 定位符打开的详情页,每页都由 niceeval/report 公开导出的组件搭成。
这份默认报告本身以 standard 为名从 niceeval/report/built-in 导出。只想在默认报告上加站点标题、GitHub 链接或统计脚本时,用 extends 在它上面叠自己的外壳,页面内容一行不用写;NiceEval 升级带来的页面改进也会自动跟过来:
niceeval/report/built-in 是内置报告的集合,每份一个名字;今天只有 standard,以后新增的内置报告也从这里按名字导入。想改页面内容本身,用同一批公开组件自己搭——你的报告文件能逐字写出同样的首页,也能只留自己要的部分:
ExperimentComparison 就是默认首页:一行组件直接对当前 Scope 计算范围摘要、成本 × 端到端成功率散点图和实验明细表,不需要额外取数。
自定义报告分为三个层次:
- 调整布局:用内置组件和
Row/Col重新排列版面,需要自己分组或过滤时写一个组合组件。 - 换口径:
defineMetric定义自己的指标,flag()/label()/runConfig()定义自己的分组和坐标轴。 - 自定义组件:表格用
<Table>自定义列,其它展示用defineComponent分别实现网页和终端渲染。
创建报告文件
先交代唯一的前置:报告文件是.tsx,写它的项目要装 react(写自定义组件的 web 面还要 @types/react),tsconfig 里 compilerOptions.jsx 设为 "react-jsx"。裸跑 niceeval show / niceeval view 不需要这些——只有自己写报告文件才需要。
用 defineReport 声明一棵树。宿主打开结果目录(包括 --results 指定的结果根),按默认规则选出当前 Scope,再把这份 Scope 交给树里每个组件——组件自己知道怎么从 Scope 取数,报告文件不需要手工传参:
ExperimentComparison、Scoreboard 这类组件省略 input 时都默认吃宿主选出的当前 Scope——不用在报告文件里手工取数再喂给它们。Scope 的挑选规则是:对每个实验、每道评估用例,取该实验历史运行里最新的那次判定;只按前缀重跑了一部分评估用例时,其余评估用例的判定从更早的运行补齐,不会因为一次局部重跑就整体退回某一份残缺快照。
默认挑法不合口径,或者要按子集分别展示时,不能在报告树里直接写 JavaScript——树只负责声明,取数发生在组合组件里。用 defineComponent 写一个组合组件,函数体里能拿到 ctx.scope(当前 Scope)和 ctx.results(结果根的完整读取面,取历史快照用它),用普通 JavaScript 加工后再传给下游组件:
--results 把结果根换成指定目录,--exp 让 Scope 只留该实验。--history 与 --report 互斥——趋势在报告里用 ctx.results 自己组织;证据视图(--source / --execution / --diff)只看证据,不渲染报告。
报告树里的组件有两种数据形态。像上面这样省略 data、直接传计算选项(rows、columns、questions 这类)是 spec 形态,组件自己在渲染前取数;需要先用 JavaScript 过滤或加工时,改用组件配套的 xxxData(scope, options) 函数手工取数,再把结果传给 data prop——两种写法产出完全相同,选哪种只看要不要在取数和渲染之间插入自己的逻辑。完整的双形态契约和每个组件的字段见报告组件。
选择警告(ScopeWarnings)组件显示覆盖不全、快照过期、运行未完成和快照读取失败等提醒,按实验分组,组头列出实验名、问题标签和可复制的重跑命令,每条原文位于可展开区域中。默认报告的每一页都包含该组件;自定义报告需要显示提醒时,在页首添加 <ScopeWarnings />。
页面里的每个组件都是双面的:网页面是 React 渲染,终端面是字符渲染,两面吃同一份算好的数据。实体列表按 experiment → Eval → Attempt 展示事实;指标表、矩阵、条形图、成绩单、散点图、趋势图和差异表展示聚合值。完整清单见报告组件。网页面的实体、格子和点深链到 Attempt 详情,终端面印出对应的 niceeval show <eval id> 下钻命令。
默认报告没有特权:它的四个页面全部由公开组件搭成。需要同样的当前 Scope 比较就写 <ExperimentComparison />;要子集就在报告里先 filter,或从 CLI 用 --exp 收窄。
排版:内置排版原语
排版原语也是双面组件,同一份布局会分别渲染到网页和终端:<Col>:纵向依次排列——网页是块级堆叠,终端是逐块输出。<Row>:并排——网页是横向排布,终端是字符分栏;终端宽度不够时自动降级为纵向,不硬挤。<Grid columns={N}>/<Stat>:自由摘要格,一格一个 label-value 卡片;columns是网页宽屏下最多摆几列,窄屏和终端都会自动减列,不丢格。见下文「自由摘要格」。<Section title="…">:带标题的块,网页是标题层级,终端是标题行加缩进;可选meta是标题行右侧的短元信息,网页同行右对齐,终端放不下时换行。<Text>:说明文字,网页是段落,终端是折行文本。<Table>:自定义列的表——网页是<table>,终端是按显示宽度对齐的字符表,中文列不撕歪。<Tabs>/<Tab title="…">:一页里的并列视图,网页是可切换的 tab 条,终端按顺序全部输出——tab 是浏览状态,不是页面,内容多到终端读不动时是升级成单独页面的信号。
<div>。要自由内容,说明文字用 <Text>,更自由的走下面的自定义组件。
按子集分组:组合组件 + filter
需要按目录前缀把 Experiment 分成几组、每组单独摆一块摘要时,不需要专门的分组组件——写一个组合组件,在ctx.scope 上 filter,把收窄后的 Scope 交给 ScopeSummary:
input 传给它们,或者用 await experimentListData(scoped) 拿到可自行过滤的数组。需要按其它维度(不是路径前缀)分组比较,用下文的 MetricTable 加自定义维度更直接。
换口径:自定义指标
实验、Eval、Attempt 的固定诊断字段由三个实体列表承接。下面的例子是另一种需求:用通用MetricTable 自由换指标口径——每个指标的计算逻辑都挂在自己身上,换口径只需要换 columns:
perEval),再跨题折成格子值(acrossEvals),两级默认都是平均。分两级不是形式主义——失败的题天然比通过的题 attempt 多(重试跑满、通过即停),平铺求均值会让分数和重试策略纠缠在一起。两级都能在 defineMetric 的 aggregate 里换:aggregate: { perEval: "max", acrossEvals: "mean" } 就是 pass@k(题内取最好一次,跨题取占比)。
两个面继承同一套诚实契约:排序方向随指标的 better,覆盖不全的格子带 12/15 角标(15 个 attempt 里 12 个测得了该指标),缺数据渲染成 — 而不是 0。指标只定义一次,两个面共用——人在网页上看到的数字,就是 agent 在 stdout 里读到的数字,判断口径永远一致。给 agent 的指引也只多一行:跑 niceeval show --report reports/golf.tsx,读 stdout。
label 可以是一份文案,也可以按语言给:label: { en: "Changed lines", "zh-CN": "改动行数" }——查看器界面切语言时,按语言给的 label 跟着切;只给一份就两种语言都用它。指标算出来的数字本身不分语言。
内置指标里 endToEndPassRate / taskPassRate / executionReliability / costUSD / durationMs / tokens 只读 Attempt 自带的判定、用量这些字段,任何一份结果目录都算得出。没有限定词的”成功率”使用 endToEndPassRate:passed 记 1,failed 和 errored 都记 0。taskPassRate 只在形成可信判定的样本上衡量答题质量,errored 不参与;展示它时应明确写”可判定任务通过率”,不能简称成功率。要区分答题质量和执行问题,把 endToEndPassRate、taskPassRate、executionReliability 三列并排。assistantTurns(o11y 事件流里的 assistant turn 数)不一样,它读 attempt.o11y()——这份数据 copySnapshots 缺省会随行,但如果发布脚本显式给了 artifacts 列表又没把 "o11y" 写进去,它就不在发布根里(见结果数据 API的”发布”一节),指标渲染成 —,不是 0。自己写的指标只要读了 o11y() / diff() 这类 artifact(就像上面 changedLines 读 attempt.diff()),发布前都要过一遍同样的检查。
换分组:三种来源
维度决定分组——表的行、矩阵的行列、散点的点、趋势图的 x 轴。每个维度槽收三种值:- 内置维度:
"agent"、"model"、"experiment"、"eval"、"evalGroup"、"snapshot",结果里现成的身份字段。 - 自定义维度:
{ name, of },一个纯函数吃 attempt、吐组名。定义只住在报告文件里,不用改任何 experiment 文件。 - 声明式变量:
flag()/label()/runConfig(),分别引用 experiment 用flags、labels声明的变量和顶层运行配置。
自定义维度:从已有数据派生分组
分组能从现有结果数据计算出来时用自定义维度,不要求为了展示方式去改任何 experiment 文件。比如把不同模型折成厂商,按厂商比通过率:of 能读 attempt 的全部已有数据:evalId、experimentId、快照与判定里的字段。它只能派生、不能补造:如果两组 experiment 的差别只体现在文件命名里(bub-baseline.ts / bub-mempal.ts),of 就只能去解析这个名字——命名约定一改,分组静默散掉。这种时候变量该搬回配置,往下看。
声明式变量:flag() / label() / runConfig()
画”并行 agent 数 × 模拟延迟 × 得分”这类 scaling 趋势时,图上的变量不该编码进 experiment id(ultra-16agents-300ms),再在报告里解析字符串抠出来——那是约定不是配置,改个命名整张图就散。变量的家是 experiment 文件里声明的字段,三种来源对应三个构造器,不猜:
flag()/numericFlag()读ExperimentDef.flags——agent 和 eval 运行时也能看见的参数,比如并发数、延迟档位。label()/numericLabel()读ExperimentDef.labels——只用于报告归类的标注,运行时不可见。runConfig()/numericRunConfig()读顶层运行配置(model、reasoningEffort、budget、runs这类),名字点明读的是这次运行落盘的配置。
series / rows / columns / points)用不加 numeric 前缀的版本按声明值分组;数值轴(MetricLine 的 x)必须用 numericFlag() / numericLabel() / numericRunConfig(),因为刻度要求真实数值:
换形态:表格用 Table,摘要卡片用 Grid/Stat,其余自己画
内置组件未覆盖的展示分三类:表格使用排版原语<Table>;label-value 的自由摘要卡片使用 <Grid> / <Stat>;通过率条形图、预算燃尽图和项目徽章这类真正需要自己画的展示,才用 defineComponent 编写双面组件。
一张表:<Table>
列是你定的,格子是你算好的显示值,<Table> 负责把网页和终端两个面都排整齐:
align: "right" 让数字列右对齐。单元格值为 null 时渲染 —,不补 0。行上包含 locator 时会增加 Attempt 列;网页链接进入 Attempt 证据页,终端把 locator 交给 niceeval show。完整字段见报告组件的”表格”一节。
自由摘要格:<Grid> / <Stat>
耗时、成本、参与率这类 label-value 速览数字,不用写 defineComponent——<Grid> 摆格子,<Stat> 摆每格的 label / 主值 / 辅助信息,两个面自动排版:
columns 是宽屏下最多摆几列,窄屏和终端都会自动减列,不丢任何一格;加 variant="boxed" 给每格描边,省略就是默认的 plain 无框。value 收 LocalizedText、number 或 null——null 显示 —,数字 0 照常显示成 0,不当成缺数据;tone(positive / negative / warning,默认 neutral)是你自己对这个数字的判断,只给主值上色,组件不会替你从正负号猜。
Grid 只管排版,不读 Scope、不聚合指标,Stat.value 必须是你已经算好的显示值。需要保留 12/15 这样的覆盖率角标和证据引用时,继续用报告组件里的指标表这类数据组件;只有这种自由摘要卡片才用 Grid / Stat。
不是表:defineComponent 加文本排版函数
defineComponent 声明两个渲染面,和 defineExperiment / defineMetric / defineReport 同一个家族。终端面要自己排字符,用 niceeval/report 导出的这组函数——官方组件排的就是这几把尺子:
不要用
String.prototype.padEnd / padStart 对齐终端输出。 它们数的是 UTF-16 码元,不是显示列宽:一个汉字占 2 列,却只算 1 个码元。用它们补齐,中文一进来列就错位,而中文 agent 名和中文题目名恰恰是最常见的情形。列宽也要随内容和 ctx.width 算,不要硬编码一个数字。
- 计算发生在拿到
ctx的地方,渲染面是纯函数。 组合组件的函数体里能await读 attempt 句柄、折数据;双面组件的web和text只认已经算好的 props,零 IO、同步——这条边界让同一棵树能被烘进静态导出。 - 渲染面接收上下文参数。
text(props, ctx)的ctx.width是可用列宽(Row分栏后会变窄),ctx.attemptCommand(ref)生成查看命令;web(props, ctx)的ctx.attemptHref(ref)生成 Attempt 证据链接。自定义组件与内置组件使用相同证据页。 - 网页面静态渲染,不 hydrate。 宿主在计算侧把
web面渲染成静态 HTML,不打包你的代码进查看器:交互用普通链接和<details>,与官方组件同一条静态契约。 - 样式随树带走。 静态导出不打包你的代码,
className引用的 CSS 用内置原语<Style>{css}</Style>放进页面树——web 面吐<style>标签,text 面渲染为空,上面的例子就是这么给.passbars上样式的。 - 诚实契约同样适用。 缺数据渲染
—不补 0,截断如实标注剩余数量——上面例子里克劳德没有样本就是—。
发布:导出即静态站
发布自定义报告和发布官方查看器是同一个动作——--out 加上 --report:
.niceeval/ 时,先用 copySnapshots 生成经过大小检查的结果目录,再执行导出,流程见查看结果。
双面组件的网页面是普通 React 组件,xxxData 函数是普通 TypeScript 函数。需要在现有内部面板中复用指标表时,可以 import 组件并传入数据。计算与渲染分开部署时(例如 CI 生成 JSON、另一个应用 fetch),两侧必须使用相同 NiceEval 版本;组件数据不带版本信息,兼容性跟随包版本。
自定义报告的边界
一次只渲染一份报告,--report 收显式文件路径——没有 reports/ 目录自动发现、没有插件注册表、没有配置文件。自定义指标和自定义组件都住在你的报告文件里,随文件一起递入,宿主不为它们长任何注册面。不传 --report 时渲染的就是默认报告,你的报告和它是同级实现。