Trace 与可观测性(轻量链路追踪)
这一页讲的是 ai4j-agent 当前已经落地的 trace 能力,不是泛泛而谈“以后可以怎么做”。
重点回答四个问题:
- Agent runtime 现在到底会产出哪些 trace 数据
reasoning / retry / handoff / team / compact这些事件怎么映射到 trace- 内置 exporter 有哪些,
OpenTelemetry是怎么接进来的 Agenttrace 和FlowGram前端调试视图之间是什么关系
1. 当前 trace 组件
AgentTraceListener- 监听
AgentEvent,把 runtime 事件折叠成TraceSpan
- 监听
TraceConfig- 控制记录开关、脱敏、字段裁剪
TraceSpan- 一条 span,包含基础字段、attributes、events、metrics
TraceSpanEvent- span 内部事件,例如
model.reasoning、model.retry
- span 内部事件,例如
TraceMetrics- 统一挂载时延、token、cost 这些指标
TracePricing/TracePricingResolver- 给模型 usage 做成本估算的可选配置
TraceExporter- 导出接口
ConsoleTraceExporter- 打印
TRACE {...}
- 打印
InMemoryTraceExporter- 测试断言和调试采样
CompositeTraceExporter- 一个 span 扇出到多个 exporter
JsonlTraceExporter- 追加写入 JSONL 文件
OpenTelemetryTraceExporter- 把 AI4J trace 桥接导出到 OTel pipeline
LangfuseTraceExporter- 输出 Langfuse 可识别的 OTel span attributes,方便接 Langfuse
2. 启用方式
最小接法:
Agent agent = Agents.react()
.modelClient(modelClient)
.model("doubao-seed-1-8-251228")
.traceConfig(TraceConfig.builder().build())
.traceExporter(new ConsoleTraceExporter())
.build();
AgentBuilder 的默认行为很简单:
- 只要你设置了
traceExporter(...) build()时就会自动挂一个AgentTraceListener- 不需要再手动注册 listener
如果你要同时打控制台、内存和文件:
TraceExporter exporter = new CompositeTraceExporter(
new ConsoleTraceExporter(),
new InMemoryTraceExporter(),
new JsonlTraceExporter("logs/agent-trace.jsonl")
);
Agent agent = Agents.react()
.modelClient(modelClient)
.model("gpt-4o-mini")
.traceExporter(exporter)
.build();
3. TraceSpan 结构
TraceSpan 当前包含:
traceIdspanIdparentSpanIdnametypestatusstartTimeendTimeerrorattributeseventsmetrics
events 里的每一项是 TraceSpanEvent:
timestampnameattributes
metrics 当前包含:
durationMillispromptTokenscompletionTokenstotalTokensinputCostoutputCosttotalCostcurrency
这意味着当前 trace 不是只有“粗粒度 span”,也支持在一个 span 内附加中间事件。
4. Span 类型
当前 TraceSpanType 已经不只四种:
RUN- 一次完整 agent 调用
STEP- 一轮 runtime loop
MODEL- 一次模型请求
TOOL- 一次工具执行
HANDOFF- 一次 subagent handoff
TEAM_TASK- 一条 team task 的生命周期
MEMORY- 一次 memory compact / compress
FLOWGRAM_TASK- 给 FlowGram runtime 复用的任务级 span 类型
FLOWGRAM_NODE- 给 FlowGram runtime 复用的节点级 span 类型
其中 FLOWGRAM_TASK / FLOWGRAM_NODE 是 trace 核心模型里的通用类型,当前 AgentTraceListener 本身不直接产出它们;FlowGram 侧走的是独立 runtime event + projection 链路。
5. 状态模型
TraceSpanStatus 当前有三种:
OKERRORCANCELED
也就是说,trace 层现在可以明确区分:
- 正常结束
- 异常失败
- 主动取消
这对 handoff、team task、FlowGram task 都是有意义的。
6. Agent 事件如何映射到 trace
6.1 运行主链路
- 第一次
STEP_START- 创建
RUN
- 创建
- 每个
STEP_START / STEP_END- 创建并结束
STEP
- 创建并结束
MODEL_REQUEST- 创建
MODEL
- 创建
TOOL_CALL / TOOL_RESULT- 创建并结束
TOOL
- 创建并结束
FINAL_OUTPUT- 结束
RUN
- 结束
ERROR- 将
RUN标记为ERROR
- 将
6.2 模型中间事件
BaseAgentRuntime.executeModel(...) 现在除了 request/response,还会发:
MODEL_REASONINGMODEL_RETRY
这些不会额外拆成独立 span,而是挂在当前 MODEL span 的 events 上:
model.reasoningmodel.retry- 流式文本增量会作为
model.response.delta
这样做的原因是:
- reasoning / retry 本质上属于同一次模型调用的内部过程
- 单独拆 span 会让层级过碎
- 挂成 span event 更适合做时间线与回放
6.3 SubAgent handoff
SubAgentToolExecutor 会发:
HANDOFF_STARTHANDOFF_END
AgentTraceListener 会把它们折叠成一个 HANDOFF span。
当前 handoff payload 里常见的字段包括:
handoffIdcallIdtoolsubagenttitledetailstatusdepthsessionModeattemptsdurationMillisoutputerror
所以 handoff trace 既能回答“有没有委派”,也能回答:
- 委派给谁
- 第几层 handoff
- 是完成、失败还是 fallback
- 花了多久
6.4 Agent Team
AgentTeamEventHook 会发:
TEAM_TASK_CREATEDTEAM_TASK_UPDATEDTEAM_MESSAGE
映射规则是:
- task create / update
- 聚合成
TEAM_TASKspan
- 聚合成
- team message
- 写入对应
TEAM_TASKspan 的team.messageevent
- 写入对应
这和 handoff 的区别是:
- handoff 更像主 agent 把一个 tool 调用委派出去
- team task 更像显式任务板上的任务生命周期
6.5 Memory compact
MEMORY_COMPRESS 现在映射为一个短生命周期 MEMORY span。
它适合挂这些信息:
- 为什么压缩
- summary / checkpoint 标识
- 是否 fallback
- 压缩发生在哪个 step
如果你在 Coding Agent 里看 compact 诊断,这一层语义和 agent trace 是能对齐的。
7. 默认记录策略
TraceConfig.builder().build() 默认就是:
recordModelInput = truerecordModelOutput = truerecordToolArgs = truerecordToolOutput = truerecordMetrics = truemaxFieldLength = 0masker = nullpricingResolver = null
也就是默认偏“全记录”,方便本地调试和研发联调。
8. 当前会记录哪些字段
8.1 模型输入
MODEL span attributes 里常见字段:
modelsystemPromptinstructionsitemstoolstoolChoiceparallelToolCallstemperaturetopPmaxOutputTokensreasoningstorestreamuserextraBody
8.2 模型输出
- 最终 raw payload ->
output - 流式文本增量 ->
model.response.deltaevent - 最终回答 ->
RUN.finalOutput - provider 返回的
usage/model/finishReason也会被抽出来单独记录
8.3 模型指标
MODEL span 在 payload 带 usage 时,会自动补齐:
metrics.durationMillismetrics.promptTokensmetrics.completionTokensmetrics.totalTokens
如果你配置了 TracePricingResolver,还会继续估算:
metrics.inputCostmetrics.outputCostmetrics.totalCostmetrics.currency
同时,RUN 和 STEP span 会聚合同一轮里的 token / cost,总结视角不需要你自己再扫一遍全部 MODEL span。
8.4 工具调用
toolcallIdarguments
8.5 工具返回
- 普通工具:
output - CodeAct 工具:
result/stdout/error
8.6 handoff / team / compact
这些数据主要落在它们各自 span 的 attributes 和 events 上,不再强行塞进 MODEL 或 TOOL。
9. 内置 exporter 的使用边界
9.1 ConsoleTraceExporter
适合:
- 本地开发
- 先快速看有没有请求、有没有工具、有没有 handoff
不适合:
- 正式存档
- 大规模查询
9.2 InMemoryTraceExporter
适合:
- 单元测试
- 集成测试里断言 span 类型和字段
9.3 JsonlTraceExporter
适合:
- 本地归档
- 调试时导出文件给别的系统离线分析
- 简单接 ELK / ClickHouse 导入任务
9.4 CompositeTraceExporter
适合:
- 同时满足调试、留档、平台接入三类需求
9.5 OpenTelemetryTraceExporter
这是当前推荐的“平台接入桥”。
它的定位不是“用 OTel 完全替代 AI4J trace 模型”,而是:
- 保留 AI4J 自己的
TraceSpan语义 - 导出时把关键字段映射到 OTel span 和 attributes
- 方便接已有的 collector / observability pipeline
接法:
OpenTelemetry openTelemetry = ...;
Agent agent = Agents.react()
.modelClient(modelClient)
.model("gpt-4o-mini")
.traceExporter(new OpenTelemetryTraceExporter(openTelemetry))
.build();
当前导出时会写入这些关键属性:
ai4j.trace_idai4j.span_idai4j.parent_span_idai4j.span_typeai4j.span_statusai4j.errorai4j.attr.*ai4j.event.*ai4j.metrics.*gen_ai.usage.input_tokensgen_ai.usage.output_tokens
要注意一件事:
- 当前它是“桥接 exporter”
- 不是把
AgentRuntime全部改造成原生 OTel instrumentation - exporter 内部会按
parentSpanId做一层缓冲重排,尽量恢复父子链路,不是简单把每个 span 独立平铺出去
所以如果你需要非常严格的 OTel context propagation / 原生父子链路管理,应该在更深层做原生埋点;如果你只是要接 OTel collector、再喂给 Langfuse 之类系统,这一层已经够用。
9.6 LangfuseTraceExporter
如果你的后端已经走 OTel pipeline,但上层想直接进 Langfuse,这是推荐接法。
OpenTelemetry openTelemetry = ...;
Agent agent = Agents.react()
.modelClient(modelClient)
.model("gpt-4o-mini")
.traceExporter(new LangfuseTraceExporter(openTelemetry, "prod", "2026-04-03"))
.build();
它做的事情不是直连 Langfuse 私有协议,而是:
- 继续输出 OTel span
- 额外写入 Langfuse 识别的 attributes
- 让你可以复用现有 collector / OTLP pipeline
当前会重点映射:
langfuse.observation.typelangfuse.observation.levellangfuse.observation.inputlangfuse.observation.outputlangfuse.observation.modellangfuse.observation.model_parameterslangfuse.observation.usage_detailslangfuse.observation.cost_detailslangfuse.observation.metadatalangfuse.trace.namelangfuse.trace.outputlangfuse.trace.metadata
10. 脱敏与裁剪
线上建议至少做两件事:
- 通过
masker脱敏 - 通过
maxFieldLength限制超长字段
示例:
TraceConfig config = TraceConfig.builder()
.maxFieldLength(4000)
.masker(text -> text == null
? null
: text.replaceAll("(?i)api[_-]?key\\s*[:=]\\s*[^,\\s]+", "apiKey=***"))
.build();
11. 与 FlowGram trace 的关系
Agent trace 和 FlowGram trace 不应该混成一层。
当前推荐边界是:
Agent- 输出
TraceSpan - 可接
OpenTelemetryTraceExporter
- 输出
FlowGram- 先产出 runtime event
- 再由后端投影成前端可消费的
FlowGramTraceView
FlowGramTraceView 当前不只是时间线快照。
在新版 starter 里,后端在返回 report/result 前还会补齐:
trace.summary.metricstrace.nodes[nodeId].metricsworkflow.nodes[nodeId].outputs.metrics
也就是说,FlowGram 前端现在可以直接拿后端 projection 看 node duration、LLM tokens 和 cost,不需要默认自己再从 rawResponse.usage 做一遍 client-side 解析。
也就是说:
- 后端平台侧可以 OTel-first
- 但给
FlowGram.ai这类前端画布时,不建议直接让前端读原始 OTel span - 应该读后端整理好的 trace projection
12. 一段 trace 怎么看
建议按这个顺序读:
- 先看
RUN- 整体耗时、整体状态、最终输出
- 再看
STEP- 有没有循环过多
- 再看
MODEL- prompt 是否正确、reasoning/retry 发生在哪
- 再看
TOOL- 调了什么工具、参数和输出是否异常
- 再看
HANDOFF / TEAM_TASK / MEMORY- 问题是在委派、协作,还是在压缩点发生的
13. 参考测试
AgentTraceListenerTestAgentTraceUsageTestCodeActRuntimeWithTraceTest