跳转到主要内容
一条评估用例跑了 40 秒,你想知道时间花在哪:是模型想得慢,还是某个工具调用卡住了。终端只告诉你这一轮总共花了多久。 调用瀑布图能回答这个问题。它在 niceeval view 里按轮列出每一次模型调用和工具执行,以及各自的耗时和 token。 如果你的应用已经在发 OTel trace,这份数据你其实已经有了。AI SDK 的 telemetry、LangGraph 导出到 LangSmith 的 trace、OpenLLMetry 或 OpenInference 的自动埋点,以及你自己按 GenAI 语义埋的点都算。让应用把 span 也发给 NiceEval 一份就行,应用的业务代码不用改,仍属于无侵入接入,见 Tier。 瀑布图只用来看,不参与判分。t.calledTool、t.maxTokens 这些断言仍然读取 Adapter 在 send 里返回的事件和用量。span 缺失或迟到只会让瀑布图不完整,不会改变通过与否。

span 从哪来、到哪去

运行评估时,NiceEval 在本机启动一个 OTLP 接收器。应用发来的 span 按 send 轮次分好,统一成 GenAI 语义,和这个 Attempt 的结果一起保存,niceeval view 再据此画瀑布图。span 不参与断言,不影响 Agent 的行为、执行错误或判定。埋点缺失、span 迟到或丢了一批,只会让瀑布图不完整。

把 span 发给 NiceEval

接入分三处改动:Adapter 带上每轮的 trace 头,应用知道往哪发,应用的埋点打开导出。 1. Adapter 带上 traceparent。 send 照常写,事件映射不变,只多一行:把这一轮的 traceparent 随请求发给应用。
用 uiMessageStreamAgent 时不用写这一行,它会自动把 ctx.telemetry.headers 加进请求头。 2. 告诉应用往哪发。 接收地址在应用启动时配置,不经过 send。标准 OTel SDK 只在进程启动时读一次 OTEL_* 环境变量。按应用的运行方式选择:
  • 你自己启动、一直在跑的服务(最常见):在 niceeval.config.ts 里固定接收端口。写了这个配置,就打开了 OTel 接入:
    服务启动时设一次 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces,之后跑多少次评估用例都不用改。用 OTel Collector 转发时,也指向这个地址。端口是固定的,所以同一台机器同时只能跑一个 niceeval 进程。端口被其它进程占用时,NiceEval 会直接报错,提示你换一个空闲端口。 告诉应用的接收地址默认是 127.0.0.1。只有应用要经过隧道或其它路由才能访问到运行 NiceEval 的机器时,才用 host 写出那个地址,例如 telemetry: { host: "otel.internal", port: 4318 }。Docker Sandbox Agent 默认在同一个 Sandbox 内启动接收器,不需要容器连回宿主机。OTLP 接收只通过这两个字段配置,不读环境变量。
  • 由 NiceEval 启动的子进程(CLI 型 Agent):什么都不用做。把 ctx.telemetry.env(标准 OTEL_* 环境变量,可直接展开)放进进程环境即可。每次运行都是新进程,读到的就是这次的地址。
3. 打开应用埋点的导出。 按你用的埋点库,各加几行配置:
推荐官方 OTel 集成(@ai-sdk/otel,产标准 GenAI 语义)。老的 experimental_telemetry(ai.*)也能画:
exporter 走标准 OTel Node SDK,endpoint 指向 NiceEval(注入的 env 或固定端口)。可跑示例:应用侧埋点见 examples/zh/origin/ai-sdk-v7(src/backend/otel.ts,官方 @ai-sdk/otel),接入后的完整评测项目见 examples/zh/tier1/ai-sdk-v7。

把 span 归属到 Turn

