
字节笔记本
2026年10月24日 · 约 11 分钟读完
从 0 造一套 Agent 可观测系统

本文是《从 0 写 Agent》系列第 25 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「Tracing:从 0 造一套可观测系统」。
Agent 出错时,你需要知道是哪一步炸的,是模型调用超时?工具执行失败?还是 handoff 出了问题?
没有 tracing,你只能看到"Run 失败了",不知道在哪一步。agents-go 自实现了一套 trace/span/processor/exporter 四层架构,不依赖 OpenTelemetry,对标 Python SDK。这一章拆这套可观测系统。
引子:Agent 调试的噩梦
Agent 一次 Run 可能涉及,5 轮循环、10 个工具调用、2 次 handoff、若干次模型调用。如果失败了,日志里只有"error: tool failed",哪个工具?第几轮?在什么上下文里?
没有 trace,调试是噩梦。 你只能一遍遍加 log,重新跑,试图捕捉错误。有了 trace,你能看到完整的执行树,每一步耗时、每一步的状态、错误在哪。
trace: 用户咨询退款
├── span: agent(分诊) 2.1s
│ ├── span: generation(模型调用) 1.5s
│ ├── span: function(get_order) 0.4s
│ └── span: handoff(→退款) 0.1s
├── span: agent(退款) 5.3s
│ ├── span: generation(模型调用) 1.2s
│ ├── span: function(refund) ❌ 0.5s 失败 ← 这里炸了
│ └── span: generation(重试) 1.8s一眼看出,退款的 refund 工具失败了。 这就是 tracing 的价值。

为什么不用 OpenTelemetry
agents-go 自实现 trace,不用 OpenTelemetry。为什么?
- 对标 Python SDK 一致性,trace ID 格式(
trace_+ 32hex)、span ID 格式(span_+ 24hex)和 Python SDK 完全一致,方便跨语言对照 - 依赖极简,不引入 OTel 庞大的依赖
- 5 种 span 类型专用,agent/generation/function/handoff/guardrail,直接对应 Agent 运行时阶段
代价,不能直接接 OTel 生态(Jaeger、Zipkin 等)。但可以写 adapter 把 agents-go 的 trace 转成 OTel 格式。这是个取舍,简洁一致 vs 生态兼容。
四层架构
agents-go 的 tracing 是经典的四层:
1. 数据模型(Trace + Span) , 表示"发生了什么"
2. Tracer , 创建和管理 trace/span
3. Processor(BatchProcessor) , 异步收集、缓冲、批处理
4. Exporter , 把 trace/span 发到目的地(控制台、HTTP、OTel)第一层:Trace 和 Span
// tracing/tracing.go:14
type Trace struct {
TraceID string // trace_ + 32hex
WorkflowName string // 工作流名(agent 名)
GroupID string
Metadata map[string]any
}
// tracing/tracing.go:39
type Span struct {
TraceID string // 属于哪个 trace
SpanID string // span_ + 24hex
ParentID string // 父 span(形成树)
Name string // span 名
Type string // agent/generation/function/handoff/guardrail
StartedAt time.Time
EndedAt time.Time
Error *SpanError // 错误信息
Data map[string]any // 结构化数据
}Trace 是一次完整 Run 的根,Span 是其中的每一步。 Span 的 ParentID 形成树状结构,工具调用 span 的父是 agent span,agent span 的父是 trace。
五种 span 类型
// tracing/tracing.go:29
const (
SpanTypeAgent = "agent" // Agent 执行
SpanTypeGeneration = "generation" // 模型调用
SpanTypeFunction = "function" // 工具调用
SpanTypeHandoff = "handoff" // Agent 交接
SpanTypeGuardrail = "guardrail" // 护栏检查
)直接对应 Agent 运行时的关键阶段。 consumer 按 Span.Type 分发(不用解析 Name 字符串)。
第二层:Tracer
Tracer 创建 span,管理父子关系。关键设计,nil Tracer 是 no-op(tracer.go:37 等处处 nil 检查),调用方不用判空:
// 即使 tracer 是 nil,这些方法也能调(no-op)
span := tracer.StartAgentSpan(agent.Name, parentID)
defer span.Finish()这是 Go 的 nil-safe 惯用法,零值可用。 不配 tracer 时,所有 trace 调用都是 no-op,零开销。
第三层:Processor
Processor(tracing.go:56)接收 trace/span 生命周期事件。最常用的是 BatchProcessor(第 26 章详讲),后台异步批处理。
第四层:Exporter
Exporter(tracing.go:83)把 trace/span 发到目的地。agents-go 内置 5 种(第 27 章详讲)。
ID 生成与 Python SDK 一致
// tracing/tracing.go:98
func NewTraceID() string { return "trace_" + randHex(16) } // trace_ + 32hex
func NewSpanID() string { return "span_" + randHex(12) } // span_ + 24hex格式刻意匹配 Python SDK。为什么?因为 agents-go 是 Python SDK 的移植版,如果你同时用 Python 和 Go 版,trace ID 格式一致,方便对照。
randHex 用 crypto/rand(tracing.go:90),注释指出 Go 1.24 起 rand.Read 永不失败(OS 随机源不可用会直接 abort,不返回 error)。所以代码里 _, _ = rand.Read(buf) 忽略 error 是安全的。
小结
读完这一章,你应该理解 tracing 的四层架构:
- Trace + Span,数据模型,Span 的 ParentID 形成执行树
- 五种 span 类型,agent/generation/function/handoff/guardrail,对应运行时阶段
- Tracer,nil-safe(不配就是 no-op),typed span 构造器自动设 Type
- Processor + Exporter,异步收集和导出
Tracing 让 Agent 从"黑盒"变成"玻璃盒",每一步都看得见。 下一章讲 BatchProcessor 怎么异步处理这些 trace。
下一篇
第 26 章,BatchProcessor:异步流水线工程。trace 不能阻塞 Agent 循环,所以要异步。BatchProcessor 用后台 goroutine 缓冲批导出,还要处理背压、孤儿 span、优雅关闭。
本文是《从 0 写 Agent》系列第 25 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

