智能体的追踪与可观测性

E5
运维 · 评估与可观测性

智能体的追踪与可观测性——轨迹就是那个数据结构。

你无法评估、调试或改进你看不见的东西,而智能体是最不可观测的一类软件:一个由模型调用与工具副作用组成的非确定循环,其决定性状态住在一个你没记录的上下文窗口里。本文论证一个想法——执行轨迹不是事后栓上去的遥测,它是智能体系统的核心数据结构——并展示每步该记录什么、如何把它建模为 span、OpenTelemetry GenAI 约定,以及为什么轨迹回放是把可观测性变成评估的关键。

STEP 1

轨迹是数据结构,不是日志。

本能是像给 web 服务加日志那样给智能体加日志:这里一行,那里一个计数器。这是反的。对智能体而言,一次运行的完整结构化轨迹——每个输入、决策、工具调用、观察与令牌——就是系统其余部分所操作的对象。你的评估框架给轨迹打分。你的调试器读轨迹。你的回归关卡对轨迹做 diff。你的微调集是被筛过的轨迹。你的评判者评判轨迹。如果轨迹有损,上述每一个都在对损坏的输入做运算。

设计规则:一次运行必须能仅凭其轨迹被完全重建。如果你得重跑智能体才能理解它做了什么,你的轨迹就不完整——而重跑一个非确定的智能体给你的是一次不同的运行,所以那份信息永远没了。

STEP 2

每步该记录什么——全部。

每个智能体步骤是一个工作单元,必须捕获足以回放并据以断言的内容。只记录最终答案是最常见的可观测性失败,它让下游每一篇文章都不可能。

  • 输入——发给模型的确切渲染后提示词/消息(模板化后、检索后)、系统提示词版本、模型 id、解码参数。不是模板——是过线的字节。
  • 决策——原始模型输出,包括推理/思考内容、解析出的工具选择与参数。
  • 工具调用——工具名、完整参数、延迟、成功/错误,以及返回的完整观察(截断观察就是丢弃智能体对下一步的实际输入)。
  • 记账——提示词/补全令牌、成本、墙钟时间、重试次数、缓存命中/未命中。
  • 关联——把该步绑到其运行、父步、会话,以及(多智能体时)发出它的那个智能体的 id。
# the minimum step record; anything less is not replayable
step = {
  "run_id": rid, "step": i, "parent": i - 1,
  "model": "<id>", "prompt_version": "sys@7",
  "input_messages": msgs,           # exact bytes sent
  "output": raw, "tool": name, "args": args,
  "observation": obs_full,           # NOT truncated
  "tokens_in": ti, "tokens_out": to,
  "latency_ms": dt, "error": err,
}
STEP 3

把它建模为 span:一次运行是一棵追踪树。

正确的形状是分布式追踪模型。一次运行是一条 trace;每个步骤、模型调用与工具调用是一个带起止、父级、属性与状态的 span。span 嵌套:一个步骤 span 含一个模型调用 span 与一个工具调用 span;一个子智能体是由上下文传播链接的子 trace。这不是比喻——它就是 APM 所用的同一个原语,这意味着智能体轨迹能直接落进现有追踪后端,而非一堆定制 JSON。

  • span 免费给你时间线——延迟去了哪、什么在并行跑、哪个工具阻塞了。
  • 父/子树就是轨迹——轨迹评估(E2)字面上就是对这棵树的断言。
  • 跨智能体的上下文传播意味着一次多智能体运行是一条连通的 trace,而非 N 条你拼不起来的孤儿日志。
STEP 4

用 OpenTelemetry GenAI 约定,而非定制 schema。

OpenTelemetry 有面向 GenAI/智能体 span 的语义约定:标准化属性如 gen_ai.operation.namegen_ai.request.modelgen_ai.usage.input_tokens/output_tokens、工具调用 span 结构,以及捕获提示词/响应内容的约定。采用它们是个高杠杆、低成本的决定。

# OTel GenAI semantic conventions on an agent span
with tracer.start_as_current_span("invoke_agent") as s:
    s.set_attribute("gen_ai.operation.name", "invoke_agent")
    s.set_attribute("gen_ai.request.model", model_id)
    s.set_attribute("gen_ai.usage.input_tokens", ti)
    s.set_attribute("gen_ai.usage.output_tokens", to)
    # tool calls become child spans: gen_ai.operation.name=execute_tool

标准约定不是官僚主义——它正是你的轨迹能在任何 OTel 后端工作、两个团队的智能体可比、以及一个厂商中立的导出器意味着你永不被锁进单一可观测性工具的原因。一份定制轨迹 schema 是一次你日后要连本带利偿还的迁移。

STEP 5

轨迹回放:可观测性变成评估之处。

一条完整轨迹可回放:把记录的输入与观察重新喂过一个候选提示词/策略,看它是否做出相同或更好的决策——无需再打活的工具。这是本文与其余各篇之间的桥。

  • 反事实调试——"提示词 v8 会避开第 12 步的坏调用吗?"用记录的上下文回放第 12 步;你几秒内得到答案,而非一次重跑。
  • 来自真实流量的回归评估——把一个采样的生产轨迹集对新构建回放;一个从好翻成坏的决策就是回归,在部署前被捕获(这就是 E6 的关卡)。
  • 评估集构建——最有信息量的评估任务是真实的失败轨迹,连同其环境冻结,变成可回放的用例。
replay: prod trace #4471 vs build candidate-92
  step  3  tool=search        same        ok
  step  7  tool=db.write      args DIFFER  ← candidate adds dry_run
  step 12  tool=delete_user   BLOCKED      ← candidate refuses, prod did it
  verdict: candidate FIXES the step-12 incident; ship behind flag

那次回放在一趟里把一起生产事故变成了一个回归测试和一次修复验证——若轨迹只是几行日志加一个最终答案,这不可能。

STEP 6

诚实的取舍。

全保真追踪不是免费的:存储增长很快,提示词/观察内容可能含 PII 与密钥(在边界处脱敏,绝不记录原始凭据),而朴素的同步导出会给热路径加延迟(异步导出,按策略采样,错误保留 100%)。但替代方案——稀薄日志加一个最终答案——会让评估、调试与改进在结构上不可能,而不仅是更难。刻意支付追踪成本,并把轨迹当作系统的首要数据结构;一个你无法完整回放的智能体,是一个你实际无法评估的智能体。