niceeval 0.10.0 → 0.10.3-canary.1
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/INDEX.md +4 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/report/components/attempt-detail/AttemptAssertions.js +2 -1
- package/dist/report/components/attempt-detail/AttemptConversation.d.ts +5 -1
- package/dist/report/components/attempt-detail/AttemptConversation.js +42 -2
- package/dist/report/components/attempt-detail/AttemptSource.js +84 -13
- package/dist/report/components/attempt-detail/compute.js +44 -1
- package/dist/report/components/attempt-detail/faces.js +4 -0
- package/dist/report/components/attempt-detail/index.d.ts +2 -2
- package/dist/report/components/attempt-detail/index.js +24 -4
- package/dist/report/components/entity-lists/ExperimentList.d.ts +1 -2
- package/dist/report/components/entity-lists/ExperimentList.js +6 -5
- package/dist/report/components/entity-lists/faces.d.ts +1 -1
- package/dist/report/components/entity-lists/faces.js +10 -9
- package/dist/report/components/entity-lists/index.d.ts +5 -7
- package/dist/report/components/entity-lists/index.js +7 -3
- package/dist/report/components/metric-views/MetricScatter.js +2 -38
- package/dist/report/index.d.ts +1 -1
- package/dist/report/model/format.d.ts +5 -5
- package/dist/report/model/format.js +34 -8
- package/dist/report/model/types.d.ts +16 -2
- package/dist/report/react/index.d.ts +1 -1
- package/dist/runner/types.d.ts +2 -1
- package/dist/sandbox/e2b.d.ts +0 -1
- package/dist/scoring/display.d.ts +7 -1
- package/dist/scoring/display.js +22 -2
- package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +5 -5
- package/docs-site/zh/explanation/runner.mdx +10 -2
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +2 -2
- package/docs-site/zh/reference/expect.mdx +21 -1
- package/docs-site/zh/reference/report-components.mdx +3 -1
- package/docs-site/zh/troubleshooting/debugging.mdx +8 -8
- package/docs-site/zh/troubleshooting/recover-after-kill.mdx +62 -0
- package/docs-site/zh/tutorials/agent-onboarding.mdx +14 -2
- package/docs-site/zh/tutorials/experiments.mdx +1 -2
- package/docs-site/zh/tutorials/quickstart.mdx +2 -2
- package/docs-site/zh/tutorials/sandbox-providers.mdx +27 -0
- package/docs-site/zh/tutorials/viewing-results.mdx +13 -10
- package/docs-site/zh/tutorials/write-experiment.mdx +1 -3
- package/package.json +3 -4
- package/src/cli.ts +10 -2
- package/src/expect/index.test.ts +39 -0
- package/src/expect/index.ts +33 -0
- package/src/o11y/prices.json +176 -99
- package/src/report/assets/styles.css +822 -0
- package/src/report/components/attempt-detail/AttemptAssertions.tsx +4 -3
- package/src/report/components/attempt-detail/AttemptConversation.tsx +55 -7
- package/src/report/components/attempt-detail/AttemptSource.tsx +187 -42
- package/src/report/components/attempt-detail/attempt-components.test.tsx +82 -4
- package/src/report/components/attempt-detail/compute.ts +50 -1
- package/src/report/components/attempt-detail/faces.ts +3 -0
- package/src/report/components/attempt-detail/index.tsx +33 -16
- package/src/report/components/attempt-detail/validate.test.ts +19 -2
- package/src/report/components/entity-lists/ExperimentList.tsx +13 -10
- package/src/report/components/entity-lists/faces.ts +10 -9
- package/src/report/components/entity-lists/index.tsx +7 -10
- package/src/report/components/metric-views/MetricScatter.tsx +2 -38
- package/src/report/components/render.test.tsx +19 -9
- package/src/report/index.ts +2 -0
- package/src/report/model/format.ts +33 -8
- package/src/report/model/types.ts +18 -2
- package/src/report/react/index.tsx +2 -0
- package/src/report/runtime/dual-render.test.tsx +43 -10
- package/src/runner/types.ts +2 -1
- package/src/sandbox/e2b-reconcile.test.ts +126 -0
- package/src/sandbox/e2b.ts +38 -2
- package/src/scoring/display.test.ts +26 -0
- package/src/scoring/display.ts +24 -2
- package/src/view/view-report.test.ts +3 -1
|
@@ -55,15 +55,19 @@ export default defineConfig({
|
|
|
55
55
|
两个要提醒用户的点:
|
|
56
56
|
|
|
57
57
|
- **key 解析不到时 Judge 断言会静默跳过**(不报错、不记分)——评估用例全绿不代表 Judge 真的跑了。所以配完先跑一条带 `t.judge` 的评估用例,在 `niceeval view` 里确认有 Judge 分数。
|
|
58
|
+
- **没有用户在场(自治接入)时不要直接跳过 Judge**:先探测环境变量里有没有可用的 key(`OPENAI_API_KEY`、`DEEPSEEK_API_KEY` 等常见变量),探测到就配上并按上一条验证;确实没有才降级到精确断言 + 形状断言(`includesUrl()` / `hasSections()` 这类,见[评分指南](/zh/tutorials/scoring-guide)),并在收尾总结里明确说明 Judge 未配置及原因。
|
|
58
59
|
- Judge 模型要**与被测 Agent 分离**,避免同一个模型给自己打分。模型解析优先级(单次调用 → 评估用例级 → 全局配置)和三种评分形状见 [Judge](/zh/explanation/judge),`judge` 字段全集见 [defineConfig 参考](/zh/reference/define-config)。
|
|
59
60
|
|
|
60
61
|
## 第 3 步:写三件套
|
|
61
62
|
|
|
62
63
|
按第 1 步选中的方向读完对应文档后,依次写:
|
|
63
64
|
|
|
64
|
-
1. **Adapter**(`agents/*.ts` 或用户项目里约定的目录)——只填 `defineAgent` 的 `send`,配置走工厂参数,不写死、不读 `process.env`。契约见 [Adapter](/zh/explanation/adapter),API 签名见 [defineAgent 参考](/zh/reference/define-agent),事件映射见[事件参考](/zh/reference/events)
|
|
65
|
+
1. **Adapter**(`agents/*.ts` 或用户项目里约定的目录)——只填 `defineAgent` 的 `send`,配置走工厂参数,不写死、不读 `process.env`。契约见 [Adapter](/zh/explanation/adapter),API 签名见 [defineAgent 参考](/zh/reference/define-agent),事件映射见[事件参考](/zh/reference/events)。两个容易踩的点:**端点/模式要选被测系统核心能力的入口,不是最容易跑通的入口**——比如被测平台既有「纯 LLM 聊天」又有「连库执行」两种模式,接前者等于评了个底层模型代理,没评到产品本身;**`coverage` 按实际映射如实声明**——只把最终文本映射出来就不要声明 complete,声明会被报告采信,虚报比不报更糟。
|
|
65
66
|
2. **Experiment**(`experiments/*.ts`)——引用上面的 Adapter,声明 `model`、`flags`、`runs` 等。模型对比写两个实验文件,各自钉一个 `model`;`evals: (eval) => boolean` 决定各自运行哪些评估用例。路径只负责 id 和批量运行,报告读取每份快照的 `selectedEvalIds`。
|
|
66
|
-
3. **评估用例**(`evals/*.eval.ts`)——**先探明这个应用是干嘛的,再写一条贴着它真实功能的评估用例**:读它的 README、路由、工具定义或系统提示,找出它的核心用例(客服机器人就问一条真实的客服问题、SQL agent
|
|
67
|
+
3. **评估用例**(`evals/*.eval.ts`)——**先探明这个应用是干嘛的,再写一条贴着它真实功能的评估用例**:读它的 README、路由、工具定义或系统提示,找出它的核心用例(客服机器人就问一条真实的客服问题、SQL agent 就给一个真实的查询任务),拿这个用例做第一条评估用例的输入和断言。两类输入都不合格:「你好」这种和应用无关的占位输入,以及「你是什么/你能做什么」这种**问被测系统它自己的元问题**——那不是用户拿它干活的用例。形式上仍从最小写起:一句输入,`t.succeeded()` + 一个针对预期回答的内容断言——但最小形式只是调通的脚手架,不是交付标准,收尾前还要满足两条:
|
|
68
|
+
- **断言在被测系统胡编时要会变红**。不要断言输入里本来就有的词(问「X 是什么」再断言回答含「X」,被测方复读题目就能通过);断言预期回答独有的实质内容——具体事实、结构(`hasSections()`)、真实链接(`includesUrl()`),或用 `t.judge` 做语义判定。
|
|
69
|
+
- **至少一条负例**。喂一个被测系统应该答不了的输入(不存在的表、检索不到的主题),断言它明确说查不到/做不到,而不是编造一个看似合理的结果——对连着真实数据/检索源的 agent,这是最值得先测的失败形态。
|
|
70
|
+
写法见[编写评估用例](/zh/tutorials/authoring),断言与评分见[评分指南](/zh/tutorials/scoring-guide),签名见 [defineEval 参考](/zh/reference/define-eval)。
|
|
67
71
|
|
|
68
72
|
参数怎么从 Experiment 流到 Adapter、静态配置和每轮动态值怎么分(工厂参数 vs `ctx`),见[接入自己的 Agent](/zh/tutorials/connect-your-agent)。
|
|
69
73
|
|
|
@@ -83,6 +87,14 @@ export default defineConfig({
|
|
|
83
87
|
|
|
84
88
|
## 第 5 步:收尾,告诉用户做了什么
|
|
85
89
|
|
|
90
|
+
总结之前先过一遍收尾自检——**任何一条不满足就回第 3 步补,不要在总结里含糊带过**:
|
|
91
|
+
|
|
92
|
+
- [ ] 评估用例的输入是被测系统的核心用例,不是问它自己的元问题或占位寒暄
|
|
93
|
+
- [ ] 每条内容断言在被测系统复读题目/胡编时会变红(断言的词不是输入里本来就有的)
|
|
94
|
+
- [ ] 至少有一条负例(应该答不了的输入,断言它明确说做不到)
|
|
95
|
+
- [ ] 有 key 时 Judge 已配置,且在 `niceeval view` 里看到过 Judge 分数;没 key 时总结里说明了
|
|
96
|
+
- [ ] Experiment 里声明的 `model` / `flags` 确实被 Adapter 消费(没有写了没人读的死配置,也没有编造被测系统不存在的 model 值)
|
|
97
|
+
|
|
86
98
|
跑通之后先总结,再谈下一步。总结要说清:接了什么被测对象、生成了哪几个文件(Adapter / Experiment / 评估用例各在哪)、`niceeval exp compare-models` 和 `niceeval view` 怎么跑、第一次运行的结果是什么样。不要在没被要求的情况下顺手重构用户已有代码,也不要在这几个文件之外新增抽象。
|
|
87
99
|
|
|
88
100
|
## 第 6 步:问用户要不要往深了接
|
|
@@ -25,8 +25,7 @@ export default defineExperiment({
|
|
|
25
25
|
description: "gpt-5.4: 对比模型",
|
|
26
26
|
agent: webAgent({ baseUrl: "http://127.0.0.1:5188" }),
|
|
27
27
|
model: "gpt-5.4", // 单个字符串;另一个模型就复制一份文件改这一行
|
|
28
|
-
runs: 2,
|
|
29
|
-
earlyExit: true,
|
|
28
|
+
runs: 2, // earlyExit 默认关,跑满 2 次才能比较 pass rate
|
|
30
29
|
});
|
|
31
30
|
```
|
|
32
31
|
|
|
@@ -8,9 +8,9 @@ description: "安装 NiceEval,写三个文件,10 分钟内对你自己的应
|
|
|
8
8
|
|
|
9
9
|
## 使用 Coding Agent 接入(推荐)
|
|
10
10
|
<Steps>
|
|
11
|
-
<Step title="
|
|
11
|
+
<Step title="接入">
|
|
12
12
|
```text
|
|
13
|
-
READ https://niceeval.com/INIT.md and
|
|
13
|
+
READ https://niceeval.com/INIT.md and set up niceeval for this repo: install it, integrate it with this project, and run the first eval end to end.
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
</Step>
|
|
@@ -46,6 +46,7 @@ export default defineExperiment({
|
|
|
46
46
|
| `dockerSandbox()` | `pnpm add dockerode @types/dockerode` |
|
|
47
47
|
| `vercelSandbox()` | `pnpm add @vercel/sandbox` |
|
|
48
48
|
| `e2bSandbox()` | `pnpm add e2b` |
|
|
49
|
+
| `localSandbox()` | 无需额外安装 |
|
|
49
50
|
|
|
50
51
|
漏装时不会静默失败:NiceEval 在创建 sandbox 的那一刻报错并直接给出上面的安装命令,例如 `Docker sandbox requires 'dockerode'. Install it with: pnpm add dockerode @types/dockerode`。
|
|
51
52
|
|
|
@@ -327,6 +328,32 @@ Docker 适合本地开发和标准 CI。优点是简单、可控、无云端依
|
|
|
327
328
|
|
|
328
329
|
Vercel Sandbox 适合需要云端隔离、更多资源或更稳定环境的任务。需要相应 token 或 OIDC 配置。
|
|
329
330
|
|
|
331
|
+
## 本地目录
|
|
332
|
+
|
|
333
|
+
`localSandbox()` 让 Agent 直接在你机器上的一个 Git 仓库里跑,不需要 Docker,也不需要云凭据:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { defineExperiment } from "niceeval";
|
|
337
|
+
import { localSandbox } from "niceeval/sandbox";
|
|
338
|
+
|
|
339
|
+
export default defineExperiment({
|
|
340
|
+
agent: myCodingAgent,
|
|
341
|
+
sandbox: localSandbox(), // 默认用当前 Git 仓库根做工作目录
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
要评别的目录,传 `localSandbox({ dir: "/path/to/repo" })`。
|
|
346
|
+
|
|
347
|
+
NiceEval 只观察、不还原:Agent 改了什么会真实落在你的工作树上;NiceEval 用一份独立的私有 Git 记录采集 diff 和评分,不碰你自己的 `.git`、暂存区和未提交改动,跑完也不会执行任何 `git reset`。要保留还是丢弃 Agent 的改动,由你决定。
|
|
348
|
+
|
|
349
|
+
使用前记住三件事:
|
|
350
|
+
|
|
351
|
+
- Agent 以你的身份、在你的机器上执行命令。只在你信任任务和 Prompt 的时候用它;要隔离就用 Docker 或云 Provider。
|
|
352
|
+
- 本地目录同一时刻只跑一个 Attempt(强制串行),`--max-concurrency` 抬不高它。
|
|
353
|
+
- 连续跑多个评估用例时,前一个的改动会留在工作树上,成为后一个的起点。要每题干净起点,用容器 Provider。
|
|
354
|
+
|
|
355
|
+
`{ root: true }` 和 `--keep-sandbox` 对本地目录不可用,会直接报错:NiceEval 不在你的机器上提权,也不需要「留存」一个本来就在你工作树里的现场。
|
|
356
|
+
|
|
330
357
|
## 自定义 Provider
|
|
331
358
|
|
|
332
359
|
用 `defineSandbox` 接入其它服务。`create` 的 `feedback` 已绑定到 `sandbox.create` 阶段,可以报告分配实例、拉镜像或恢复沙箱快照的状态:
|
|
@@ -9,21 +9,24 @@ description: "用 niceeval show 在终端按 @<locator> 查看 Attempt 的评估
|
|
|
9
9
|
## 控制台输出
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
|
-
|
|
12
|
+
╭─ PLAN ─────────────────────────────────────────────────────────────────────────╮
|
|
13
|
+
│ 45 attempts · 9 evals × 5 configs · concurrency 19 │
|
|
14
|
+
│ 6 of 45 carried in from cache · 39 to run │
|
|
15
|
+
╰────────────────────────────────────────────────────────────────────────────────╯
|
|
13
16
|
✗ @12h8m4k1 fixtures/button [compare/claude-e2b] errored · sandbox.create
|
|
14
17
|
sandbox-rate-limit: E2B sandbox allocation failed after 5 attempts
|
|
15
18
|
Inspect: niceeval show @12h8m4k1
|
|
16
19
|
|
|
17
|
-
niceeval exp compare
|
|
18
|
-
45 total · 6 reused · 19 running · 12 queued · 8 completed
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
● memory/agent-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
╭─ niceeval exp compare ──────────────────────────────────────────────── 2m 14s ─╮
|
|
21
|
+
│ 45 total · 6 reused · 19 running · 12 queued · 8 completed │
|
|
22
|
+
├─ ACTIVE ───────────────────────────────────────────────────────────────────────┤
|
|
23
|
+
│ ● memory/agent-029-use-cache compare/bub-e2b 1m 42s running tests │
|
|
24
|
+
│ ● memory/agent-030-app-route compare/codex 1m 18s editing src/app.ts │
|
|
25
|
+
│ … 17 more active │
|
|
26
|
+
╰──────────────────────────────────────────────────────────────────────── $0.84 ─╯
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
在终端里跑的时候,运行中的那个框会原位刷新总数和 active slots,不把历史帧推进 scrollback;失败、错误和去重后的 diagnostic 以无框行留下来,都带 locator。输出被管道接走或终端太窄时,同样的内容会退化成不带框的纯文本。跑完框会换成结论、失败清单和下一步命令三块。完整的运行、读取、修复与重跑流程见 [Coding Agent 反馈闭环](/zh/tutorials/agent-feedback-loop)。
|
|
27
30
|
|
|
28
31
|
## `.niceeval/<experiment>/<快照>/`
|
|
29
32
|
|
|
@@ -254,7 +257,7 @@ agent.setup 41.5s
|
|
|
254
257
|
├─ shell · npm install -g @openai/codex… 39.8s
|
|
255
258
|
└─ shell · write ~/.codex/config.toml 1.7s
|
|
256
259
|
eval.run 50.9s
|
|
257
|
-
└─
|
|
260
|
+
└─ turn1 50.9s ✗ agent-runtime-error
|
|
258
261
|
└─ shell · codex exec … 50.7s
|
|
259
262
|
├─ agent · codex.exec 50.5s OTel
|
|
260
263
|
└─ model · chat 44.2s OTel
|
|
@@ -38,7 +38,6 @@ export default defineExperiment({
|
|
|
38
38
|
promptVariant: "v1",
|
|
39
39
|
},
|
|
40
40
|
runs: 1,
|
|
41
|
-
earlyExit: true,
|
|
42
41
|
budget: 5,
|
|
43
42
|
});
|
|
44
43
|
```
|
|
@@ -58,7 +57,6 @@ export default defineExperiment({
|
|
|
58
57
|
promptVariant: "v1",
|
|
59
58
|
},
|
|
60
59
|
runs: 1,
|
|
61
|
-
earlyExit: true,
|
|
62
60
|
budget: 5,
|
|
63
61
|
});
|
|
64
62
|
```
|
|
@@ -120,7 +118,7 @@ npx niceeval exp prompts/concise
|
|
|
120
118
|
| `reasoningEffort` | 单个推理努力程度(如 `"high"`),经 `ctx.reasoningEffort` / `t.reasoningEffort` 透传,归属与 `model` 一致 |
|
|
121
119
|
| `flags` | 实验条件(A/B 里的 feature flag),任意 JSON 对象,经 `ctx.flags` / `t.flags` 透传 |
|
|
122
120
|
| `runs` | 每个评估用例 × 配置最多跑几次 |
|
|
123
|
-
| `earlyExit` |
|
|
121
|
+
| `earlyExit` | 多次运行里通过一次就提前停止;默认关,`runs` 默认跑满测完整通过率 |
|
|
124
122
|
| `evals` | `"*"`、id 前缀数组,或遍历只读 eval 描述并返回 boolean 的函数 |
|
|
125
123
|
| `timeoutMs` | 单个 attempt 的超时 |
|
|
126
124
|
| `budget` | 这一格配置的预算上限 |
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niceeval",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3-canary.1",
|
|
4
4
|
"description": "Agent-native eval tool — eval agents, services, functions, and coding-agent fixtures",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "git+https://github.com/NiceEval/NiceEval.git"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=18"
|
|
@@ -105,14 +105,13 @@
|
|
|
105
105
|
"clsx": "^2.1.1",
|
|
106
106
|
"dockerode": "^4.0.2",
|
|
107
107
|
"e2b": "^2.31.0",
|
|
108
|
-
"jsdom": "^29.1.1",
|
|
109
108
|
"lucide-react": "^1.21.0",
|
|
110
109
|
"mixpanel-browser": "^2.80.0",
|
|
111
110
|
"next": "16.2.10",
|
|
112
111
|
"prism-react-renderer": "^2.4.1",
|
|
113
112
|
"react": "^19.2.7",
|
|
114
113
|
"react-dom": "^19.2.7",
|
|
115
|
-
"react-grab": "^0.1.
|
|
114
|
+
"react-grab": "^0.1.48",
|
|
116
115
|
"shiki": "^4.3.0",
|
|
117
116
|
"tailwind-merge": "^3.6.0",
|
|
118
117
|
"tailwindcss": "^4.3.1",
|
package/src/cli.ts
CHANGED
|
@@ -190,8 +190,9 @@ const FLAG_OPTIONS = {
|
|
|
190
190
|
force: { type: "boolean" },
|
|
191
191
|
/** CI 中推荐使用:让软阈值(`soft`)失败也计入整条 eval 的 verdict。 */
|
|
192
192
|
strict: { type: "boolean" },
|
|
193
|
-
/** 某个 eval 的一次 attempt 通过后,停止该 eval 剩余的 attempts。 */
|
|
193
|
+
/** 某个 eval 的一次 attempt 通过后,停止该 eval 剩余的 attempts;省略默认关(`runs` 默认跑满、测完整通过率)。 */
|
|
194
194
|
"early-exit": { type: "boolean" },
|
|
195
|
+
/** 强制关闭首过即停,即使实验文件里写了 `earlyExit: true`。 */
|
|
195
196
|
"no-early-exit": { type: "boolean" },
|
|
196
197
|
/** `view` 命令专用:启动后自动打开浏览器(默认行为)。 */
|
|
197
198
|
open: { type: "boolean" },
|
|
@@ -430,6 +431,12 @@ async function initProject(cwd: string): Promise<void> {
|
|
|
430
431
|
"",
|
|
431
432
|
"export default defineConfig({",
|
|
432
433
|
" // Add experiments/ with defineExperiment(...) to run evals.",
|
|
434
|
+
" //",
|
|
435
|
+
" // TODO(judge): semantic assertions (t.judge.*) are silently skipped until a judge",
|
|
436
|
+
" // model is configured — an all-green run does not mean the judge ran. Any",
|
|
437
|
+
" // OpenAI-compatible /chat/completions service works; the key is read from",
|
|
438
|
+
" // OPENAI_API_KEY unless apiKeyEnv says otherwise.",
|
|
439
|
+
' // judge: { model: "gpt-5.4-mini" },',
|
|
433
440
|
"});",
|
|
434
441
|
"",
|
|
435
442
|
].join("\n"),
|
|
@@ -695,7 +702,7 @@ async function main(): Promise<void> {
|
|
|
695
702
|
reasoningEffort: exp.reasoningEffort,
|
|
696
703
|
flags: exp.flags ?? {},
|
|
697
704
|
runs: flags.runs ?? envNumber("NICEEVAL_RUNS") ?? exp.runs ?? 1,
|
|
698
|
-
earlyExit: flags.earlyExit ?? exp.earlyExit ??
|
|
705
|
+
earlyExit: flags.earlyExit ?? exp.earlyExit ?? false,
|
|
699
706
|
sandbox: exp.sandbox ?? config.sandbox,
|
|
700
707
|
timeoutMs: flags.timeout ?? envNumber("NICEEVAL_TIMEOUT") ?? exp.timeoutMs ?? config.timeoutMs,
|
|
701
708
|
budget: flags.budget ?? envNumber("NICEEVAL_BUDGET") ?? exp.budget,
|
|
@@ -709,6 +716,7 @@ async function main(): Promise<void> {
|
|
|
709
716
|
// 不再取所有选中实验的最小值钳全局——那会让一个串行实验拖慢整批基线。
|
|
710
717
|
maxConcurrency: exp.maxConcurrency,
|
|
711
718
|
setup: exp.setup,
|
|
719
|
+
teardown: exp.teardown,
|
|
712
720
|
});
|
|
713
721
|
}
|
|
714
722
|
} else {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/scoring/cases.md
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { hasSections, includesUrl } from "./index.ts";
|
|
4
|
+
|
|
5
|
+
describe("includesUrl", () => {
|
|
6
|
+
it("scores 1 when the text contains a real http(s) link", async () => {
|
|
7
|
+
const m = includesUrl();
|
|
8
|
+
expect(await m.score("依据见 https://example.com/report 第 3 节")).toBe(1);
|
|
9
|
+
expect(m.severity).toBe("gate");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("scores 0 on no link, and bare domains without a protocol do not count", async () => {
|
|
13
|
+
expect(await includesUrl().score("来源:example.com(官网)")).toBe(0);
|
|
14
|
+
expect(await includesUrl().score("一段没有任何引用的回答")).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("counts distinct URLs: the same link repeated is one", async () => {
|
|
18
|
+
const twice = "见 https://a.dev/x 与 https://a.dev/x";
|
|
19
|
+
expect(await includesUrl(2).score(twice)).toBe(0);
|
|
20
|
+
expect(await includesUrl(2).score("见 https://a.dev/x 与 https://b.dev/y")).toBe(1);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("hasSections", () => {
|
|
25
|
+
it("scores 1 when the text has at least min markdown headings", async () => {
|
|
26
|
+
const m = hasSections();
|
|
27
|
+
expect(await m.score("# 概述\n正文\n## 结论\n正文")).toBe(1);
|
|
28
|
+
expect(m.severity).toBe("gate");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("scores 0 on flowing text without headings; a mid-line # does not count", async () => {
|
|
32
|
+
expect(await hasSections().score("一整段没有结构的流水回答,写了很多但没有小节。")).toBe(0);
|
|
33
|
+
expect(await hasSections(1).score("价格是 #1 的选择\n继续正文")).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("counts a level-6 heading too", async () => {
|
|
37
|
+
expect(await hasSections(1).score("###### 附录\n内容")).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/expect/index.ts
CHANGED
|
@@ -165,6 +165,39 @@ export function similarity(expected: string): ValueAssertion {
|
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* 文本含至少 min 条(默认 1)去重后的 http(s) 链接则 1,否则 0。默认硬门槛。
|
|
170
|
+
* 「回答有没有引用真实来源」的形状断言:没有 Judge key 时,它是「至少引用一条来源链接」
|
|
171
|
+
* 的最低成本兜底——被测方复读题目糊弄不过去,编造的链接则留给负例或 Judge 去抓。
|
|
172
|
+
*/
|
|
173
|
+
export function includesUrl(min = 1): ValueAssertion {
|
|
174
|
+
return createAssertion(
|
|
175
|
+
`includesUrl(min=${min})`,
|
|
176
|
+
"gate",
|
|
177
|
+
(value) => {
|
|
178
|
+
const urls = new Set(String(value).match(/https?:\/\/[^\s<>()"'`]+/g) ?? []);
|
|
179
|
+
return urls.size >= min ? 1 : 0;
|
|
180
|
+
},
|
|
181
|
+
undefined,
|
|
182
|
+
{ expected: `contains >= ${min} distinct http(s) URL(s)` },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 文本含至少 min 个(默认 2)Markdown 标题(行首 # 到 ######)则 1,否则 0。默认硬门槛。
|
|
188
|
+
* 「回答是不是一份有结构的文档」的形状断言,适合研究报告、方案文档这类产出型回答——
|
|
189
|
+
* 一段没有任何小标题的流水文本过不了。
|
|
190
|
+
*/
|
|
191
|
+
export function hasSections(min = 2): ValueAssertion {
|
|
192
|
+
return createAssertion(
|
|
193
|
+
`hasSections(min=${min})`,
|
|
194
|
+
"gate",
|
|
195
|
+
(value) => ((String(value).match(/^#{1,6}\s+\S/gm) ?? []).length >= min ? 1 : 0),
|
|
196
|
+
undefined,
|
|
197
|
+
{ expected: `contains >= ${min} markdown heading(s)` },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
168
201
|
/** 谓词为真则 1,否则 0。默认硬门槛;name 带上 label 便于报告辨认。 */
|
|
169
202
|
export function satisfies(predicate: (v: unknown) => boolean, label?: string): ValueAssertion {
|
|
170
203
|
const name = label ? `satisfies(${label})` : "satisfies(predicate)";
|