ATIF 与 OpenTelemetry:Agent 轨迹的两种打开方式
想复现一次 Agent 的失败现场,你会先翻什么?终端里滚走的输出、Agent 自己那份格式私有 .jsonl 会话文件、还是某个平台后台里的 trace 列表?在评测场景里这个问题更尖锐:换一个 Agent 跑同一批任务,日志格式就换一套方言,可视化、训练管线、故障排查全部要重写解析器。
Harbor Framework——一个在容器化环境里评测和优化 Agent 与模型的开源框架——给出的答案是两件套:一份标准化的轨迹文件 ATIF,加一条通往 OpenTelemetry 的官方转换管道。一个负责"完整记录",一个负责"接入生态"。这篇文章把这两块拼图拆开看。
本文大纲:
- ATIF 是什么:给 Agent 轨迹立的 JSON 标准
- 走读 trajectory.json:从字段到版本演进
- 原生轨迹 vs ATIF:可移植性的分界线
- atif2otel:把轨迹变成 OpenTelemetry spans
- 生态汇流:NVIDIA 的三步棋
- 分工,而不是取代
ATIF 是什么:给 Agent 轨迹立的 JSON 标准
ATIF(Agent Trajectory Interchange Format)是 Harbor 的 RFC 0001 定义的一套 JSON 规范,用来记录一个 Agent 的完整交互历史:用户消息、Agent 的推理、工具调用、环境观察结果,以及每一步和整趟的 metrics。
它的动机写在 RFC 开头:现有的 Agent 日志各说各话——对话式日志、显式动作序列(如 MiniSweAgent 的做法)、可回放的数据结构(如 OpenHands 的做法)——每一条下游链路(查看器、数据集、SFT/RL 训练管线)都得为每种 Agent 写一个解析器。ATIF 想要的是一个稳定表示,让采集下来的数据立刻可用于调试、可视化、监督微调和强化学习。
在 Harbor 的运行结果里,这份文件通常就叫 trajectory.json,落在试验结果的 agent/ 目录下。声明了 capabilities.atif = true 的 Agent 会自动往自己的日志目录写它,Harbor 用它做两件事:渲染——本地结果查看器的 Trajectory 标签页直接读它展示每一步;加载——用它去 seed 一个新会话。
走读 trajectory.json:从字段到版本演进
一份最小但完整的 ATIF 轨迹长这样(当前格式 ATIF-v1.7):
{
"schema_version": "ATIF-v1.7",
"session_id": "session-123",
"agent": {
"name": "my-agent",
"version": "1.0.0",
"model_name": "openai/gpt-5.6-sol"
},
"steps": [
{
"step_id": 1,
"source": "user",
"message": "Create hello.txt."
},
{
"step_id": 2,
"source": "agent",
"message": "I'll create it.",
"tool_calls": [
{
"tool_call_id": "call-1",
"function_name": "write_file",
"arguments": {"path": "hello.txt", "content": "Hello"}
}
],
"observation": {
"results": [
{"source_call_id": "call-1", "content": "File created"}
]
}
}
]
}根级字段里,agent 描述产生轨迹的系统(名称、版本、默认模型,可选带上完整的 tool_definitions);steps 是按序交互,step_id 从 1 开始严格连续;Agent 步骤里的 tool_calls 与 observation.results 通过 source_call_id 配对;可选的 final_metrics 汇总全程的 token、成本和步数;subagent_trajectories 则能把子 Agent 的完整轨迹嵌进同一个文件。
几个版本演进的节点很能说明这套格式的野心:
| 版本 | 增加了什么 | 为谁服务 |
|---|---|---|
| v1.3 / v1.4 | completion_token_ids / prompt_token_ids |
RL 训练:存下真实送进 LLM 的 token,避免重分词漂移 |
| v1.5 | tool_definitions |
SFT:让训练管线拿到完整的函数签名与文档 |
| v1.6 | 图片多模态内容 | 多模态轨迹 |
| v1.7 | 子轨迹嵌入、trajectory_id、llm_call_count |
多 Agent 工作流单文件存档 |
| v1.8(RFC) | 音频 ContentPart | 语音输入型轨迹 |
注意 v1.7 的子 Agent 语义是 NVIDIA 工程师贡献的——这个伏笔在第五节会再次出现。Harbor 为格式配了 Pydantic 模型(harbor.models.trajectories)和一个校验器,检查 schema、step ID 连续性、tool-call 引用、时间戳甚至引用的本地图片文件:
uv run python -m harbor.utils.trajectory_validator path/to/trajectory.json原生轨迹 vs ATIF:可移植性的分界线
Harbor 支持两种轨迹加载,分界线很清晰:
- 原生轨迹是 Agent 私有的会话文件——
claude-code的在agent/sessions/projects/-app/<session-id>.jsonl,codex的在按日期组织的rollout-*.jsonl。加载无损,但只能喂回同一个 Agent。 - ATIF 轨迹是可移植的中间格式。加载时 Harbor 把它转换成目标 Agent 的原生格式,保留消息、工具调用和工具结果——意味着 claude-code 跑出来的轨迹可以拿来 seed 一次 codex 会话。转换不承诺无损,Agent 特有的细节、系统消息和非文本内容可能被省略。
两种能力分开声明:写 ATIF 是 capabilities.atif,加载原生和加载 ATIF 各有独立的 capability 开关。目前 claude-code 和 codex 已同时支持两种加载。这套设计的实际价值在于:评测基建终于可以把"轨迹"当作一等公民来搬运、对比、复用,而不是被某个 Agent 的私有格式绑死。
atif2otel:把轨迹变成 OpenTelemetry spans
到这里为止,ATIF 解决的是记录与交换。但轨迹文件躺在磁盘上,终究不在观测生态的查询路径里。Harbor 的解法是官方插件之一:atif2otel(另一个官方插件是同步 LangSmith 的),把每个 Agent 的 ATIF trajectory 转换成 OpenTelemetry spans——model 调用、tool 调用、subagent 各自成为 span,父子关系天然对应轨迹的嵌套结构。
uv tool install --with harbor-atif2otel harbor
# 流式导出:OTLP endpoint 直传 MLflow
export OPENAI_API_KEY="..."
export MLFLOW_TRACKING_TOKEN="..."
harbor run \
-t hello-world/hello-world \
-a codex -m openai/gpt-5.6-sol \
--plugin atif2otel \
--pk endpoint=https://mlflow.example.com \
--pk experiment_name=harbor-evals它有两种输出模式,由 mode=auto 自动决定:设了 endpoint(即 OTEL_EXPORTER_OTLP_ENDPOINT)就流式上报,设了 output_dir 就把 OTLP JSON 写成文件,两个都设就两条路都走。内置的上传器把每次试验导到一个 MLflow server——而 MLflow 的 tracing 功能本身就是用 OpenTelemetry 构建的,等于 OTLP 一出,Collector 后面的所有观测后端(Jaeger、Tempo、各类商业平台)理论上都能接。
这一步转换的意义值得说透:轨迹文档面向"人和训练管线",优化的是完整性与可回放性;span 流面向"查询与关联",优化的是实时性与生态兼容。atif2otel 没有让谁取代谁,而是在两者之间架了一座单向桥。
flowchart LR
A["Agent trial in container"] --> B["agent/trajectory.json (ATIF)"]
A --> C["Native session .jsonl"]
B --> D["Results viewer
Trajectory tab"]
B --> E["Cross-agent seeding
claude-code to codex"]
B --> F["SFT / RL datasets
token_ids, tool_definitions"]
B --> G["atif2otel plugin"]
G --> H["OTel spans
model / tool / subagent calls"]
H --> I["OTLP endpoint"]
I --> J["MLflow / Collector / trace backends"]
C -->|"lossless, same agent only"| K["Resume in same agent"]生态汇流:NVIDIA 的三步棋
ATIF 与 OTel 的合流不是 Harbor 单方面的设想,NVIDIA 已经用三步把它变成了现实:
- 贡献格式演进。ATIF v1.7 的子轨迹嵌入、
trajectory_id体系由 NVIDIA 工程师 Bryan Bednarski 和 Anuradha Karuppiah 贡献——多 Agent 场景正是推理服务厂商最关心的地方。 - 标识符对齐。NVIDIA 的推理服务框架 Dynamo 不输出 ATIF,它输出 serving 导向的
dynamo.agent.trace.v1(请求时延、token、cache 命中、队列深度、worker 位置)。但它的身份字段刻意与 ATIF 同名——通过在每次 LLM 请求里附带nvext.agent_context(session_id/trajectory_id/parent_trajectory_id),serving 侧 trace 与 harness 侧的 ATIF 文件"无需改名即可 join"。官方文档的原话是 "the two formats are complementary and join cleanly because identifier names match"。 - 双格式摄入。NVIDIA NeMo Platform 的 Intake 服务同时接受两条摄入路径: instrumented Agent 通过 OpenTelemetry / OpenInference exporter 发来的 OTLP spans,以及直接 POST 的 ATIF 轨迹(
/ingest/atif端点),两者被归一化成同一套可查询的 spans 和 traces。
三步合起来,图景已经很完整:一端是完整轨迹文档,一端是运行时遥测流,两边开始共享同一套身份体系。
分工,而不是取代
把两条线放在一起看,各自的定位其实非常清楚:
- OpenTelemetry 回答"此刻系统里发生了什么"——实时、低开销、面向运维和告警,span 是事件的切片。
- ATIF 回答"这个 Agent 完整经历了什么"——完整、可回放、面向评测与训练。SFT 需要的
tool_definitions、RL 需要的prompt_token_ids/completion_token_ids、多 Agent 需要的subagent_trajectories,这些信息在 span 模型里根本没有位置。
观测性领域的历史正在重演:先是方言林立,然后出现翻译层(atif2otel 这样的桥),最后身份对齐、双格式汇入同一个后端。当年 OpenTelemetry 用一套 API/SDK 统一了 metrics、logs、traces 的采集端,如今 Agent 生态在轨迹这一层走的是同一条路——只是这一次,"完整回放"和"实时观测"两种需求从一开始就注定要用两种载体。
如果你在搭自己的评测或观测链路,比较务实的路径是:评测跑在 Harbor 里,轨迹以 ATIF-v1.7 落盘(让 Pydantic 模型和校验器守住 schema 底线);要看板就挂 atif2otel 或 LangSmith 插件;自研 Agent 与 serving 层对接时,把 session_id / trajectory_id 这套命名当公共语言——将来无论往哪个后端汇,都不用再改名。
作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
本文首发于 AI人工智能时代,转载请注明出处。