并行跑多条评估用例时,同一个接收器会同时收到多条会话的 span。NiceEval 用两种方式把它们分到各自的轮次:
  • 按 traceparent(推荐,并发也准):send 发请求时把 ctx.telemetry.headers 展开进请求头,每轮带一个新的 traceparent。应用埋点支持 trace context 传播时(标准 OTel HTTP 服务端埋点都支持),这一轮的 span 会挂到 NiceEval 给的 trace 下,按 traceId 准确对上。
  • 按时间(备用):应用不传播 trace context 时,按 send 开始和结束的时间判断。这种方式只在逐个执行时可靠,所以 NiceEval 会让这个 Agent 的轮次逐个执行,并在日志里提示,不会把不同会话的 span 混在一起。确认 traceparent 生效后,自动恢复并发。
应用要及时导出 span。用 SimpleSpanProcessor,或每轮结束时 flush。BatchSpanProcessor 会缓冲一阵再发,span 可能晚到下一轮,瀑布图偶尔缺尾巴多半是这个原因。

保留现有 OTel 后端并双发

应用多半已经把 trace 发给自己的观测后端,例如 Langfuse、SigNoz 或生产环境的 collector。接 NiceEval 不用换后端,也不用第二套埋点。TracerProvider 可以挂多个 SpanProcessor,同一批 span 发往两处:
不方便改应用代码时,用 OTel Collector 转发:应用只发给 collector,collector 配两个 exporter,一个指向你的后端,一个指向 NiceEval 的固定地址。代价是多维护一个组件。

用语义映射控制瀑布图内容

瀑布图里一条 span 显示成模型调用、工具执行还是子 Agent,取决于它带了什么属性。NiceEval 画图前把每条 span 统一到 GenAI 语义,主要看 gen_ai.operation.name(标准操作名)和统一后的 kind: 应用直接按 GenAI 语义约定埋点(上文「自己埋的 gen_ai」)时,这些都自动成立。AI SDK、LangSmith、OpenLLMetry / OpenInference 的常见 span 也能被自动识别。

私有埋点:自己写映射

瀑布图里全是灰色的 other,通常是应用用了自己的 span 命名,自动识别认不出。有两种办法:
  • 改埋点(推荐):在应用里给 span 加一个 gen_ai.operation.name 属性。改动只有一行,你自己的观测后端也能受益。
  • 写 spanMapper:不方便改应用时,在 Agent 上声明一个纯函数,画图前把自定义 span 翻译成上表的语义。tagSpan 把分类写回 span,只添加属性、不改原有属性;heuristicTag 是自动识别用的同一套判断。两者都从 niceeval/adapter 导出:
niceeval/adapter 导出的 mapCodexSpans 是一个可以参考的 spanMapper 实现。spanMapper 只影响瀑布图。映射写错只会让分类或颜色不准,不影响断言。

检查结果

跑完一次评估,用 pnpm exec niceeval view 打开浏览器,进入某个 Attempt 查看每一轮的瀑布图。终端里用 pnpm exec niceeval show @<locator> 查看这个 Attempt 的结果和日志提示。
  • 瀑布图为空:整次运行一条 span 都没收到,通常是应用没连上接收地址,例如环境变量没设上,或改了配置后服务没重启。NiceEval 会在日志里提示,断言照常判定。
  • 瀑布图偶尔缺尾巴:先查应用是否用了 BatchSpanProcessor,见上文「把 span 归属到 Turn」。
  • span 都显示成 other:见上文「私有埋点:自己写映射」。

OTel 接入不负责的事

  • 断言的数据全部来自 send。工具调用要映射进 events(用内置转换器或手写映射,见编写 Send),用量要放进 send 的返回值。只出现在 span 里、events 里没有的数据,不参与断言。
  • 多轮会话和 HITL 照常在 send 里做。span 不表达“等人输入”,会话续接也由应用接口决定(会话续接见编写 Send,HITL 见 HITL)。

相关阅读

  • 接入你的 Agent —— send 事件映射与断言的数据来源。
  • 编写 Send —— 手写 Adapter 的完整教程,第六步是本页的 Adapter 侧配置。
  • 事件流参考 —— 断言读取的事件结构。