跳到主要内容

通用多步 ReAct 循环(MultiStepAgent)

本章讲所有 agent 共用的骨架:一次 run 怎么被拆成一步步,记忆怎么回灌成对话,规划步怎么插进来,循环怎么终止。CodeAgent 和 ToolCallingAgent 都继承它,只是「一步怎么行动」不同——那部分留到 02

1. 它要解决的小问题

单次模型调用做不了多步任务。你需要一个循环:让模型说一步、系统执行一步、把结果喂回、再问下一步,直到模型宣布完成。这套「Reason(想)→ Act(做)→ Observe(看)」反复迭代的模式,业界叫 ReActMultiStepAgent 就是 ReAct 的通用实现(src/smolagents/agents.py:268,MultiStepAgent)。

2. 思路/直觉

把骨架和「行动」解耦:

  • 骨架(本章) 管:排第几步、要不要先规划、把记忆变成模型输入、捕获错误、判断到没到 max_steps、什么时候算拿到最终答案。
  • 「一步怎么行动」 是一个抽象方法 _step_stream(agents.py:772),由子类填。CodeAgent 填「写代码 + 跑代码」,ToolCallingAgent 填「解析 JSON 工具调用 + 执行」。

整个循环用 Python 生成器(generator) 驱动:每步产出的东西(流式 token、工具调用、行动结果、这一步的记录)都 yield 出来。这样同一套代码既能「一次跑到底只要最后答案」,也能「边跑边把中间过程吐给调用方」。

3. 图示:一次 run 的分层

怎么读:外层 run 决定「要不要流式」,真正干活的是内层生成器 _run_stream;它每轮先看要不要规划,再跑一个行动步。

run(task, stream=False) agents.py:436
│ 重置记忆/监控,把 task 存成 TaskStep
│ 给执行器灌初始变量与工具
│ stream=True? → 直接把生成器还给调用方
│ stream=False? → list(...) 把生成器跑干,取最后一个 FinalAnswerStep

_run_stream(task, max_steps) agents.py:540
while 未拿到最终答案 且 step_number ≤ max_steps:
├─ 到规划间隔? → _generate_planning_step(...) ← 见 §6
├─ 新建 ActionStep(step_number, 计时)
├─ for out in _step_stream(action_step): ← 子类实现「怎么行动」
│ yield out
│ 若 out 是「最终答案」→ 记下,置 returned_final_answer
├─ 捕获 AgentError:记到 action_step.error(不中断,下一轮重试)
└─ finally:落盘这一步进记忆,step_number += 1
末尾:yield FinalAnswerStep(最终答案)

4. 原理演示(简化,建立直觉)

下面这段把循环骨架的精神演出来。重点看:子类只需实现 act_one_step,骨架负责重复、捕错、判终止。

# 示意,非源码
def run(task):
memory.add_task(task)
step = 1
while step <= max_steps:
messages = memory.to_messages() # 记忆 → 对话
try:
output, is_final = act_one_step(messages) # 子类实现这一步
except AgentError as e:
memory.add_error(e) # 出错不崩,记下来下轮重试
step += 1
continue
memory.add_observation(output) # 结果回写记忆
if is_final:
return output # 模型宣布完成
step += 1
return provide_final_answer(task) # 用尽步数,强行让模型收个尾

5. 真实实现:一步的生命周期

循环主体在 _run_stream(agents.py:540)。 关键几处:

  • 每步先把记忆变成消息:子类的 _step_stream 开头调 write_memory_to_messages()(agents.py:758)。它把 system_prompt + 所有记忆步各自 to_messages() 拼成一条对话历史(细节见 05-models-and-memory.md)。
  • 错误分两类处理(agents.py:594-599):AgentGenerationError(实现层/代码 bug)直接抛出退出;其它 AgentError(多半是模型犯错)记进 action_step.error,不中断——下一轮这条错误会被 to_messages() 拼进上下文,提示模型「别再犯同样的错」(memory.py:138-148)。
  • finally 保证每步都落盘(agents.py:600-604):无论成败,这一步的 ActionStep 都会被 append 进 self.memory.stepsyield 出去,step_number 自增。
  • 终止判定:子类 _step_stream 产出的 ActionOutput.is_final_answer 为真时,循环置位退出(agents.py:582-592)。

用尽步数的兜底:若循环走完仍没拿到最终答案,_handle_max_steps_reached(agents.py:625)会调 provide_final_answer(agents.py:810)——把整段历史再喂一次模型,逼它基于已有信息给个答案,并把这步标成 AgentMaxStepsError

6. 关键细节:规划步(planning)

如果构造 agent 时传了 planning_interval,循环会周期性插入一个「规划步」:第 1 步一定规划,之后每隔 planning_interval 步再规划一次(agents.py:550-551)。

规划步做什么(_generate_planning_step,agents.py:639):

  • 首次:用 planning.initial_plan 模板让模型先列「已知事实 + 行动计划」,不进入执行。
  • 后续:先用 write_memory_to_messages(summary_mode=True) 拿一份「精简历史」,再让模型基于进展更新计划summary_mode 会丢掉系统提示和上一版计划,避免旧计划过度锚定新思考(agents.py:682-684,memory.py:174-183)。

直觉:规划步是给 agent 的「停下来想想大局」时刻,和干活的行动步交替进行。

7. 关键细节:两种运行模式共用一套生成器

runstream 参数只决定「怎么消费生成器」,不改变循环逻辑(agents.py:494-503):

模式行为返回
stream=True_run_stream(...) 生成器原样返回,调用方自己迭代生成器(逐步产出)
stream=False内部 list(...) 把生成器跑干,断言最后一个是 FinalAnswerStep最终答案(或 RunResult)

return_full_result=True,run 还会汇总各步 token 用量、状态(success/max_steps_error)打包成 RunResult(agents.py:505-536)。

8. 边界与坑

  • 错误即上下文:模型犯的错不会让 run 崩,而是变成下一轮的提示。这让 agent 有「自我纠错」能力,但也意味着一个反复失败的任务会烧掉全部 max_steps
  • interrupt():外部可置 interrupt_switch 让循环在下一轮开头抛出中断(agents.py:546-547754)。
  • 记忆跨 run 可续:run(reset=False) 不清空记忆,可在上一轮基础上继续(agents.py:478-480)。

9. 代码地图

主题文件符号
循环骨架基类src/smolagents/agents.pyMultiStepAgent
外层入口(决定流式与否)src/smolagents/agents.pyMultiStepAgent.run
真正的 while 循环src/smolagents/agents.pyMultiStepAgent._run_stream
一步怎么行动(抽象)src/smolagents/agents.pyMultiStepAgent._step_stream
记忆回灌成消息src/smolagents/agents.pyMultiStepAgent.write_memory_to_messages
规划步src/smolagents/agents.pyMultiStepAgent._generate_planning_step
用尽步数兜底src/smolagents/agents.py_handle_max_steps_reached / provide_final_answer
一步的记录对象src/smolagents/memory.pyActionStep