每个 niceeval eval 都是一个 TypeScript 文件,默认导出 defineEval(...)。核心原则是:路径即身份、一个文件一个 eval、线性书写并就地断言。
defineEval 的结构
import { defineEval } from "niceeval";
export default defineEval({
description?: string;
agent?: string;
tags?: string[];
judge?: JudgeConfig;
reporters?: Reporter[];
timeoutMs?: number;
metadata?: Record<string, unknown>;
async test(t) { /* interactions + assertions */ },
});
不要设置 id 或 name。evals/weather/brooklyn.eval.ts 会自动变成 ID weather/brooklyn。
单轮 eval
import { defineEval } from "niceeval";
import { includes } from "niceeval/expect";
export default defineEval({
description: "Brooklyn weather query",
async test(t) {
await t.send("What's the weather like in Brooklyn today?");
t.succeeded();
t.calledTool("get_weather", { input: { city: "Brooklyn" }, count: 1 });
t.check(t.reply, includes("sunny"));
},
});
t.send() 驱动一次交互,t.succeeded() 和 t.calledTool() 是作用域断言,t.check() 是立即记录的值断言。
Turn 对象
| 属性 | 说明 |
|---|
turn.events | 标准事件流,主要事实来源 |
turn.data | 结构化输出 |
turn.status | "completed"、"failed" 或 "waiting" |
turn.usage | token / cost 等 usage |
turn.message | assistant 文本回复 |
turn.toolCalls | 本轮工具调用 |
多轮 eval
export default defineEval({
description: "Draft an email, then send it on confirmation",
async test(t) {
const draft = await t.send("Draft a follow-up email.");
draft.expectOk();
t.check(draft.message, includes("Best"));
await t.send("Looks good, send it.");
t.calledTool("send_email");
},
});
需要并行独立会话时,用 t.newSession()。
Dataset Fanout
一个文件可以导出 eval 数组:
export default rows.map((row) =>
defineEval({
description: row.task,
async test(t) {
await t.send(row.prompt);
t.check(t.reply, equals(row.expected));
},
}),
);
生成 ID 为 sql/0000、sql/0001 等。详见 Dataset Fanout。
Sandbox fixtures
Coding-agent eval 可以用目录约定:
evals/fixtures/create-button/
├─ PROMPT.md
├─ EVAL.ts
├─ package.json
└─ src/
PROMPT.md 给 agent 看,EVAL.ts 只在验证阶段出现。详见 Fixtures。
命名约定
文件名
只有 .eval.ts 会被 runner 发现。
目录分组
evals/billing/refund.eval.ts 的 ID 是 billing/refund。
Fixtures
适合 coding agent,需要真实文件系统和验证测试。