☰
【三个月 AI Agent 实战学习】Day 25:Agent 的“思考”过程解析 —— 深入调试与回调
2026/10/3 8:45:02 网站建设 项目流程

Day 25:Agent 的“思考”过程解析 —— 深入调试与回调

欢迎来到第二十五天!昨天我们成功构建了第一个 Agent,并看到它在verbose=True时打印出了思考、行动和观察的轨迹。但作为开发者,我们需要更深入地理解 Agent 的内部运作,以便在出现问题时能够快速定位和修复。今天我们将聚焦于Agent 的思考过程解析,学习如何使用回调函数、日志和中间步骤来追踪 Agent 的每一步推理,并理解输出解析器是如何工作的。这将为你后续构建复杂、可靠的 Agent 打下坚实的调试基础。


一、今日学习目标

  1. 理解 AgentExecutor 的内部循环:它是如何调用 Agent、执行工具、更新 scratchpad 的。
  2. 掌握使用verbose=True之外的调试手段:自定义回调函数(Callback Handlers)来捕获每个步骤的详细信息。
  3. 学会从 AgentExecutor 的结果中提取中间步骤(intermediate_steps),并分析其结构。
  4. 了解 LangChain 的 ReAct 输出解析器的工作原理,以及它如何从模型输出中提取“行动”和“行动输入”。
  5. 能够编写自定义回调,在 Agent 运行过程中记录日志或执行其他操作。

二、详细实现步骤

步骤 1:回顾 AgentExecutor 的执行流程

在 LangChain 中,AgentExecutor的核心循环大致如下:

  1. 初始化agent_scratchpad(空字符串)。
  2. 调用 Agent(Runnable),传入用户输入和当前 scratchpad,获得一个AgentAction或AgentFinish。
  3. 如果是AgentFinish,返回最终答案。
  4. 如果是AgentAction,根据 action 中的工具名称找到对应工具,执行工具,获得观察结果(observation)。
  5. 将 action 和 observation 格式化后追加到 scratchpad 中。
  6. 重复步骤 2-5,直到达到终止条件(获得最终答案或达到最大迭代次数)。

在这个过程中,模型每次看到的提示词都包含了之前的思考、行动和观察,这使它能基于历史进行推理。

步骤 2:使用return_intermediate_steps=True获取中间步骤

昨天我们运行 Agent 时,只获得了最终答案。现在让我们运行同一个 Agent,但开启return_intermediate_steps=True,看看返回结果中的中间步骤长什么样。

继续使用昨天的代码(假设已定义好llm、tools、agent_executor),我们重新运行并提取中间步骤:

result=agent_executor.invoke({"input":"请帮我计算 (12 + 7) * 3,然后搜索一下北京天气"},return_intermediate_steps=True)print("最终答案:",result["output"])print("\n中间步骤:")forstepinresult["intermediate_steps"]:action,observation=stepprint(f"行动:{action.tool},输入:{action.tool_input}")print(f"观察:{observation}")print("-"*40)

运行后,你会看到类似输出:

行动:calculator,输入:'(12 + 7) * 3' 观察:57 ---------------------------------------- 行动:search,输入:'北京天气' 观察:北京今天晴,26°C,湿度 40%。 ----------------------------------------

intermediate_steps是一个列表,每个元素是一个元组(AgentAction, observation)。AgentAction对象包含tool(工具名称)和tool_input(输入参数),而 observation 是工具返回的字符串。这为我们提供了完整的执行轨迹。

步骤 3:深入理解 AgentAction 对象

让我们打印一个AgentAction对象的全部属性:

action=result["intermediate_steps"][0][0]print("类型:",type(action))print("工具名:",action.tool)print("工具输入:",action.tool_input)print("日志:",action.log)# 记录模型输出的原始文本片段

action.log包含了模型在生成该行动时的原始输出文本(通常包括 Thought 和 Action 部分),这对于调试非常有用,因为它显示了模型当时是如何思考的。

步骤 4:使用回调函数记录详细日志

LangChain 提供了回调机制,允许我们在运行过程中插入自定义逻辑。我们可以创建一个自定义回调处理器,在特定事件(如 LLM 调用开始、结束,工具调用开始、结束)时打印信息。

新建debug_agent.py,定义回调类:

fromlangchain_core.callbacksimportBaseCallbackHandlerclassMyCallbackHandler(BaseCallbackHandler):defon_llm_start(self,serialized,prompts,**kwargs):print(f"[LLM Start] 提示词(前100字符):{prompts[0][:100]}...")defon_llm_end(self,response,**kwargs):print(f"[LLM End] 输出:{response.generations[0][0].text[:100]}...")defon_tool_start(self,serialized,input_str,**kwargs):print(f"[Tool Start] 工具:{serialized.get('name')},输入:{input_str}")defon_tool_end(self,output,**kwargs):print(f"[Tool End] 输出:{output}")

然后创建 AgentExecutor 时传入回调:

