Agent 可观测性实战:日志、Trace、Debug,生产环境不裸奔
写在前面的实话
上篇我拆了 Agent 记忆系统的三层架构——短期记忆靠 Checkpointer、长期记忆用向量库、知识图谱上 Neo4j。写完在本地跑起来之后,我发现一个更头疼的问题:Agent 出了 bug 怎么查?
传统程序出 bug,你打个断点、看下堆栈、复现一下,八九不离十。但 Agent 不一样——同样的 user query,两次调用 LLM 可能给出完全不同的回复。这周花了不少时间调试一个生产环境的 Agent 错误:用户问「帮我查一下上周五订单号 OD20260710 的物流信息」,Agent 调了三个工具,最后一步参数拼错了,返回空结果。用户说「你们的 AI 是智障吗」。
我蹲在终端前,面对几千行日志,脑壳疼。
这篇文章就记录一下我这段时间趟出来的 Agent 可观测性方案——从最基础的日志结构到完整的 Trace 链路和 Debug 工作流。
一、Agent 日志为什么比普通程序难写
先看一个对比。传统后端服务,请求进来 —> 处理 —> 返回,日志是线性的,一条请求一条链路,清晰得很。但 Agent 不一样:
| 维度 | 传统后端 | Agent 系统 |
|---|---|---|
| 执行路径 | 确定(代码决定) | 不确定(LLM 决定调什么工具) |
| 输出结果 | 确定(相同输入相同输出) | 不确定(温度 > 0 每次不同) |
| 错误来源 | 代码异常 / 网络 / DB | 代码 + LLM 幻觉 + 工具返回 + Prompt 设计 |
| 复现难度 | 低(固定的请求 + 数据即可) | 高(依赖 LLM 采样、上下文窗口、Token 顺序) |
| 调试手段 | 断点 / 堆栈 / 日志 | 需要 Trace + LLM 调用记录 + Tool 调用记录 |
一句话总结:Agent 的不确定性使得传统「打日志 — 复现 — 修 bug」的闭环断掉了。你必须换一套思路。
二、我的日志结构:每条记录都要回答五个问题
我从第一天搭 Agent 就踩了一个坑:日志打得太糙。只记了「用户说了什么」和「Agent 回了什么」,中间发生了什么完全不知道。
后来我查了一些实践资料,总结了一个五问日志模板——每条 Agent 日志必须能回答:
- 谁调用了谁? — Session ID、User ID、Graph ID
- LLM 收到了什么? — 完整 Prompt(含 System Prompt + 历史 + Tool Schema)
- LLM 回复了什么? — Raw Completion 文本
- Agent 决定做什么? — 调了哪个 Tool、参数是什么
- Tool 返回了什么? — Tool 执行结果、状态码、耗时
我写了一个简单的结构化日志函数:
import json, logging, time, uuid
from datetime import datetime
class AgentLogger:
def __init__(self, session_id=None):
self.session_id = session_id or str(uuid.uuid4())
self.logger = logging.getLogger(f"agent.{self.session_id}")
self.steps = []
def log_llm_call(self, prompt, completion, model, latency_ms):
entry = {
"event": "llm_call",
"session_id": self.session_id,
"timestamp": datetime.utcnow().isoformat(),
"model": model,
"latency_ms": latency_ms,
"prompt_preview": prompt[:500],
"completion": completion,
}
self.logger.info(json.dumps(entry, ensure_ascii=False))
self.steps.append(entry)
def log_tool_call(self, tool_name, arguments, result, latency_ms):
entry = {
"event": "tool_call",
"session_id": self.session_id,
"timestamp": datetime.utcnow().isoformat(),
"tool": tool_name,
"arguments": arguments,
"result_preview": str(result)[:300],
"latency_ms": latency_ms,
}
self.logger.info(json.dumps(entry, ensure_ascii=False))
self.steps.append(entry)
每条日志都是一行 JSON,后面用 jq 或者 ELK 都能解析。生产上我直接用 python-json-logger 这个库打结构化日志,省得自己拼字符串。
三、Trace 链路:把每一步串起来
结构化日志解决了「每步发生了什么」的问题,但还有一个更棘手的:怎么把三步前的 LLM 调用和最终的错误答案串起来?
传统的 trace(比如 OpenTelemetry)用 Span 和 Trace ID 搞定。我直接复用这套标准——每个 Agent Session 就是一个 Trace,每次 LLM 调用和 Tool 调用都是一个 Span。
我用的是 LangSmith——LangChain 官方的可观测性平台,对 LangGraph 天然支持。但如果你没用 LangChain 生态,也可以用 OpenTelemetry 的 Python SDK 手动打 Span。
核心代码示例(用 OpenTelemetry 手动 trace):
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor
# 初始化 Tracer
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("agent-inference")
def run_agent_step(user_input, context):
with tracer.start_as_current_span("agent_step") as span:
span.set_attribute("session_id", context.session_id)
span.set_attribute("user_input", user_input)
# LLM 调用
with tracer.start_as_current_span("llm_call") as llm_span:
response = llm.invoke(user_input)
llm_span.set_attribute("model", model_name)
llm_span.set_attribute("latency_ms", elapsed_ms)
llm_span.set_attribute("completion_length", len(response))
# Tool 调用(如果有)
if response.get("tool_calls"):
for tc in response["tool_calls"]:
with tracer.start_as_current_span(f"tool_{tc['function']['name']}") as tool_span:
tool_result = execute_tool(tc)
tool_span.set_attribute("result", str(tool_result)[:200])
span.set_attribute("final_response", final_answer)
这段代码跑起来之后,你能在 Jaeger、Zipkin 或 LangSmith 的可视化面板上看到一棵 Span 树——从用户输入到最终回复,每一层嵌套都看得一清二楚。
我这边的真实效果:之前排查一个「Agent 重复调用同一工具 7 次」的 bug,看了 Trace 面板一秒定位——原来是因为 LLM 返回的 Tool Call 中有一个参数类型错误,工具返回错误后 LLM 又重试,陷入死循环。
四、可复现性:给 Agent 加个「回放」模式
Agent 调试最让我崩溃的是不可复现。同样的 prompt、同样的工具、甚至同样的温度参数,两次跑结果不一样。怎么回放?
我的做法是:记录 LLM 的 seed + temperature + top_p,保存每次调用的完整 completion。
对 OpenAI / Anthropic 类的 API,设置 seed 参数:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7,
seed=42, # 设种子,相同输入尽量出相同输出
logprobs=True, # 记录每个 token 的概率分布,方便分析
)
注意:seed 只能保证 尽力而为的确定性,不是 100%(OpenAI 文档明确说了)。但足够让你调试同一个 bug 时,大概率复现到相似的行为。
我更推荐的方案是 Prompt + Completion 快照——把每次 LLM 调用的完整请求和响应存下来,回放时可以重新跑或者直接看原始输出分析。
import json
from pathlib import Path
class TraceRecorder:
def __init__(self, save_dir="traces/"):
self.save_dir = Path(save_dir)
self.save_dir.mkdir(exist_ok=True)
def snapshot(self, session_id, step_idx, request, response, metadata=None):
record = {
"session_id": session_id,
"step_idx": step_idx,
"timestamp": datetime.utcnow().isoformat(),
"request": {
"model": request.get("model"),
"messages": request.get("messages"),
"temperature": request.get("temperature"),
"seed": request.get("seed"),
"tools": request.get("tools"),
},
"response": {
"id": response.id,
"choices": [
{
"index": c.index,
"finish_reason": c.finish_reason,
"message": c.message.model_dump(),
"logprobs": c.logprobs.model_dump() if c.logprobs else None,
}
for c in response.choices
],
"usage": response.usage.model_dump() if response.usage else None,
},
"metadata": metadata or {},
}
filepath = self.save_dir / f"{session_id}_{step_idx}.json"
filepath.write_text(json.dumps(record, ensure_ascii=False, indent=2))
return filepath
这样每个 Agent 决策步骤都是一个 JSON 文件。回放时重新加载该文件,可以精确复现当时的 Prompt 和 LLM 回复。配合上一步的 Trace,你能把整个会话时间线拉出来逐帧播放。
五、生产环境的部署检查清单
上完前面三个方案,我整理了一个上线前的检查清单,分享出来供参考:
| # | 检查项 | 说明 |
|---|---|---|
| 1 | 结构化的请求日志 | 每步有 Session ID + Timestamp + 事件类型 |
| 2 | LLM Prompt & Completion 快照 | 原始请求与回复存文件,便于回放分析 |
| 3 | Trace 链路(OpenTelemetry / LangSmith) | 可视化 Span 树,快速定位问题在哪一步 |
| 4 | 异常自动告警 | 连续 N 次 Tool 调用失败 / 超时,触发告警 |
| 5 | LLM 调用耗时监控 | P50 / P95 / P99 延迟,异常尖峰预警 |
| 6 | Token 消耗统计 | 按 Session / 用户 / 时间段统计成本 |
| 7 | 确定性 Seed 策略 | 调试环境设固定 seed,生产环境可调随机 |
小结
Agent 的可观测性不是可有可无的锦上添花——没有它,Agent 就是黑盒,出了问题你只能拍脑袋猜。我的核心经验就三条:结构化日志打底、Trace 链路串起每步决策、快照机制保证可复现。这三板斧用上之后,我调试 Agent 的效率提升了不止一个量级。
下一篇我会分享多 Agent 协作架构的设计实践——多个 Agent 怎么分工、怎么通信、怎么避免互相打架。
参考文献
- LangSmith 官方文档 — https://docs.smith.langchain.com/
- OpenTelemetry Python SDK — https://opentelemetry.io/docs/languages/python/
- OpenAI API — 种子参数与确定性 — https://platform.openai.com/docs/guides/text-generation/reproducible-outputs
- LangGraph Debugging & Tracing — https://docs.langchain.com/oss/python/langgraph/troubleshooting/debugging
- Arize AI — LLM Observability — https://docs.arize.com/arize/llm-documentation/overview
- python-json-logger — https://pypi.org/project/python-json-logger/


