跳到主要内容

openmanus — 本课题摘录

读了哪几篇: 01-agent-core(最小骨架)、02-react-toolcall(把模型说的话变成动作)。 其余五篇(工具与 MCP 桥、规划流、模型层、沙箱、巧妙之处)本轮没读。

这一家是本课题的"最小骨架"参照:它把"一个 agent 至少需要哪几件东西"回答得最干净。

它对本课题回答了什么

决定四:一个 agent 最少需要回答的四个问题

它开门见山列了四个问题和自己的答案:

问题它的答案
什么时候停?步数上限,或状态变成"完成"
上一步做了啥,下一步怎么知道?一条不断追加的消息列表
中途出错了怎么办?用上下文管理器守住状态字段,异常时置成"出错"
模型开始鬼打墙怎么办?数最近几条助手消息是否一字不差重复

(依据:Agent 库 · OpenManus · BaseAgent:主循环、状态机、记忆与卡死检测 —— 一个 agent 至少要回答四个问题——什么时候停(max_steps 或 FINISHED)、上一步怎么知道(不断追加的 Memory)、出错怎么办(上下文管理器守状态,异常置 ERROR)、鬼打墙怎么办(数重复))

这四问就是本课题该讲清的最小集合。 我们的讲义可以直接照这个骨架搭。

决定一:循环本身完全不知道"工具"和"模型"是什么

while 步数 < 上限 且 状态 ≠ 完成:
步数 += 1
结果 = await 这一步() ← 抽象方法,由子类实现

循环体只有三行,而且它不知道工具是什么、模型是什么。 (依据:Agent 库 · OpenManus · BaseAgent:主循环、状态机、记忆与卡死检测 —— BaseAgent 的主循环完全不知道「工具」「模型」是什么,step() 是抽象方法,换一套 step 实现就能换一种智能体范式;整个 base.py 只有 196 行)

换一套"这一步"的实现就能换一种范式。 整个基类只有 196 行。

继承链是逐层加一个职责:

基类 主循环 · 状态 · 记忆 · 卡死检测
│ 加:把一步切成「想」和「做」两半
ReAct 层 两个抽象方法
│ 加:工具表 · 工具选择模式 · 特殊工具
工具调用层
│ 加:换一套工具表 + 换一套提示词
具体的各种 agent

它自己点评了这条链的价值:越往下越具体,每一层只承担一个新职责;要造新 agent,通常只需要在最后一层改两个字段。

决定四:收工靠"特殊工具",而且拆成三个可替换的钩子

这是这一家最值得抄的一条。

有一个"特殊工具名字列表",默认只有一个"终止"。每次工具执行完都走一遍判断:

不是特殊工具 → 什么也不做
是特殊工具 且 判定该收工 → 把状态改成「完成」

收工被拆成三个钩子:

钩子默认行为谁改过它
哪些名字算特殊工具只有"终止"
怎么判断是不是特殊工具忽略大小写比名字
判定该不该真收工恒为真工具全来自远程的那个 agent 改成"名字必须精确等于终止才收工"

(依据:Agent 库 · OpenManus · ReActAgent / ToolCallAgent:把模型说的话变成真的动作 —— 收工机制拆成 special_tool_names / _is_special_tool / _should_finish_execution 三个可替换钩子;MCPAgent 覆盖第三个,因为它的工具全是远程来的、名字可能撞上,所以多加一道判断)

最后一行的理由值得记:工具全是远程来的时候,名字可能撞上——所以要多加一道判断。

而且它点破了一个第一次读容易迷惑的地方:

"终止"工具本身的执行只是返回一句话,真正让循环停下来的是那个副作用。 工具本身不做任何事。

(依据:Agent 库 · OpenManus · ReActAgent / ToolCallAgent:把模型说的话变成真的动作 —— Terminate 工具的 execute 只是返回一句话,真正让循环停下来的是 _handle_special_tool 里把 state 改成 FINISHED 的副作用,工具本身不做任何事)

这跟 cline 的"完成是工具的一个属性"、smolagents 的"调它就抛异常"是同一件事的三种落法。 openmanus 这一份最容易懂:工具是个空壳,判定在执行之后。

决定一:记忆就是一个会滑窗的列表

class 记忆:
消息列表
最多 100

追加后超过 100 条就切掉最前面的。没有摘要、没有向量检索、没有分层记忆——这是它刻意选的极简路线。 (依据:Agent 库 · OpenManus · BaseAgent:主循环、状态机、记忆与卡死检测 —— Memory 就是一个列表加一个 max_messages=100 的上限,超过就切掉最前面的;没有摘要、没有向量检索、没有分层记忆,是刻意选的极简路线)

这条对我们的第一版直接有用:最小原型的历史管理就该是这样。 我们前面看了 headroom 的四道防线、opencode 的六个预算常量、deepagents 的分层—— 但第一版应该从"一个列表 + 一个上限"开始,而不是一上来就上压缩。

决定四补充:状态字段用上下文管理器守

直觉写得很好:状态字段最怕"改了没改回来"。

用上下文管理器包住:进入时改成新状态,退出时无论正常还是异常都还原;捕获到异常就置成"出错"。

四个状态:空闲 / 运行中 / 完成 / 出错。

决定四再补充:卡死检测朴素得可爱

看最后一条消息的内容,在之前的助手消息里出现过几次;达到阈值就判定卡死,往提示里塞一句警告。 (依据:Agent 库 · OpenManus · BaseAgent:主循环、状态机、记忆与卡死检测 —— 卡死检测是看最后一条消息的内容在之前的 assistant 消息里出现过几次,达到阈值就在提示里塞一句警告)

对比 cline 的"工具名 + 排序后参数当签名"——openmanus 数的是消息内容重复,cline 数的是工具调用重复。 两者互补:前者抓"车轱辘话",后者抓"反复调同一个工具"。

步数用完时追加一句"已终止:达到最大步数",而不是静默结束。

它没回答什么

  • 不用原生工具调用怎么办——它用厂商原生的工具调用。
  • 历史怎么压——刻意不做。
  • 循环状态怎么存下来续跑——没有这一层。

坑与代价

  • 滑窗式记忆会静默丢掉最早的内容。 一百条一到就切,不摘要、不提示。长任务里"模型忘了最开始的要求"是这个设计的必然结果。

    判断(无锚): 对最小原型这是可接受的,因为我们的任务本来就短。但至少应该在切掉时打一条日志,否则出问题查不到原因。 如果错,会错在: 如果第一批任务就会跑到几十轮(比如"把这个目录里所有文件都改一遍"),那一百条很快就到,静默丢失会直接毁掉任务。

  • 卡死检测只看助手消息,看不到"工具反复失败"。 模型可以每次说不同的话、调同一个失败的工具——这种情况它抓不住。
  • 196 行是干净的代价:它什么都不管。 压缩、并发、审批、状态持久化全没有。它是个骨架,不是产品。