851 字
4 分钟
Agent 工程(十一):一次运行应当记录为因果树

Agent 的一次运行不是按时间排列的一串日志,而是由模型调用、工具执行、子任务和重试组成的因果树。可观测性的核心不是收集更多文本,而是保留“哪个动作由谁触发、耗时和结果是什么”的父子关系。

日志回答事件,Trace 回答因果#

并发工具和子 Agent 会让多条日志交错。只凭时间戳,很难确定某次模型调用触发了哪些工具、某个重试属于哪条分支,以及总延迟消耗在哪里。

每段操作可以记录为一个 Span:

Agent Run
├─ Model Call
├─ Tool Call: search
├─ Tool Call: read
└─ Sub Agent
├─ Model Call
└─ Tool Call: test

Span 包含稳定的 trace_id、自身标识、父节点、名称、起止时间、状态和结构化属性。完整运行由这些父子关系组成 Trace。指标用于回答整体成功率和延迟是否异常,Trace 用于解释某一次异常如何发生,日志则补充具体事件文本。

Checkpoint 与 Trace 不能互相替代#

Checkpoint 服务于恢复,需要保存下一步执行所需状态;Trace 服务于解释,记录已经发生的操作与耗时。

把完整 Prompt、工具结果和状态全部复制到每个 Span,会产生存储膨胀与隐私风险。默认应记录模型、Token、耗时、工具名称、退出状态和引用标识,具体内容只在失败采样或受控调试中保存。

同样,Trace 不能作为恢复来源。观测数据可能采样、延迟或丢失,而恢复状态必须满足一致性要求。

是否采用 OpenTelemetry 取决于传播复杂度#

单进程、同步、层次很浅的系统,用结构化日志加父标识即可满足需求。OpenTelemetry 的主要价值在于提供跨线程、异步任务和跨服务的上下文传播,以及统一的导出协议。

SDK 只负责生成和发送数据,不负责长期存储与查询;仍需要后端接收 Trace。若系统尚未明确要回答的问题,直接引入完整平台只会增加部署与字段治理成本。

我会按以下顺序落地:

  1. 先定义运行、模型、工具和子任务四类节点;
  2. 记录父子关系、耗时、状态与关键资源用量;
  3. 用真实故障验证能否定位慢点和失败点;
  4. 出现跨服务传播需求后再接入 OpenTelemetry;
  5. 最后根据隐私和成本决定内容采样策略。

我的选择:默认记录结构,按需记录内容#

所有运行都记录因果结构、耗时、模型和工具结果状态;完整 Prompt、输出和文件内容默认不进入可观测系统,只在失败样本、授权调试或脱敏后采集。

这种做法牺牲了随时回看全部上下文的便利,但降低了敏感数据扩散和平方级存储增长。若系统只有单进程短链路,我会保持轻量实现;当异步、并发和跨服务成为常态时,再使用标准 Trace 上下文避免自行维护传播协议。

下一篇继续讨论跨运行状态:四类记忆为什么应该统一接口,而不是统一存储