强烈推荐收藏!AI Agent调试从入门到生产:从print到LangFuse全链路可观测性
·
强烈推荐收藏!AI Agent 调试从入门到生产:从 print 到 LangFuse 全链路可观测性——4大调试层级+8个真实Bug案例
Agent 开发的噩梦:LLM 选错工具了、循环调了 14 次、Token 烧了 $15 什么都没做出来。更糟的是——你根本不知道它内部发生了什么。print 打出来的是 Token 序列堆,根本读不懂。本文用 4 层调试架构 + LangFuse 全链路追踪 + 8 个真实 Bug 案例,让你从「盲飞」变成「上帝视角」。
一、为什么 print 对 Agent 不管用
1.1 传统调试 vs Agent 调试
# 传统程序:print 够用
def calculate(a, b):
result = a + b
print(f"a={a}, b={b}, result={result}") # 清晰
return result
# Agent:print 完全不够
def agent_loop(user_input):
response = llm.chat(messages) # 1000+ Token 的思维链
print(response) # 一大坨文本,关键信息淹没在 3000 字里
tool_result = execute(tool_call) # 工具返回 500 Token
print(tool_result) # 又是 500 字
# 问题:你不知道 LLM 为什么选这个工具、花了多少 Token、这一步的耗时
1.2 Agent 调试的四个维度
一个完整的 Agent 调试体系需要覆盖:
┌─────────────────────────────────────────┐
│ Agent 调试四层金字塔 │
├─────────────────────────────────────────┤
│ L4 成本层:Token 用了多少?花了多少钱? │
│ L3 循环层:Agent 走了几步?有没有死循环? │
│ L2 工具层:调了什么工具?参数是什么?结果? │
│ L1 代码层:传统异常、类型错误、变量状态 │
└─────────────────────────────────────────┘
二、L1 代码层:结构化日志替代 print
2.1 给 Agent 加有意义的日志
import logging
import time
from functools import wraps
logger = logging.getLogger("agent")
def log_step(step_name: str):
"""装饰器:自动记录每一步的耗时"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start = time.time()
try:
result = await func(*args, **kwargs)
elapsed = time.time() - start
logger.info(
f"[{step_name}] ✓ {elapsed:.2f}s",
extra={"step": step_name, "elapsed": elapsed, "status": "success"}
)
return result
except Exception as e:
elapsed = time.time() - start
logger.error(
f"[{step_name}] ✗ {elapsed:.2f}s: {e}",
extra={"step": step_name, "elapsed": elapsed, "status": "error", "error": str(e)}
)
raise
return wrapper
return decorator
# 使用
@log_step("LLM推理")
async def llm_think(messages):
return await client.chat.completions.create(model="gpt-4o", messages=messages)
@log_step("工具执行")
async def execute_tool(name, args):
func = available_tools[name]
return await func(**args)
输出效果:
2026-05-20 14:23:01 [LLM推理] ✓ 1.23s
2026-05-20 14:23:03 [工具执行] ✓ 0.45s → get_weather({"city":"北京"})
三、L2 工具层:追踪每一次 Tool Call
3.1 记录完整的工具调用链
from dataclasses import dataclass, field
from datetime import datetime
import json
@dataclass
class ToolTrace:
tool_name: str
arguments: dict
result: str
start_time: datetime
end_time: datetime
success: bool
error: str = ""
@dataclass
class AgentTrace:
session_id: str
steps: list[ToolTrace] = field(default_factory=list)
total_cost: float = 0.0
total_tokens: int = 0
class TraceableAgent:
def __init__(self):
self.trace = AgentTrace(session_id=generate_session_id())
async def execute_tool(self, tool_call):
trace = ToolTrace(
tool_name=tool_call.function.name,
arguments=json.loads(tool_call.function.arguments),
start_time=datetime.now(),
end_time=None,
success=False,
result=""
)
try:
func = self.tools[trace.tool_name]
trace.result = await func(**trace.arguments)
trace.success = True
except Exception as e:
trace.success = False
trace.error = str(e)
trace.result = f"错误:{e}"
finally:
trace.end_time = datetime.now()
self.trace.steps.append(trace)
# 实时输出给人看
icon = "✓" if trace.success else "✗"
elapsed = (trace.end_time - trace.start_time).total_seconds()
print(f" {icon} {trace.tool_name}({trace.arguments}) → {elapsed:.2f}s")
return trace.result
3.2 可视化工具调用链
def render_agent_trace(trace: AgentTrace):
"""把 Agent 的执行轨迹渲染成可读的文本"""
print(f"\n{'='*60}")
print(f"Agent 执行报告 | Session: {trace.session_id}")
print(f"{'='*60}")
print(f"总步骤数: {len(trace.steps)}")
print(f"总 Token: {trace.total_tokens:,}")
print(f"总成本: ${trace.total_cost:.4f}")
print()
for i, step in enumerate(trace.steps, 1):
status = "✅" if step.success else "❌"
print(f"步骤 {i} {status} {step.tool_name}")
print(f" 参数: {json.dumps(step.arguments, ensure_ascii=False)}")
print(f" 结果: {step.result[:150]}{'...' if len(step.result)>150 else ''}")
if step.error:
print(f" 错误: {step.error}")
elapsed = (step.end_time - step.start_time).total_seconds()
print(f" 耗时: {elapsed:.3f}s")
print()
四、L3 循环层:防止死循环和疯狂烧钱
4.1 循环监控器
class LoopMonitor:
"""监控 Agent 循环,检测异常"""
def __init__(self, max_steps: int = 10, max_cost: float = 1.0):
self.max_steps = max_steps
self.max_cost = max_cost
self.action_history = []
self.total_cost = 0.0
self.warnings = []
def record(self, step: int, tool_name: str, args: dict, cost: float):
self.action_history.append({
"step": step, "tool": tool_name, "args": args, "cost": cost
})
self.total_cost += cost
def check(self, step: int) -> str:
"""返回状态:ok / warn / abort"""
# 检查 1:步数超限
if step >= self.max_steps:
return "abort:steps"
# 检查 2:成本超限
if self.total_cost > self.max_cost:
return "abort:cost"
# 检查 3:连续相同动作(死循环检测)
if len(self.action_history) >= 3:
recent = self.action_history[-3:]
if len(set((a["tool"], str(a["args"])) for a in recent)) == 1:
self.warnings.append(f"步骤 {step}: 连续 3 次调用 {recent[0]['tool']}")
return "warn:loop"
return "ok"
# 集成到 Agent 中
monitor = LoopMonitor(max_steps=8, max_cost=0.5)
for step in range(max_steps):
status = monitor.check(step)
if status.startswith("abort"):
print(f"🛑 {status}")
messages.append({
"role": "system",
"content": "任务已被中止。" + (
"超过了最大步骤数。" if "steps" in status
else "Token 成本超过了预算。请立即给出你能给出的最佳答案。"
)
})
break
if status.startswith("warn"):
print(f"⚠️ {monitor.warnings[-1]}")
messages.append({
"role": "system",
"content": "你连续调用了相同的工具。请尝试不同的方法解决问题。"
})
4.2 成本实时追踪
# 每次 LLM 调用后更新成本
def estimate_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float:
PRICES = {
"gpt-4o": (0.0025, 0.01), # 输入/输出 每千Token
"claude-sonnet": (0.003, 0.015),
"deepseek-chat": (0.00014, 0.00028), # ¥/千Token
}
input_price, output_price = PRICES.get(model, (0, 0))
return (prompt_tokens * input_price + completion_tokens * output_price) / 1000
# 每次 LLM 调用后
monitor.record(step, tool_call.function.name, args,
estimate_cost(model, usage.prompt_tokens, usage.completion_tokens))
五、L4 可观测层:LangFuse 全链路追踪
5.1 为什么需要 LangFuse
print → 知道当前值
logging → 知道历史值
LangFuse → 知道"为什么 Agent 这样决策" + "哪个版本的 Agent 最好"
5.2 LangFuse 接入
pip install langfuse openai
from langfuse import Langfuse
from langfuse.decorators import observe, langfuse_context
langfuse = Langfuse(
public_key="pk-lf-...",
secret_key="sk-lf-...",
host="https://cloud.langfuse.com" # 或自部署
)
@observe(as_type="generation")
async def llm_call(messages: list, model: str = "gpt-4o"):
"""LLM 调用 - 自动记录 input/output/tokens/cost"""
response = await client.chat.completions.create(
model=model, messages=messages
)
return response
@observe()
async def agent_step(step_num: int, user_input: str):
"""Agent 单步 - 自动记录整个步骤的耗时和嵌套调用"""
langfuse_context.update_current_trace(
name=f"agent-step-{step_num}",
metadata={"step": step_num}
)
# LLM 推理
response = await llm_call(messages)
# 工具调用
if response.choices[0].message.tool_calls:
for tc in response.choices[0].message.tool_calls:
langfuse_context.update_current_observation(
metadata={
"tool": tc.function.name,
"args": tc.function.arguments
}
)
result = await execute_tool(tc)
return response
@observe()
async def agent_session(session_id: str, user_input: str):
"""Agent 完整会话 - 顶层 Trace"""
langfuse_context.update_current_trace(
name=f"session-{session_id}",
user_id="user-123",
session_id=session_id,
tags=["production", "agent-v2.1"]
)
for step in range(MAX_STEPS):
result = await agent_step(step, user_input)
if not result.choices[0].message.tool_calls:
break
# 打分(可选:用于后续评估)
langfuse.score(name="task_completed", value=1.0)
LangFuse 能看到的:
- 每个 Trace 的完整调用树(LLM→Tool→LLM→Tool→…)
- 每一步的耗时、Token 用量、成本
- 哪个版本 Prompt 效果最好
- 哪些用户遇到了最多的错误
5.3 自部署 LangFuse(无需付费)
# Docker Compose 一键部署
git clone https://github.com/langfuse/langfuse.git
cd langfuse
docker compose up -d
# 访问 http://localhost:3000
六、真实 Bug 案例库
Bug 1:LLM 选择了不存在的工具
症状:Agent 返回 "调用 send_sms",但工具列表里根本没有 send_sms
原因:System Prompt 太长,LLM 混淆了可用工具和示例工具
修复:工具列表不要超过 8 个;如果多,做工具分类路由
# 防御代码
if tool_name not in registered_tools:
return json.dumps({
"error": f"工具 {tool_name} 不存在。可用工具:{list(registered_tools.keys())}"
})
Bug 2:Agent 连续重试同一个失败操作
症状:Agent 连调 14 次 read_file,每次都失败,每次都重试
原因:LLM 对错误信息的理解偏差——它认为是「还没成功」而不是「永远不可能成功」
修复:LoopMonitor 检测 3 次连续相同操作 → 强制注入「请换方法」提示
Bug 3:上下文悄悄溢出
症状:Agent 跑了 20 轮后突然「失忆」,忘记 System Prompt
原因:对话历史 + 工具返回 > Context Window,最前面的 System Prompt 被截断
修复:每 5 轮自动估算 Token 量,超过 80% 阈值时压缩早期对话
threshold = model_context_window * 0.8
if estimate_tokens(messages) > threshold:
messages = compress_early_messages(messages)
Bug 4:并行工具调用顺序错乱
症状:查了 10 个城市的天气,返回结果和城市对不上
原因:异步并行 + tool_call_id 匹配错误
修复:用 tool_call_id 精确匹配,不要用索引
Bug 5:流式输出中的工具调用被截断
症状:SSE 流中 tool_call 的 arguments JSON 被截断,解析失败
原因:流式传输时,tool_call 的参数可能跨多个 chunk
修复:缓存未完成的 tool_call,等全部到齐后再解析
Bug 6:Token 成本偷偷飙升
症状:月账单从 $60 涨到 $400,代码没改过
原因:新版本 Prompt 多了 500 Token,每天 1000 次调用 = 多 50 万 Token/天
修复:每次改 Prompt 时,估算 Token 增量 × 日均调用量 = 月增量成本
Bug 7:不同模型对 JSON 格式的理解不同
症状:GPT-4 完美输出 JSON,DeepSeek 加了 "```json" 包装
原因:不同模型的输出习惯不同
修复:解析前做格式清洗
import re
def safe_json_parse(text: str) -> dict:
text = text.strip()
# 去掉 markdown 代码块包装
text = re.sub(r'^```(?:json)?\s*', '', text)
text = re.sub(r'\s*```$', '', text)
return json.loads(text)
Bug 8:LangFuse 在生产环境的内存泄漏
症状:LangFuse SDK 默认在内存中缓存所有事件,高并发下 OOM
修复:设置 flush 间隔和最大批次大小
langfuse = Langfuse(
flush_at=5, # 每 5 个事件 flush 一次
flush_interval=10, # 或每 10 秒 flush 一次
enabled=True
)
七、调试 Checklist
□ 代码层
□ 每个函数入口/出口有结构化日志
□ 异常被捕获并记录了上下文
□ 关键变量在日志中可见
□ 工具层
□ 每个 tool_call 记录了工具名 + 参数 + 结果 + 耗时
□ 工具不存在时有明确的错误返回
□ 工具调用之间有依赖关系记录
□ 循环层
□ max_steps 限制
□ 成本预算上限
□ 连续相同操作检测(死循环防护)
□ Context 溢出监控
□ 可观测层
□ LangFuse 或 LangSmith 接入
□ 每次部署有版本标签
□ 用户反馈有分数回传
□ Token 用量有日报
八、总结
| 层级 | 工具 | 解决什么问题 |
|---|---|---|
| L1 代码层 | 结构化日志 + 装饰器 | 异常定位、变量状态 |
| L2 工具层 | ToolTrace + 调用树 | 工具选对没、参数对没 |
| L3 循环层 | LoopMonitor | 死循环、成本失控 |
| L4 可观测层 | LangFuse | 全局视角、A/B 对比、历史回放 |
Agent 调试和传统程序调试的根本区别:你调试的不是一段代码,而是一个在不确定环境中自主决策的系统。print 能看到代码状态,但看不到 LLM 的决策逻辑。LangFuse 让你同时看到两者。
标签:#Agent调试 #LangFuse #可观测性 #AI工程 #生产实践 #程序员必读
更多推荐
所有评论(0)