跳到主要内容

智能体核心:BaseAgent 的运行循环、状态机、记忆

这章讲什么: 所有 OpenManus 的 agent 都继承自 BaseAgent。这一层不关心"用哪个 LLM、调哪个工具",只负责一件事——把 agent 反复推进一步一步执行,并在合适的时候停下来。理解它,就理解了"agent 到底在循环什么"。


1. 它要解决的小问题

LLM 一次只能回一段话。可"完成一个任务"往往要好多步:想一下、做一下、看结果、再想、再做……谁来管这个反复推进的循环?谁来决定什么时候停? 这就是 BaseAgent 的职责。

它把 agent 抽象成一台状态机 + 步进循环

  • 状态机:agent 任一时刻处于四种状态之一。
  • 步进循环:只要还在跑、没到上限,就反复执行"一步"(step()),具体一步做什么留给子类。

2. 四态状态机

agent 的生命就在这四个状态间流转:

状态含义
IDLE空闲,还没开始(初始态)
RUNNING正在循环执行
FINISHED任务完成,正常收尾
ERROR出错

定义见 app/schema.py:32AgentState

2.1 状态切换靠上下文管理器(巧妙处)

状态不是随手赋值的,而是用一个 async with 上下文管理器 state_context 来"临时切换、自动还原":进入时切到新状态,正常退出还原旧状态,一旦抛异常就自动落到 ERROR

# 示意,非源码:state_context 的核心逻辑
previous = self.state
self.state = new_state # 进入:临时切换
try:
yield # 在新状态下干活
except Exception:
self.state = AgentState.ERROR # 出错 → 自动进 ERROR
raise
finally:
self.state = previous # 无论如何还原旧状态

真实实现见 app/agent/base.py:58BaseAgent.state_context重点看:错误状态是"异常自动触发"的,不用在每个可能出错的地方手写 state = ERROR


3. 运行循环:run()

这是整个骨架的心脏。BaseAgent.runapp/agent/base.py:116)逻辑很短,值得吃透:

run(request):
1. 若不是 IDLE 就报错(不能重入)
2. 有 request 就存进 memory(user 消息)
3. with state_context(RUNNING):
while 当前步数 < max_steps 且 状态 ≠ FINISHED:
current_step += 1
step_result = await step() # ← 子类实现的「一步」
if is_stuck(): handle_stuck_state() # 卡死检测
记录 step_result
若跑满 max_steps:归零、状态回 IDLE、追加「Terminated」
4. 清理沙箱
5. 返回每一步结果拼成的字符串

三个关键设计:

  • 两个出口条件current_step < max_steps(防止无限循环烧钱)和 state != FINISHED(任务干完就停)。max_steps 默认 10,Manus 覆盖成 20(app/agent/manus.py:28)。
  • step() 是抽象方法app/agent/base.py:156),本层不实现——"循环怎么转"和"一步做什么"被干净地分开了。
  • 收尾必清理:循环结束后调 SANDBOX_CLIENT.cleanup()ToolCallAgent.run 还在 finally 里额外 cleanup() 工具(app/agent/toolcall.py:245)。

4. 记忆:Memory 就是一个消息列表

agent 的"记忆"没有玄学——就是一个 Message 列表(app/schema.py:159Memory)。每一步的想法、工具结果、用户输入,都作为一条 Message 追加进去,下一轮 think 时整个列表发给 LLM。

  • 消息有四种角色system / user / assistant / toolapp/schema.py:7Role)。
  • update_memoryapp/agent/base.py:84 用一张 message_map 把 role 映射到对应的构造方法(Message.user_message 等),统一入口写记忆。
  • 有上限Memory.max_messages 默认 100,超了就只留最近 100 条app/schema.py:167)——一种最朴素的"记忆窗口"。

直觉:把 Memory 当成对话历史。agent 每"想"一次,就是把这份历史整个念给 LLM 听,让它接着往下说。


5. 卡死检测(一个实用小设计)

LLM 有时会"鬼打墙"——连续给出一模一样的回答,陷入死循环。OpenManus 用一个简单启发式来发现并打破它。

5.1 怎么判定卡死

is_stuck()app/agent/base.py:170):看最后一条消息的内容,往回数有多少条 assistant 消息内容与它完全相同;达到阈值 duplicate_threshold(默认 2)就算卡死。

# 示意,非源码:卡死判定
last = memory.messages[-1]
duplicate = 数一数(前面的 assistant 消息里, content == last.content 的条数)
return duplicate >= 2 # 重复 2 次以上 ⇒ 卡死

5.2 卡死了怎么办

handle_stuck_state()app/agent/base.py:163)不重启、不报错,而是往下一步的提示词前面插一句话,逼 LLM 换策略:

"Observed duplicate responses. Consider new strategies and avoid repeating ineffective paths already attempted."

这句被拼到 next_step_prompt 最前面,下一轮 think 时 LLM 就会看到"别再重复了"的提醒。妙在:用提示词软性干预,而不是硬编码逃逸逻辑。


6. 关键细节 / 坑

  • 不能重入run() 开头检查 state != IDLE 就直接抛 RuntimeError。同一个 agent 实例不能并发跑两个任务。
  • 跑满上限不算 FINISHED:达到 max_steps 时状态被重置回 IDLE(而非 FINISHED),并追加一条 "Terminated: Reached max steps"。
  • step() 的返回值只是日志用的字符串,真正的"状态"变化(比如置 FINISHED)发生在 step() 内部对 self.state 的修改——见下一章工具执行如何触发 FINISHED。

7. 代码地图

主题文件路径符号名
运行循环app/agent/base.pyBaseAgent.run
状态切换(含错误捕获)app/agent/base.pyBaseAgent.state_context
写记忆app/agent/base.pyBaseAgent.update_memory
卡死检测app/agent/base.pyBaseAgent.is_stuck, BaseAgent.handle_stuck_state
状态枚举app/schema.pyAgentState
记忆结构app/schema.pyMemory, Message
消息角色app/schema.pyRole