niceeval 0.2.1 → 0.4.0
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 +34 -34
- package/README.zh.md +29 -15
- package/docs/README.md +2 -1
- package/docs/adapters/collection.md +1 -1
- package/docs/adapters/contract.md +6 -5
- package/docs/adapters/reference/claude-code-otel-telemetry.md +1 -1
- package/docs/architecture.md +20 -20
- package/docs/assertions.md +0 -1
- package/docs/capabilities-by-construction.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/concepts.md +2 -2
- package/docs/e2e-ci.md +135 -98
- package/docs/eval-authoring.md +3 -3
- package/docs/experiments.md +7 -4
- package/docs/observability.md +9 -5
- package/docs/origin-integration.md +18 -18
- package/docs/references.md +1 -1
- package/docs/reports.md +551 -0
- package/docs/results-format.md +3 -3
- package/docs/results-lib.md +191 -0
- package/docs/runner.md +1 -1
- package/docs/sandbox.md +1 -1
- package/docs/source-map.md +22 -2
- package/docs/tier-sync.md +74 -58
- package/docs/view.md +41 -4
- package/package.json +29 -3
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/claude-code.ts +18 -1
- package/src/agents/codex.ts +3 -2
- package/src/agents/index.ts +16 -0
- package/src/agents/openai-compat.test.ts +56 -0
- package/src/agents/openai-compat.ts +151 -0
- package/src/agents/types.ts +3 -1
- package/src/cli.ts +2 -1
- package/src/context/context.test.ts +78 -1
- package/src/context/context.ts +19 -11
- package/src/context/session.ts +2 -0
- package/src/context/types.ts +11 -5
- package/src/i18n/en.ts +8 -4
- package/src/i18n/zh-CN.ts +6 -3
- package/src/o11y/cost.test.ts +40 -0
- package/src/o11y/cost.ts +27 -5
- package/src/o11y/derive.ts +2 -2
- package/src/o11y/otlp/mappers/claude-code.test.ts +31 -0
- package/src/o11y/otlp/mappers/claude-code.ts +24 -0
- package/src/o11y/otlp/mappers/index.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +127 -0
- package/src/o11y/prices.json +176 -119
- package/src/o11y/types.ts +2 -1
- package/src/report/aggregate.ts +215 -0
- package/src/report/compute.ts +405 -0
- package/src/report/format.ts +47 -0
- package/src/report/index.ts +40 -0
- package/src/report/metrics.ts +96 -0
- package/src/report/react/CaseList.tsx +70 -0
- package/src/report/react/DeltaTable.tsx +79 -0
- package/src/report/react/MetricMatrix.tsx +75 -0
- package/src/report/react/MetricScatter.tsx +217 -0
- package/src/report/react/MetricTable.tsx +71 -0
- package/src/report/react/RunOverview.tsx +87 -0
- package/src/report/react/Scoreboard.tsx +87 -0
- package/src/report/react/cell.tsx +49 -0
- package/src/report/react/colors.ts +42 -0
- package/src/report/react/data.ts +17 -0
- package/src/report/react/fixtures.ts +236 -0
- package/src/report/react/format.ts +34 -0
- package/src/report/react/index.tsx +34 -0
- package/src/report/react/render.test.tsx +303 -0
- package/src/report/react/styles.css +302 -0
- package/src/report/report.test.ts +667 -0
- package/src/report/types.ts +210 -0
- package/src/results/copy.ts +200 -0
- package/src/results/format.ts +75 -0
- package/src/results/index.ts +27 -0
- package/src/results/open.ts +207 -0
- package/src/results/results.test.ts +359 -0
- package/src/results/select.ts +101 -0
- package/src/results/types.ts +98 -0
- package/src/runner/attempt.ts +3 -1
- package/src/runner/report.test.ts +111 -0
- package/src/runner/report.ts +52 -1
- package/src/runner/reporters/artifacts.ts +1 -1
- package/src/runner/reporters/braintrust.test.ts +106 -0
- package/src/runner/reporters/braintrust.ts +197 -0
- package/src/runner/reporters/console.ts +3 -3
- package/src/runner/reporters/index.ts +3 -1
- package/src/runner/reporters/live.ts +17 -5
- package/src/runner/reporters/shared.ts +3 -0
- package/src/runner/reporters/table.ts +1 -0
- package/src/runner/run.ts +31 -18
- package/src/runner/types.ts +20 -1
- package/src/sandbox/types.ts +3 -3
- package/src/shared/format.ts +8 -2
- package/src/view/app/App.tsx +82 -17
- package/src/view/app/components/AttemptModal.tsx +9 -2
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CostScoreChart.tsx +127 -0
- package/src/view/app/components/LazyArtifact.tsx +2 -1
- package/src/view/app/i18n.ts +13 -1
- package/src/view/app/lib/artifact-url.ts +6 -0
- package/src/view/app/lib/attempt-route.test.ts +89 -0
- package/src/view/app/lib/attempt-route.ts +52 -0
- package/src/view/app/lib/chart.ts +72 -0
- package/src/view/app/lib/outcome.ts +1 -1
- package/src/view/app/types.ts +4 -5
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/src/view/index.ts +36 -7
- package/src/view/loader.ts +14 -7
- package/src/view/server.ts +7 -0
- package/src/view/shared/types.ts +14 -1
- package/src/view/styles.css +43 -0
- package/src/view/template.html +0 -1
package/docs/e2e-ci.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
# E2E CI(
|
|
1
|
+
# E2E CI(部分落地)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 状态:核心骨架已落地并在本地跑绿(2026-07-07)。`e2e/` 目录已建:shared factory/profile 套件、从 tier1 拷来的五个被测应用(`e2e/apps/`)、五个 SDK 薄项目(`e2e/projects/`)、`e2e/scripts/verify.mjs`——全矩阵 6 次 CLI 调用(5 个 ci 全绿 exit 0 + ai-sdk-v7 verdicts 按期望 exit 1)全部符合期望表。**尚未落地**:`.github/workflows/e2e.yml`(GitHub secrets 还没建)、`aisdk-transformer` / `http-mapping` 两个纯框架项目、示例冒烟 job(第 6 节,P0 文档债也还没修)、L1 nightly 沙箱矩阵。与原提案的实现偏差:verdicts fixture 挂在最便宜的 ai-sdk-v7 上(纯框架项目未建);`ciExperiment(agent)` 不吃 profile——能力过滤靠"stub 文件是否存在",ci/verdicts 切分靠 `evals` 谓词按 `deliberate-` 前缀;profile 实际字段是 `weatherToolName / calcToolName / usage / sandboxTools / workspaceDir`(hitl 由 `calcToolName` 非空推导);`e2e/apps` 不是逐字节拷贝而是**只保 backend 的协议夹具**——前端、vite/开发工作流、langgraph 的 node 侧文件和 Jaeger compose 都已裁掉(前端不参与被测协议,裁掉能显著缩短 CI 安装),将来 tier1 → e2e/apps 的 drift check 对比范围只框 `src/backend/`;另注意 `budget` 护栏对不报 usage 的 agent 无法执行(见 `memory/e2e-suite-landing-gotchas.md`)。
|
|
4
|
+
>
|
|
5
|
+
> 本文其余部分保持原提案文本——用真实的 `niceeval exp` 全链路(发现 → 调度 → 断言 → 评分 → 工件 → 退出码)验证框架和官方适配路径,而不是只跑 typecheck。当前 `.github/workflows/ci.yml` 只有 typecheck / site:build / docs 校验,没有任何 workflow 真正执行过 eval。**e2e 全程不允许假 AI**:所有被测应用和框架级项目都调真实模型,CI 里的 key 从 GitHub Actions secrets 注入,本地开发者自己在 `.env` 放真实 key——这和 `examples/zh/origin` 那 6 个示例的既有政策一致(见 `memory/origin-examples-real-ai-credentials.md`),e2e 不搞例外。
|
|
4
6
|
|
|
5
7
|
## 1. 目标
|
|
6
8
|
|
|
7
9
|
一次 e2e CI 要同时证明五件事:
|
|
8
10
|
|
|
9
11
|
1. **完整路径**:从 `evals/` 发现、experiment 展开运行矩阵、`t.*` 断言收集、gate/soft 判决、`.niceeval/<run>/` 工件落盘,到进程退出码,每一环都被真实执行并被机器校验——包括"该红的时候红"(deliberate-fail 必须 exit 1),不是只测 happy path。
|
|
10
|
-
2. **一套 suite
|
|
12
|
+
2. **一套 suite,全矩阵可靠通过**:所有 SDK 适配项目共用**同一份** eval/experiment 定义(单一事实来源,见第 3 节),CI 把每个项目都跑一遍。真实模型跑真实调用,单次尝试允许抖动,但有限重试(见 4.1)吸收不了的失败一定是真回归——任何一个 SDK 的适配层回归都会把矩阵打红,不会被"模型这次没答好"这种噪音长期掩盖,也不会被 retry 永久掩盖。
|
|
11
13
|
3. **正反验证所有功能**:套件对每个功能都配正反两条用例——该调工具时 `t.calledTool`、不该调时 `t.notCalledTool`/`t.usedNoTools`、HITL 有 approve 也有 deny、会话有记忆也有 `newSession` 隔离;再加 deliberate-fail/error 验证"断言真的会挂"。防止"断言永真"这类静默失效。
|
|
12
|
-
4. **重复运行统计**:一个 experiment 配 `runs:
|
|
14
|
+
4. **重复运行统计**:一个 experiment 配 `runs: N, earlyExit: false` 跑重复调度、并发、pass 率计数(niceeval 的字段就叫 `runs`,不叫 pass/trials;`earlyExit` 不关掉的话第一次通过就会 abort 其余 attempt,拿不到完整分布)。真实模型调用要花钱,PR 门禁上 `N` 取小值(如 10),完整的大样本统计(如 100)挪到 nightly 跑,见第 4 节。
|
|
13
15
|
5. **官方适配矩阵**:内建 `uiMessageStreamAgent`、`fromAiSdk`/`fromClaudeSdkMessages`/`fromCodexThreadEvents`/`fromPiAgentEvents` 转换器、自写 `defineAgent` 映射,以及沙箱型 claude-code / codex / bub × docker / e2b / vercel,每条官方路径至少一条 CI 覆盖。
|
|
14
16
|
|
|
15
17
|
## 2. 现状盘点
|
|
@@ -24,11 +26,17 @@ e2e 从 tier1 拷的是**被测应用和 adapter**(各 SDK 真正不同的部分
|
|
|
24
26
|
|
|
25
27
|
所以 e2e 的"一套 eval"不能靠字面复制或软链实现,要靠参数化新写(见第 3 节);tier1 的这些差异正好告诉我们 profile 里要有哪些开关。另外源码层面确认:`src/runner/discover.ts` 固定扫 `<root>/evals` 与 `<root>/experiments`,`walkFiles` 用 Dirent 的 `isDirectory()/isFile()` 判断——**symlink 条目两者都返回 false,逐文件或逐子目录软链不会被发现**;整个 `evals/` 目录本身做软链虽可行(readdir 跟随路径),但前提是文件逐字同构,上面已经否定了。
|
|
26
28
|
|
|
29
|
+
另外,tier1 各项目本来就已经在调真实模型——`claude-sdk`/`pi-sdk`/`langgraph` 的 backend 用 `process.env.AGENT_MODEL ?? "deepseek-v4-flash"` 走 DeepSeek 兼容端点,`codex-sdk` 默认 `"gpt-5.4"` 走 Codex 代理,凭据映射和 `examples/zh/origin` 一致(见 `memory/origin-examples-real-ai-credentials.md`)。e2e/apps 直接复用这套映射即可,不需要为"跑得起来"发明新机制,只需要在 CI 里把对应 key 从 GitHub Actions secrets 注入(见第 3.2、7 节)。
|
|
30
|
+
|
|
31
|
+
### `examples/zh/ai-sdk` 的 README 是需要清理的过期文档
|
|
32
|
+
|
|
33
|
+
`examples/zh/ai-sdk/README.md` 现在写着"默认是 `AGENT_MODE=mock`,不需要 API key",但实测 `src/server.ts`、`src/ai-sdk-runtime.ts`、`src/models.ts` 里**完全没有** `AGENT_MODE` 分支或任何 mock provider——`resolveModel()` 无条件要求真实 `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`,`.env.example` 里也不提 `AGENT_MODE`。这条 mock 声明本身就是过期文档,不是 e2e 该保留或恢复的能力——落地 L1(第 6 节)之前要先把 README 这几行改成真实 key 说明,而不是去"实现"一个文档里写着但代码里没有、而且按项目政策也不该有的 mock 分支。
|
|
34
|
+
|
|
27
35
|
### 框架侧对 CI 重要的事实(源码已确认)
|
|
28
36
|
|
|
29
|
-
- **退出码**:全过/跳过 → 0;任一 failed 或 errored → 1;框架崩溃 → 2(`src/cli.ts:
|
|
30
|
-
- **指纹缓存会静默跳过上次 passed 的 eval**(`src/runner/run.ts
|
|
31
|
-
- **judge 无 key 时 no-op**(`src/scoring/judge.ts
|
|
37
|
+
- **退出码**:全过/跳过 → 0;任一 failed 或 errored → 1;框架崩溃 → 2(`src/cli.ts:461,468`)。CI 判成败首选退出码,细分读 `summary.json`。
|
|
38
|
+
- **指纹缓存会静默跳过上次 passed 的 eval**(`src/runner/run.ts` 的 `run()`,匹配逻辑在文件开头 ~60 行附近,以 `cacheKey`/`computeFingerprint` 为核心)。CI 必须加 `--force`,或保证 `.niceeval/` 不跨 run 复用,否则回归会被缓存掩盖。
|
|
39
|
+
- **judge 无 key 时 no-op**(`src/scoring/judge.ts` 的 `resolveJudge()`/`buildJudge()`,`if (!resolved.apiKey) return noOpJudge()`):不配 judge key,`t.judge.autoevals.*` 断言静默跳过、不判红。e2e 里"judge 真的在判"必须显式配 judge key 验证,不能靠这条 no-op 蒙混过关——这一点和是否用真实模型无关,是两个独立的开关。
|
|
32
40
|
- **可用 flags**:`--runs`、`--no-early-exit`、`--force`、`--junit <path>`、`--strict`、`--max-concurrency`。**不要用** `--json`(死 flag)、`--reporter`(不存在)、`--agent`/`--model`(exp 下报错)、`--sandbox`(已移除)。
|
|
33
41
|
- **`runs` 的语义**:每个 `(agent × model × eval)` 组合跑 `runs` 次;被 earlyExit abort 的 attempt 不计入分母。
|
|
34
42
|
|
|
@@ -41,9 +49,9 @@ e2e/
|
|
|
41
49
|
shared/ # 唯一一份 eval / experiment 定义(单一事实来源)
|
|
42
50
|
profile.ts # AgentProfile 类型:工具名、能力开关
|
|
43
51
|
evals.ts # weatherTool(p) / basicQa(p) / hitlApprove(p) ... 的 factory
|
|
44
|
-
experiments.ts # ciExperiment(agent, p) /
|
|
52
|
+
experiments.ts # ciExperiment(agent, p) / passN(agent, p) factory
|
|
45
53
|
verdicts.ts # deliberateFail() / deliberateError() factory(只进 verdicts 实验)
|
|
46
|
-
apps/ # 被测应用,从 examples/zh/tier1/<name>
|
|
54
|
+
apps/ # 被测应用,从 examples/zh/tier1/<name> 拷来,原样保留真实调用
|
|
47
55
|
ai-sdk-v7/ claude-sdk/ codex-sdk/ pi-sdk/ langgraph/
|
|
48
56
|
projects/ # 每个 SDK 一个 niceeval 项目:adapter + profile + 3 行 stub
|
|
49
57
|
ai-sdk-v7/
|
|
@@ -54,10 +62,9 @@ e2e/
|
|
|
54
62
|
evals/... # 其余 stub,一 eval 一文件,保住可读的 eval id
|
|
55
63
|
experiments/ci.ts # stub:export default ciExperiment(agent, profile)
|
|
56
64
|
claude-sdk/ codex-sdk/ pi-sdk/ langgraph/
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
aisdk-transformer/ # 纯框架项目:defineAgent + 官方 fromAiSdk,进程内直调真实 DeepSeek
|
|
66
|
+
http-mapping/ # 纯框架项目:defineAgent + 手写映射,进程内直调真实 DeepSeek REST
|
|
59
67
|
scripts/
|
|
60
|
-
mock-server.mjs # mock-ai-sdk / mock-http 共用的确定性本地 server
|
|
61
68
|
verify.mjs # 元校验:跑 CLI 子进程,对照期望表检查退出码 + summary.json
|
|
62
69
|
```
|
|
63
70
|
|
|
@@ -66,12 +73,14 @@ e2e/
|
|
|
66
73
|
eval 定义本来就不 import agent(agent 由 experiment 绑定),所以共享 eval 在框架模型上是顺的;挡路的只有两件事:discover 只认 `<root>/evals` 下的实体文件(symlink 条目不被发现,见第 2 节),以及各 SDK 的协议差异(工具名、usage、HITL 支持)。两个问题一个解法——共享层导出**参数化的 factory**,每个项目用一个 `profile.ts` 声明自己的协议现实,`evals/` 下只放 stub 文件喂给 discover:
|
|
67
74
|
|
|
68
75
|
```typescript
|
|
69
|
-
// e2e/shared/profile.ts
|
|
76
|
+
// e2e/shared/profile.ts(与源码同步)
|
|
70
77
|
export interface AgentProfile {
|
|
71
|
-
weatherToolName: string;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
weatherToolName: string | null; // "get_weather" 或 MCP 命名空间名;coding agent 为 null
|
|
79
|
+
calcToolName: string | null; // 经审批门控的计算器;不支持 HITL(codex-sdk)为 null
|
|
80
|
+
searchToolName: string | null; // 网络搜索工具;只有 ai-sdk-v7 的被测应用注册了
|
|
81
|
+
usage: boolean; // 协议是否携带 usage(决定 basic-qa 是否断言 maxTokens)
|
|
82
|
+
sandboxTools: boolean; // 是否是 coding agent(决定 create-file/run-command 等是否生效)
|
|
83
|
+
workspaceDir?: string; // coding agent 的工作目录(eval 直接读磁盘核实)
|
|
75
84
|
}
|
|
76
85
|
```
|
|
77
86
|
|
|
@@ -131,56 +140,89 @@ export function noToolChitchat(p: AgentProfile) {
|
|
|
131
140
|
|
|
132
141
|
**防静默失配**:profile 关掉一个能力,对应 eval stub 就不该存在于该项目——verify.mjs 按 profile 算出每个项目的期望 eval 数,和 `summary.json` 的 results 数对账,防止"少排了用例还全绿"。
|
|
133
142
|
|
|
134
|
-
### 3.2 apps:拷贝自 tier1
|
|
143
|
+
### 3.2 apps:拷贝自 tier1,真实 key 从 secrets 注入
|
|
135
144
|
|
|
136
|
-
`e2e/apps/<name>` 从 `examples/zh/tier1/<name>`
|
|
145
|
+
`e2e/apps/<name>` 从 `examples/zh/tier1/<name>` 拷贝,保留原有的高位端口分配,**不加任何 mock 分支**——tier1 里这些应用本来就已经是真实调用(`AGENT_MODEL ?? "deepseek-v4-flash"` / Codex 代理),e2e 直接复用同一套凭据映射即可。CI 里跑这些应用时,workflow 把 `DEEPSEEK_API_KEY`(claude-sdk / pi-sdk / langgraph / ai-sdk-v7,分别走 Anthropic 兼容和 OpenAI 兼容两个 base URL)、`CODEX_API_KEY`(codex-sdk)从 GitHub Actions secrets 注入进子进程环境,本地开发者跑同样的套件就在自己的 `.env` 里放同一把 key(和 tier1 项目现在的开发方式完全一致,没有新概念)。
|
|
137
146
|
|
|
138
|
-
|
|
147
|
+
为了让 PR 门禁的花费可控,e2e/apps 统一钉住**便宜模型**(DeepSeek `deepseek-v4-flash` / Codex 代理默认模型),更贵的模型对比矩阵挪到 4.2 的 nightly 层。
|
|
139
148
|
|
|
140
|
-
|
|
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 | 待验证 |
|
|
149
|
+
拷贝的代价是和 tier1 漂移。缓解:`scripts/sync-tiers.mjs` 已经在做 origin → tier1 的同步,给它加一条 tier1 → `e2e/apps` 的 check,`pnpm tiers:check` 顺带守住。e2e/apps 的定位是"协议保真的测试夹具",不承担 tier1 的文档叙事职责,允许它为可测性加代码(比如把端口做成可配置的),但不允许加假响应。
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
### 3.3 aisdk-transformer / http-mapping:不依赖任何 app 进程的框架基线
|
|
149
152
|
|
|
150
|
-
|
|
153
|
+
PR 门禁需要一个启动成本最低、最便宜的真实基线,不依赖起 5 个 SDK app 进程。`e2e/projects/aisdk-transformer` 和 `http-mapping` 不需要 `e2e/apps` 里的任何应用——adapter 在进程内直接调真实 DeepSeek,不经过任何被测服务器:
|
|
151
154
|
|
|
152
|
-
|
|
155
|
+
```typescript
|
|
156
|
+
// e2e/projects/aisdk-transformer/agents/aisdk-transformer.ts(示意)
|
|
157
|
+
import { defineAgent } from "niceeval";
|
|
158
|
+
import { fromAiSdk } from "niceeval/agents/ai-sdk";
|
|
159
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
160
|
+
import { generateText, tool } from "ai";
|
|
161
|
+
import { z } from "zod";
|
|
162
|
+
|
|
163
|
+
const deepseek = createOpenAI({
|
|
164
|
+
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
165
|
+
baseURL: "https://api.deepseek.com",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export default defineAgent({
|
|
169
|
+
name: "aisdk-transformer",
|
|
170
|
+
async send(ctx) {
|
|
171
|
+
const result = await generateText({
|
|
172
|
+
model: deepseek.chat("deepseek-v4-flash"),
|
|
173
|
+
messages: ctx.messages,
|
|
174
|
+
tools: {
|
|
175
|
+
get_weather: tool({
|
|
176
|
+
inputSchema: z.object({ city: z.string() }),
|
|
177
|
+
execute: async ({ city }) => ({ city, tempC: 25, condition: "晴" }),
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
return fromAiSdk(result); // 官方转换器,真实的 generateText 结果直喂
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
```
|
|
153
185
|
|
|
154
|
-
- **
|
|
155
|
-
- **
|
|
186
|
+
- **aisdk-transformer**:`defineAgent` + 官方 `fromAiSdk` 转换器,喂真实 `generateText` 结果(不是伪造成 AI SDK 形状的假数据)。
|
|
187
|
+
- **http-mapping**:`defineAgent` + 手写映射,直接 `fetch` DeepSeek 的 chat completions REST 接口,自己把 JSON 响应映射成标准事件流,实现参照 `examples/zh/ai-sdk/adapter/adapter.ts` 裁剪。
|
|
156
188
|
|
|
157
|
-
两个项目复用同一份 shared 套件(profile:`usage: false, hitl: false`),分别覆盖"官方转换器"和"自写映射"
|
|
189
|
+
两个项目复用同一份 shared 套件(profile:`usage: false, hitl: false`),分别覆盖"官方转换器"和"自写映射"两条官方支持的事件流生成方式,都是对真实 DeepSeek 端点的真实调用——省的是"起被测服务器进程"和"更贵的模型"这两项成本,不是省"真实推理"这一项。`runs: N` 统计、deliberate-fail/error、缓存行为这些框架级专项都挂在这两个项目上,因为它们最便宜、启动最快。
|
|
158
190
|
|
|
159
|
-
deliberate-fail / deliberate-error 这类"故意红"的 fixture
|
|
191
|
+
deliberate-fail / deliberate-error 这类"故意红"的 fixture 设计成**对模型输出不敏感**——比如断言一个真实压根不会被调用的工具名(`t.calledTool("this_tool_does_not_exist")`),或者给 adapter 喂一个格式错误的输入触发运行时异常——不管真实模型这次答得好不好,这两条断言都必然会挂,不依赖任何伪造的坏响应。只进 `verdicts` 实验(不进 `ci`),由 verify.mjs 以"期望 exit 1"的方式正向消费——这是第 1 节"正反验证"在退出码层的那一半。
|
|
160
192
|
|
|
161
|
-
## 4.
|
|
193
|
+
## 4. 两层执行模型
|
|
162
194
|
|
|
163
|
-
|
|
195
|
+
不再按"要不要秘密"分层(e2e 全程都要真实 key),而是按"多久跑一次、花多少钱、跑多大的模型/样本量"分两层:
|
|
164
196
|
|
|
165
|
-
| 层 | 触发 |
|
|
197
|
+
| 层 | 触发 | 模型档位 & runs | 跑什么 |
|
|
166
198
|
|---|---|---|---|
|
|
167
|
-
| **L0
|
|
168
|
-
| **L1
|
|
169
|
-
| **L2 真实层** | nightly cron + 手动 dispatch | 各家 API key;Docker | e2e/projects 切真模型跑同一份套件(验证 mock 没有把真实协议测歪)+ 沙箱矩阵 + judge 真产分 |
|
|
199
|
+
| **L0 PR 门禁**(真实 key,便宜模型) | 每个 PR + push main | `deepseek-v4-flash` 等便宜模型;`runs: 3, earlyExit: true`(第一次过就收尾,吸收单次抖动) | `e2e/projects/*` 全矩阵:aisdk-transformer / http-mapping 必跑;apps 里已接入的 SDK 项目逐个纳入。共享套件通过 + verdicts 期望 exit 1 + `runs` 统计(小样本) + 缓存行为 |
|
|
200
|
+
| **L1 真实矩阵**(nightly) | schedule cron + 手动 dispatch | 完整官方模型矩阵(gpt-4o、gpt-5.4 等) + `runs: 20~100`,统计更稳的通过率分布 | 同一份共享套件切到完整模型矩阵 + judge 真产分(配 judge key)+ 沙箱矩阵 |
|
|
170
201
|
|
|
171
|
-
L0
|
|
202
|
+
L0 是合并门禁;L1 是每日健康信号(模型矩阵更贵、样本更大,允许波动,不阻塞 PR)。注意 L0 里"CI 起 app"不违反 tier1 的"eval 不代管进程"原则——起应用的是 CI workflow(扮演用户),不是 eval 框架。`examples/zh/ai-sdk` 的示例冒烟(原第 6 节的"L1")并入 L0 的 PR 门禁,作为独立 job 跑,详见第 6 节。
|
|
172
203
|
|
|
173
|
-
### 4.1 L0
|
|
204
|
+
### 4.1 L0 的通过语义:确定性部分精确断言,模型相关部分容忍抖动
|
|
174
205
|
|
|
175
|
-
|
|
206
|
+
verify.mjs 对每个项目区分两类断言:
|
|
176
207
|
|
|
177
|
-
-
|
|
178
|
-
- `
|
|
179
|
-
|
|
208
|
+
- **确定性部分**(和真实模型答得好不好无关):discover 出的 eval 数量按 profile 精确对账(防止少排用例);`verdicts` 实验里 deliberate-fail/error 的失败/报错次数是精确断言,因为这些 fixture 本来就设计成对模型输出不敏感。
|
|
209
|
+
- **模型相关部分**(工具是否调对、judge 打分):允许有限重试吸收单次抖动——`runs: 3, earlyExit: true`,一次通过就算过。断言口径从"exact count"改成"通过率下限",例如 `ci` 实验断言 `passed >= 期望 eval 数 × 0.95`(留一点余量给真实模型的单次波动;如果某个 eval 连续 3 次都没过,说明是适配层真回归,不是抖动,矩阵照样打红)。
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
// e2e/scripts/verify.mjs(示意,节选)
|
|
213
|
+
const PROJECTS = [
|
|
214
|
+
{ dir: "projects/aisdk-transformer", exp: "ci", expectExit: 0, expect: toleratedCounts(profileOf("aisdk-transformer")) },
|
|
215
|
+
{ dir: "projects/aisdk-transformer", exp: "verdicts", expectExit: 1, expect: { failedAtLeast: 1, erroredAtLeast: 1 } },
|
|
216
|
+
{ dir: "projects/aisdk-transformer", exp: "pass-n", expectExit: 0, expect: { passedAtLeast: 18 /* 2 条 eval × 10 runs,留 10% 余量 */ } },
|
|
217
|
+
{ dir: "projects/http-mapping", exp: "ci", expectExit: 0, expect: toleratedCounts(profileOf("http-mapping")) },
|
|
218
|
+
{ dir: "projects/ai-sdk-v7", exp: "ci", expectExit: 0, expect: toleratedCounts(profileOf("ai-sdk-v7")) },
|
|
219
|
+
// ... 其余已接入的 SDK 项目
|
|
220
|
+
];
|
|
221
|
+
```
|
|
180
222
|
|
|
181
|
-
### 4.2
|
|
223
|
+
### 4.2 L1:nightly 真实矩阵 + 沙箱
|
|
182
224
|
|
|
183
|
-
两块内容。第一块:e2e/projects
|
|
225
|
+
两块内容。第一块:e2e/projects 里的共享套件切到**完整官方模型矩阵**,`--runs 20` 起步,加 judge key 顺带验证 judge 断言真的产出分数而非 no-op;更大的模型矩阵和样本量本来就更贵更慢,放 nightly 而不是 PR 门禁,失败发通知(GitHub issue / 通知渠道)而不是标红 main。
|
|
184
226
|
|
|
185
227
|
第二块:沙箱矩阵,按"官方适配器 × 沙箱后端"取最小生成集,不做全叉乘:
|
|
186
228
|
|
|
@@ -196,54 +238,34 @@ mock 是确定性的,所以期望值是精确的,verify.mjs 对每个项目断
|
|
|
196
238
|
|
|
197
239
|
已知约束(来自项目 memory,落地时直接采纳):e2b 的 `base` 模板 node20/481MB 会 OOM,必须用预制的 `fasteval-agents` 模板;vercel 后端默认并发 1(避免 429),session 上限约 360s;沙箱内不要 hardcode `/home/node`。
|
|
198
240
|
|
|
199
|
-
|
|
241
|
+
L1 每个 experiment 都设 `budget`(建议单 job ≤ $2)和 `timeoutMs`,workflow 层再加 job timeout 兜底——PR 门禁(L0)因为跑得频繁,单个 job 的 `budget` 应该卡得更紧(建议 ≤ $0.5),便宜模型 + 小 runs 是控制单次 PR 成本的主要手段。
|
|
200
242
|
|
|
201
243
|
## 5. 元校验脚本(e2e 的"真正的测试")
|
|
202
244
|
|
|
203
|
-
L0 的本体不是那些 eval——eval 是 fixture;本体是 `e2e/scripts/verify.mjs`:它把 CLI 当黑盒子进程跑,对照期望表校验退出码和 `summary.json
|
|
245
|
+
L0 的本体不是那些 eval——eval 是 fixture;本体是 `e2e/scripts/verify.mjs`:它把 CLI 当黑盒子进程跑,对照期望表校验退出码和 `summary.json`(完整示意见 4.1)。verify.mjs 还负责两个专项:
|
|
204
246
|
|
|
205
|
-
|
|
206
|
-
|
|
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 还负责两个专项:
|
|
247
|
+
- **缓存行为**:同一 experiment 连跑两次,第二次**不带** `--force` → 断言 summary 里携入的 cached 结果仍计为 passed 且没有真跑(比较 `durationMs` 或 attempt 工件时间戳,顺带省一次真实调用的钱);第二次**带** `--force` → 断言全部真跑。这把"CI 忘了 --force 会静默跳过"的坑变成被测行为。
|
|
248
|
+
- **工件形状**:抽查一个 attempt 目录,断言 `events.json` 是 JSON array、`summary.json` 顶层有 `format/schemaVersion/producer/passed/failed/errored/results[]`、`results[].artifactsDir` 指向存在的目录。防止 results-format 无声漂移。
|
|
228
249
|
|
|
229
|
-
|
|
230
|
-
- **工件形状**:抽查一个 attempt 目录,断言 `events.json` 是 JSON array、`summary.json` 顶层有 `passed/failed/errored/results[]`、`results[].artifactsDir` 指向存在的目录。防止 results-format 无声漂移(当前 writer 没有 schemaVersion,只能靠这层守)。
|
|
250
|
+
## 6. L0 里的示例冒烟 job
|
|
231
251
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
目的只有一个:公开示例必须能按 README 跑通。首选 `examples/zh/ai-sdk`,因为它的 server 有零 key 的 `AGENT_MODE=mock`:
|
|
252
|
+
目的只有一个:公开示例必须能按 README 跑通。首选 `examples/zh/ai-sdk`——落地前必须先完成第 2 节提到的清理:把 README 里"默认是 `AGENT_MODE=mock`"那几行改成真实 key 说明,因为代码里从来就没有这个开关。清理之后,这个 job 和 L0 里其它项目一样,从 GitHub Actions secrets 注入真实 key:
|
|
235
253
|
|
|
236
254
|
```yaml
|
|
237
255
|
# job 示意
|
|
238
256
|
- run: pnpm install --dir examples/zh/ai-sdk
|
|
239
|
-
- run:
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
- run: node server & # 起 127.0.0.1:5188,等端口就绪
|
|
258
|
+
env:
|
|
259
|
+
OPENAI_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
|
260
|
+
OPENAI_BASE_URL: https://api.deepseek.com
|
|
261
|
+
AGENT_MODEL: deepseek-v4-flash
|
|
262
|
+
- run: pnpm exec niceeval list # eval 发现冒烟
|
|
263
|
+
- run: pnpm exec niceeval exp compare-models --runs 3 --force --junit junit.xml
|
|
242
264
|
```
|
|
243
265
|
|
|
244
|
-
验收前提(
|
|
266
|
+
验收前提(落地时先确认,不满足就先修示例本身,而不是加 mock 绕过去):`weather-tool` 等 gate 断言必须能在真实 DeepSeek 调用下稳定通过——即真实模型对天气提问确实会触发 `get_weather` 工具调用。judge 断言没配 judge key 时自动跳过,是否额外配 judge key 走完整评分是 nightly(4.2)的事,不是这个冒烟 job 的职责。
|
|
245
267
|
|
|
246
|
-
|
|
268
|
+
这个 job 和 e2e/projects 矩阵的分工:e2e/projects 测的是框架和适配层(fixture 允许为可测性改造);这个 job 测的是"用户照 README 操作会不会翻车"(fixture 一个字都不许为 CI 改)。`examples/zh/tier1/*` 不用这个 job 覆盖——它们的可测化版本已经在 `e2e/apps` 里了。
|
|
247
269
|
|
|
248
270
|
## 7. Workflow 编排
|
|
249
271
|
|
|
@@ -253,43 +275,58 @@ verify.mjs 还负责两个专项:
|
|
|
253
275
|
on:
|
|
254
276
|
push: { branches: [main] }
|
|
255
277
|
pull_request:
|
|
256
|
-
schedule: [{ cron: "0 3 * * *" }] #
|
|
278
|
+
schedule: [{ cron: "0 3 * * *" }] # L1 nightly
|
|
257
279
|
workflow_dispatch:
|
|
258
280
|
|
|
259
281
|
jobs:
|
|
260
282
|
e2e-matrix: # L0,PR 门禁
|
|
261
283
|
runs-on: ubuntu-latest
|
|
262
|
-
|
|
284
|
+
env:
|
|
285
|
+
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
|
286
|
+
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
|
|
287
|
+
steps: [checkout, pnpm install, 起已接入的 apps, node e2e/scripts/verify.mjs]
|
|
263
288
|
|
|
264
|
-
e2e-examples: #
|
|
289
|
+
e2e-examples: # 第 6 节的示例冒烟,PR 门禁
|
|
265
290
|
runs-on: ubuntu-latest
|
|
291
|
+
env:
|
|
292
|
+
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
|
266
293
|
# 见第 6 节
|
|
267
294
|
|
|
268
|
-
e2e-
|
|
295
|
+
e2e-nightly: # L1,仅 schedule / dispatch
|
|
269
296
|
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-
|
|
297
|
+
strategy: { fail-fast: false, matrix: { target: [claude-docker, codex-docker, bub-docker, claude-e2b, claude-vercel, sdk-projects-full-matrix] } }
|
|
271
298
|
runs-on: ubuntu-latest # ubuntu runner 自带 Docker
|
|
299
|
+
env:
|
|
300
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
301
|
+
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
|
|
302
|
+
BUB_API_KEY: ${{ secrets.BUB_API_KEY }}
|
|
303
|
+
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
|
|
304
|
+
NICEEVAL_JUDGE_KEY: ${{ secrets.NICEEVAL_JUDGE_KEY }}
|
|
272
305
|
```
|
|
273
306
|
|
|
307
|
+
需要建的 GitHub repo secrets:`DEEPSEEK_API_KEY`(claude-sdk / pi-sdk / langgraph / ai-sdk-v7 / examples/zh/ai-sdk 共用,分饰 Anthropic 兼容与 OpenAI 兼容两个 base URL,同一把 key)、`CODEX_API_KEY`(codex-sdk & codex 沙箱)、`ANTHROPIC_API_KEY`(claude-code 沙箱)、`BUB_API_KEY`、`E2B_API_KEY`、Vercel token、`NICEEVAL_JUDGE_KEY`(nightly judge)。值可以复用 `examples/zh/origin` 已经在用的那套凭据(见 `memory/origin-examples-real-ai-credentials.md`),不需要重新申请。
|
|
308
|
+
|
|
274
309
|
所有 job 统一约定:
|
|
275
310
|
|
|
276
311
|
- 每次调用 `niceeval exp` 都带 `--force`(禁指纹缓存)+ `--junit <path>`;junit 和整个 `.niceeval/` 目录用 `actions/upload-artifact` 上传,失败时可下载 `events.json` / `trace.json` 排查。
|
|
277
|
-
- 成败判据 = 进程退出码;verify.mjs
|
|
278
|
-
-
|
|
312
|
+
- 成败判据 = 进程退出码;verify.mjs 层再做计数级校验(L0 用容忍区间,见 4.1)。
|
|
313
|
+
- 只有 pull_request 触发的 workflow 才能读到 repo secrets(同仓库 PR;fork 来的 PR 默认拿不到 secrets,这是 GitHub 本身的安全模型,不是本设计要解决的问题)——如果未来要支持 fork PR 跑 e2e,需要额外做 `pull_request_target` + 手动审核这类隔离,不在本提案范围内。
|
|
279
314
|
|
|
280
315
|
## 8. 分阶段落地
|
|
281
316
|
|
|
282
|
-
1. **
|
|
283
|
-
2. **
|
|
284
|
-
3. **
|
|
285
|
-
4. **
|
|
286
|
-
5.
|
|
317
|
+
1. **P0(先修文档债)**:把 `examples/zh/ai-sdk/README.md` 里过期的 `AGENT_MODE=mock` 声明改成真实 key 说明,确认 `weather-tool` 等 gate 断言在真实 DeepSeek 调用下能稳定通过——这是第 6 节能落地的前提,和 e2e 骨架搭建可以并行。
|
|
318
|
+
2. **P1(先立骨架)**:建 `e2e/shared` + `e2e/projects/{aisdk-transformer,http-mapping}` + verify.mjs(共享套件、正反 eval、deliberate-fail/error、`runs: N` 统计),申请 `DEEPSEEK_API_KEY` secret,加 `e2e-matrix` job 进 PR 门禁。这一步依赖最小、成本最低——完整路径、正反断言、重复运行统计、退出码、缓存行为全部落网,factory/profile 机制也在最便宜的项目上定型。
|
|
319
|
+
3. **P2**:拷 `examples/zh/tier1/ai-sdk-v7` 进 `e2e/apps`,`e2e/projects/ai-sdk-v7` 接入共享套件、复用 P0 已修好的 secrets——第一个真实 SDK 协议栈进 L0 矩阵,加 `e2e-examples` job。
|
|
320
|
+
4. **P3**:langgraph、pi-sdk、claude-sdk、codex-sdk 逐个接入 `e2e/apps` 并纳入矩阵(凭据映射直接照抄 tier1,不需要额外调研)。给 `sync-tiers.mjs` 加 tier1 → e2e/apps 的 drift check。
|
|
321
|
+
5. **P4**:补 `sandbox-smoke` factory;开 L1 nightly 的 claude-code × docker(第一条真实沙箱链路),随后铺满 L1 矩阵(codex / bub / e2b / vercel + sdk-projects 完整模型矩阵 + judge)。
|
|
322
|
+
6. **之后**:`e2e/projects/{codex-sdk,langgraph}` 的 OTel 瀑布图链路(`spanMapper` / 固定端口)补一条"trace.json 非空"的冒烟断言,纳入 L1;评估 L0 的 retry/容忍区间参数(3.1 提到的 `runs: 3`、95% 阈值)是否需要按项目调整——不同 SDK 的真实抖动率可能不一样。
|
|
287
323
|
|
|
288
324
|
## 9. 明确不做的
|
|
289
325
|
|
|
290
326
|
- 不做字面共享:不把同一份 `.eval.ts` 软链/复制进多个项目——discover 不跟随 symlink 条目,且各 SDK 的工具名和能力差异是协议现实,抹平它们等于测一个不存在的协议。共享的单位是 factory,差异收敛进 profile。
|
|
291
|
-
- 不和 tier1 共享 eval:`examples/zh/tier1` 的 eval 继续服务文档叙事,e2e 的套件独立编写、独立演进,两边不建同步关系;从 tier1 拷的只有应用和 adapter(受 drift check 约束)
|
|
327
|
+
- 不和 tier1 共享 eval:`examples/zh/tier1` 的 eval 继续服务文档叙事,e2e 的套件独立编写、独立演进,两边不建同步关系;从 tier1 拷的只有应用和 adapter(受 drift check 约束)。
|
|
328
|
+
- 不用任何形式的假 AI/本地 mock server:e2e 全程调真实模型,费用通过便宜模型档位、小 `runs`、per-experiment `budget` 护栏控制,不通过伪造响应控制;PR 门禁和 nightly 的区别是"模型档位、样本量、跑多勤",不是"真的假的"。
|
|
292
329
|
- 不测"OTel 派生事件"这类断言——OTel 只喂瀑布图,e2e 要测的是它产出了 `trace.json`,不是它替代了事件映射。
|
|
293
|
-
- 不在 PR 门禁里跑任何真模型——随机性 + 费用 + secret 暴露面都不适合。
|
|
294
330
|
- 不用 `--tag` 做 CI 内的用例切分(当前实现只收单值,与文档不一致;分层用 experiment 的 `evals` 过滤器表达,更明确)。
|
|
295
|
-
-
|
|
331
|
+
- 不支持 fork 来的 PR 跑需要 secrets 的 e2e job(GitHub 默认不把 secrets 传给 fork PR;需要更严格的隔离方案的话另开提案)。
|
|
332
|
+
- 版本字段只做格式契约校验,不把具体 `producer.version` 写死到测试里;版本不兼容的读取行为由 view loader 自己测。
|
package/docs/eval-authoring.md
CHANGED
|
@@ -51,7 +51,7 @@ export default rows.map((row) =>
|
|
|
51
51
|
|
|
52
52
|
- `t.*` 保留"聚合整个 eval run"的语义——这次 eval 执行的全部轮次、含 `t.newSession()` 开的额外 session,直接对应 eve 的 `timing: "final"` 层。这一层聚合是有意为之,不是要移除的"黑箱"。
|
|
53
53
|
- `session.*`(`t.newSession()` 的返回值)复用 `t.*` 的同一套**作用域断言词汇**,但只看这个 session 在断言记录时已有的事件。
|
|
54
|
-
- `turn.*`(`t.send()` 的返回值)也复用同一套**作用域断言词汇**,但只看这一轮自己的事件和用量,不再是旧版文档里的 4 个手写方法。`turn.
|
|
54
|
+
- `turn.*`(`t.send()` 的返回值)也复用同一套**作用域断言词汇**,但只看这一轮自己的事件和用量,不再是旧版文档里的 4 个手写方法。`turn.outputEquals` / `turn.outputMatches` 是 turn 独有的(只对单轮结果有意义,聚合层不需要),继续保留。
|
|
55
55
|
|
|
56
56
|
也就是:**接收者决定作用域,不是断言名字决定作用域。** author-facing 接收者是 `t` / `session` / `turn`;`Attempt` 只作为 runner/result 里的执行单位存在,不是写 eval 时要操作的一层。完整清单见 [Assertions · 作用域规则](assertions.md#作用域规则)。
|
|
57
57
|
|
|
@@ -96,7 +96,7 @@ export default defineEval({
|
|
|
96
96
|
});
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
`t.reply` 是最后一条 assistant 消息;`t.sessionId` 是当前主会话 id;`t.events` 是主 session 目前捕获到的强类型事件流。`t.send(input)` 接受字符串或结构化消息,返回一个不可变的 **Turn**,带 `message` / `data`(结构化输出)/ `toolCalls` / `status` / `events
|
|
99
|
+
`t.reply` 是最后一条 assistant 消息;`t.sessionId` 是当前主会话 id;`t.events` 是主 session 目前捕获到的强类型事件流。`t.send(input)` 接受字符串或结构化消息,返回一个不可变的 **Turn**,带 `message` / `data`(结构化输出)/ `toolCalls` / `status` / `events`。带本地文件的一轮用 `t.sendFile(path, text?)`,文件会作为 data URL 附加到这一轮输入里,MIME 类型按 `path` 扩展名推断。
|
|
100
100
|
|
|
101
101
|
## 多轮
|
|
102
102
|
|
|
@@ -111,7 +111,7 @@ export default defineEval({
|
|
|
111
111
|
description: "先拟稿,确认后再发送",
|
|
112
112
|
async test(t) {
|
|
113
113
|
const draft = await t.send("帮我拟一封跟进邮件。");
|
|
114
|
-
draft.
|
|
114
|
+
draft.succeeded(); // 只看这一轮:失败就记一条断言
|
|
115
115
|
t.check(draft.message, includes("此致"));
|
|
116
116
|
draft.judge.autoevals.closedQA("语气是否专业").atLeast(0.6);
|
|
117
117
|
|
package/docs/experiments.md
CHANGED
|
@@ -25,6 +25,7 @@ export default defineExperiment({
|
|
|
25
25
|
description?: string; // 人读
|
|
26
26
|
agent: Agent; // 跑哪个 agent(adapter 实例)
|
|
27
27
|
model?: string; // 单个模型(agent 留空);省略=原生默认。跨模型对比写多个实验文件
|
|
28
|
+
reasoningEffort?: string; // 推理努力程度(agent 留空);省略=原生默认。经 ctx.reasoningEffort / t.reasoningEffort 透传
|
|
28
29
|
flags?: Record<string, unknown>; // feature flags,透传到 ctx.flags / t.flags(见下)
|
|
29
30
|
runs?: number; // 每个 (agent × model × eval) 跑几次(默认 1)
|
|
30
31
|
earlyExit?: boolean; // 先过一次即停其余(默认 true)
|
|
@@ -35,22 +36,24 @@ export default defineExperiment({
|
|
|
35
36
|
});
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
> experiment 只有"运行矩阵"
|
|
39
|
+
> experiment 只有"运行矩阵"字段,**没有 run / experiment 级环境起停钩子**。要在跑 agent 前准备环境,放 `EvalDef.setup` / `test(t)`(单个 eval 的沙箱预置)、`SandboxAgent.setup`(连 agent / 装 CLI)或外部编排(整轮共享服务),见 [环境预置放哪](sandbox.md#环境预置放哪)。
|
|
39
40
|
|
|
40
41
|
id 从**路径**推导:`experiments/compare/bub-gpt-5.4.ts` → `compare/bub-gpt-5.4`(路径即身份,和 eval 一致,禁止手写 id);其中目录段 `compare/` 就是"可对比组",见 [下节](#实验怎么组织文件夹--一组可对比的实验)。
|
|
41
42
|
|
|
42
|
-
### model 与 flags:agent 留空,实验决定
|
|
43
|
+
### model / reasoningEffort 与 flags:agent 留空,实验决定
|
|
43
44
|
|
|
44
|
-
agent 定义里**不写死模型、不写死开关**(那样就锁死了复用)
|
|
45
|
+
agent 定义里**不写死模型、不写死开关**(那样就锁死了复用)。这几样由实验给,经 `ctx` 透传:
|
|
45
46
|
|
|
46
47
|
- **`model`** —— 单个模型字符串,agent 的 `send` 从 `ctx.model` 拿;省略则不传 `--model`,用 agent CLI 的原生默认。**跨模型对比写多个实验文件**(各钉一个 model),别在一个实验里塞数组。
|
|
47
|
-
- **`
|
|
48
|
+
- **`reasoningEffort`** —— 单个推理努力程度字符串(取值由具体模型定义,如 `"low"`/`"medium"`/`"high"`),归属与 `model` 完全一致:agent 的 `send` 从 `ctx.reasoningEffort` 拿,eval 的 `test` 从 `t.reasoningEffort` 拿;省略则用 agent 原生默认。跨档位对比同样写多个实验文件。
|
|
49
|
+
- **`flags`** —— 任意 KV 的 feature flags,**两处可见**:agent 的 `send`(`ctx.flags`)、eval 的 `test`(`t.flags`)。用来开关联网、注入某个 skill、或让某条 eval 只在某 flag 下断言。
|
|
48
50
|
|
|
49
51
|
```typescript
|
|
50
52
|
// experiments/research-mode.experiment.ts
|
|
51
53
|
export default defineExperiment({
|
|
52
54
|
agent: codexAgent(),
|
|
53
55
|
model: "opus", // 模型在实验给,agent 留空
|
|
56
|
+
reasoningEffort: "high", // → ctx.reasoningEffort / t.reasoningEffort
|
|
54
57
|
flags: { webResearch: true, skill: "memory-v2" }, // → ctx.flags / t.flags
|
|
55
58
|
runs: 3,
|
|
56
59
|
});
|
package/docs/observability.md
CHANGED
|
@@ -136,7 +136,7 @@ otel 在 agent 定义里其实是**两个互不相干的责任,分开放**,别
|
|
|
136
136
|
|
|
137
137
|
**为什么要分:** 导出配置是「沙箱里怎么发」,mapper 是「发回来怎么读」—— 一个需要沙箱、一个是纯函数,生命周期和测试方式都不同。混在 `setup`/`send` 里,既难单测 mapper、又让 adapter 把 otel 拼装逻辑揉进主流程。`ctx.telemetry` 则统一带上 `{ endpoint, env? }`:env-based agent 拿 `env` 直接 spread,file-based agent 在 `configure` 里用 `endpoint`。
|
|
138
138
|
|
|
139
|
-
> **claude-code
|
|
139
|
+
> **claude-code:** 已接原生 OTLP(beta 遥测):adapter 的 `tracing.env` 注入 `CLAUDE_CODE_ENABLE_TELEMETRY=1` + `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1` + `OTEL_TRACES_*`(见 `src/agents/claude-code.ts`)。span 层级 `claude_code.interaction → llm_request / tool`,只有结构与计时、内容默认脱敏——内容与断言仍来自 transcript 旁读,trace 只补瀑布图。span 量级小(每回合个位数),`selectTraceSpans` 的 small-trace 路径整段保留,不需要专属 mapper。曾考虑过的「transcript 时间戳合成 span」方案不再需要。
|
|
140
140
|
|
|
141
141
|
### span 怎么归属到轮
|
|
142
142
|
|
|
@@ -302,18 +302,22 @@ interface Reporter {
|
|
|
302
302
|
- **`Artifacts()`** —— 默认写 `.niceeval/<timestamp>/summary.json` 与 attempt 级 JSON 工件(`events.json`、`sources.json`、`trace.json`、`o11y.json`、`diff.json`),供 `niceeval view` 读取。具体格式见 [Results Format](results-format.md)。
|
|
303
303
|
- **`JUnit(path)`** —— JUnit XML,接 CI 测试报告 UI。
|
|
304
304
|
- **`Json(path)`** —— 机器可读全量。
|
|
305
|
-
-
|
|
305
|
+
- **`Braintrust(config?)`** —— 把一次运行作为一个 Braintrust experiment 上报,每个 attempt 一行:soft 断言按名字记分,gate 断言记在 `gate:` 前缀下(实验 diff 里 gate 回归和 soft 分数回归用同一套机制看);metrics 带 start/end、token 用量与估算成本,metadata 带 agent / model / experiment / flags 身份维度与失败断言明细。`braintrust` 包是可选 peer 依赖(动态 import,没装时 onRunStart 报错并提示安装);鉴权走 `BRAINTRUST_API_KEY` 或工厂参数 `apiKey`。源码 `src/runner/reporters/braintrust.ts`。
|
|
306
306
|
|
|
307
307
|
配置全局或单 eval 专用:
|
|
308
308
|
|
|
309
309
|
```typescript
|
|
310
|
-
|
|
311
|
-
defineConfig({ reporters: [Console(), JUnit(".niceeval/junit.xml")] });
|
|
310
|
+
import { Braintrust, JUnit } from "niceeval/reporters";
|
|
312
311
|
|
|
313
|
-
//
|
|
312
|
+
// niceeval.config.ts —— 全局,观测所有 eval(Console / Artifacts 由 CLI 始终自带,不用写)
|
|
313
|
+
defineConfig({ reporters: [JUnit(".niceeval/junit.xml"), Braintrust({ project: "weather" })] });
|
|
314
|
+
|
|
315
|
+
// 某个 eval 专用:实例只观测引用它的 eval
|
|
314
316
|
defineEval({ reporters: [Braintrust({ project: "weather" })], async test(t) { ... } });
|
|
315
317
|
```
|
|
316
318
|
|
|
319
|
+
eval 级 reporter 经作用域包装接入(`scopeReporter`,见 `src/runner/report.ts`):`onEvalComplete` 按 eval id 过滤,`onRunComplete` 收到重新计数的子集汇总;同一实例被多个 eval 引用时合并观测集(共享一个目的地,比如同一个 Braintrust 实验),已经挂在全局 `reporters` 里的实例在 eval 上再列一遍也不会重复上报。
|
|
320
|
+
|
|
317
321
|
## 失败分类(可选,沙箱型)
|
|
318
322
|
|
|
319
323
|
沙箱型可开 AI 失败分类:跑完用一个小模型(给它只读探索结果目录的工具:list/read/grep)把失败归为三类:
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Origin 应用接入手册
|
|
2
2
|
|
|
3
|
-
这份文档最初是**工单**:`examples/zh/origin/` 下五个独立应用,按应用逐个说明怎么把它们接进 niceeval。**五个应用已经全部接完**(见文末「现状」)
|
|
3
|
+
这份文档最初是**工单**:`examples/zh/origin/` 下五个独立应用,按应用逐个说明怎么把它们接进 niceeval。**五个应用已经全部接完**(见文末「现状」),产出按分档铺在 `examples/zh/tier1|tier2|tier3/<name>/`(分层索引见 [examples/zh · 接入分层](../examples/zh/origin/README.md#接入分层origin--tier1--tier2--tier3))。本文现在的作用是记录接入时踩过的坑和当前实现的形状,再接一个类似的应用时可以照抄;当前实现的权威来源永远是各 tier 目录的源码和各自的 README,本文只是导读。
|
|
4
4
|
|
|
5
5
|
先记住三条铁律(接类似应用时依然适用):
|
|
6
6
|
|
|
7
|
-
1. **不改 origin 的任何文件。** 接入产物放 `examples/zh/tier1/<同名>/`:从 origin 复制整个应用,被复制的文件保持逐字节不变(除 `package.json` / `pnpm-workspace.yaml` / `tsconfig.json`
|
|
7
|
+
1. **不改 origin 的任何文件。** 接入产物放 `examples/zh/tier1/<同名>/`:从 origin 复制整个应用,被复制的文件保持逐字节不变(除 `package.json` / `pnpm-workspace.yaml` / `tsconfig.json` 三个集成脚手架文件,以及 `.env.example`——tier 侧要补 judge 独立凭证等 eval 变量),接入代码全部是**新增**文件。`pnpm run gen:diff-code` 会 diff origin 和 tier1 两个目录生成 before/after 文档页,"应用侧零改动"是这些页面的核心卖点,改一个字节都会破坏它;这条铁律由 CI 的 `pnpm tiers:check`(verbatim 校验)看守,见 [tier-sync](tier-sync.md)。
|
|
8
8
|
2. **协议以实际输出为准。** 动手写映射之前,先把应用跑起来,`curl -N` 打一轮 `/api/chat` 把 SSE 帧看一遍。本文的帧格式描述来自当前代码,但代码会演化,别背文档。
|
|
9
9
|
3. **被测应用由你自己按它的方式启动,eval 不代管进程、不另开端口。** adapter 只经环境变量(如 `CODEX_SDK_URL`)指向一个已经在跑的实例——没有 `server-lifecycle.ts` 这类"eval 侧拉起子进程"的机制,这是刻意的取舍,理由见[接入你的 Agent · 为什么不直调](../docs-site/zh/guides/connect-your-agent.mdx)同一条脉络(eval 不代管被测进程)。
|
|
10
10
|
|
|
11
|
-
## Tier
|
|
11
|
+
## Tier 是什么,产出长什么样
|
|
12
12
|
|
|
13
13
|
接入分三档(定义见 [docs-site · Tier](../docs-site/zh/concepts/tier.mdx)):**Tier 1(只接 send)**、**Tier 2(send + OTel)**、**Tier 3(侵入改造 + experiment flags)**。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
三档现在各有物化目录,同一个应用逐层叠 delta:`tier1/<name>`(纯无侵入,全套断言)、`tier2/<name>`(有 OTel 输出的三个应用:ai-sdk-v7、codex-sdk、langgraph——加 telemetry 配置与 spanMapper/收尾宽限,换瀑布图)、`tier3/<name>`(五个应用都有——按文末「Tier 3 侵入点」改应用内部代码,暴露 experiment flags)。哪个应用有哪几层见 [examples/zh 分层索引](../examples/zh/origin/README.md#接入分层origin--tier1--tier2--tier3);层间用 `pnpm tiers:sync` 保持同步(机制见 [tier-sync](tier-sync.md))。本文余下部分讲 Tier 1 的接入配方——那是每个应用的地基。
|
|
16
16
|
|
|
17
17
|
## 统一的接入配方
|
|
18
18
|
|
|
@@ -87,11 +87,11 @@ adapter 要这样做:
|
|
|
87
87
|
|
|
88
88
|
没有审批流的应用(codex-sdk)跳过这一整节,永远不返回 `waiting`。
|
|
89
89
|
|
|
90
|
-
### OTel:只画瀑布图,和事件映射完全无关
|
|
90
|
+
### OTel:只画瀑布图,和事件映射完全无关(这一节的产物在 tier2)
|
|
91
91
|
|
|
92
92
|
不管应用有没有 OTel 输出,事件映射(上面几节讲的)都一样要做——**OTel 现在只喂 `niceeval view` 的调用瀑布图,不产出任何事件,也不影响任何断言**,详见 [Observability · OTLP traces](observability.md#otlp-traces--统一瀑布图)。
|
|
93
93
|
|
|
94
|
-
五个应用里,ai-sdk-v7、codex-sdk、langgraph 发 OTel span,claude-sdk、pi-sdk 不发(claude-sdk 的 CLI 遥测只有 metrics+logs,niceeval 只消费 trace spans;pi-agent-core 没有官方 OTel 集成)
|
|
94
|
+
五个应用里,ai-sdk-v7、codex-sdk、langgraph 发 OTel span,claude-sdk、pi-sdk 不发(claude-sdk 的 CLI 遥测只有 metrics+logs,niceeval 只消费 trace spans;pi-agent-core 没有官方 OTel 集成)。这层接线是 Tier 2 的 delta,落在 `examples/zh/tier2/<name>/`(tier1 不配 telemetry、没有瀑布图);有 span 的应用接法都一样:
|
|
95
95
|
|
|
96
96
|
1. `niceeval.config.ts` 钉住固定接收端口:`defineConfig({ telemetry: { port: 4318 } })`——写了这个配置就等于给非沙箱 agent 打开了 OTel 接收。
|
|
97
97
|
2. 启动应用时,把标准 OTel 环境变量指向这个端口(`OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces` 或应用自己的等价配置)。
|
|
@@ -142,7 +142,7 @@ adapter 要这样做:
|
|
|
142
142
|
|
|
143
143
|
### langgraph
|
|
144
144
|
|
|
145
|
-
- 唯一的 Python 应用,也是唯一**完全手写帧映射、零 OTel 依赖用于事件**的应用:自定义 JSON 帧(`tool-input` → `action.called`、`tool-output` → `action.result`(completed)、`tool-output-denied` → `action.result`(rejected)、`text-delta` 累积成 `message`、`tool-approval-request` → `input.requested` + `waiting`)。
|
|
145
|
+
- 唯一的 Python 应用,也是唯一**完全手写帧映射、零 OTel 依赖用于事件**的应用:自定义 JSON 帧(`tool-input` → `action.called`、`tool-output` → `action.result`(completed,帧带 `isError: true` 时 failed)、`tool-output-denied` → `action.result`(rejected)、`text-delta` 累积成 `message`、`tool-approval-request` → `input.requested` + `waiting`)。工具异常靠 `ToolRetryMiddleware(max_retries=0, on_failure="continue")` 落成 `status="error"` 的 ToolMessage——`create_agent` 默认让工具异常炸穿整张图,没有这层就表达不了"执行了但失败"。
|
|
146
146
|
- HITL 标准配方,**approve 端点字段是 `toolCallId`**(别照抄 claude/pi 的 `toolUseId`)。
|
|
147
147
|
- session 是 `InMemorySaver`,同 pi:应用不要中途重启。
|
|
148
148
|
- OTel:LangSmith 导出的 span 只用来画瀑布图(`niceeval.config.ts` 固定端口 + 应用侧 `LANGSMITH_TRACING` / `LANGSMITH_OTEL_ENABLED` / `LANGSMITH_OTEL_ONLY` 三个环境变量),事件断言完全不依赖它。LangSmith 的 `BatchSpanProcessor` 调度和 SSE 流关闭是两条独立时间线,adapter 在轮次结束后主动等一小段(grace period)把最后一批 span 收进瀑布图。
|
|
@@ -167,21 +167,21 @@ experiment 至少两个:单配置基线 + 一个 `compare-models/` 实验组(ai-
|
|
|
167
167
|
- [ ] 有 OTel 的:view 瀑布图非空
|
|
168
168
|
- [ ] eval README 写明这个应用做不到什么(没有 OTel 的写清楚、负断言不完全靠事件完整性证明的写清楚)
|
|
169
169
|
|
|
170
|
-
## Tier 3
|
|
170
|
+
## Tier 3 侵入点(已实现,产出在 examples/zh/tier3/)
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
feature A/B test 已落地:每个应用挑一个最小侵入点做成**请求体可选字段**(不是环境变量——环境变量意味着换变体要重启应用,一次 experiment 运行里 A/B 不了;默认行为不变的铁律不变):
|
|
173
173
|
|
|
174
|
-
- ai-sdk-v7:system prompt
|
|
175
|
-
- claude-sdk
|
|
176
|
-
- codex-sdk:`threadOptions`
|
|
177
|
-
- pi-sdk:system prompt /
|
|
178
|
-
- langgraph
|
|
174
|
+
- ai-sdk-v7:system prompt(`instructions`)、工具子集(`tools`)→ `tier3/ai-sdk-v7`;
|
|
175
|
+
- claude-sdk:system prompt → `tier3/claude-sdk`;
|
|
176
|
+
- codex-sdk:`threadOptions` 的 sandbox mode → `tier3/codex-sdk`;
|
|
177
|
+
- pi-sdk:system prompt → `tier3/pi-sdk`;
|
|
178
|
+
- langgraph:system prompt(变体各自编译图、共享 checkpointer)→ `tier3/langgraph`。
|
|
179
179
|
|
|
180
|
-
experiment 侧用 `flags` → `ctx.flags` 透传,写法见 [Experiments](experiments.md)
|
|
180
|
+
experiment 侧用 `flags` → `ctx.flags` 透传,写法见 [Experiments](experiments.md);每个 tier3 目录的 README 有完整的 flags 流动链路。
|
|
181
181
|
|
|
182
|
-
## 现状(
|
|
182
|
+
## 现状(五个应用已全部接完,三档分层落位)
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
本工单描述的五个应用已全部接入并实测通过,Tier 1 产出在 `examples/zh/tier1/<name>/`;OTel 接线后来按分档定义剥进 `tier2/`,侵入改造落在 `tier3/`(见上文各节)。实现比本工单最初设想的更简单,主要是两处收紧:
|
|
185
185
|
|
|
186
186
|
- **没有 `server-lifecycle.ts` 这类"eval 侧拉起应用子进程"的机制。** 五个应用都由使用者自己按各自方式启动(`pnpm start` / `python server.py`),adapter 只经环境变量(`<NAME>_URL`)指向一个已经在跑的实例。这与"eval 不代管被测进程"的既定原则一致,比最初设想的自动拉起子进程更简单也更贴合真实用法。
|
|
187
187
|
- **OTel 只画瀑布图,不派生任何事件。** 五个应用的事件断言全部来自 `send` 里的官方转换器或手写帧映射,与有没有接 OTel 无关。
|
|
@@ -192,4 +192,4 @@ experiment 侧用 `flags` → `ctx.flags` 透传,写法见 [Experiments](experim
|
|
|
192
192
|
- **`fromCodexThreadEvents` 补全**:ThreadEvent 的工具项(`command_execution` / `mcp_tool_call` / `file_change` → 配对的 `action.*`)与 `turn.completed` 的 usage 聚合,现在是 `tier1/codex-sdk` 事件断言的唯一数据来源(不再依赖任何 span 派生)。
|
|
193
193
|
- **`mapCodexSpans`**(`src/o11y/otlp/mappers/codex.ts`,从 `niceeval/adapter` 导出):把 codex 自家 span 命名归一成 canonical GenAI 语义,只用来让瀑布图和内置 `codexAgent` 保持一致。
|
|
194
194
|
|
|
195
|
-
五个 `niceeval exp`
|
|
195
|
+
五个 `niceeval exp` 基线在 Tier 1 阶段全绿实测过(`ai-sdk-v7` 的多模型对比也验证过),五个 before/after 文档页已生成并挂进 `docs-site/docs.json` 导航。tier3 的五组 compare-* 实验也已对着真实 key 整轮实测:四个 prompt A/B 全绿(pi-sdk 的极简变体曾让模型心算跳过工具,见 `tier3/pi-sdk/experiments/compare-prompts/concise.ts` 注释),codex-sdk 的 compare-sandbox 按预期呈现 read-only 变体下 create-file 断言失败。这轮实测还顺带揪出并修掉了 runner 的一个丢结果 bug(`memory/runner-earlyexit-key-misses-experiment.md`)。
|