Agent系统与传统单次调用的程序不同,它的执行过程是一条动态决策链:模型思考、选择工具、执行工具、观察结果、再思考。这条链路上任何一环出错,都可能导致最终答案偏离预期。而更麻烦的是,Agent的中间过程往往被框架封装在内部,只暴露最终输出,一旦结果不对,你很难知道是模型推理错了、工具参数传错了,还是上下文被截断了。因此,日志记录与中间状态查看是Agent开发中不可或缺的两大调试利器。

一、为什么Agent调试比普通程序更难
普通程序的执行路径是确定的,设个断点就能看到每一步的变量值。而Agent的执行路径由大模型在运行时决定,同一段代码每次运行可能走完全不同的分支:第一次调用搜索工具,第二次直接回答,第三次陷入循环调用。这种不确定性使得传统的断点调试效率低下。
此外,Agent出错的表现形式往往具有很强的迷惑性。例如模型明明拿到了正确的历史消息,却给出了错误的工具调用参数;或者工具返回了正确结果,但模型在下一轮推理中忽略了它。这类问题不查看中间状态根本无从判断。常见的问题类型包括:
- 上下文丢失:历史消息过长被截断,导致模型忘记任务目标
- 参数幻觉:模型编造了工具不存在的参数名或传入了错误格式的值
- 循环调用:模型反复调用同一个工具,没有从结果中提取有效信息
- 提示词污染:工具返回的内容干扰了模型的推理方向
要定位这些问题,必须让Agent的每一步都留下痕迹。这就要靠日志体系和状态快照两个手段。
二、搭建Agent结构化日志体系
最直接的方式是使用Python标准库logging,但Agent场景对日志有特殊要求:你需要同时记录模型输入输出、工具调用细节和最终结果,并且最好以结构化格式存储,方便后续过滤分析。下面是一个完整的示例,展示如何为一个自研ReAct风格Agent加上分层日志。
import logging
import json
# 配置结构化日志
logger = logging.getLogger("agent")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler("agent_debug.log", encoding="utf-8")
formatter = logging.Formatter(
'%(asctime)s | %(levelname)s | %(name)s | %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
def log_json(event: str, **kwargs):
"""以JSON格式记录事件,便于后续用脚本分析"""
payload = {"event": event}
payload.update(kwargs)
logger.info(json.dumps(payload, ensure_ascii=False, default=str))
class DebuggableAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = {t.name: t for t in tools}
self.messages = []
def run(self, user_input: str, max_steps: int = 8):
self.messages.append({"role": "user", "content": user_input})
log_json("user_input", text=user_input)
for step in range(max_steps):
# 记录发送给模型的完整上下文
log_json("llm_request", step=step, messages=self.messages)
response = self.llm.chat(self.messages)
# 记录模型的原始返回,这是排查问题的关键
log_json("llm_response", step=step, raw=response)
action = self.parse_action(response)
if action is None:
log_json("final_answer", step=step, answer=response)
return response
tool_name = action["tool"]
tool_args = action["args"]
if tool_name not in self.tools:
log_json("tool_not_found", step=step, tool=tool_name)
observation = f"错误:工具 {tool_name} 不存在"
else:
# 记录工具入参,参数幻觉问题在这里现形
log_json("tool_call", step=step, tool=tool_name, args=tool_args)
try:
observation = self.tools[tool_name].run(**tool_args)
except Exception as e:
log_json("tool_error", step=step, tool=tool_name,
error=str(e))
observation = f"工具执行失败:{e}"
# 记录工具返回结果
log_json("tool_result", step=step, tool=tool_name,
observation=observation)
self.messages.append(
{"role": "user", "content": f"观察结果:{observation}"}
)
log_json("max_steps_reached", max_steps=max_steps)
return "已达最大步数,任务未完成"
这套日志体系的核心设计在于三个记录点:llm_request记录发送给模型的完整上下文,llm_response记录模型的原始输出,tool_call与tool_result记录工具的入参和返回。其中llm_request尤其重要,它能让你直观看到上下文是如何一步步增长的,从而发现消息被截断或格式混乱的问题。
相比直接使用print,logging方案有三个显著优势:一是支持日志级别,调试期开DEBUG、上线后只保留WARNING以上;二是可以同时输出到文件和控制台,便于长期留存排查;三是JSON格式可以用简单的Python脚本做统计分析,例如统计某个工具被调用的频率,快速定位循环调用问题。
三、中间状态查看与回放调试
日志是流式的,适合看过程;而中间状态是快照式的,适合看某一时刻Agent的完整内部情况。对于复杂的Stateful Agent,建议在每个关键节点dump一次状态,保存为一个可回放的轨迹文件。
import json
from datetime import datetime
class StateRecorder:
"""记录Agent每一步的完整中间状态,支持回放"""
def __init__(self, session_id: str):
self.session_id = session_id
self.trajectory = []
def capture(self, step: int, phase: str, state: dict):
snapshot = {
"session": self.session_id,
"time": datetime.now().isoformat(),
"step": step,
"phase": phase, # thinking / tool_calling / observing
"state": {
"messages": state.get("messages", []),
"memory": state.get("memory", {}),
"plan": state.get("plan", ""),
"pending_actions": state.get("pending_actions", []),
},
}
self.trajectory.append(snapshot)
def save(self, filename: str):
with open(filename, "w", encoding="utf-8") as f:
json.dump(self.trajectory, f, ensure_ascii=False, indent=2)
@classmethod
def replay(cls, filename: str):
"""回放轨迹,逐步检查中间状态"""
with open(filename, encoding="utf-8") as f:
trajectory = json.load(f)
for snap in trajectory:
print(f"--- 第{snap['step']}步 [{snap['phase']}] ---")
print(json.dumps(snap["state"], ensure_ascii=False, indent=2))
使用时在Agent的每个阶段调用capture方法,任务结束后save保存轨迹。当结果不符合预期时,用replay逐步检查:先看thinking阶段模型的计划和推理是否合理,再看tool_calling阶段传入的参数是否正确,最后看observing阶段模型是否正确消化了工具返回。这种逐帧回放的方式,几乎可以覆盖所有类型的Agent问题定位。
对于多Agent协作系统,状态记录还要注意保存Agent之间的消息传递。可以在消息总线层面统一拦截,为每条消息打上发送方、接收方和时间戳,这样当子Agent给出错误结论时,可以追溯到它收到了哪些上游信息。
四、基于框架回调机制的调试方案
如果使用LangChain或LangGraph这类框架,不必自己造轮子,它们提供了回调机制可以直接接入日志体系。以LangChain为例,自定义一个回调处理器即可捕获所有模型调用和工具执行事件。
from langchain_core.callbacks import BaseCallbackHandler
class AgentDebugHandler(BaseCallbackHandler):
"""捕获Agent执行过程中的所有关键事件"""
def on_llm_start(self, serialized, prompts, **kwargs):
print(f"[LLM开始] 输入提示:{prompts}")
def on_llm_end(self, response, **kwargs):
print(f"[LLM结束] 输出内容:{response.generations}")
def on_tool_start(self, serialized, input_str, **kwargs):
print(f"[工具开始] 名称:{serialized.get('name')},"
f"参数:{input_str}")
def on_tool_end(self, output, **kwargs):
print(f"[工具结束] 返回:{output}")
def on_tool_error(self, error, **kwargs):
print(f"[工具异常] {error}")
# 使用方式:传入callbacks参数
# agent.invoke(
# {"input": "查询北京天气"},
# config={"callbacks": [AgentDebugHandler()]}
# )
回调方案的优点是与业务代码完全解耦,不需要改动Agent核心逻辑,加一行配置即可开启。LangGraph还提供了更强大的get_state方法,可以在执行的任意节点读取图的当前状态,配合中断机制,实现类似断点调试的效果。
另外值得推荐的是LangSmith这类可视化追踪平台,它能自动记录每一步的输入输出并生成依赖图,适合团队协作场景。不过对于个人开发或对数据隐私敏感的场景,本地日志加状态快照的组合已经足够应对绝大多数调试需求。
五、调试实践中的经验总结
综合来看,高效的Agent调试需要日志、状态、回放三层手段配合:日志保证过程可追溯,状态快照保证内部可见,回放机制保证问题可复现。落地时有几点经验值得注意。第一,日志要记录模型的原始返回而非解析后的结果,很多问题恰恰出在解析环节。第二,工具调用的参数务必完整记录,参数幻觉是最高频的问题类型。第三,给每次会话分配唯一ID贯穿所有日志,避免多个请求的日志混在一起。第四,长上下文场景下要定期检查llm_request中的消息列表长度,防止静默截断。
最后提醒一点:调试日志本身也可能成为性能负担和信息泄露风险。生产环境应控制日志级别,对用户敏感信息脱敏,并设置合理的日志保留周期。开发期详细、上线后精简,才是日志体系的正确打开方式。