Agent 可观测性:日志、链路追踪与调试
用结构化日志、分布式链路追踪和 span 级成本追踪在生产环境调试 AI Agent。该抓什么、该忽略什么,以及那些藏住真实故障的坑。
TL;DR — Agent 可观测性不是”把 prompt 和响应记下来”那么简单。你需要给每次模型调用和工具调用打 span、记录每一步完整的消息历史、每个 span 的 token 和成本,再用一个稳定的 trace ID 把它们串起来。没这些,一次失败的 Agent 运行就是个黑盒。有了,你几秒就能定位坏掉的那一步。OpenTelemetry 加一个追踪工具(Langfuse、LangSmith、Arize Phoenix)基本就够了。
黑盒问题
用户报告你的 Agent 给了个错答案。你打开日志,找到的是……最终响应。八步里哪一步歪了、第 4 步模型看到了什么、它为什么调错了工具——一概没有。你在盲调。
Agent 可观测性就是来干掉这个黑盒的。传统应用可观测性假设代码是确定性的:同样输入、同样路径、同样输出。Agent 打破了这个假设。同样的输入,每次运行可能走不同步数、调不同工具、花不同的钱。看不见的东西没法调,而 Agent 的决策过程,除非你刻意埋点,否则是不可见的。
我盯 Agent trace 熬过的夜,足够让我对”该抓什么”有很强的看法。下面是真正能让你在生产环境调试 Agent 的那套配置,以及那些大家埋了点、结果发现是噪声的东西。
一次 Agent 运行到底包含什么
埋点之前,先把心智模型摆正。一次 Agent 运行是一棵树,不是一条线:
flowchart TD
T[Trace:一个用户请求] --> S1[Span:模型调用 #1]
S1 --> S2[Span:工具调用 - 搜索]
S2 --> S3[Span:模型调用 #2]
S3 --> S4[Span:工具调用 - 抓取]
S4 --> S5[Span:模型调用 #3 - 最终]
每个方块是一个 span。整棵树是一条 trace,靠 trace ID 串起来。这和 OpenTelemetry 给微服务用的模型是一样的,这不是巧合:Agent 就是个分布式系统,只不过”服务”是模型调用和工具。这么一看,工具选型就明了了。
你必须抓的三层
1. 结构化日志(发生了什么)
忘掉 print(response)。每个事件都应该是一条之后能查询的结构化记录。
import json, time, uuid
def log_event(trace_id, span_id, event_type, payload):
record = {
"ts": time.time(),
"trace_id": trace_id,
"span_id": span_id,
"type": event_type, # model_call | tool_call | error
"payload": payload,
}
print(json.dumps(record)) # 发到你的日志管线
trace_id = str(uuid.uuid4())
log_event(trace_id, "span-1", "model_call", {
"model": "claude-sonnet-4",
"messages": messages, # 模型在这一步看到的【完整】历史
"tools_offered": [t.name for t in tools],
"tokens_in": 1820, "tokens_out": 240,
"latency_ms": 1430,
})
绝不能省的字段是模型在那一步看到的完整消息历史。Agent 出问题时,90% 的原因在上下文里:注入了一条过时记忆、某个工具返回了垃圾污染了下一轮、或者系统 prompt 被截断了。如果你只记用户问题和最终答案,你永远找不到它。
2. 分布式链路追踪(什么时候、花多久)
日志告诉你发生了什么;trace 告诉你形状和时序。一条 trace 让你看到第 3 步花了 8 秒(慢工具),或者 Agent 在第 4、5 步之间来回循环了三次。用 OpenTelemetry span,数据就能在工具之间通用。
from opentelemetry import trace
tracer = trace.get_tracer("agent")
def run_agent(query):
with tracer.start_as_current_span("agent_run") as root:
root.set_attribute("user.query", query)
for step in range(MAX_STEPS):
with tracer.start_as_current_span(f"step_{step}") as span:
resp = call_model(history)
span.set_attribute("tokens.in", resp.usage.input)
span.set_attribute("tokens.out", resp.usage.output)
if not resp.tool_calls:
span.set_attribute("terminal", True)
return resp.content
3. 成本与 token 追踪(花了多少)
给每个 span 挂上 token 数,你的 trace 就顺带成了成本账本。可观测性在这里直接回本:你能逮到那个本该 3 步、却悄悄走了 15 步的 Agent,或者那个返回 40KB JSON、害得模型之后每一轮都要重读一遍的工具。这俩在聚合仪表盘里看不见,在单条 trace 里一目了然。归因不清的 token 账单是没法优化的,而控制成本的设计模式首先依赖你能看见钱花在哪。
选你的工具
这套不用从零造。2026 年的生态已经很扎实了。
| 工具 | 强项 | 适合 |
|---|---|---|
| Langfuse | 开源、可自托管、OTel 原生 | 想掌握数据所有权的团队 |
| LangSmith | 深度集成 LangChain/LangGraph | 重度用 LangChain 的栈 |
| Arize Phoenix | 评测 + 追踪组合强 | 也做离线评测的团队 |
| OpenTelemetry(原生) | 厂商中立标准 | 接入已有基建(Grafana、Datadog) |
我的默认做法:用 OpenTelemetry 埋点,导出到上面任意一个。这样你不会被锁死。哪天追踪厂商不够用了,你换的是 exporter,不是整套埋点。
大家容易过度埋的点
三样团队常抓、但基本只制造噪声的东西:
- Embedding 向量。 每次检索都记那个 1536 维原始向量,是你永远不会读的几个 GB 数据。记检索到的文本和分数,别记向量。
- 流式输出的每个 token。 你要的是最终拼好的输出和 token 数,不是 200 个单独的 delta 事件。在 span 边界聚合。
- 冗长的框架内部回调。 LangChain 之流会喷出一大堆内部回调。全抓下来会把真正重要的三个事件埋掉。过滤到模型调用、工具调用、错误。
可观测性的功夫不在抓得更多,而在以对的粒度抓对的东西。一条 30 秒能读完的 trace,胜过一条啥都有、要花 30 分钟解析的。
真正管用的调试流程
一次运行失败时,下面这个顺序定位 bug 最快:
- 打开 trace,找最后一个正常的步骤。 Agent 的状态从哪儿开始不对劲了?
- 读下一步的完整输入。 通常就崩在这:上下文坏了、工具输出被污染了、记忆丢了。
- 看工具结果,不只是工具调用。 Agent 用对的参数调了对的工具,但工具返回了一个错误字符串,模型却把它当数据用了。极其常见。
- 找循环。 同一个工具、同样的参数、出现多次 = Agent 卡住了还没察觉。你的设计模式该给它封顶,但可观测性才是你发现”顶设太高”的方式。
- 比较各步的 token 数。 突然飙升意味着上下文膨胀,常常是某个工具倒了太多数据。
关于生产安全的提醒
Agent 日志是个隐私面。完整消息历史里经常有用户 PII、内部数据,以及泄进上下文的密钥。上结构化日志之前,先定好哪些要脱敏、设好留存期限,并确认追踪厂商的数据驻留地符合你的合规要求。OWASP LLM Top 10 把敏感信息泄露列为首要风险:把密钥明文悄悄存下来的可观测性,本身就是一桩等着发生的事故,尤其当它和需要安全沙箱的代码执行 Agent 配在一起时。
FAQ
我需要专门的工具,还是用现有 APM 就行? 你可以把 OpenTelemetry span 路由到 Datadog 或 Grafana,看延迟和错误足够了。但 Agent 专用工具(Langfuse、LangSmith、Phoenix)会以通用 APM 做不到的方式渲染消息历史和工具 I/O,而那恰恰是你调质量问题要的数据。
最重要的一件事记什么? 模型在每一步看到的完整消息数组。只能记一样,就记它。几乎每个 Agent bug 都是上下文 bug。
可观测性的开销有多大? 如果异步导出,追踪带来的延迟可以忽略(每个 span 微秒级)。真正的成本是存储,所以才要过滤到模型调用、工具调用和错误,而不是什么都记。
多 Agent 系统怎么追踪? 在各 Agent 之间传递同一个 trace ID,让每个 Agent 的工作成为子 span。trace 树就会展示完整协作,包括是哪个子 Agent 导致了失败。
这能用来评估质量,不只是调试吗? 能。Phoenix、Langfuse 这类工具让你把评测分数挂到 trace 上,于是你可以拿一个裁判模型跑过往的 trace,追踪质量随时间的回退,而不只是抓一次性的失败。


