ATIF:Agent 可观测的轨迹标准
为什么 agent 可观测需要"轨迹"
传统服务的可观测靠日志、指标和链路追踪,排查问题时关心的是"哪个接口慢了、哪里报错了"。Agent 不一样:它的行为由模型在运行时决定,同样的输入可能走出完全不同的路径。要回答"它为什么这么做",必须看到完整的决策过程:每一轮看到了什么、想了什么、调了哪个工具、工具返回了什么、花了多少 token。
这条完整的过程就是轨迹(trajectory)。问题在于,每个 agent 框架都有自己的日志格式:OpenHands 是事件文件,Claude Code 是 JSONL 会话日志,Gemini CLI 又是另一套。可视化工具、评测系统、训练流水线想消费这些数据,就得给每种 agent 单独写解析器。
ATIF(Agent Trajectory Interchange Format)就是为了解决这个碎片化问题而提出的统一轨迹格式。
ATIF 是什么
ATIF 是一份基于 JSON 的规范,用来记录 LLM agent 的完整交互历史:消息、推理、工具调用、观察结果和指标。它由 agent 评测框架 Harbor(Terminal-Bench 背后的框架)以 RFC 0001 的形式提出,并作为 Harbor 的标准日志格式。
RFC 对"轨迹"的定义是:用户与 agent 之间的一串交互,包括 agent 的内部推理、动作和观察,覆盖单轮任务和多轮对话。它的设计目标是把三类需求统一起来:
- 对话日志(谁说了什么)
- 显式动作序列(MiniSweAgent 那种逐条命令的记录)
- 可回放的数据结构(OpenHands 那种事件流)
统一之后,同一份数据可以直接用于调试、可视化、监督微调(SFT)和强化学习(RL)。
版本演进
| 版本 | 主要变化 |
|---|---|
| v1.8 | 支持音频内容(按路径引用音频文件) |
| v1.7 | 子 agent 轨迹内嵌、trajectory_id、llm_call_count、上下文管理约定;由 NVIDIA 贡献 |
| v1.6 | 支持图片等多模态内容 |
| v1.5 | agent.tool_definitions,方便构造 SFT 数据 |
| v1.4 | prompt_token_ids |
| v1.3 | completion_token_ids,避免 RL 训练时重新分词造成偏差 |
| v1.2 | system step 也可以带 observation;明确 prompt_tokens 包含缓存命中部分 |
| v1.1 | 根级 extra 字段 |
| v1.0 | 初版 |
RFC 文档已更新到 v1.8,Harbor 文档中写的当前格式是 v1.7,可接受 v1.0 到 v1.7。实际接入时以所用工具链支持的版本为准。
数据结构
一条 ATIF 轨迹是一个根对象。它的核心是按顺序排列的 steps 数组,外面包着 agent 元信息和汇总指标。
根对象
| 字段 | 必填 | 说明 |
|---|---|---|
schema_version | 是 | 例如 "ATIF-v1.7" |
agent | 是 | name、version 必填;可选 model_name、tool_definitions(OpenAI function calling 格式) |
steps | 是 | 有序的交互步骤 |
session_id | 否 | 运行级 ID,父轨迹和子轨迹、续写轨迹可以共用 |
trajectory_id | 否 | 文档级唯一 ID;内嵌的子 agent 轨迹必须有 |
final_metrics | 否 | 整条轨迹的 token、成本、步数汇总 |
subagent_trajectories | 否 | 内嵌的子 agent 轨迹,每条都是完整合法的 ATIF |
continued_trajectory_ref | 否 | 上下文压缩导致轨迹拆成多个文件时,指向续写文件 |
notes / extra | 否 | 备注和自定义元数据 |
Step:一步交互
每个 step 的 source 是 system、user、agent 三者之一。一个 agent step 表示一次完整的回合:模型推理、执行动作、拿到观察结果。
message:消息正文。system step 放系统提示,user step 放用户输入,agent step 放模型回复。reasoning_content:模型的显式推理过程,只出现在 agent step。tool_calls:本步发起的工具调用,一次输出可以包含多个。每个调用有tool_call_id、function_name、arguments。observation.results:工具返回结果,通过source_call_id和对应的tool_call_id关联;也可以通过subagent_trajectory_ref指向一条子 agent 轨迹。metrics:本步的 LLM 指标。llm_call_count、is_copied_context:分别标记本步包含几次推理、是否是从上一段轨迹复制过来的上下文,下文细讲。
Metrics:token 与成本
| 字段 | 含义 |
|---|---|
prompt_tokens | 本步输入 token 总数,包含缓存命中部分 |
cached_tokens | prompt_tokens 中命中缓存的子集 |
completion_tokens | 输出 token 数,含推理和工具调用部分 |
cost_usd | 按执行当时价格计算的实际花费 |
prompt_token_ids / completion_token_ids / logprobs | 给 RL 训练用的 token 级数据 |
extra | 厂商特有指标,例如 reasoning_tokens、cache_creation_input_tokens |
规范刻意不记录单价,只存算好的 cost_usd。原因是价格会变、各家框架通常不记价格、不同厂商和地区定价也不同。像 Anthropic 的缓存写入这类额外计费项,放在 metrics.extra 里,并以厂商返回的真实成本为准。
一个最小示例
{
"schema_version": "ATIF-v1.7",
"session_id": "run-20261029-001",
"agent": { "name": "my-agent", "version": "1.0.0", "model_name": "claude-sonnet" },
"steps": [
{ "step_id": 1, "source": "user", "message": "创建 hello.txt,内容为 Hello" },
{
"step_id": 2,
"source": "agent",
"message": "我来创建这个文件。",
"reasoning_content": "只需要一次写文件调用。",
"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" } ]
},
"llm_call_count": 1,
"metrics": { "prompt_tokens": 520, "cached_tokens": 200, "completion_tokens": 80, "cost_usd": 0.00045 }
}
],
"final_metrics": { "total_prompt_tokens": 520, "total_completion_tokens": 80, "total_cached_tokens": 200, "total_steps": 2 }
}ATIF 在可观测里的位置
ATIF 的价值在于:一份数据,四种用途。同一个 trajectory.json 能直接用于排障、成本分析、评测和训练,不用为每个场景单独采集一遍。
- 调试与回放:每一步的输入、推理、工具调用和返回都在,能还原"agent 当时看到了什么、为什么这么选"。Harbor 的结果查看器就是直接读
agent/trajectory.json,在 Trajectory 标签页里逐步展示。 - 成本与性能分析:每步都有 token 和
cost_usd,可以定位哪一步最贵、缓存命中率如何、哪类工具调用最耗 token。 - 评测:评测器可以基于轨迹打分,例如检查 agent 是否按顺序调用了正确的工具。NVIDIA 的 NeMo Evaluator SDK 就支持以 ATIF 作为评测证据格式。
- 训练数据:
tool_definitions加上逐步消息可以直接构造 SFT 样本;token ID 和 logprobs 用于 RL,避免重新分词带来的偏差。
和 OpenTelemetry trace 的关系
两者是互补的,不是替代关系:
| OpenTelemetry trace | ATIF 轨迹 | |
|---|---|---|
| 关注点 | 调用链路、耗时、错误,基于 span 树 | agent 的交互语义:消息、推理、工具调用、观察 |
| 粒度 | 任意 span,包括 HTTP、DB 等基础设施 | 以 step 为单位的对话回合 |
| 主要消费方 | APM 平台、告警、性能分析 | 回放查看器、评测器、训练流水线 |
| 能否直接当训练数据 | 不能,需要再加工 | 可以,规范就是按此设计的 |
实践中的做法是两者都采集,再用同一个 ID 关联起来。NeMo Relay 的建议就是用运行时的 UUID 把 ATIF 和 OpenTelemetry/OpenInference trace 对齐:ATIF 的 session_id 就是顶层 agent scope 的 UUID。
读轨迹时容易踩的坑
step 数不等于 LLM 调用次数
v1.7 规定了"一次推理一个 step"的约定:框架能拿到每次调用的边界(如 LLM_START/LLM_END)时,导出方应当每次推理输出一个 agent step。拆不开时(比如一个工具内部自己调了好几次模型),必须用 llm_call_count 写明实际次数。
llm_call_count | 含义 |
|---|---|
1 | 本步恰好对应一次推理 |
> 1 | 指标是多次调用的合计,无法按次拆分 |
0(agent step) | 确定性调度,没有调用模型,例如编排引擎或规则流水线直接发起的工具调用;这类 step 不能带 metrics,SFT 时必须过滤掉 |
| 不填 | 导出方没有统计(兼容旧版本) |
此外,user 和 system step 本身不涉及推理。所以统计调用次数时,应当对 agent step 的 llm_call_count 求和,而不是数 step。字段缺失的旧轨迹,只能按 agent step 数做近似估计。
prompt_tokens 包含缓存命中
规范定义 prompt_tokens 是全部输入 token,cached_tokens 是其中命中缓存的子集。不少厂商 API 返回的 input tokens 只算未命中部分,导出时如果直接照搬,总量会被严重低估。开源项目 evener 就踩过这个坑:它在一次 Terminal-Bench 运行中,导出的 prompt token 总数比实际少了约 115 倍,后来按 RFC 的口径修正了(evener#4926)。
上下文压缩后的"复制步"
agent 做摘要或压缩后,会把一部分旧步骤带进新的上下文。这些步骤要标记 is_copied_context = true:统计成本时不能重复计算,构造 SFT 数据时必须过滤掉。v1.7 还约定,在 system step 的 extra.context_management 里声明压缩方式。当 boundary 为 replace 时,后续步骤的有效上下文只包括这一步的摘要内容以及之后的新步骤,评测工具重建输入时要按这个规则来。
生态与落地
生产 ATIF 有两种方式:agent 运行时直接写出,或者事后把各家的原生日志转换过来。
无论轨迹从哪里来,只要格式统一,下游的查看器、评测器和训练流水线都只需要对接一种格式。
- Harbor:自带转换器,能把 OpenHands、Mini-SWE-Agent、Gemini CLI、Claude Code 等 agent 的日志转成 ATIF。自研 agent 声明
AgentCapabilities(atif=True)后,需要写出trajectory.json。参考实现是一组 Pydantic 模型,位于harbor.models.trajectories。 - NVIDIA NeMo Relay:可观测插件可以按顶层 agent scope 导出 ATIF,把模型调用事件映射为 agent step,工具事件映射为工具调用和观察结果。导出目标支持本地文件、S3 和 HTTP,可以同时写多个。
- 其他实现:Rust 生态有
vtcode-exec-events的 ATIF 构建器;Go 官方的x/exp仓库里也有 ATIF 的解析代码,测试数据直接取自 Harbor 的 golden 轨迹。
校验
Harbor 提供了校验器,会检查 schema、step ID 是否从 1 开始连续、source_call_id 能否对上 tool_call_id、时间戳格式,以及引用的本地图片:
uv run python -m harbor.utils.trajectory_validator path/to/trajectory.json注意 Harbor 的 Pydantic 模型会拒绝未声明的字段,自定义数据一律放进各级的 extra。
在自己的平台接入的建议
- 先定导出点:在 LLM 网关或 agent runtime 里拿到每次调用的开始和结束事件,按"一次推理一个 step"输出;拆不开的地方填上
llm_call_count。 - token 口径对齐 RFC:
prompt_tokens要包含缓存命中部分,缓存写入等额外计费项放进metrics.extra,cost_usd以厂商返回的实际费用为准。 - ID 打通:轨迹的
session_id和 OTel trace ID、网关请求 ID 关联起来,这样可以从轨迹跳到链路,也可以从账单反查到轨迹。 - 多 agent 场景:子 agent 的轨迹用
subagent_trajectories内嵌,或者用trajectory_path引用外部文件,父 step 的 observation 里通过subagent_trajectory_ref指向它。 - 入库前过一遍校验器,并把
final_metrics和网关账单做抽样对账。
总结
对 agent 可观测来说,OTel 回答的是"慢在哪、错在哪",ATIF 回答的是"agent 为什么这么做、花了多少"。它把消息、推理、工具调用、观察结果和 token 成本放进同一份 JSON,这份数据可以直接用于回放、评测和训练。
落地时最重要的三件事:按"一次推理一个 step"导出,并填写 llm_call_count;token 口径按 RFC 来(prompt_tokens 包含缓存命中);用统一的 ID 把轨迹和 trace、账单关联起来。