niceeval 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/README.zh.md +14 -14
- package/docs/README.md +119 -0
- package/docs/adapters/README.md +60 -0
- package/docs/adapters/authoring.md +179 -0
- package/docs/adapters/coding-agent-skills-plugins.md +324 -0
- package/docs/adapters/collection.md +128 -0
- package/docs/adapters/contract.md +263 -0
- package/docs/adapters/reference/agent-eval.md +215 -0
- package/docs/adapters/reference/agent-loop-apis.md +69 -0
- package/docs/adapters/reference/claude-code-otel-telemetry.md +100 -0
- package/docs/adapters/reference/eve-protocol.md +127 -0
- package/docs/adapters/reference/otel-genai.md +107 -0
- package/docs/adapters/reference/otel-instrumentation.md +65 -0
- package/docs/adapters/targets.md +99 -0
- package/docs/architecture.md +140 -0
- package/docs/assertions.md +388 -0
- package/docs/capabilities-by-construction.md +48 -0
- package/docs/cli.md +171 -0
- package/docs/concepts.md +106 -0
- package/docs/e2e-ci.md +295 -0
- package/docs/eval-authoring.md +319 -0
- package/docs/experiments.md +154 -0
- package/docs/getting-started.md +224 -0
- package/docs/multi-agent.md +145 -0
- package/docs/observability.md +333 -0
- package/docs/origin-integration.md +195 -0
- package/docs/references.md +35 -0
- package/docs/results-format.md +228 -0
- package/docs/runner.md +104 -0
- package/docs/sandbox.md +276 -0
- package/docs/scoring.md +119 -0
- package/docs/source-map.md +106 -0
- package/docs/tier-sync.md +177 -0
- package/docs/view.md +157 -0
- package/docs/vision.md +88 -0
- package/package.json +46 -17
- package/src/agents/ai-sdk-otel.ts +0 -0
- package/src/agents/ai-sdk.test.ts +377 -0
- package/src/agents/ai-sdk.ts +577 -0
- package/src/agents/bub.ts +30 -37
- package/src/agents/claude-code.ts +7 -4
- package/src/agents/codex.ts +15 -10
- package/src/agents/index.ts +43 -1
- package/src/agents/sdk-streams.test.ts +145 -0
- package/src/agents/sdk-streams.ts +457 -0
- package/src/agents/shared.ts +77 -39
- package/src/agents/streaming.test.ts +152 -0
- package/src/agents/streaming.ts +195 -0
- package/src/agents/types.ts +218 -0
- package/src/agents/ui-message-stream.test.ts +249 -0
- package/src/agents/ui-message-stream.ts +372 -0
- package/src/cli.ts +124 -77
- package/src/context/context.test.ts +203 -7
- package/src/context/context.ts +158 -49
- package/src/context/session.test.ts +96 -0
- package/src/context/session.ts +119 -9
- package/src/context/types.ts +205 -0
- package/src/define.ts +4 -14
- package/src/expect/index.ts +8 -71
- package/src/i18n/core.ts +28 -0
- package/src/i18n/en.ts +47 -5
- package/src/i18n/index.ts +5 -17
- package/src/i18n/zh-CN.ts +47 -5
- package/src/index.ts +12 -0
- package/src/loaders/index.ts +8 -39
- package/src/o11y/otlp/canonical.ts +7 -6
- package/src/o11y/otlp/mappers/codex.ts +10 -8
- package/src/o11y/otlp/mappers/index.ts +9 -21
- package/src/o11y/otlp/receiver.ts +25 -7
- package/src/o11y/otlp/sandbox-receiver.ts +57 -21
- package/src/o11y/otlp/turn-otel.test.ts +110 -0
- package/src/o11y/otlp/turn-otel.ts +125 -0
- package/src/o11y/parsers/bub.ts +13 -11
- package/src/o11y/parsers/claude-code.ts +27 -51
- package/src/o11y/parsers/codex.ts +29 -46
- package/src/o11y/parsers/index.ts +5 -14
- package/src/o11y/tool-names.test.ts +61 -0
- package/src/o11y/tool-names.ts +74 -0
- package/src/o11y/types.ts +135 -0
- package/src/runner/attempt.ts +456 -0
- package/src/runner/fingerprint.ts +59 -0
- package/src/runner/remote-sandbox.ts +53 -0
- package/src/runner/report.ts +53 -0
- package/src/runner/reporters/artifacts.ts +25 -4
- package/src/runner/reporters/console.ts +2 -8
- package/src/runner/reporters/live.ts +2 -8
- package/src/runner/reporters/shared.ts +15 -0
- package/src/runner/reporters/table.ts +10 -62
- package/src/runner/run.ts +114 -619
- package/src/runner/types.ts +237 -0
- package/src/sandbox/checkpoint.ts +15 -13
- package/src/sandbox/docker-stream.ts +87 -0
- package/src/sandbox/docker.ts +42 -120
- package/src/sandbox/e2b.ts +24 -75
- package/src/sandbox/local-files.ts +33 -0
- package/src/sandbox/paths.test.ts +85 -0
- package/src/sandbox/paths.ts +45 -0
- package/src/sandbox/resolve.ts +10 -34
- package/src/sandbox/shell.ts +17 -0
- package/src/sandbox/source-files.ts +42 -2
- package/src/sandbox/types.ts +158 -0
- package/src/sandbox/vercel.ts +55 -71
- package/src/scoring/collector.ts +3 -2
- package/src/scoring/judge.ts +72 -146
- package/src/scoring/match.ts +79 -0
- package/src/scoring/types.ts +76 -0
- package/src/shared/aggregate.ts +48 -0
- package/src/shared/format.ts +20 -0
- package/src/shared/outcome.ts +48 -0
- package/src/shared/types.ts +37 -0
- package/src/types.ts +20 -911
- package/src/view/aggregate.ts +103 -0
- package/src/view/app/App.tsx +26 -5
- package/src/view/app/components/AttemptModal.tsx +12 -3
- package/src/view/app/components/CodeView.tsx +9 -5
- package/src/view/app/components/CopyControls.tsx +4 -4
- package/src/view/app/components/ExperimentTable.tsx +5 -5
- package/src/view/app/i18n.ts +31 -7
- package/src/view/app/lib/format.ts +17 -20
- package/src/view/app/lib/outcome.ts +4 -16
- package/src/view/app/main.tsx +3 -5
- package/src/view/app/pages/RunsPage.tsx +2 -2
- package/src/view/app/pages/TracesPage.tsx +2 -2
- package/src/view/app/shared.ts +2 -1
- package/src/view/app/types.ts +6 -43
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/src/view/index.ts +24 -401
- package/src/view/loader.ts +234 -0
- package/src/view/server.ts +136 -0
- package/src/view/shared/types.ts +64 -0
- package/src/view/styles.css +60 -9
package/docs/cli.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# CLI 参考
|
|
2
|
+
|
|
3
|
+
`niceeval` 的命令行。设计目标:运行配置可签入、可复现;真正执行 eval 时必须通过 `experiments/` 里的 experiment。
|
|
4
|
+
|
|
5
|
+
> 本篇描述 CLI 的目标模型和当前约定。若代码实现与这里的设计不一致,应进一步讨论并决定是修代码、修设计,还是记录为明确的阶段性差异。
|
|
6
|
+
|
|
7
|
+
## 两类参数:`exp` 选择「怎么跑」,后续位置参数筛「哪些 eval」
|
|
8
|
+
|
|
9
|
+
执行 eval 时先选 experiment,再可选地用 eval id 前缀缩小范围:
|
|
10
|
+
|
|
11
|
+
- `niceeval exp [组|配置]` = 跑**哪个运行配置**。agent、model、flags、runs、预算等都来自 experiment 文件。
|
|
12
|
+
- `niceeval exp [组|配置] [eval id 前缀...]` = 在该 experiment 下只跑部分 eval。这个位置参数只筛 eval,不是 agent 名、URL 或模型。
|
|
13
|
+
- flags 只做调度级临时覆盖,例如 `--runs`、`--max-concurrency`、`--timeout`。`--agent` / `--model` 不覆盖 experiment;要换 agent 或 model,新增或复制一个 experiment 文件。沙箱后端同理不接受 CLI 覆盖——写在 experiment(或 config)的 `sandbox` 字段里。
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
niceeval exp <实验组|配置> <选哪些 eval> <flag:调度覆盖>
|
|
17
|
+
# └怎么跑┘ └eval filter┘ └可选覆盖┘
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
裸 `niceeval weather` 不会运行;要写成 `niceeval exp local weather` 或 `niceeval exp compare weather`。这样结果始终带 experiment 身份,能复现也能在 `niceeval view` 里按配置对比。
|
|
21
|
+
|
|
22
|
+
## 命令
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
niceeval exp [组|配置] [pattern...] # 跑实验:全部 / 一组 / 单个配置;可再用 eval id 前缀过滤
|
|
26
|
+
niceeval init # 生成 evals/ 与 niceeval.config.ts
|
|
27
|
+
niceeval list # 只列出发现到的 eval,不运行
|
|
28
|
+
niceeval clean # 删除 .niceeval/ 历史运行工件
|
|
29
|
+
niceeval watch # 监听文件变化,改即重跑(规划中)
|
|
30
|
+
niceeval view [结果目录|summary.json] # 起本地 web 查看器并打开浏览器,读 .niceeval/ 历史运行出图
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
实验 = 可签入的运行配置(哪个 agent/几次/预算)。**一文件一配置,一文件夹一组可对比实验**(`niceeval exp <组>` 跑整组)。详见 [Experiments](experiments.md)。
|
|
34
|
+
|
|
35
|
+
`pattern` 是 `exp` 命令后的 eval id 前缀过滤,可多个。匹配规则:`id === pattern` 或 `id` 以 `pattern + "/"` 开头(精确或目录前缀):
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
niceeval exp # 跑 experiments/ 下全部实验
|
|
39
|
+
niceeval exp local weather # 在 local 实验里跑 weather 或 weather/*
|
|
40
|
+
niceeval exp local weather/brooklyn # 在 local 实验里跑单个 eval
|
|
41
|
+
niceeval exp compare fixtures/ billing/ # 在 compare 组里跑多个前缀,并集
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 选择被测对象(agent,写进 experiment)
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
niceeval exp <组|配置> # agent / model 来自 experiments/<组|配置>.ts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
agent、model、feature flags、sandbox 后端都属于 experiment,不是临时 CLI flag。要连你自己的服务,写一个 agent adapter,再在 experiment 里引用它;URL 和鉴权是 adapter / experiment 的私有配置,不是位置参数。沙箱型 agent 在哪跑(docker / vercel / e2b / 三方)写在 experiment(或 config)的 `sandbox` 字段里,用 `dockerSandbox()` / `vercelSandbox()` / `e2bSandbox()`(从 `niceeval/sandbox` 导入)——没有默认值,没写就在起沙箱时直接报错。
|
|
51
|
+
|
|
52
|
+
## 调度
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
niceeval exp compare --runs <n> # 临时覆盖每个 eval 的 runs
|
|
56
|
+
niceeval exp compare --early-exit # 先过一次即停其余(默认开)
|
|
57
|
+
niceeval exp compare --no-early-exit # 关掉,要完整通过率分布
|
|
58
|
+
niceeval exp compare --max-concurrency <n> # 并发上限
|
|
59
|
+
niceeval exp compare --timeout <ms> # 单 eval 超时
|
|
60
|
+
niceeval exp compare --force # 忽略指纹缓存,全量重跑
|
|
61
|
+
niceeval exp compare --tag <tag> # 按标签过滤(可重复)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 评分与退出
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
niceeval exp compare --strict # soft 低于阈值也判红(CI 用)
|
|
68
|
+
niceeval exp compare --budget <usd> # 整轮估算成本上限,累计超了就停止派发新 attempt
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
沙箱型里跑什么校验命令、跑不跑,是 `test(t)` 里的普通代码:`t.sandbox.runCommand` 跑命令,`t.check(result, commandSucceeded())` 断言退出码。
|
|
72
|
+
|
|
73
|
+
退出码:有 `outcome=failed`(含 `--strict` 下 soft 未达标而改判的)或 `outcome=errored` → 非零;全 `passed` / `skipped` 时返回 `0`。`failed` 与 `errored` 在报告里分开统计。
|
|
74
|
+
|
|
75
|
+
每个 eval 的 token 用量与估算成本会出现在控制台和 `summary.json`,跨 agent 对比即得「质量 × 成本」。详见 [Observability](observability.md#用量与成本token--计费)。
|
|
76
|
+
|
|
77
|
+
## 查看结果
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
niceeval view # 起本地 web,自动打开浏览器,读 .niceeval/
|
|
81
|
+
niceeval view .niceeval/<run>/summary.json
|
|
82
|
+
niceeval view --port 4317 # 固定端口;被占用时向后找可用端口
|
|
83
|
+
niceeval view --no-open # 只启动服务并打印 URL(适合远端 shell / CI)
|
|
84
|
+
niceeval view --out .niceeval/report.html # 导出静态 HTML,不启动服务
|
|
85
|
+
niceeval clean # 清掉这些历史运行结果
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
每次 `exp` 运行会写 `.niceeval/<timestamp>/summary.json` 与 attempt 级 JSON 工件;`view` 启动本地服务后默认用系统浏览器打开 URL,并直接读这些结构化工件。当前查看器先提供 Next.js evals 风格的密集榜单:按 experiment 聚合、可排序、可搜索、可展开看单个 eval attempt 的断言/错误/用量。结果保存格式见 [Results Format](results-format.md)。
|
|
89
|
+
|
|
90
|
+
## 输出
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
niceeval exp compare --junit <path> # 写 JUnit XML
|
|
94
|
+
niceeval exp compare --quiet # 只出最终汇总
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 干跑
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
niceeval exp compare --dry # 只发现、不真正调用 agent / LLM
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--dry` 用来检查发现和过滤是否如预期,不烧 token。
|
|
104
|
+
|
|
105
|
+
## 环境变量
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
# agent 的 API key(按 adapter 决定读哪个)
|
|
109
|
+
ANTHROPIC_API_KEY=sk-ant-... # claude-code / 直连
|
|
110
|
+
OPENAI_API_KEY=sk-... # codex / 直连
|
|
111
|
+
BUB_API_KEY=... # bub
|
|
112
|
+
AI_GATEWAY_API_KEY=... # 经网关的变体
|
|
113
|
+
|
|
114
|
+
# 沙箱后端认证(auto 探测用)
|
|
115
|
+
VERCEL_API_TOKEN=... # 用 vercel sandbox
|
|
116
|
+
VERCEL_TEAM_ID=... # Vercel team
|
|
117
|
+
# 都没有 → 自动用本地 docker
|
|
118
|
+
|
|
119
|
+
# 评判模型(LLM-as-judge)
|
|
120
|
+
# 复用上面对应 vendor 的 key,或在 config 里单独指定
|
|
121
|
+
NICEEVAL_JUDGE_MODEL=... # config / eval 未指定 judge.model 时的兜底(没有内置默认模型)
|
|
122
|
+
|
|
123
|
+
# 调度项(标志 > 环境变量 > config > 内置默认,见下节)
|
|
124
|
+
NICEEVAL_RUNS=3 # 对应 --runs
|
|
125
|
+
NICEEVAL_MAX_CONCURRENCY=4 # 对应 --max-concurrency
|
|
126
|
+
NICEEVAL_TIMEOUT=600000 # 对应 --timeout(毫秒)
|
|
127
|
+
NICEEVAL_BUDGET=5 # 对应 --budget(美元)
|
|
128
|
+
|
|
129
|
+
# CLI 输出语言
|
|
130
|
+
NICEEVAL_LANG=en # en / zh-CN;覆盖系统 locale
|
|
131
|
+
NICEEVAL_LOCALE=zh-CN # NICEEVAL_LANG 未设置时生效
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
CLI 文案默认跟随 `NICEEVAL_LANG` / `NICEEVAL_LOCALE`、`LC_ALL` / `LC_MESSAGES` / `LANG`。检测到 `zh` 使用 `zh-CN`,检测到其它语言使用 `en`;都没有时默认 `zh-CN`。这只影响 CLI / runtime 输出,不改变 eval 结果里的机器字段,也不翻译 LLM judge prompt。
|
|
135
|
+
|
|
136
|
+
## 配置优先级
|
|
137
|
+
|
|
138
|
+
同一项调度配置的覆盖顺序(高 → 低):
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
CLI 标志 → 环境变量 → niceeval.config.ts → 内置默认
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
例:`--max-concurrency 4` 压过 config 里的 `maxConcurrency: 8`。agent / model / flags 不走 CLI 覆盖,复制 experiment 文件修改。
|
|
145
|
+
|
|
146
|
+
## 典型用法
|
|
147
|
+
|
|
148
|
+
下面每行都先选 experiment,再可选 eval id 前缀:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
# 本地开发:零云依赖评一个 coding agent
|
|
152
|
+
ANTHROPIC_API_KEY=sk-ant-... niceeval exp local fixtures/
|
|
153
|
+
|
|
154
|
+
# 衡量稳定性:跑 10 次看通过率,不早停
|
|
155
|
+
niceeval exp local fixtures/button --runs 10 --no-early-exit
|
|
156
|
+
|
|
157
|
+
# 评已部署服务的会话质量(URL 是 agent / experiment 私事)
|
|
158
|
+
niceeval exp prod-smoke weather billing
|
|
159
|
+
|
|
160
|
+
# CI:严格模式 + JUnit
|
|
161
|
+
niceeval exp ci --strict --junit .niceeval/junit.xml
|
|
162
|
+
|
|
163
|
+
# 只看会发现什么,不烧钱
|
|
164
|
+
niceeval exp compare --dry
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 相关阅读
|
|
168
|
+
|
|
169
|
+
- [Getting Started](getting-started.md) —— 从安装到第一次运行。
|
|
170
|
+
- [Runner](runner.md) —— 这些标志背后的调度语义。
|
|
171
|
+
- [Config](concepts.md#配置词汇) —— `niceeval.config.ts` 字段。
|
package/docs/concepts.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Concepts
|
|
2
|
+
|
|
3
|
+
什么时候读这一篇:
|
|
4
|
+
|
|
5
|
+
- 你碰到一个不认识的 niceeval 术语;
|
|
6
|
+
- 你在写文档 / 代码,想跟现有用法保持一致;
|
|
7
|
+
- 你需要一页纸把整套词汇过一遍。
|
|
8
|
+
|
|
9
|
+
这是一份术语表。两个同义词并存时,**首选写法**用粗体。
|
|
10
|
+
|
|
11
|
+
## 评测核心词汇
|
|
12
|
+
|
|
13
|
+
**Eval** —— 评测的最小单元。一个 Eval = 一个 [Task](#task) 跑在一个 [Agent](#agent) 上,由若干 [Scorer](#scorer) 判分。统一由 `defineEval` 定义——会话型和沙箱型不是两个定义函数,`test(t)` 里 `t` 要不要带 `t.sandbox`,取决于引用的 [Agent](#agent) 是不是 `defineSandboxAgent` 构造的(`kind: "sandbox"`),不取决于用哪个 define 函数。每个 Eval 有一个从路径推导的 **id**。
|
|
14
|
+
|
|
15
|
+
**Task** —— 要让被测对象完成的"那件事"。不管会话型还是沙箱型,都是一串 `t.send(...)` 的输入——沙箱型只是 `t` 多了 `t.sandbox`,任务本身照样写在 `t.send(...)` 里。Task 描述意图,不描述如何判分。
|
|
16
|
+
|
|
17
|
+
**Agent** —— "一条连到 AI 的连接"的抽象,由 experiment 引用。`Agent.kind` 只有两类:`"remote"`(按你自己服务的协议发请求,`defineAgent` 产出)、`"sandbox"`(在 [Sandbox](#sandbox) 里 spawn coding agent 的 CLI,`defineSandboxAgent` 产出)。进程内直调你的函数不是独立的第三类——它只是 `kind: "remote"` 的 `send` 里选择怎么实现的一种写法,而且不是推荐写法(测函数不等于测生产路径,详见[接入你的 Agent · 为什么不直调](../docs-site/zh/guides/connect-your-agent.mdx))。运行器只认统一动词 `send`,`t` 上暴露哪些动作由 Agent 的[能力](#capability)决定——能力不是声明出来的,是 `send` 实际做到了什么的构造证据。niceeval 不定义任何 agent 协议,所以没有 `--url`、没有通用 http target —— 连你自己的服务也是写一个 agent,URL 是它的内部配置。详见 [Agents 与 Adapters](adapters/README.md)。
|
|
18
|
+
|
|
19
|
+
**Scorer** / **评分器** —— 把"结果"映射成分数的东西。三类:**值级断言**(`expect` 里的 `includes`/`equals`/`matches`…;`check` 记录并继续,`require` 作为前置条件立即等待并失败中止)、**作用域断言**(`t.succeeded()`/`t.calledTool()`…,在 `test` 结束后对本次 eval run 聚合评估;同一套断言挂在 [Session](#运行与结果) 上则只看这条 session,挂在 [Turn](#运行与结果) 上则只看这一轮)、**LLM-as-judge**(用一个评判模型给开放式回答打分)。沙箱型里,手工在沙箱内跑验证命令,再用 `t.check(result, commandSucceeded())` 判定,本身也是一种 Scorer。
|
|
20
|
+
|
|
21
|
+
**Assertion** / **断言** —— Scorer 的一次具体应用,带名字、严重级([gate / soft](#severity))、可选阈值,产出一个 0–1 的分数和过/挂。
|
|
22
|
+
|
|
23
|
+
**Outcome** / **判决** —— 一个 Eval 的评分结论,只有四态:`passed` / `failed` / `errored` / `skipped`。规则:显式 `t.skip(reason)` → `skipped`;执行出错(超时、异常、作者错误)→ `errored`;任一 gate 断言不过,或 `--strict` 下有 soft 断言低于阈值 → `failed`;否则 → `passed`。**没有 `scored` 这个中间态**——soft 断言没达标,在非 `--strict` 下就是 `passed`,分数照样如实记录、供横向对比,只是不影响这四态判定。`failed` 只表示断言/评分不通过,`errored` 是环境、超时、adapter、agent runtime 等执行问题,两者互斥,报告、JUnit、CI 都按这个口径分开统计,别把 `errored` 当成 agent 任务做错了。
|
|
24
|
+
|
|
25
|
+
**Severity** / **严重级** —— 断言的两档。**gate**:硬性要求,不过即判 `failed`,任何时候都生效。**soft**:质量分,不会单独让 eval 立即 `failed`——`.atLeast(x)` 本身就是 soft 带阈值的写法:非 `--strict` 下低于阈值仍判 `passed`(分数如实记录),`--strict` 下才降级为 `failed`;不调 `.atLeast()` 时走匹配器自己的默认档(如 judge 默认 soft、无阈值,纯记分永不 fail)。
|
|
26
|
+
|
|
27
|
+
## 被测对象与适配器
|
|
28
|
+
|
|
29
|
+
**Adapter** / **适配器** —— 某个 [Agent](#agent) 的具体实现,**由用户编写**(niceeval 也内置几个常用 coding agent 的 adapter)。一个 Adapter 实现一个 Agent:远程型(`kind: "remote"`)按你服务的协议发请求,沙箱型(`kind: "sandbox"`)则拥有该 agent 的 CLI 参数、认证方式、默认模型、transcript 位置等全部特殊性。接新 agent = 加一个 Adapter,不动核心。同一个 agent 可有多个变体(如直连 API vs 经网关)。
|
|
30
|
+
|
|
31
|
+
**Sandbox** / **沙箱** —— 封装"在哪里、如何隔离地跑命令"的对象。统一接口:`workdir` / `runCommand` / `readFile` / `writeFiles` / `uploadDirectory` / `stop`。实现包括 Docker、Vercel Sandbox、其它三方。命令工作目录通过 `runCommand` / `runShell` 的 `cwd` option 表达,不提供可变的 working directory。
|
|
32
|
+
|
|
33
|
+
**workdir** —— 沙箱内 agent 的默认工作目录,也是 git 基线和 diff 采集的锚点;绝对值随后端不同(docker `/home/sandbox/workspace`、e2b `/home/user/workspace`、vercel `/vercel/sandbox`)。API 里所有沙箱侧相对路径、省略的 `targetDir` / `cwd` 都解析到它;eval 作者用相对路径写完整条 eval,必须要绝对路径时读 `t.sandbox.workdir`。详见 [Sandbox · 路径与 workdir](sandbox.md#路径与-workdir一个坐标系)。
|
|
34
|
+
|
|
35
|
+
**Sandbox author API** / **沙箱作者 API** —— 沙箱型 eval 里暴露给 `test(t)` 的 `t.sandbox`。它分三类:文件 IO(`writeFiles` / `readFile`)、命令执行(`runCommand` / `runShell`)和结果断言 / diff(`fileChanged` / `diff` / `file`)。沙箱生命周期由 runner 管,`stop()` 不暴露给 eval 作者。
|
|
36
|
+
|
|
37
|
+
**Backend** / **后端** —— 某个 Sandbox 的具体实现选择(`docker` / `vercel` / …)。`auto` 表示按环境探测(有云 token 用云,否则用 Docker)。
|
|
38
|
+
|
|
39
|
+
**Capability** / **能力** —— `t` 上暴露哪些动作(会话续接、工具调用观测、文件 diff、trace…),完全由**构造证据**决定,不是声明式的能力位:`send` 里接了 `ctx.session` 的续接存取器就有多轮,返回过 `waiting` + `input.requested` 就有 HITL,用官方转换器就带完整性证明(负断言可信),`defineSandboxAgent` 构造就有 `t.sandbox`。`Agent` 接口上不存在 `capabilities` 字段。核心**按构造证据分发**,不按名字分支。这是 [Vision](vision.md) 的承重墙。逐能力的精确义务见[能力参考](../docs-site/zh/reference/capabilities.mdx)与[适配器契约](adapters/contract.md#能力从哪来构造证明不是问卷)。
|
|
40
|
+
|
|
41
|
+
**Integration tier** / **接入 Tier** —— 按「Adapter 接到哪里、额外拿到什么观测数据」给接入方式分的三档(和下面的**模型档**是两回事,后者说的是给 agent 指定哪个模型)。**Tier 1(只接 send)**:应用代码一行不改——adapter 适配应用现有对外接口实现 `send`,靠手写事件映射或官方转换器拿到工具断言;应用接口本身暴露模型选择的话,**模型对比**类 [Experiment](#experiment) 也在这一档(`model` 经 `ctx.model` 透传)。**Tier 2(send + OTel)**:还是同一个 `send`,事件来源换成应用发给 niceeval 的 OTel span(应用已埋点则零代码,未埋点补一段通用 OTel 初始化)——买到的是观测质量的跃升:事件流免手写映射、负断言带完整性证明、trace 瀑布图。**Tier 3(侵入改造 + experiment flags)**:改应用内部代码,把内部可变点(prompt、工具集、feature flag)暴露成 experiment 可选的配置(经 `flags` → `ctx.flags` 透传),解锁**完整的 feature A/B test**——对照的不再只是模型,而是应用内部的功能变体。前两档都是**无侵入**的:应用按自己的方式启动,eval 侧不 spawn 应用进程、不另开端口,Adapter 只对着用户前端本来就在用的那个接口收发。三档递进不互斥,详见 [docs-site · Tier](../docs-site/zh/concepts/tier.mdx)。
|
|
42
|
+
|
|
43
|
+
**Model tier** / **模型档** —— 给 agent 指定模型的标识(如 `opus`、`vendor/model?reasoningEffort=high`)。由 [Experiment](#experiment) 的 `model` 字段指定;省略则用 agent 原生默认,不经额外的策略层决定。
|
|
44
|
+
|
|
45
|
+
## 数据集与发现
|
|
46
|
+
|
|
47
|
+
**Dataset** / **数据集** —— 一组共享同一 `test` 逻辑、只有输入不同的 case。用 `loadYaml`/`loadJson` 读进来,`.map(row => defineEval(...))` 扇出。生成的 id 形如 `sql/0000`、`sql/0001`(零填充 4 位)。
|
|
48
|
+
|
|
49
|
+
**Discovery** / **发现** —— 运行器扫 `evals/` 找 `*.eval.ts` 文件,据路径推导 id 并排序。没有目录层面的隐式发现——沙箱型 eval 和会话型 eval 一样,必须有一个 `.eval.ts` 文件;起始文件靠 `test()` 里手工 `t.sandbox.writeFiles` / `uploadFiles` 放进沙箱(见 [Eval Authoring](eval-authoring.md#沙箱型手工把文件放进沙箱)),不靠运行器扫目录。
|
|
50
|
+
|
|
51
|
+
## 运行与结果
|
|
52
|
+
|
|
53
|
+
**Runner** / **运行器** —— 调度引擎。负责发现、有界并发执行、重试、早停、缓存,以及把结果交给报告器。详见 [Runner](runner.md)。
|
|
54
|
+
|
|
55
|
+
**Experiment** / **实验** —— 一份可签入的**运行配置**,描述「怎么跑这批 eval」:用哪个 [Agent](#agent)、跑几次、过滤哪些、预算多少。由 `defineExperiment` 定义在 `experiments/` 下,id 从路径推导。**一文件 = 一个单一配置**;**一个文件夹 = 一组要并排对比的实验**(`niceeval exp <组>` 跑整组),可比性由目录表达。它**不碰评分**——「怎么算对」是 eval 的事。详见 [Experiments](experiments.md)。
|
|
56
|
+
|
|
57
|
+
**Comparison group** / **可对比组** —— `experiments/` 下的一个文件夹,装一组"要并排比较"的单一配置(如同模型下 bub vs codex)。同组互为对照、`niceeval view` 并列展示;不同组是不同的对比维度。比文件内数组多表达了"可比性"语义。详见 [实验怎么组织](experiments.md#实验怎么组织文件夹--一组可对比的实验)。
|
|
58
|
+
|
|
59
|
+
**Run** —— 一次 `niceeval` 调用对一批 eval 的完整执行,产出一份 **summary**。
|
|
60
|
+
|
|
61
|
+
**Attempt** / **尝试** —— 同一个 eval 的第 i 次重复运行(`runs > 1` 时取通过率用)。这是 runner/result 的执行单位,不是 author-facing API 层。`t` 上的作用域断言会在一个 Attempt 的范围内聚合(全部 session + 全部 turn),不跨 Attempt、也不是上面 Run 那么大的范围。
|
|
62
|
+
|
|
63
|
+
**Session** —— 一条会话线。`t` 驱动主 session;`t.newSession()` 返回独立 session,用于并行或隔离的多会话测试。`session.*` 作用域断言只看这条 session 已经发生的事件;这些事件仍会汇入 `t.*` run 级断言。
|
|
64
|
+
|
|
65
|
+
**Turn** —— `t.send()` 的一次返回值,对应这一轮的标准事件流片段。带 `message` / `data` / `toolCalls` / `status` / `usage` / `events` 等只读字段,以及 `expectOk()` 和一套与 `t` 同名的作用域断言(`turn.calledTool`/`turn.succeeded`/…),作用域收窄成只看这一轮,详见 [Assertions](assertions.md#作用域规则)。
|
|
66
|
+
|
|
67
|
+
**EarlyExit** / **早停** —— 一个 eval 取通过率时,先过一次即中止其余 attempt 的策略(可关)。
|
|
68
|
+
|
|
69
|
+
**Fingerprint** / **指纹** —— `(eval 代码 + 配置)` 的哈希,用于缓存去重:指纹未变且已通过的,默认跳过。
|
|
70
|
+
|
|
71
|
+
**Transcript** —— agent 一次运行的逐事件记录。原始形态是各 agent 自己的 JSONL,被**归一化**成统一事件模型(message / tool_call / tool_result / thinking / error)后供断言和报告消费。详见 [Observability](observability.md)。
|
|
72
|
+
|
|
73
|
+
**o11y summary** —— 从**标准事件流**(见 [Observability](observability.md))派生的统计:工具调用计数、读/改的文件、shell 命令、web 请求、思考块数、**耗时、token 用量、估算成本**等。会注入沙箱(`__niceeval__/results.json`),让你在沙箱内手工跑的验证测试能断言 agent 的**行为**而不只是结果。
|
|
74
|
+
|
|
75
|
+
**Usage** / **用量** —— 一次运行的 token 计数(`inputTokens` / `outputTokens` / 可选 cache 读写)。随结果带回:remote agent 由 `send` 返回,沙箱型由 transcript 解析器从 agent 的 JSONL 抠出累加。可经 `t.usage` 读、`t.maxTokens()` 断言。
|
|
76
|
+
|
|
77
|
+
**Cost** / **成本** —— 用量经配置的价格表(模型 → 每百万 token 单价)换算的估算金额(`estimatedCostUSD`)。让跨 agent 对比从 pass-rate 升级为**质量 × 成本**。`--budget <usd>` 给整轮设上限。详见 [Observability](observability.md#用量与成本token--计费)。
|
|
78
|
+
|
|
79
|
+
**Reporter** / **报告器** —— 消费运行结果的插件,可实现分阶段 `onEvent`(`run:start` / `eval:start` / `eval:complete` / `run:summary` 等),也兼容 `onRunStart` / `onEvalComplete` / `onRunComplete`。内置控制台、JUnit、JSON;可接第三方实验跟踪平台。报告器在独立的串行队列上回调,不阻塞执行池。详见 [Reporters](observability.md#reporters)。
|
|
80
|
+
|
|
81
|
+
**Artifact** / **工件** —— 落盘的结构化产物,位于 `.niceeval/<时间戳>/`:run 级 `summary.json`,以及 attempt 级 `events.json`、`sources.json`、`trace.json`、`o11y.json`、`diff.json`。每个文件都是 JSON,不是 JSONL / NDJSON。详见 [Results Format](results-format.md)。
|
|
82
|
+
|
|
83
|
+
## 配置词汇
|
|
84
|
+
|
|
85
|
+
**`niceeval.config.ts`** —— 项目级配置,`defineConfig` 默认导出。完整字段:
|
|
86
|
+
|
|
87
|
+
| 字段 | 类型 | 作用 |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `judge` | `JudgeConfig` | 默认评判模型(见 [Scoring](scoring.md#3-llm-as-judge)) |
|
|
90
|
+
| `reporters` | `Reporter[]` | 全局报告器(见 [Observability](observability.md#reporters)) |
|
|
91
|
+
| `maxConcurrency` | `number` | 并发上限(见 [Runner](runner.md#调度有界并发)) |
|
|
92
|
+
| `timeoutMs` | `number` | 单 eval 超时 |
|
|
93
|
+
| `sandbox` | `SandboxOption` | 项目默认 sandbox spec(`dockerSandbox()` 等工厂产出);experiment 可覆盖。两处都没设、又用了沙箱型 agent 时直接报错——没有隐式默认,也没有 `--sandbox` 这种 CLI 覆盖 |
|
|
94
|
+
| `pricing` | `Record<string, Price>` | 价格表覆盖,合并在内置快照之上(见 [Observability](observability.md#换算成本价格表从哪来)) |
|
|
95
|
+
|
|
96
|
+
agent 不在 config 里注册:每个 experiment 直接引用一个 agent adapter(见 [Experiments](experiments.md#defineexperiment-的形状))。config 只管项目级默认与全局资源。
|
|
97
|
+
|
|
98
|
+
**Strict mode** / **严格模式** —— 默认情况下 soft 断言低于阈值仍判 `passed`;`--strict` 下同样的情况改判 `failed`。用于 CI 把质量回归当成红灯。
|
|
99
|
+
|
|
100
|
+
**环境预置** —— niceeval 把准备逻辑放在普通代码里。要在跑 agent 前准备环境,放三处之一:这条 eval 的沙箱预置写在 `test(t)` 里(手工 `t.sandbox.writeFiles` / `runCommand`),连 agent / 装 CLI 写在 [`SandboxAgent.setup`](adapters/contract.md#agent-契约),整轮共享的外部服务(mock API、DB)用外部编排(`docker compose` / CI 脚本)起停、经 env 传入。详见 [Sandbox · 环境预置放哪](sandbox.md#环境预置放哪)。
|
|
101
|
+
|
|
102
|
+
## 相关阅读
|
|
103
|
+
|
|
104
|
+
- [Architecture](architecture.md) —— 这些名词在模块图里各自的位置。
|
|
105
|
+
- [Authoring](eval-authoring.md) —— Eval / Task / Dataset 怎么写。
|
|
106
|
+
- [Scoring](scoring.md) —— Scorer / Assertion / Severity / Outcome 的完整手册。
|
package/docs/e2e-ci.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# E2E CI(设计提案,未实现)
|
|
2
|
+
|
|
3
|
+
> 状态:设计提案。本文描述 niceeval 的端到端 CI 测试方案——用真实的 `niceeval exp` 全链路(发现 → 调度 → 断言 → 评分 → 工件 → 退出码)验证框架和官方适配路径,而不是只跑 typecheck。当前 `.github/workflows/ci.yml` 只有 typecheck / site:build / docs 校验,没有任何 workflow 真正执行过 eval。
|
|
4
|
+
|
|
5
|
+
## 1. 目标
|
|
6
|
+
|
|
7
|
+
一次 e2e CI 要同时证明五件事:
|
|
8
|
+
|
|
9
|
+
1. **完整路径**:从 `evals/` 发现、experiment 展开运行矩阵、`t.*` 断言收集、gate/soft 判决、`.niceeval/<run>/` 工件落盘,到进程退出码,每一环都被真实执行并被机器校验——包括"该红的时候红"(deliberate-fail 必须 exit 1),不是只测 happy path。
|
|
10
|
+
2. **一套 suite,全矩阵 100% pass**:所有 SDK 适配项目共用**同一份** eval/experiment 定义(单一事实来源,见第 3 节),CI 把每个项目都跑一遍,期望全部 pass——任何一个 SDK 的适配层回归都会把矩阵打红。
|
|
11
|
+
3. **正反验证所有功能**:套件对每个功能都配正反两条用例——该调工具时 `t.calledTool`、不该调时 `t.notCalledTool`/`t.usedNoTools`、HITL 有 approve 也有 deny、会话有记忆也有 `newSession` 隔离;再加 deliberate-fail/error 验证"断言真的会挂"。防止"断言永真"这类静默失效。
|
|
12
|
+
4. **重复运行统计**:一个 experiment 配 `runs: 100, earlyExit: false`,验证 100 次重复的调度、并发、pass 率计数正确(niceeval 的字段就叫 `runs`,不叫 pass/trials;`earlyExit` 不关掉的话第一次通过就会 abort 其余 attempt,拿不到完整分布)。
|
|
13
|
+
5. **官方适配矩阵**:内建 `uiMessageStreamAgent`、`fromAiSdk`/`fromClaudeSdkMessages`/`fromCodexThreadEvents`/`fromPiAgentEvents` 转换器、自写 `defineAgent` 映射,以及沙箱型 claude-code / codex / bub × docker / e2b / vercel,每条官方路径至少一条 CI 覆盖。
|
|
14
|
+
|
|
15
|
+
## 2. 现状盘点
|
|
16
|
+
|
|
17
|
+
### tier1 的 eval 只当证据看,不复用
|
|
18
|
+
|
|
19
|
+
e2e 从 tier1 拷的是**被测应用和 adapter**(各 SDK 真正不同的部分);eval/experiment 套件在 e2e 里围绕功能覆盖矩阵**全新编写**,不搬 tier1 五个项目的 eval 文件——那些是文档叙事的示例,职责不同。但 tier1 的 eval 仍然值得盘点一次,因为它们是"一套 eval 能不能字面共享"的现成证据:`examples/zh/tier1/{ai-sdk-v7,claude-sdk,pi-sdk,langgraph}` 四个项目的 `evals/` 文件名完全一致(`basic-qa` / `weather-tool` / `hitl-approve` / `hitl-deny` / `session-isolation`;`codex-sdk` 是 coding agent,另有 `create-file` / `run-command`),实际 diff 过,**字面并不同构**,差异全部来自 SDK 协议本身:
|
|
20
|
+
|
|
21
|
+
- claude-sdk 的工具名是 MCP 命名空间下的 `mcp__demo-tools__get_weather`,不是裸的 `get_weather`;
|
|
22
|
+
- basic-qa 是否断言 `t.maxTokens` 取决于该 SDK 协议给不给 usage(UI Message Stream 协议帧里没有 usage,pi-sdk 的 `message_end` 里有);
|
|
23
|
+
- codex-sdk 没有 HITL 和 weather 工具,功能集不同。
|
|
24
|
+
|
|
25
|
+
所以 e2e 的"一套 eval"不能靠字面复制或软链实现,要靠参数化新写(见第 3 节);tier1 的这些差异正好告诉我们 profile 里要有哪些开关。另外源码层面确认:`src/runner/discover.ts` 固定扫 `<root>/evals` 与 `<root>/experiments`,`walkFiles` 用 Dirent 的 `isDirectory()/isFile()` 判断——**symlink 条目两者都返回 false,逐文件或逐子目录软链不会被发现**;整个 `evals/` 目录本身做软链虽可行(readdir 跟随路径),但前提是文件逐字同构,上面已经否定了。
|
|
26
|
+
|
|
27
|
+
### 框架侧对 CI 重要的事实(源码已确认)
|
|
28
|
+
|
|
29
|
+
- **退出码**:全过/跳过 → 0;任一 failed 或 errored → 1;框架崩溃 → 2(`src/cli.ts:414`)。CI 判成败首选退出码,细分读 `summary.json`。
|
|
30
|
+
- **指纹缓存会静默跳过上次 passed 的 eval**(`src/runner/run.ts:117-135`)。CI 必须加 `--force`,或保证 `.niceeval/` 不跨 run 复用,否则回归会被缓存掩盖。
|
|
31
|
+
- **judge 无 key 时 no-op**(`src/scoring/judge.ts:162`):不配 judge key,`t.judge.autoevals.*` 断言静默跳过、不判红。mock 层利用这一点零 key 跑;"judge 真的在判"要单独在有 key 的层验证。
|
|
32
|
+
- **可用 flags**:`--runs`、`--no-early-exit`、`--force`、`--junit <path>`、`--strict`、`--max-concurrency`。**不要用** `--json`(死 flag)、`--reporter`(不存在)、`--agent`/`--model`(exp 下报错)、`--sandbox`(已移除)。
|
|
33
|
+
- **`runs` 的语义**:每个 `(agent × model × eval)` 组合跑 `runs` 次;被 earlyExit abort 的 attempt 不计入分母。
|
|
34
|
+
|
|
35
|
+
## 3. 核心设计:`e2e/` 里一份共享套件 + 每个 SDK 一个薄项目
|
|
36
|
+
|
|
37
|
+
新建 `e2e/` 目录(与 `examples/`、`src/` 平级),里面三种东西:一份**全新编写**的共享 eval/experiment 定义(不复用 tier1 的 eval 文件)、从 tier1 拷来的被测应用与 adapter、每个 SDK 一个只剩"绑定"职责的薄 niceeval 项目:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
e2e/
|
|
41
|
+
shared/ # 唯一一份 eval / experiment 定义(单一事实来源)
|
|
42
|
+
profile.ts # AgentProfile 类型:工具名、能力开关
|
|
43
|
+
evals.ts # weatherTool(p) / basicQa(p) / hitlApprove(p) ... 的 factory
|
|
44
|
+
experiments.ts # ciExperiment(agent, p) / pass100(agent, p) factory
|
|
45
|
+
verdicts.ts # deliberateFail() / deliberateError() factory(只进 verdicts 实验)
|
|
46
|
+
apps/ # 被测应用,从 examples/zh/tier1/<name> 拷来,加确定性 mock 开关
|
|
47
|
+
ai-sdk-v7/ claude-sdk/ codex-sdk/ pi-sdk/ langgraph/
|
|
48
|
+
projects/ # 每个 SDK 一个 niceeval 项目:adapter + profile + 3 行 stub
|
|
49
|
+
ai-sdk-v7/
|
|
50
|
+
niceeval.config.ts
|
|
51
|
+
agents/ai-sdk-v7.ts # 拷自 tier1——adapter 是各 SDK 真正不同的部分,不共享
|
|
52
|
+
profile.ts # 该项目的能力声明(见下)
|
|
53
|
+
evals/weather-tool.eval.ts # stub:export default weatherTool(profile)
|
|
54
|
+
evals/... # 其余 stub,一 eval 一文件,保住可读的 eval id
|
|
55
|
+
experiments/ci.ts # stub:export default ciExperiment(agent, profile)
|
|
56
|
+
claude-sdk/ codex-sdk/ pi-sdk/ langgraph/
|
|
57
|
+
mock-ai-sdk/ # 纯框架项目:defineAgent + fromAiSdk 消费本地 mock server
|
|
58
|
+
mock-http/ # 纯框架项目:defineAgent + 手写映射,自定义 JSON 协议
|
|
59
|
+
scripts/
|
|
60
|
+
mock-server.mjs # mock-ai-sdk / mock-http 共用的确定性本地 server
|
|
61
|
+
verify.mjs # 元校验:跑 CLI 子进程,对照期望表检查退出码 + summary.json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3.1 为什么是 factory + profile,不是软链
|
|
65
|
+
|
|
66
|
+
eval 定义本来就不 import agent(agent 由 experiment 绑定),所以共享 eval 在框架模型上是顺的;挡路的只有两件事:discover 只认 `<root>/evals` 下的实体文件(symlink 条目不被发现,见第 2 节),以及各 SDK 的协议差异(工具名、usage、HITL 支持)。两个问题一个解法——共享层导出**参数化的 factory**,每个项目用一个 `profile.ts` 声明自己的协议现实,`evals/` 下只放 stub 文件喂给 discover:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// e2e/shared/profile.ts
|
|
70
|
+
export interface AgentProfile {
|
|
71
|
+
weatherToolName: string; // "get_weather" 或 "mcp__demo-tools__get_weather"
|
|
72
|
+
usage: boolean; // 协议是否携带 usage(决定 basic-qa 是否断言 maxTokens)
|
|
73
|
+
hitl: boolean; // 是否支持 approve/deny(codex-sdk 为 false)
|
|
74
|
+
sandboxTools: boolean; // 是否是 coding agent(决定 create-file/run-command 是否生效)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// e2e/projects/claude-sdk/profile.ts
|
|
80
|
+
import type { AgentProfile } from "../../shared/profile.ts";
|
|
81
|
+
|
|
82
|
+
export default {
|
|
83
|
+
weatherToolName: "mcp__demo-tools__get_weather", // MCP 命名空间是协议现实,不抹平
|
|
84
|
+
usage: true,
|
|
85
|
+
hitl: true,
|
|
86
|
+
sandboxTools: false,
|
|
87
|
+
} satisfies AgentProfile;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// e2e/projects/claude-sdk/evals/weather-tool.eval.ts —— 整个文件就这三行
|
|
92
|
+
import { weatherTool } from "../../../shared/evals.ts";
|
|
93
|
+
import profile from "../profile.ts";
|
|
94
|
+
|
|
95
|
+
export default weatherTool(profile);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// e2e/shared/evals.ts(节选)
|
|
100
|
+
import { defineEval } from "niceeval";
|
|
101
|
+
import { includes } from "niceeval/expect";
|
|
102
|
+
import type { AgentProfile } from "./profile.ts";
|
|
103
|
+
|
|
104
|
+
export function weatherTool(p: AgentProfile) {
|
|
105
|
+
return defineEval({
|
|
106
|
+
description: "问天气必须调用天气工具(正调)",
|
|
107
|
+
async test(t) {
|
|
108
|
+
await t.send("北京今天天气怎么样?");
|
|
109
|
+
t.succeeded();
|
|
110
|
+
t.calledTool(p.weatherToolName, { input: { city: "北京" } });
|
|
111
|
+
t.notCalledTool("send_email");
|
|
112
|
+
t.check(t.reply, includes("25"));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function noToolChitchat(p: AgentProfile) {
|
|
118
|
+
return defineEval({
|
|
119
|
+
description: "寒暄不该调用任何工具(反调)",
|
|
120
|
+
async test(t) {
|
|
121
|
+
await t.send("你好,介绍一下你自己。");
|
|
122
|
+
t.succeeded();
|
|
123
|
+
t.usedNoTools();
|
|
124
|
+
t.notCalledTool(p.weatherToolName);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
断言逻辑改一处、全矩阵生效;某个 SDK 的协议差异只体现在它自己的 profile 里,diff 一眼可见。experiment 同理:`ciExperiment(agent, profile)` 按 profile 过滤 eval 集(`hitl: false` 的项目不排 hitl 用例),每个项目的 `experiments/ci.ts` 也是 3 行 stub。
|
|
131
|
+
|
|
132
|
+
**防静默失配**:profile 关掉一个能力,对应 eval stub 就不该存在于该项目——verify.mjs 按 profile 算出每个项目的期望 eval 数,和 `summary.json` 的 results 数对账,防止"少排了用例还全绿"。
|
|
133
|
+
|
|
134
|
+
### 3.2 apps:拷贝自 tier1,允许受控漂移
|
|
135
|
+
|
|
136
|
+
`e2e/apps/<name>` 从 `examples/zh/tier1/<name>` 拷贝,保留原有的高位端口分配,只加一件事:**确定性 mock 开关**——CI 要 100% pass,被测应用就不能连真模型。规则和现有 `examples/zh/ai-sdk` 的 `AGENT_MODE=mock` 一致:提示词含"天气"就走一次天气工具调用并回答含 "25" 的固定文案,否则纯文本寒暄作答。mock 发生在**模型层**,SDK 的协议栈(流式帧、工具调用事件、HITL 中断)全部真实走一遍——这正是 e2e 要测的东西。
|
|
137
|
+
|
|
138
|
+
各 SDK 的 mock 可行性不同,按下表分层(不确定的先验证再落地,验证结果记 `memory/`):
|
|
139
|
+
|
|
140
|
+
| app | mock 策略 | 进 PR 门禁? |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| ai-sdk-v7 | app 内加 mock 分支(AI SDK 支持自定义 LanguageModel,或规则函数直出) | 是 |
|
|
143
|
+
| langgraph | LangChain 系有 FakeChatModel 类工具,Python 侧替换模型即可 | 是 |
|
|
144
|
+
| pi-sdk | 待验证 pi SDK 的模型注入点;不可行则留 nightly | 待验证 |
|
|
145
|
+
| claude-sdk | Claude Agent SDK 走 claude CLI → Anthropic API,mock 要 `ANTHROPIC_BASE_URL` 指本地 API 形状 mock,可行性待验证;不可行则留 nightly | 待验证 |
|
|
146
|
+
| codex-sdk | 同理,待验证 codex 的 base-url 注入;不可行则留 nightly | 待验证 |
|
|
147
|
+
|
|
148
|
+
拷贝的代价是和 tier1 漂移。缓解:`scripts/sync-tiers.mjs` 已经在做 origin → tier1 的同步,给它加一条 tier1 → `e2e/apps` 的 check(允许 mock 开关相关文件在白名单里差异),`pnpm tiers:check` 顺带守住。e2e/apps 的定位是"协议保真的测试夹具",不承担 tier1 的文档叙事职责,允许它为可测性加代码。
|
|
149
|
+
|
|
150
|
+
### 3.3 mock-ai-sdk / mock-http:不依赖任何 app 的框架基线
|
|
151
|
+
|
|
152
|
+
即使 claude-sdk / codex-sdk 的 mock 验证不可行,PR 门禁也必须有一个永远零依赖的全绿基线。`e2e/projects/mock-ai-sdk` 和 `mock-http` 不需要 `e2e/apps` 里的任何应用,只消费 `scripts/mock-server.mjs` 起的本地确定性 server:
|
|
153
|
+
|
|
154
|
+
- **mock-ai-sdk**:`defineAgent` + 官方 `fromAiSdk` 转换器。server 的 `/chat` 返回贴近 AI SDK `generateText` 结果形状的 JSON(`fromAiSdk` 不 import `ai` 包、只认形状,见 `src/agents/ai-sdk.ts`),adapter `fetch` 后用 `fromAiSdk` 转标准事件流,不手写映射。
|
|
155
|
+
- **mock-http**:`defineAgent` + 手写映射,自定义 JSON 协议,实现参照 `examples/zh/ai-sdk/adapter/adapter.ts` 裁剪。
|
|
156
|
+
|
|
157
|
+
两个项目复用同一份 shared 套件(profile:`usage: false, hitl: false`),分别覆盖"官方转换器"和"自写映射"两条官方支持的事件流生成方式。`runs: 100`、deliberate-fail/error、缓存行为这些框架级专项也都挂在这两个项目上,因为它们最便宜、最确定。
|
|
158
|
+
|
|
159
|
+
deliberate-fail / deliberate-error 这类"故意红"的 fixture 只进 `verdicts` 实验(不进 `ci`),由 verify.mjs 以"期望 exit 1"的方式正向消费——这是第 1 节"正反验证"在退出码层的那一半。
|
|
160
|
+
|
|
161
|
+
## 4. 三层执行模型
|
|
162
|
+
|
|
163
|
+
按"需要什么秘密、花多少钱、多久跑一次"分三层:
|
|
164
|
+
|
|
165
|
+
| 层 | 触发 | 秘密/外部依赖 | 跑什么 |
|
|
166
|
+
|---|---|---|---|
|
|
167
|
+
| **L0 框架 + 适配矩阵**(mock) | 每个 PR + push main | 无 | `e2e/projects/*` 全矩阵:mock-ai-sdk / mock-http 必跑;apps 里 mock 已落地的 SDK 项目逐个纳入。共享套件 100% pass + verdicts 期望 exit 1 + `runs: 100` 统计 + 缓存行为 |
|
|
168
|
+
| **L1 示例冒烟**(mock) | 每个 PR + push main | 无 | `examples/zh/ai-sdk` 在 `AGENT_MODE=mock` 下真的能 `niceeval exp` 跑绿——保证公开示例不烂 |
|
|
169
|
+
| **L2 真实层** | nightly cron + 手动 dispatch | 各家 API key;Docker | e2e/projects 切真模型跑同一份套件(验证 mock 没有把真实协议测歪)+ 沙箱矩阵 + judge 真产分 |
|
|
170
|
+
|
|
171
|
+
L0/L1 是合并门禁;L2 是每日健康信号(真模型有随机性,不阻塞 PR)。注意 L0 里"CI 起 app"不违反 tier1 的"eval 不代管进程"原则——起应用的是 CI workflow(扮演用户),不是 eval 框架。
|
|
172
|
+
|
|
173
|
+
### 4.1 L0 的 100% pass 语义
|
|
174
|
+
|
|
175
|
+
mock 是确定性的,所以期望值是精确的,verify.mjs 对每个项目断言:
|
|
176
|
+
|
|
177
|
+
- `ci` 实验:exit 0,且 `passed === 期望 eval 数 × runs, failed === 0, errored === 0`;期望 eval 数由该项目 profile 推出(对账防少排,见 3.1)。
|
|
178
|
+
- `verdicts` 实验(仅 mock-ai-sdk / mock-http):exit 1,failed 与 errored 计数符合期望。
|
|
179
|
+
- `pass-100` 实验(仅 mock-ai-sdk):`runs: 100, earlyExit: false, maxConcurrency: 16`,断言 `passed === 200`(正反两条 eval × 100)。任何一次计数不对都说明调度/评分/汇总有回归。
|
|
180
|
+
|
|
181
|
+
### 4.2 L2:真实适配层(nightly)
|
|
182
|
+
|
|
183
|
+
两块内容。第一块:e2e/projects 里已接真模型开关的项目,用**同一份共享套件**跑 `--runs 2`,加 judge key 顺带验证 judge 断言真的产出分数而非 no-op;真模型允许波动,失败发通知(GitHub issue / 通知渠道)而不是标红 main。
|
|
184
|
+
|
|
185
|
+
第二块:沙箱矩阵,按"官方适配器 × 沙箱后端"取最小生成集,不做全叉乘:
|
|
186
|
+
|
|
187
|
+
| Job | agent | sandbox | 秘密 | eval 集 |
|
|
188
|
+
|---|---|---|---|---|
|
|
189
|
+
| claude-code × docker | `claudeCodeAgent()` | `dockerSandbox()` | `ANTHROPIC_API_KEY` | `sandbox-smoke` + `examples/zh/coding-agent-skill` 子集(`--runs 1`) |
|
|
190
|
+
| codex × docker | `codexAgent()` | `dockerSandbox()` | `CODEX_API_KEY` | `sandbox-smoke`;额外断言 `trace.json` 产生(codex 是唯一原生发 OTLP 的内置 agent,顺带覆盖 tracing 接收链路) |
|
|
191
|
+
| bub × docker | `bubAgent()` | `dockerSandbox()` | `BUB_API_KEY` | `sandbox-smoke` |
|
|
192
|
+
| claude-code × e2b | `claudeCodeAgent()` | `e2bSandbox({ template: "fasteval-agents" })` | `ANTHROPIC_API_KEY` + `E2B_API_KEY` | `sandbox-smoke` |
|
|
193
|
+
| claude-code × vercel | `claudeCodeAgent()` | `vercelSandbox()` | `ANTHROPIC_API_KEY` + Vercel token | `sandbox-smoke`(注意 vercel session 寿命 ~360s 上限,eval 要够小) |
|
|
194
|
+
|
|
195
|
+
`sandbox-smoke` 也是 shared 里的 factory(`sandboxTools: true` 的 profile 才排):让 agent 创建一个指定内容的文件,断言 `t.sandbox.fileChanged` + `diff` 内容——目标是验证"沙箱起得来、agent 装得上、transcript 读得回",不是考模型能力,所以提示词要简单到几乎不可能失败。
|
|
196
|
+
|
|
197
|
+
已知约束(来自项目 memory,落地时直接采纳):e2b 的 `base` 模板 node20/481MB 会 OOM,必须用预制的 `fasteval-agents` 模板;vercel 后端默认并发 1(避免 429),session 上限约 360s;沙箱内不要 hardcode `/home/node`。
|
|
198
|
+
|
|
199
|
+
L2 每个 experiment 都设 `budget`(建议单 job ≤ $2)和 `timeoutMs`,workflow 层再加 job timeout 兜底。
|
|
200
|
+
|
|
201
|
+
## 5. 元校验脚本(e2e 的"真正的测试")
|
|
202
|
+
|
|
203
|
+
L0 的本体不是那些 eval——eval 是 fixture;本体是 `e2e/scripts/verify.mjs`:它把 CLI 当黑盒子进程跑,对照期望表校验退出码和 `summary.json`:
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
// e2e/scripts/verify.mjs(示意)
|
|
207
|
+
import { execFileSync } from "node:child_process";
|
|
208
|
+
import { readFileSync } from "node:fs";
|
|
209
|
+
|
|
210
|
+
const PROJECTS = [
|
|
211
|
+
{ dir: "projects/mock-ai-sdk", exp: "ci", expectExit: 0, expect: exactCounts(profileOf("mock-ai-sdk")) },
|
|
212
|
+
{ dir: "projects/mock-ai-sdk", exp: "verdicts", expectExit: 1, expect: { failedAtLeast: 1, erroredAtLeast: 1 } },
|
|
213
|
+
{ dir: "projects/mock-ai-sdk", exp: "pass-100", expectExit: 0, expect: { passed: 200, failed: 0, errored: 0 } },
|
|
214
|
+
{ dir: "projects/mock-http", exp: "ci", expectExit: 0, expect: exactCounts(profileOf("mock-http")) },
|
|
215
|
+
{ dir: "projects/ai-sdk-v7", exp: "ci", expectExit: 0, expect: exactCounts(profileOf("ai-sdk-v7")) },
|
|
216
|
+
// ... 其余已接 mock 的 SDK 项目
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
for (const c of PROJECTS) {
|
|
220
|
+
const code = runNiceeval(c.dir, ["exp", c.exp, "--force", "--junit", `junit-${c.exp}.xml`]);
|
|
221
|
+
assert.equal(code, c.expectExit, `${c.dir}/${c.exp} 退出码`);
|
|
222
|
+
const summary = JSON.parse(readFileSync(latestRunDir(c.dir) + "/summary.json", "utf8"));
|
|
223
|
+
assertCounts(summary, c.expect); // exactCounts 按 profile 推期望 eval 数,对账防少排
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
verify.mjs 还负责两个专项:
|
|
228
|
+
|
|
229
|
+
- **缓存行为**:同一 experiment 连跑两次,第二次**不带** `--force` → 断言 summary 里携入的 cached 结果仍计为 passed 且没有真跑(比较 `durationMs` 或 attempt 工件时间戳);第二次**带** `--force` → 断言全部真跑。这把"CI 忘了 --force 会静默跳过"的坑变成被测行为。
|
|
230
|
+
- **工件形状**:抽查一个 attempt 目录,断言 `events.json` 是 JSON array、`summary.json` 顶层有 `passed/failed/errored/results[]`、`results[].artifactsDir` 指向存在的目录。防止 results-format 无声漂移(当前 writer 没有 schemaVersion,只能靠这层守)。
|
|
231
|
+
|
|
232
|
+
## 6. L1:示例冒烟层
|
|
233
|
+
|
|
234
|
+
目的只有一个:公开示例必须能按 README 跑通。首选 `examples/zh/ai-sdk`,因为它的 server 有零 key 的 `AGENT_MODE=mock`:
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
# job 示意
|
|
238
|
+
- run: pnpm install --dir examples/zh/ai-sdk
|
|
239
|
+
- run: AGENT_MODE=mock node server & # 起 127.0.0.1:5188,等端口就绪
|
|
240
|
+
- run: pnpm exec niceeval list # eval 发现冒烟
|
|
241
|
+
- run: pnpm exec niceeval exp compare-models --runs 1 --force --junit junit.xml
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
验收前提(落地时先确认,不满足就先修示例的 mock):mock 模式下 `weather-tool` 等 gate 断言必须可过——即 mock server 对天气提问要真的产生 `get_weather` 工具调用事件。judge 断言无 key 自动跳过,正好符合本层"零秘密"的定位。
|
|
245
|
+
|
|
246
|
+
注意 L1 和 L0 的分工:L0 测的是框架和适配层(fixture 允许为可测性改造);L1 测的是"用户照 README 操作会不会翻车"(fixture 一个字都不许为 CI 改)。`examples/zh/tier1/*` 不进 L1——它们的可测化版本已经在 `e2e/apps` 里了。
|
|
247
|
+
|
|
248
|
+
## 7. Workflow 编排
|
|
249
|
+
|
|
250
|
+
新增 `.github/workflows/e2e.yml`:
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
on:
|
|
254
|
+
push: { branches: [main] }
|
|
255
|
+
pull_request:
|
|
256
|
+
schedule: [{ cron: "0 3 * * *" }] # L2 nightly
|
|
257
|
+
workflow_dispatch:
|
|
258
|
+
|
|
259
|
+
jobs:
|
|
260
|
+
e2e-matrix: # L0,PR 门禁
|
|
261
|
+
runs-on: ubuntu-latest
|
|
262
|
+
steps: [checkout, pnpm install, 起 mock server 与已接 mock 的 apps, node e2e/scripts/verify.mjs]
|
|
263
|
+
|
|
264
|
+
e2e-examples: # L1,PR 门禁
|
|
265
|
+
runs-on: ubuntu-latest
|
|
266
|
+
# 见第 6 节
|
|
267
|
+
|
|
268
|
+
e2e-real: # L2,仅 schedule / dispatch
|
|
269
|
+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
270
|
+
strategy: { fail-fast: false, matrix: { target: [claude-docker, codex-docker, bub-docker, claude-e2b, claude-vercel, sdk-projects-real] } }
|
|
271
|
+
runs-on: ubuntu-latest # ubuntu runner 自带 Docker
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
所有 job 统一约定:
|
|
275
|
+
|
|
276
|
+
- 每次调用 `niceeval exp` 都带 `--force`(禁指纹缓存)+ `--junit <path>`;junit 和整个 `.niceeval/` 目录用 `actions/upload-artifact` 上传,失败时可下载 `events.json` / `trace.json` 排查。
|
|
277
|
+
- 成败判据 = 进程退出码;verify.mjs 层再做计数级校验。
|
|
278
|
+
- L0/L1 不配置任何 API key secret——judge 断言 no-op 是预期行为,一旦某个 mock 层 job 意外需要 key,说明 fixture 写错了。
|
|
279
|
+
|
|
280
|
+
## 8. 分阶段落地
|
|
281
|
+
|
|
282
|
+
1. **P1(先立骨架)**:建 `e2e/shared` + `e2e/projects/{mock-ai-sdk,mock-http}` + verify.mjs(共享套件、正反 eval、deliberate-fail/error、pass-100),加 `e2e-matrix` job 进 PR 门禁。这一步零外部依赖,收益最大——完整路径、正反断言、100 次统计、退出码、缓存行为全部落网,factory/profile 机制也在最便宜的项目上定型。
|
|
283
|
+
2. **P2**:拷 `examples/zh/tier1/ai-sdk-v7` 进 `e2e/apps`,加 mock 模型分支,`e2e/projects/ai-sdk-v7` 接入共享套件——第一个真实 SDK 协议栈进 L0 矩阵。同步确认/修好 `examples/zh/ai-sdk` 的 mock 可过 gate 断言,加 `e2e-examples` job。
|
|
284
|
+
3. **P3**:langgraph、pi-sdk 逐个验证 mock 注入点并进矩阵;claude-sdk / codex-sdk 的 base-url mock 可行性调研,结论记 `memory/`,不可行的留 L2。给 `sync-tiers.mjs` 加 tier1 → e2e/apps 的 drift check。
|
|
285
|
+
4. **P4**:补 `sandbox-smoke` factory;开 L2 的 claude-code × docker(第一条真实沙箱链路),随后铺满 L2 矩阵(codex / bub / e2b / vercel + sdk-projects 真模型 + judge)。
|
|
286
|
+
5. **之后**:给 mock 加"确定性失败注入"(如 server 每第 4 次请求返回坏答案)以校验非 0/100 的 pass 率计算;`e2e/projects/{codex-sdk,langgraph}` 的 OTel 瀑布图链路(`spanMapper` / 固定端口)补一条"trace.json 非空"的冒烟断言,纳入 L2。
|
|
287
|
+
|
|
288
|
+
## 9. 明确不做的
|
|
289
|
+
|
|
290
|
+
- 不做字面共享:不把同一份 `.eval.ts` 软链/复制进多个项目——discover 不跟随 symlink 条目,且各 SDK 的工具名和能力差异是协议现实,抹平它们等于测一个不存在的协议。共享的单位是 factory,差异收敛进 profile。
|
|
291
|
+
- 不和 tier1 共享 eval:`examples/zh/tier1` 的 eval 继续服务文档叙事,e2e 的套件独立编写、独立演进,两边不建同步关系;从 tier1 拷的只有应用和 adapter(受 drift check 约束),也不要求公开示例携带 CI 专用的 mock 开关(`examples/zh/ai-sdk` 本来就有的除外)。
|
|
292
|
+
- 不测"OTel 派生事件"这类断言——OTel 只喂瀑布图,e2e 要测的是它产出了 `trace.json`,不是它替代了事件映射。
|
|
293
|
+
- 不在 PR 门禁里跑任何真模型——随机性 + 费用 + secret 暴露面都不适合。
|
|
294
|
+
- 不用 `--tag` 做 CI 内的用例切分(当前实现只收单值,与文档不一致;分层用 experiment 的 `evals` 过滤器表达,更明确)。
|
|
295
|
+
- 不依赖 `summary.json` 里的版本字段(writer 尚未写 `schemaVersion`),形状校验只断言当前实际字段。
|