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_*环境变量,可直接展开)放进进程环境即可。每次运行都是新进程,读到的就是这次的地址。
- AI SDK
- LangGraph / LangChain
- OpenLLMetry
- OpenInference
- 自己埋的 gen_ai
推荐官方 OTel 集成(exporter 走标准 OTel Node SDK,endpoint 指向 NiceEval(注入的 env 或固定端口)。可跑示例:应用侧埋点见
@ai-sdk/otel,产标准 GenAI 语义)。老的 experimental_telemetry(ai.*)也能画: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生效后,自动恢复并发。
SimpleSpanProcessor,或每轮结束时 flush。BatchSpanProcessor 会缓冲一阵再发,span 可能晚到下一轮,瀑布图偶尔缺尾巴多半是这个原因。
保留现有 OTel 后端并双发
应用多半已经把 trace 发给自己的观测后端,例如 Langfuse、SigNoz 或生产环境的 collector。接 NiceEval 不用换后端,也不用第二套埋点。TracerProvider 可以挂多个 SpanProcessor,同一批 span 发往两处:用语义映射控制瀑布图内容
瀑布图里一条 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 侧配置。
- 事件流参考 —— 断言读取的事件结构。