niceeval 0.4.3 → 0.4.5

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.
Files changed (179) hide show
  1. package/README.md +21 -12
  2. package/README.zh.md +8 -9
  3. package/docs-site/.mintignore +13 -0
  4. package/docs-site/AGENTS.md +46 -0
  5. package/docs-site/concepts/adapter.mdx +287 -0
  6. package/docs-site/concepts/assert.mdx +243 -0
  7. package/docs-site/concepts/drive.mdx +118 -0
  8. package/docs-site/concepts/evals.mdx +108 -0
  9. package/docs-site/concepts/experiment.mdx +37 -0
  10. package/docs-site/concepts/hitl.mdx +83 -0
  11. package/docs-site/concepts/judge.mdx +129 -0
  12. package/docs-site/concepts/overview.mdx +98 -0
  13. package/docs-site/concepts/tier.mdx +42 -0
  14. package/docs-site/docs-ref/00-index.md +23 -0
  15. package/docs-site/docs-ref/01-page-types.md +43 -0
  16. package/docs-site/docs-ref/03-style-rules.md +45 -0
  17. package/docs-site/docs-ref/04-code-examples.md +37 -0
  18. package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
  19. package/docs-site/docs-ref/06-checklists.md +53 -0
  20. package/docs-site/docs.json +256 -0
  21. package/docs-site/example/ai-agent-application.mdx +148 -0
  22. package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
  23. package/docs-site/example/claude-code-codex-skill.mdx +147 -0
  24. package/docs-site/example/showcase.mdx +39 -0
  25. package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
  26. package/docs-site/example/tier1-claude-sdk.mdx +119 -0
  27. package/docs-site/example/tier1-codex-sdk.mdx +111 -0
  28. package/docs-site/example/tier1-langgraph.mdx +119 -0
  29. package/docs-site/example/tier1-pi-sdk.mdx +119 -0
  30. package/docs-site/favicon.svg +5 -0
  31. package/docs-site/github-diff.css +135 -0
  32. package/docs-site/github-diff.js +11 -0
  33. package/docs-site/guides/authoring.mdx +129 -0
  34. package/docs-site/guides/ci-integration.mdx +97 -0
  35. package/docs-site/guides/connect-otel.mdx +209 -0
  36. package/docs-site/guides/connect-your-agent.mdx +221 -0
  37. package/docs-site/guides/dataset-fanout.mdx +98 -0
  38. package/docs-site/guides/experiments.mdx +71 -0
  39. package/docs-site/guides/fixtures.mdx +147 -0
  40. package/docs-site/guides/reporters.mdx +113 -0
  41. package/docs-site/guides/runner.mdx +79 -0
  42. package/docs-site/guides/sandbox-agent.mdx +138 -0
  43. package/docs-site/guides/sandbox-backends.mdx +64 -0
  44. package/docs-site/guides/scoring-guide.mdx +92 -0
  45. package/docs-site/guides/viewing-results.mdx +195 -0
  46. package/docs-site/guides/write-experiment.mdx +81 -0
  47. package/docs-site/guides/write-send.mdx +347 -0
  48. package/docs-site/images/adapter-tiers-zh.svg +60 -0
  49. package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
  50. package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
  51. package/docs-site/images/hitl-handshake-zh.svg +89 -0
  52. package/docs-site/images/logo.svg +6 -0
  53. package/docs-site/index.mdx +181 -0
  54. package/docs-site/introduction.mdx +141 -0
  55. package/docs-site/quickstart.mdx +136 -0
  56. package/docs-site/reference/builtin-agents.mdx +161 -0
  57. package/docs-site/reference/capabilities.mdx +76 -0
  58. package/docs-site/reference/cli.mdx +120 -0
  59. package/docs-site/reference/define-agent.mdx +168 -0
  60. package/docs-site/reference/define-config.mdx +41 -0
  61. package/docs-site/reference/define-eval.mdx +160 -0
  62. package/docs-site/reference/events.mdx +131 -0
  63. package/docs-site/reference/expect.mdx +112 -0
  64. package/docs-site/tracker.js +8 -0
  65. package/docs-site/zh/concepts/adapter.mdx +286 -0
  66. package/docs-site/zh/concepts/assert.mdx +243 -0
  67. package/docs-site/zh/concepts/drive.mdx +118 -0
  68. package/docs-site/zh/concepts/evals.mdx +124 -0
  69. package/docs-site/zh/concepts/experiment.mdx +37 -0
  70. package/docs-site/zh/concepts/hitl.mdx +83 -0
  71. package/docs-site/zh/concepts/judge.mdx +130 -0
  72. package/docs-site/zh/concepts/overview.mdx +109 -0
  73. package/docs-site/zh/concepts/tier.mdx +44 -0
  74. package/docs-site/zh/example/ai-agent-application.mdx +152 -0
  75. package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
  76. package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
  77. package/docs-site/zh/example/showcase.mdx +39 -0
  78. package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
  79. package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
  80. package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
  81. package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
  82. package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
  83. package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
  84. package/docs-site/zh/guides/authoring.mdx +129 -0
  85. package/docs-site/zh/guides/ci-integration.mdx +97 -0
  86. package/docs-site/zh/guides/connect-otel.mdx +208 -0
  87. package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
  88. package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
  89. package/docs-site/zh/guides/experiments.mdx +72 -0
  90. package/docs-site/zh/guides/fixtures.mdx +147 -0
  91. package/docs-site/zh/guides/official-adapters.mdx +132 -0
  92. package/docs-site/zh/guides/reporters.mdx +113 -0
  93. package/docs-site/zh/guides/runner.mdx +82 -0
  94. package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
  95. package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
  96. package/docs-site/zh/guides/scoring-guide.mdx +92 -0
  97. package/docs-site/zh/guides/viewing-results.mdx +195 -0
  98. package/docs-site/zh/guides/write-experiment.mdx +101 -0
  99. package/docs-site/zh/guides/write-send.mdx +353 -0
  100. package/docs-site/zh/index.mdx +180 -0
  101. package/docs-site/zh/introduction.mdx +140 -0
  102. package/docs-site/zh/quickstart.mdx +137 -0
  103. package/docs-site/zh/reference/builtin-agents.mdx +364 -0
  104. package/docs-site/zh/reference/capabilities.mdx +76 -0
  105. package/docs-site/zh/reference/cli.mdx +140 -0
  106. package/docs-site/zh/reference/define-agent.mdx +461 -0
  107. package/docs-site/zh/reference/define-config.mdx +107 -0
  108. package/docs-site/zh/reference/define-eval.mdx +674 -0
  109. package/docs-site/zh/reference/events.mdx +252 -0
  110. package/docs-site/zh/reference/expect.mdx +208 -0
  111. package/package.json +3 -2
  112. package/src/agents/bub.ts +1 -1
  113. package/src/agents/claude-code.ts +8 -5
  114. package/src/agents/codex.ts +9 -5
  115. package/src/agents/sdk-streams.test.ts +4 -4
  116. package/src/agents/sdk-streams.ts +14 -9
  117. package/src/agents/types.ts +40 -1
  118. package/src/agents/ui-message-stream.ts +1 -0
  119. package/src/cli.ts +40 -10
  120. package/src/context/types.ts +140 -0
  121. package/src/expect/index.ts +15 -8
  122. package/src/i18n/en.ts +20 -6
  123. package/src/i18n/zh-CN.ts +20 -6
  124. package/src/o11y/parsers/bub.test.ts +71 -0
  125. package/src/o11y/parsers/bub.ts +5 -0
  126. package/src/o11y/types.ts +27 -2
  127. package/src/runner/attempt.ts +2 -1
  128. package/src/runner/reporters/artifacts.ts +11 -3
  129. package/src/runner/reporters/live.ts +45 -8
  130. package/src/runner/run.test.ts +50 -0
  131. package/src/runner/run.ts +135 -41
  132. package/src/runner/types.ts +55 -2
  133. package/src/sandbox/types.ts +18 -0
  134. package/src/scoring/types.ts +5 -0
  135. package/src/shared/types.ts +3 -0
  136. package/src/util.test.ts +26 -1
  137. package/src/util.ts +16 -0
  138. package/src/view/app/App.tsx +2 -2
  139. package/src/view/app/components/AttemptModal.tsx +2 -0
  140. package/src/view/app/components/CopyControls.tsx +87 -28
  141. package/src/view/app/i18n.ts +3 -3
  142. package/src/view/client-dist/app.css +1 -1
  143. package/src/view/client-dist/app.js +18 -18
  144. package/docs/README.md +0 -120
  145. package/docs/adapters/README.md +0 -60
  146. package/docs/adapters/authoring.md +0 -179
  147. package/docs/adapters/coding-agent-skills-plugins.md +0 -324
  148. package/docs/adapters/collection.md +0 -128
  149. package/docs/adapters/contract.md +0 -264
  150. package/docs/adapters/reference/agent-eval.md +0 -215
  151. package/docs/adapters/reference/agent-loop-apis.md +0 -69
  152. package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
  153. package/docs/adapters/reference/eve-protocol.md +0 -127
  154. package/docs/adapters/reference/otel-genai.md +0 -107
  155. package/docs/adapters/reference/otel-instrumentation.md +0 -65
  156. package/docs/adapters/targets.md +0 -99
  157. package/docs/architecture.md +0 -140
  158. package/docs/assertions.md +0 -387
  159. package/docs/capabilities-by-construction.md +0 -48
  160. package/docs/cli.md +0 -173
  161. package/docs/concepts.md +0 -106
  162. package/docs/e2e-ci.md +0 -332
  163. package/docs/eval-authoring.md +0 -319
  164. package/docs/experiments.md +0 -157
  165. package/docs/getting-started.md +0 -224
  166. package/docs/multi-agent.md +0 -145
  167. package/docs/observability.md +0 -337
  168. package/docs/origin-integration.md +0 -195
  169. package/docs/references.md +0 -35
  170. package/docs/reports.md +0 -551
  171. package/docs/results-format.md +0 -228
  172. package/docs/results-lib.md +0 -191
  173. package/docs/runner.md +0 -104
  174. package/docs/sandbox.md +0 -276
  175. package/docs/scoring.md +0 -119
  176. package/docs/source-map.md +0 -126
  177. package/docs/tier-sync.md +0 -193
  178. package/docs/view.md +0 -194
  179. package/docs/vision.md +0 -88
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: "让 coding agent 读结果并自主迭代"
3
+ sidebarTitle: "Agent 反馈闭环"
4
+ description: "NiceEval 的运行输出为 coding agent 设计:跑完直接给出 summary.json 路径,agent 读结构化结果和 artifacts,修完再跑,直到全绿。"
5
+ ---
6
+
7
+ Eval 的价值在闭环:不只是人看报告,而是让 coding agent(Claude Code、Codex 等)自己跑 eval、自己读失败原因、自己改被测程序、再跑验证。NiceEval 的输出为这个循环设计——每次运行结束,CLI 直接打出结构化结果的路径,agent 不需要解析人类向的流式输出。
8
+
9
+ ## 一次完整的循环
10
+
11
+ <Steps>
12
+ <Step title="跑">
13
+ ```bash
14
+ npx niceeval exp local
15
+ ```
16
+ </Step>
17
+ <Step title="读">
18
+ 运行结束时 CLI 输出结构化结果入口:
19
+
20
+ ```text
21
+ Structured results: .niceeval/2026-07-09T10-00-00-000Z/summary.json
22
+ ```
23
+
24
+ `summary.json` 里每条结果带 `outcome`、失败断言明细和 `artifactsDir`。`artifactsDir` 指向该次 attempt 的 artifacts 目录:`events.json` 是完整的 agent transcript(含工具调用),`trace.json` 是执行 trace,`diff.json` 是 sandbox 里的文件改动。字段说明见[查看结果](/zh/guides/viewing-results)。
25
+ </Step>
26
+ <Step title="修">
27
+ 先判断缺陷在哪一侧:被测程序,还是 eval 本身(断言过紧、fixture 错误、setup 缺失)。修对应的一侧,不要为了变绿而放宽断言。
28
+ </Step>
29
+ <Step title="再跑">
30
+ ```bash
31
+ npx niceeval exp local weather
32
+ ```
33
+
34
+ 位置参数按 eval id 前缀只重跑失败的部分。已通过的 eval 会被指纹缓存跳过;`--force` 全量重跑。
35
+ </Step>
36
+ </Steps>
37
+
38
+ 给 agent 的提示词可以直接这样写:
39
+
40
+ ```text
41
+ 跑 npx niceeval exp local,读输出末尾的 summary.json 和失败结果的 artifacts,
42
+ 修掉失败的 eval,再跑,直到全部通过。
43
+ ```
44
+
45
+ ## AGENTS.md 指引区块
46
+
47
+ 模型的训练数据里没有 NiceEval。`npx niceeval init` 会在项目的 `AGENTS.md` 里写入一个托管区块,告诉 agent 先读随包文档再动手:
48
+
49
+ ```md
50
+ <!-- BEGIN:niceeval-agent-rules -->
51
+ # niceeval is NOT in your training data
52
+
53
+ Its APIs and conventions may differ from anything you have seen. Read the relevant
54
+ guide in `node_modules/niceeval/docs-site/` (English at the top level, Chinese under
55
+ `zh/`) before writing any eval, experiment, adapter, or niceeval config. After a run,
56
+ read the `summary.json` path the CLI prints for structured results.
57
+ <!-- END:niceeval-agent-rules -->
58
+ ```
59
+
60
+ 标记之外的内容不会被改动;升级 NiceEval 后重跑 `npx niceeval init` 会刷新标记之内的指引。文档随 npm 包发布在 `node_modules/niceeval/docs-site/`,agent 读到的永远是和安装版本一致的行为,而不是训练数据里的旧记忆。
61
+
62
+ ## 报错自带修法
63
+
64
+ 配置和运行期错误直接给出修法与随包文档路径,agent 读到报错就能自修,不必先搜索:
65
+
66
+ ```text
67
+ Could not find niceeval.config.ts.
68
+ Ways to fix:
69
+ - [init] Run `npx niceeval init` to scaffold niceeval.config.ts and evals/
70
+ - [cd] Run from the project root that contains niceeval.config.ts
71
+ Docs: node_modules/niceeval/docs-site/quickstart.mdx
72
+ ```
73
+
74
+ ## 从查看器转交给 agent
75
+
76
+ 人在 `npx niceeval view` 里看到失败时,不必手工整理上下文:
77
+
78
+ - 榜单右上角的 **Copy fix prompt** 把全部失败打包成一段可直接粘给 agent 的修复 prompt——失败清单、artifacts 路径、判断与重跑步骤都在里面。
79
+ - 打开单个 attempt 的弹窗,同名按钮只打包这一条失败,适合逐条转交。
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: "编写 eval: 单轮、多轮和数据集模式"
3
+ sidebarTitle: "编写 eval"
4
+ description: "学习如何用 defineEval 编写 NiceEval eval,包括单轮断言、多轮对话、数据驱动测试和 sandbox workspace。"
5
+ ---
6
+
7
+ 每个 [NiceEval](https://niceeval.com/) eval 都是一个 TypeScript 文件,默认导出 `defineEval(...)`。核心原则是:**路径即身份**、**一个文件一个 eval**、**线性书写并就地断言**。
8
+
9
+ ## `defineEval` 的结构
10
+
11
+ ```ts
12
+ import { defineEval } from "niceeval";
13
+
14
+ export default defineEval({
15
+ description?: string;
16
+ tags?: string[];
17
+ judge?: JudgeConfig;
18
+ reporters?: Reporter[];
19
+ timeoutMs?: number;
20
+ metadata?: Record<string, unknown>;
21
+ async test(t) { /* interactions + assertions */ },
22
+ });
23
+ ```
24
+
25
+ <Warning>
26
+ 不要设置 `id` 或 `name`。`evals/weather/brooklyn.eval.ts` 会自动变成 ID `weather/brooklyn`。
27
+ </Warning>
28
+
29
+ ## 单轮 eval
30
+
31
+ ```ts
32
+ import { defineEval } from "niceeval";
33
+ import { includes } from "niceeval/expect";
34
+
35
+ export default defineEval({
36
+ description: "Brooklyn weather query",
37
+ async test(t) {
38
+ await t.send("What's the weather like in Brooklyn today?");
39
+ t.succeeded();
40
+ t.calledTool("get_weather", { input: { city: "Brooklyn" }, count: 1 });
41
+ t.check(t.reply, includes("sunny"));
42
+ },
43
+ });
44
+ ```
45
+
46
+ `t.send()` 驱动一次交互,`t.succeeded()` 和 `t.calledTool()` 是作用域断言,`t.check()` 是立即记录的值断言。
47
+
48
+ ### `Turn` 对象
49
+
50
+ | 属性 | 说明 |
51
+ |---|---|
52
+ | `turn.events` | 标准事件流,主要事实来源 |
53
+ | `turn.data` | 结构化输出 |
54
+ | `turn.status` | `"completed"`、`"failed"` 或 `"waiting"` |
55
+ | `turn.usage` | token / cost 等 usage |
56
+ | `turn.message` | assistant 文本回复 |
57
+ | `turn.toolCalls` | 本轮工具调用 |
58
+
59
+ ## 多轮 eval
60
+
61
+ ```ts
62
+ export default defineEval({
63
+ description: "Draft an email, then send it on confirmation",
64
+ async test(t) {
65
+ const draft = await t.send("Draft a follow-up email.");
66
+ draft.succeeded();
67
+ t.check(draft.message, includes("Best"));
68
+
69
+ await t.send("Looks good, send it.");
70
+ t.calledTool("send_email");
71
+ },
72
+ });
73
+ ```
74
+
75
+ 需要并行独立会话时,用 `t.newSession()`。
76
+
77
+ ## 数据驱动测试(dataset fan-out)
78
+
79
+ 一个文件可以导出 eval 数组:
80
+
81
+ ```ts
82
+ export default rows.map((row) =>
83
+ defineEval({
84
+ description: row.task,
85
+ async test(t) {
86
+ await t.send(row.prompt);
87
+ t.check(t.reply, equals(row.expected));
88
+ },
89
+ }),
90
+ );
91
+ ```
92
+
93
+ 生成 ID 为 `sql/0000`、`sql/0001` 等。详见 [数据驱动测试](/zh/guides/dataset-fanout)。
94
+
95
+ ## Sandbox workspace
96
+
97
+ Coding-agent eval 仍然是普通 `.eval.ts` 文件,只是 test 里会准备 sandbox workspace、发送任务并检查文件结果:
98
+
99
+ ```ts
100
+ import { defineEval } from "niceeval";
101
+
102
+ export default defineEval({
103
+ description: "Create a Button component",
104
+ async test(t) {
105
+ await t.sandbox.uploadDirectory("../workspaces/ts-starter");
106
+ await t.send("Create src/components/Button.tsx with label and onClick props.").then((turn) => turn.succeeded());
107
+ t.sandbox.fileChanged("src/components/Button.tsx");
108
+ },
109
+ });
110
+ ```
111
+
112
+ 详见 [Fixtures](/zh/guides/fixtures)。
113
+
114
+ ## 命名约定
115
+
116
+ <CardGroup cols={2}>
117
+ <Card title="文件名" icon="file">
118
+ 只有 `.eval.ts` 会被 runner 发现。
119
+ </Card>
120
+ <Card title="目录分组" icon="folder">
121
+ `evals/billing/refund.eval.ts` 的 ID 是 `billing/refund`。
122
+ </Card>
123
+ <Card title="数据集" icon="database">
124
+ 适合大量结构相同、输入不同的 case。
125
+ </Card>
126
+ <Card title="Sandbox workspace" icon="box">
127
+ 适合 coding agent,需要真实文件系统、命令和 diff。
128
+ </Card>
129
+ </CardGroup>
@@ -0,0 +1,97 @@
1
+ ---
2
+ title: "在 GitHub Actions 和 CI 中运行 NiceEval"
3
+ sidebarTitle: "CI 集成"
4
+ description: "把 NiceEval 接入 GitHub Actions 或任意 CI。eval 失败时非零退出,输出 JUnit XML,并通过缓存加速重复运行。"
5
+ ---
6
+
7
+ Evals 应该和测试一样进入 CI。它们能在 PR 阶段发现 agent 行为回退,也能在 nightly job 中跟踪模型、成本和延迟变化。
8
+
9
+ ## 退出码
10
+
11
+ 默认情况下,只要存在失败的 gate,[NiceEval](https://niceeval.com/) 将以非零状态码退出。CI 中通常使用 `--strict`,让失败更明确。
12
+
13
+ ```bash
14
+ npx niceeval exp ci --strict
15
+ ```
16
+
17
+ ## GitHub Actions 示例
18
+
19
+ ```yaml
20
+ name: evals
21
+ on:
22
+ pull_request:
23
+ workflow_dispatch:
24
+
25
+ jobs:
26
+ evals:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-node@v4
31
+ with:
32
+ node-version: 22
33
+ cache: npm
34
+ - run: npm ci
35
+ - run: npx niceeval exp ci --strict
36
+ env:
37
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
39
+ ```
40
+
41
+ ## 配置 secrets
42
+
43
+ <Steps>
44
+ <Step title="在仓库中添加 secrets">
45
+ 把 provider token 放进 GitHub Actions secrets。
46
+ </Step>
47
+ <Step title="在 workflow 中传入 env">
48
+ 只在运行 eval 的 step 暴露必要环境变量。
49
+ </Step>
50
+ <Step title="验证变量存在">
51
+ 用最小 eval 或 `list` / dry run 先验证配置。
52
+ </Step>
53
+ </Steps>
54
+
55
+ ## JUnit reporter
56
+
57
+ ```ts
58
+ import { defineConfig } from "niceeval";
59
+ import { Console, JUnit } from "niceeval/reporters";
60
+
61
+ export default defineConfig({
62
+ reporters: [Console(), JUnit(".niceeval/junit.xml")],
63
+ });
64
+ ```
65
+
66
+ CI 可以上传 JUnit XML,让失败出现在测试报告 UI 里。想把结果同时上报到 Braintrust 这类实验平台,见 [Reporter 上报](./reporters)。
67
+
68
+ ## 只检查发现
69
+
70
+ ```bash
71
+ npx niceeval list
72
+ ```
73
+
74
+ 适合快速验证 eval 文件能加载、配置没有明显错误。
75
+
76
+ ## 缓存 `.niceeval/`
77
+
78
+ 可以在 CI 中缓存 `.niceeval/`,但要确保 fingerprint 覆盖了影响结果的输入。对于 nightly 基准,通常保留完整 artifacts 更有价值。
79
+
80
+ ## 控制并发
81
+
82
+ ```bash
83
+ npx niceeval exp ci --max-concurrency 2
84
+ ```
85
+
86
+ 标准 GitHub-hosted runner 上,sandbox eval 并发不宜过高。远程 HTTP eval 可以按服务限流能力调高。
87
+
88
+ ## 推荐模式
89
+
90
+ <CardGroup cols={2}>
91
+ <Card title="PR runs" icon="code-pull-request">
92
+ 只跑关键 eval 和高风险路径,保持反馈快。
93
+ </Card>
94
+ <Card title="Nightly 全量矩阵运行" icon="moon">
95
+ 运行完整 experiment,记录 pass rate、成本和延迟趋势。
96
+ </Card>
97
+ </CardGroup>
@@ -0,0 +1,208 @@
1
+ ---
2
+ title: "OTel 接入"
3
+ sidebarTitle: "OTel 接入"
4
+ description: "把应用已经在发的 OTel span 也发给 NiceEval 一份,niceeval view 里就有每轮的调用瀑布图。断言不从这里来——接好 send 那一刻断言就齐了。"
5
+ ---
6
+
7
+ 先说清这条接入**不**改变什么:断言。`t.calledTool`、`t.maxTokens`、耗时这些判决的依据,全部来自你的 adapter 在 `send` 里返回的 `Turn`(`events` + `usage`)——接好 send 的那一刻,全套断言就齐了,和 OTel 没有关系(见[接入你的 agent](/zh/guides/connect-your-agent))。
8
+
9
+ OTel 接入买到的是另一样东西:**`niceeval view` 里的调用瀑布图**。应用内部每次模型调用、每次工具执行、各自的耗时和 token,按轮铺开成一条时间线——失败的 eval 为什么失败、慢的轮次慢在哪一步,经常一眼就在瀑布图里。
10
+
11
+ 如果你的应用已经在发 OTel trace——AI SDK 的 telemetry、LangGraph 的 LangSmith 导出、OpenLLMetry / OpenInference 自动埋点,或自己按 GenAI 语义埋的点——那瀑布图的数据你已经在生产了:让应用把 span 也发给 [NiceEval](https://niceeval.com/) 一份即可,应用代码一行不改,仍是无侵入(见 [Tier](/zh/concepts/tier))。
12
+
13
+ ## 原理(一段话)
14
+
15
+ [NiceEval](https://niceeval.com/) 运行时起一个本机 OTLP 接收器,应用把 span 发过来;每轮 `send` 收到的 span 归属到那一轮,归一成 GenAI 语义后写进 `EvalResult.trace`,跑完 `npx niceeval view` 就是瀑布图。**span 只进瀑布图,不进事件流、不喂断言**——埋点缺一块、span 迟到或丢批,只影响瀑布图的完整性,不影响任何判决。
16
+
17
+ ## 接法
18
+
19
+ **1. adapter 侧**——`send` 照常写(事件映射还是你的映射),只多一行:把本轮的 `traceparent` 随请求带过去:
20
+
21
+ ```ts
22
+ // agents/support-bot.ts
23
+ import { defineAgent } from "niceeval/adapter";
24
+
25
+ export default defineAgent({
26
+ name: "support-bot",
27
+ async send(input, ctx) {
28
+ // 示例地址:换成你自己 agent 的真实端点
29
+ const r = await fetch("http://localhost:5188/chat", {
30
+ method: "POST",
31
+ // traceparent 随请求带过去:本轮 span 挂到 NiceEval 的 trace 下,并发下归属才精确
32
+ headers: { "content-type": "application/json", ...ctx.telemetry?.headers },
33
+ body: JSON.stringify({ message: input.text, model: ctx.model }),
34
+ signal: ctx.signal,
35
+ });
36
+ const body = await r.json();
37
+ return {
38
+ status: r.ok ? "completed" : "failed",
39
+ events: mapToEvents(body), // 断言的依据在这里,和没接 OTel 时一样
40
+ };
41
+ },
42
+ });
43
+ ```
44
+
45
+ 内置件不用做这件事:`uiMessageStreamAgent` 总会自动把 `ctx.telemetry.headers` 并入请求头。
46
+
47
+ **2. 端点怎么交给应用**——[NiceEval](https://niceeval.com/) 的接收端点是**启动期配置,不从 send 传**(标准 OTel SDK 也做不到"运行时换端点":`OTEL_*` 环境变量只在进程启动时读一次)。按部署形态选:
48
+
49
+ - **你自己长驻的服务(最常见)**:用**固定端口模式**,在 `niceeval.config.ts` 里钉住接收端口——写了这个配置就等于打开了 OTel 接入:
50
+
51
+ ```ts
52
+ // niceeval.config.ts
53
+ export default defineConfig({
54
+ telemetry: { port: 4318 }, // 接收器固定监听 http://localhost:4318/v1/traces
55
+ });
56
+ ```
57
+
58
+ 服务启动时一次性配 `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces`,之后跑多少次 eval 都不用改。代价:端口共享意味着同一台机器同时只能跑一个 niceeval 进程;OTel Collector 扇出场景同理指向这个固定端点。`port` 被其它进程占用时 [NiceEval](https://niceeval.com/) 会直接报错提示换一个空闲端口,不会静默失败。
59
+
60
+ 报给应用的接收端 hostname 默认是 `127.0.0.1`;docker 型 sandbox tracing 需要 `host.docker.internal`、或配了隧道的远程接入需要别的 hostname 时,加 `host` 覆盖:`telemetry: { host: "host.docker.internal", port: 4318 }`。这两个字段是 [NiceEval](https://niceeval.com/) 里配置 OTLP 接收的唯一入口,不读环境变量。
61
+
62
+ - **子进程 / 由 [NiceEval](https://niceeval.com/) 拉起的进程**(CLI 型 agent):什么都不用做。`ctx.telemetry.env`(标准 `OTEL_*` 环境变量,ready-to-spread)注入进程环境,每次 run 是新进程、读到新端点。
63
+
64
+ **3. 应用侧**——按你的埋点生态各自几行配置:
65
+
66
+ <Tabs>
67
+ <Tab title="AI SDK">
68
+ 推荐官方 OTel 集成(`@ai-sdk/otel`,产标准 GenAI 语义);老的 `experimental_telemetry`(`ai.*`)也能画:
69
+
70
+ ```ts
71
+ import { generateText } from "ai";
72
+
73
+ const result = await generateText({
74
+ model, tools, messages,
75
+ experimental_telemetry: { isEnabled: true },
76
+ });
77
+ ```
78
+
79
+ exporter 走标准 OTel Node SDK,endpoint 指向 [NiceEval](https://niceeval.com/)(注入的 env 或固定端口)。可跑示例:应用侧埋点见 [`examples/zh/origin/ai-sdk-v7`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/origin/ai-sdk-v7)(`src/backend/otel.ts`,官方 `@ai-sdk/otel`),接入后的完整评测项目见 [`examples/zh/tier1/ai-sdk-v7`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1/ai-sdk-v7)。
80
+ </Tab>
81
+ <Tab title="LangGraph / LangChain">
82
+ 零依赖路线,三个环境变量:
83
+
84
+ ```bash
85
+ LANGSMITH_TRACING=true \
86
+ LANGSMITH_OTEL_ENABLED=true \
87
+ LANGSMITH_OTEL_ONLY=true \
88
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces \
89
+ node server.js # 端点值按「端点怎么交给应用」一节选:注入的 env 或固定端口
90
+ ```
91
+
92
+ `LANGSMITH_OTEL_ONLY` 表示只发 OTLP、不发 LangSmith 云端(要双发就去掉它)。这句话在 **Python** 版 `langsmith` SDK 上是真·零代码(import 时自动挂 OTel hook);**JS** 版(`langsmith@0.7.x`)还需要显式调用一次 `initializeOTEL()`(来自 `langsmith/experimental/otel/setup`),否则只打警告、不产生 span——三个环境变量本身仍然不变。可跑示例(Python 后端,零接线代码):[`examples/zh/origin/langgraph`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/origin/langgraph);接入后的完整评测项目见 [`examples/zh/tier1/langgraph`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1/langgraph)。
93
+ </Tab>
94
+ <Tab title="OpenLLMetry">
95
+ ```ts
96
+ import * as traceloop from "@traceloop/node-server-sdk";
97
+
98
+ traceloop.initialize({ disableBatch: true });
99
+ // endpoint 用标准 OTEL_EXPORTER_OTLP_ENDPOINT 环境变量
100
+ ```
101
+ </Tab>
102
+ <Tab title="OpenInference">
103
+ ```python
104
+ from openinference.instrumentation.langchain import LangChainInstrumentor
105
+ from phoenix.otel import register
106
+
107
+ register() # 或标准 OTel SDK;endpoint 走 OTEL_EXPORTER_OTLP_ENDPOINT
108
+ LangChainInstrumentor().instrument()
109
+ ```
110
+ </Tab>
111
+ <Tab title="自己埋的 gen_ai">
112
+ 按 [OTel GenAI 语义约定](https://opentelemetry.io/docs/specs/semconv/gen-ai/)埋:模型调用 span 名 `chat {model}`,工具 span 名 `execute_tool {tool}`,属性带 `gen_ai.operation.name`、`gen_ai.tool.name` / `gen_ai.tool.call.id`。这是 [NiceEval](https://niceeval.com/) 归一的目标语义,画出来的瀑布图最完整。
113
+ </Tab>
114
+ </Tabs>
115
+
116
+ ## span 怎么归属到轮
117
+
118
+ 并行跑多条 eval 时,同一个接收器会同时收到多条会话的 span,[NiceEval](https://niceeval.com/) 按两条路把它们归到各自的轮:
119
+
120
+ - **traceparent(推荐,并发安全)**:`send` 发请求时把 `ctx.telemetry.headers`(W3C trace context,每轮一个新 `traceparent`)spread 进请求头。应用的埋点支持 context 传播的话(标准 OTel HTTP 服务端埋点都支持),本轮 span 自动挂到 [NiceEval](https://niceeval.com/) 给的 trace 下,按 traceId 精确归属。
121
+ - **时间窗口(兜底)**:应用不传播 trace context 时,按 send 前后的时间窗归属。窗口只在串行下可靠,所以这种情况 [NiceEval](https://niceeval.com/) 会把这个 agent 的轮次串行执行并在日志里提示,不会静默混流;一旦确认 traceparent 生效,自动恢复并发。
122
+
123
+ 应用侧要及时导出:瀑布图在意的是"这一轮的 span 及时到齐",用 `SimpleSpanProcessor`(或每轮 flush)——`BatchSpanProcessor` 的缓冲会让 span 跨轮迟到,瀑布图偶发缺尾巴多半是它。
124
+
125
+ ## 已经有自己的 OTel 后端?双发,不用换
126
+
127
+ 应用多半已经把 trace 发给自己的观测后端(Langfuse / SigNoz / 生产 collector)。接 [NiceEval](https://niceeval.com/) **不需要换后端、也不需要第二套埋点**:TracerProvider 支持挂多个 SpanProcessor——同一批 span,两个出口:
128
+
129
+ ```ts
130
+ // instrumentation.ts —— 应用启动时初始化一次:一份埋点,两个出口
131
+ import { NodeTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
132
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
133
+
134
+ export const provider = new NodeTracerProvider({
135
+ spanProcessors: [
136
+ // 出口 1:你自己的后端,一直发
137
+ new SimpleSpanProcessor(new OTLPTraceExporter({ url: process.env.MY_COLLECTOR_URL })),
138
+ // 出口 2:NiceEval。线上没有这个变量时,这个出口根本不存在——线上线下同一份代码
139
+ ...(process.env.OTEL_EXPORTER_OTLP_ENDPOINT
140
+ ? [new SimpleSpanProcessor(new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }))]
141
+ : []),
142
+ ],
143
+ });
144
+ provider.register();
145
+ ```
146
+
147
+ 不方便改应用代码的话,OTel Collector 扇出——应用只发给 collector,collector 配两个 exporter(你的后端 + [NiceEval](https://niceeval.com/) 的固定端点)。代价是多运维一个组件。
148
+
149
+ ## 瀑布图画得准不准:映射到位什么,就亮起什么
150
+
151
+ [NiceEval](https://niceeval.com/) 画瀑布图前把每条 span 归一到 GenAI 语义,判定只看两样:`gen_ai.operation.name`(标准操作名)和归一出的 `kind`(语义角色)。对照表——你在 span 上给出什么,瀑布图上就亮起什么:
152
+
153
+ | 你在 span 上给出什么 | 瀑布图上亮起什么 |
154
+ |---|---|
155
+ | `gen_ai.operation.name: "chat"`(或 `text_completion` / `embeddings`) | 按**模型调用**着色分类 |
156
+ | `gen_ai.operation.name: "execute_tool"`,或属性里有 `tool_name` | 按**工具执行**着色分类 |
157
+ | `gen_ai.operation.name: "invoke_agent"` / `"create_agent"` | 按**子 agent** 着色分类 |
158
+ | 属性 `call_id` = 你 send 事件里的 `callId` | 该工具 span 自动补上真实入参/出参(`io.input` / `io.output`,从 send 返回的事件按 callId join 过来——很多埋点默认不采内容,这条路不受影响) |
159
+ | span 状态 = error | 红色标错 |
160
+ | 什么都没给(归一成 `other`) | 时间线还在;大 trace(>150 span)时按内部噪声折叠掉 |
161
+ | 其余属性 | 原样保留,view 里点开下钻 |
162
+
163
+ 应用直接按 GenAI semconv 埋点(上文「自己埋的 gen_ai」tab)时这一切自动成立;主流格式(AI SDK、LangSmith、OpenLLMetry / OpenInference)的常见形状也在通用兜底的识别范围内。
164
+
165
+ ### 私有埋点:自己写映射
166
+
167
+ 埋点是应用私有形状、通用兜底认不出时,两条路:
168
+
169
+ - **改埋点(推荐)**:在应用侧给 span 补一个 `gen_ai.operation.name` 属性——一行改动,你自己的观测后端也同样受益。
170
+ - **写 `spanMapper`**:不方便动应用时,在 agent 上声明一个纯函数,渲染前把私有 span 翻成上表的语义。`tagSpan`(把判定写回 span,原有属性只增不改)和 `heuristicTag`(通用兜底判定)都从 `niceeval/adapter` 导出:
171
+
172
+ ```ts
173
+ import { defineAgent, tagSpan, heuristicTag } from "niceeval/adapter";
174
+ import type { TraceSpan } from "niceeval";
175
+
176
+ function mapMySpans(spans: TraceSpan[]): TraceSpan[] {
177
+ return spans.map((span) => {
178
+ // 私有命名 → 标准语义:op 写进 gen_ai.operation.name,kind 定着色
179
+ if (span.name === "my.llm.request") return tagSpan(span, { op: "chat", kind: "model" });
180
+ if (span.name === "my.tool.exec") {
181
+ // 顺手把私有的调用 id 抄成 call_id,工具 I/O 就能从 send 事件里 join 过来
182
+ const attributes = { ...span.attributes, call_id: String(span.attributes?.["my.callId"] ?? "") };
183
+ return tagSpan({ ...span, attributes }, { op: "execute_tool", kind: "tool" });
184
+ }
185
+ return tagSpan(span, heuristicTag(span)); // 其余交给通用兜底
186
+ });
187
+ }
188
+
189
+ export default defineAgent({
190
+ name: "my-bot",
191
+ spanMapper: mapMySpans,
192
+ async send(input, ctx) { /* 同上文接法 */ },
193
+ });
194
+ ```
195
+
196
+ 内置的 `mapCodexSpans`(同样从 `niceeval/adapter` 导出)就是一个现成的 spanMapper 实现,可当参考。spanMapper 是纯观测代码:写错了最多瀑布图着色不对,断言不受任何影响。
197
+
198
+ ## 边界
199
+
200
+ - **断言相关的一切都在 send**。想断工具调用,把它映射进 `events`(官方转换器或手写映射,见[写 send](/zh/guides/write-send));想断 usage,`send` 返回里带上。不存在"span 里有、events 里没有,于是断言看 span"的路径。
201
+ - **多轮会话、HITL 不归 span 管**。span 没有"等人输入"语义,会话续接也是应用协议的事——这两样照常在 `send` 里做(会话续接见[写 send](/zh/guides/write-send),HITL 概念见 [HITL](/zh/concepts/hitl))。
202
+ - **收不到 span 会有提示**。整轮 0 span 通常是端点没接上(env 没注入、服务没重启),[NiceEval](https://niceeval.com/) 会在日志里提示;瀑布图为空,断言照常判。
203
+
204
+ ## 相关阅读
205
+
206
+ - [接入你的 agent](/zh/guides/connect-your-agent) —— 断言从哪来:send 的事件映射。
207
+ - [写 send](/zh/guides/write-send) —— 手写 adapter 的完整教程,第六步就是本页的 adapter 侧接法。
208
+ - [事件流参考](/zh/reference/events) —— 断言消费的事件长什么样。