> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# OTel 接入

> 把应用已有的 OTel span 发送给 NiceEval，在 niceeval view 中查看每轮调用瀑布图；评估用例断言仍以 Send 返回的事件和用量为准。

OTel 接入**不改变断言的数据来源**。`t.calledTool`、`t.maxTokens` 和耗时判定都读取 Adapter 在 `send` 中返回的 `Turn`（`events` 与 `usage`），见[接入你的 Agent](/docs/zh/tutorials/connect-your-agent)。

OTel span 用于生成 **`niceeval view` 中的调用瀑布图**。瀑布图按轮显示模型调用、工具执行、耗时和 token，帮助定位评估用例失败或轮次变慢的具体步骤。

如果你的应用已经在发 OTel trace——AI SDK 的 telemetry、LangGraph 的 LangSmith 导出、OpenLLMetry / OpenInference 自动埋点，或自己按 GenAI 语义埋的点——那瀑布图的数据你已经在生产了：让应用把 span 也发给 [NiceEval](https://niceeval.com/) 一份即可，应用代码一行不改，仍是无侵入（见 [Tier](/docs/zh/explanation/tier)）。

## 原理（一段话）

[NiceEval](https://niceeval.com/) 运行时启动本机 OTLP 接收器。应用发送的 span 会归属到对应 `send` 轮次，归一成 GenAI 语义后写入 `EvalResult.trace`，并由 `npx niceeval view` 显示为瀑布图。**span 只用于瀑布图，不进入事件流，也不参与断言**。埋点缺失、span 迟到或丢批只影响瀑布图完整性，不影响判定。

## 接法

**1. adapter 侧**——`send` 照常写（事件映射还是你的映射），只多一行：把本轮的 `traceparent` 随请求带过去：

```ts theme={null}
// agents/support-bot.ts
import { defineAgent } from "niceeval/adapter";

export default defineAgent({
  name: "support-bot",
  async send(input, ctx) {
    // 示例地址：换成你自己 agent 的真实端点
    const r = await fetch("http://localhost:5188/chat", {
      method: "POST",
      // traceparent 随请求带过去:本轮 span 挂到 NiceEval 的 trace 下,并发下归属才精确
      headers: { "content-type": "application/json", ...ctx.telemetry?.headers },
      body: JSON.stringify({ message: input.text, model: ctx.model }),
      signal: ctx.signal,
    });
    const body = await r.json();
    return {
      status: r.ok ? "completed" : "failed",
      events: mapToEvents(body),   // 断言的依据在这里,和没接 OTel 时一样
    };
  },
});
```

内置件不用做这件事：`uiMessageStreamAgent` 总会自动把 `ctx.telemetry.headers` 并入请求头。

**2. 向应用提供端点。** [NiceEval](https://niceeval.com/) 的接收端点属于**启动期配置，不从 `send` 传递**。标准 OTel SDK 只在进程启动时读取一次 `OTEL_*` 环境变量。按部署形态选择配置方式：

* **你自己长驻的服务（最常见）**：用**固定端口模式**，在 `niceeval.config.ts` 里钉住接收端口——写了这个配置就等于打开了 OTel 接入：

  ```ts theme={null}
  // niceeval.config.ts
  export default defineConfig({
    telemetry: { port: 4318 },   // 接收器固定监听 http://localhost:4318/v1/traces
  });
  ```

  服务启动时一次性配 `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces`，之后跑多少次评估用例都不用改。代价：端口共享意味着同一台机器同时只能跑一个 niceeval 进程；OTel Collector 扇出场景同理指向这个固定端点。`port` 被其它进程占用时 [NiceEval](https://niceeval.com/) 会直接报错提示换一个空闲端口，不会静默失败。

  报给应用的接收端 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 接收的唯一入口，不读环境变量。

* **子进程 / 由 [NiceEval](https://niceeval.com/) 拉起的进程**（CLI 型 agent）：什么都不用做。`ctx.telemetry.env`（标准 `OTEL_*` 环境变量，ready-to-spread）注入进程环境，每次 run 是新进程、读到新端点。

**3. 应用侧**——按你的埋点生态各自几行配置：

<Tabs>
  <Tab title="AI SDK">
    推荐官方 OTel 集成（`@ai-sdk/otel`，产标准 GenAI 语义）；老的 `experimental_telemetry`（`ai.*`）也能画：

    ```ts theme={null}
    import { generateText } from "ai";

    const result = await generateText({
      model, tools, messages,
      experimental_telemetry: { isEnabled: true },
    });
    ```

    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)。
  </Tab>

  <Tab title="LangGraph / LangChain">
    零依赖路线，三个环境变量：

    ```bash theme={null}
    LANGSMITH_TRACING=true \
    LANGSMITH_OTEL_ENABLED=true \
    LANGSMITH_OTEL_ONLY=true \
    OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces \
    node server.js   # 端点值按「向应用提供端点」一节选择：注入的 env 或固定端口
    ```

    `LANGSMITH_OTEL_ONLY` 表示只发 OTLP、不发 LangSmith 云端；需要双发时移除该变量。**Python** 版 `langsmith` SDK 会在 import 时自动注册 OTel hook。**JS** 版（`langsmith@0.7.x`）还需要显式调用 `langsmith/experimental/otel/setup` 导出的 `initializeOTEL()`，否则只输出警告，不产生 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)。
  </Tab>

  <Tab title="OpenLLMetry">
    ```ts theme={null}
    import * as traceloop from "@traceloop/node-server-sdk";

    traceloop.initialize({ disableBatch: true });
    // endpoint 用标准 OTEL_EXPORTER_OTLP_ENDPOINT 环境变量
    ```
  </Tab>

  <Tab title="OpenInference">
    ```python theme={null}
    from openinference.instrumentation.langchain import LangChainInstrumentor
    from phoenix.otel import register

    register()  # 或标准 OTel SDK;endpoint 走 OTEL_EXPORTER_OTLP_ENDPOINT
    LangChainInstrumentor().instrument()
    ```
  </Tab>

  <Tab title="自己埋的 gen_ai">
    按 [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/) 归一的目标语义，画出来的瀑布图最完整。
  </Tab>
</Tabs>

## 把 span 归属到 Turn

并行跑多条评估用例时，同一个接收器会同时收到多条会话的 span，[NiceEval](https://niceeval.com/) 按两条路把它们归到各自的轮：

* **traceparent（推荐，并发安全）**：`send` 发请求时把 `ctx.telemetry.headers`（W3C trace context，每轮一个新 `traceparent`）spread 进请求头。应用的埋点支持 context 传播的话（标准 OTel HTTP 服务端埋点都支持），本轮 span 自动挂到 [NiceEval](https://niceeval.com/) 给的 trace 下，按 traceId 精确归属。
* **时间窗口（兜底）**：应用不传播 trace context 时，按 send 前后的时间窗归属。窗口只在串行下可靠，所以这种情况 [NiceEval](https://niceeval.com/) 会把这个 agent 的轮次串行执行并在日志里提示，不会静默混流；一旦确认 traceparent 生效，自动恢复并发。

应用侧要及时导出：瀑布图在意的是"这一轮的 span 及时到齐"，用 `SimpleSpanProcessor`（或每轮 flush）——`BatchSpanProcessor` 的缓冲会让 span 跨轮迟到，瀑布图偶发缺尾巴多半是它。

## 保留现有 OTel 后端并双发

应用多半已经把 trace 发给自己的观测后端（Langfuse / SigNoz / 生产 collector）。接 [NiceEval](https://niceeval.com/) **不需要换后端、也不需要第二套埋点**：TracerProvider 支持挂多个 SpanProcessor——同一批 span，两个出口：

```ts theme={null}
// instrumentation.ts —— 应用启动时初始化一次:一份埋点,两个出口
import { NodeTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

export const provider = new NodeTracerProvider({
  spanProcessors: [
    // 出口 1:你自己的后端,一直发
    new SimpleSpanProcessor(new OTLPTraceExporter({ url: process.env.MY_COLLECTOR_URL })),
    // 出口 2:NiceEval。线上没有这个变量时,这个出口根本不存在——线上线下同一份代码
    ...(process.env.OTEL_EXPORTER_OTLP_ENDPOINT
      ? [new SimpleSpanProcessor(new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }))]
      : []),
  ],
});
provider.register();
```

不方便改应用代码的话，OTel Collector 扇出——应用只发给 collector，collector 配两个 exporter（你的后端 + [NiceEval](https://niceeval.com/) 的固定端点）。代价是多运维一个组件。

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

[NiceEval](https://niceeval.com/) 在绘制瀑布图前把每条 span 归一到 GenAI 语义。映射读取 `gen_ai.operation.name`（标准操作名）和归一后的 `kind`（语义角色）：

| Span 语义                                                             | 瀑布图内容                                                                                           |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `gen_ai.operation.name: "chat"`（或 `text_completion` / `embeddings`） | 按**模型调用**着色分类                                                                                   |
| `gen_ai.operation.name: "execute_tool"`，或属性里有 `tool_name`           | 按**工具执行**着色分类                                                                                   |
| `gen_ai.operation.name: "invoke_agent"` / `"create_agent"`          | 按**子 agent** 着色分类                                                                               |
| 属性 `call_id` = 你 send 事件里的 `callId`                                 | 该工具 span 自动补上真实入参/出参（`io.input` / `io.output`，从 send 返回的事件按 callId join 过来——很多埋点默认不采内容，这条路不受影响） |
| span 状态 = error                                                     | 红色标错                                                                                            |
| 什么都没给（归一成 `other`）                                                  | 时间线还在；大 trace（>150 span）时按内部噪声折叠掉                                                               |
| 其余属性                                                                | 原样保留，view 里点开下钻                                                                                 |

应用直接按 GenAI semconv 埋点（上文「自己埋的 gen\_ai」tab）时这一切自动成立；主流格式（AI SDK、LangSmith、OpenLLMetry / OpenInference）的常见形状也在通用兜底的识别范围内。

### 私有埋点：自己写映射

埋点是应用私有形状、通用兜底认不出时，两条路：

* **改埋点（推荐）**：在应用侧给 span 补一个 `gen_ai.operation.name` 属性——一行改动，你自己的观测后端也同样受益。
* **写 `spanMapper`**：不方便动应用时，在 agent 上声明一个纯函数，渲染前把私有 span 翻成上表的语义。`tagSpan`（把判定写回 span，原有属性只增不改）和 `heuristicTag`（通用兜底判定）都从 `niceeval/adapter` 导出：

```ts theme={null}
import { defineAgent, tagSpan, heuristicTag } from "niceeval/adapter";
import type { TraceSpan } from "niceeval";

function mapMySpans(spans: TraceSpan[]): TraceSpan[] {
  return spans.map((span) => {
    // 私有命名 → 标准语义:op 写进 gen_ai.operation.name,kind 定着色
    if (span.name === "my.llm.request") return tagSpan(span, { op: "chat", kind: "model" });
    if (span.name === "my.tool.exec") {
      // 把私有调用 id 映射成 call_id，使工具 I/O 能与 send 事件关联
      const attributes = { ...span.attributes, call_id: String(span.attributes?.["my.callId"] ?? "") };
      return tagSpan({ ...span, attributes }, { op: "execute_tool", kind: "tool" });
    }
    return tagSpan(span, heuristicTag(span));   // 其余交给通用兜底
  });
}

export default defineAgent({
  name: "my-agent",
  spanMapper: mapMySpans,
  async send(input, ctx) { /* 同上文接法 */ },
});
```

`niceeval/adapter` 导出的 `mapCodexSpans` 是一个可参考的 `spanMapper` 实现。`spanMapper` 只影响观测展示；映射错误会导致瀑布图分类或着色不准确，不影响断言。

## 边界

* **断言数据全部来自 `send`**。工具调用需要映射进 `events`（使用内置转换器或手写映射，见[编写 Send](/docs/zh/tutorials/write-send)）；用量需要包含在 `send` 返回值中。Span 中存在但 `events` 中缺失的数据不会参与断言。
* **多轮会话、HITL 不归 span 管**。span 没有"等人输入"语义，会话续接也是应用协议的事——这两样照常在 `send` 里做（会话续接见[写 send](/docs/zh/tutorials/write-send)，HITL 概念见 [HITL](/docs/zh/explanation/hitl)）。
* **收不到 span 会有提示**。整个 run 0 span 通常是端点没接上（env 没注入、服务没重启），[NiceEval](https://niceeval.com/) 会在日志里提示；瀑布图为空，断言照常判。

## 相关阅读

* [接入你的 Agent](/docs/zh/tutorials/connect-your-agent) —— `send` 事件映射与断言的数据来源。
* [编写 Send](/docs/zh/tutorials/write-send) —— 手写 Adapter 的完整教程，第六步是本页的 Adapter 侧配置。
* [事件流参考](/docs/zh/reference/events) —— 断言读取的事件结构。