agent_executor=AgentExecutor(agent=agent,tools=tools,verbose=False,# 关闭默认 verbosecallbacks=[MyCallbackHandler()],return_intermediate_steps=True,max_iterations=5)result=agent_executor.invoke({"input":"计算 5 * 8"})

运行后,你会在控制台看到每个阶段的自定义日志。这比verbose=True更灵活,因为你可以控制日志格式、记录到文件、或发送到监控系统。

步骤 5:理解 ReAct 输出解析器

LangChain 的 ReAct Agent 使用一个解析器(通常是ReActSingleInputOutputParser)来从模型输出中提取AgentAction或AgentFinish。它依赖于正则表达式来匹配Action:和Action Input:等标记。如果模型输出不符合预期格式,解析器会抛出异常,AgentExecutor的handle_parsing_errors=True会捕获这个异常并将其反馈给模型,让模型重新输出。

我们可以手动测试解析器的行为:

fromlangchain.agents.output_parsersimportReActSingleInputOutputParserfromlangchain_core.agentsimportAgentAction,AgentFinish parser=ReActSingleInputOutputParser()# 模拟一个包含 Action 的输出output1=""" Thought: 我需要计算 Action: calculator Action Input: 5 * 8 """parsed1=parser.parse(output1)print(type(parsed1),parsed1)# 模拟一个包含 Final Answer 的输出output2=""" Thought: 我已经算出来了 Final Answer: 40 """parsed2=parser.parse(output2)print(type(parsed2),parsed2)

运行后,你会看到第一个解析结果为AgentAction,第二个为AgentFinish。这有助于你理解解析器的规则,并知道如何调整提示词来让模型输出符合要求。

步骤 6:处理模型输出格式不规范的情况

有时模型会输出多余的文字、错误的标记(如使用“行动”而不是“Action”),或者将输入放在多行。我们可以通过以下方式改善:

  • 在提示词中提供更明确的示例。
  • 使用正则表达式更宽松的解析器(自定义)。
  • 降低温度至 0,提高格式遵循度。
  • 在AgentExecutor中设置handle_parsing_errors=True,让框架自动修复。

我们尝试故意制造一个格式错误(例如修改提示词,不提供示例),观察 AgentExecutor 如何处理:

# 创建一个不包含格式要求的简单提示词bad_prompt=PromptTemplate.from_template("你是一个助手,回答问题:{input}。你可以使用工具:{tools}。思考:{agent_scratchpad}")bad_agent=create_react_agent(llm,tools,bad_prompt)bad_executor=AgentExecutor(agent=bad_agent,tools=tools,verbose=True,handle_parsing_errors=True,max_iterations=3)result=bad_executor.invoke({"input":"计算 3 + 5"})print(result["output"])

观察到模型输出可能不包含Action:标记,导致解析失败,然后handle_parsing_errors会反馈错误,模型可能重新输出,最终成功。这个实验能加深你对解析错误处理的理解。


三、常见问题与调试

Q1:如何查看 Agent 每一步的完整提示词?
→ 使用自定义回调on_llm_start打印prompts,或者在verbose=True时观察控制台输出(LangChain 会打印提示词)。更高级的调试可以使用 LangSmith(后续会介绍)。

Q2:AgentAction中的log字段是什么?
→log字段记录了生成该 Action 时模型的原始输出文本,包括 Thought、Action、Action Input 等。这对于回溯模型推理过程非常有用。

Q3:我想自定义输出解析器,应该怎么做?
→ 可以继承AgentOutputParser并实现parse方法,返回AgentAction或AgentFinish。然后在create_react_agent中传入自定义解析器(使用output_parser参数)。但通常默认解析器已经足够,除非你有特殊的格式要求。

Q4:handle_parsing_errors=True是否会掩盖真正的问题?
→ 它只会处理解析错误,并将错误信息反馈给模型重新生成。如果模型持续输出错误格式,可能导致死循环(受max_iterations限制)。如果频繁出现解析错误,应优先改进提示词或降低温度。

Q5:能否将中间步骤保存到数据库或日志文件?
→ 使用自定义回调,在on_tool_end中将工具调用信息写入日志文件或数据库。这是生产环境中的常用做法。

Q6:LangSmith 是什么?我们需要用吗?
→ LangSmith 是 LangChain 官方的调试和监控平台,可以可视化 Agent 的每个步骤、Token 消耗、延迟等。需要注册并获取 API Key,配置后即可自动追踪。今天我们使用自定义回调,但 LangSmith 功能更强大。后续可以尝试。


四、今日总结与作业

今天你完成了:

  • ✅ 通过return_intermediate_steps=True获取并分析了 Agent 的中间步骤。
  • ✅ 深入了解了AgentAction和AgentFinish对象的结构。
  • ✅ 编写了自定义回调处理器,实现了细粒度的日志记录。
  • ✅ 理解了 ReAct 输出解析器的工作原理,并体验了解析错误处理。
  • ✅ 学会了如何调试 Agent 的推理过程。

今日作业(必做):

  1. 使用自定义回调记录日志,运行三个不同的问题,并将日志保存到文件中(例如使用 Python 的logging模块)。分析每个问题的执行步骤和 Token 消耗(如果可以获取)。
  2. 修改 Agent 的提示词,故意去掉格式示例,观察 Agent 是否会出现解析错误,以及handle_parsing_errors如何帮助恢复。记录你的观察。
  3. 尝试手动调用解析器,解析你自己写的一段包含多个换行和多余文字的模型输出,看看解析器能否正确识别 Action,如果不能,分析原因并考虑如何改进提示词。
  4. 思考:在构建生产级 Agent 时,你需要记录哪些信息以便于故障排查?请列出至少 5 项。

明日预告:我们将继续深入 Agent 的工程化,学习如何编写高质量的工具描述,以及如何处理工具调用中的错误,让 Agent 更加健壮。

有任何问题欢迎随时提问!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询