跳到主要内容

数据截至 (上游 commit 3309bf4e416f)

07 — 巧妙之处、边界与局限、横向对比

这章讲什么: 前六章是「它怎么转」,这一章是「你能带走什么」和「别踩什么」。 所有条目都锚到具体代码。


1. 巧妙之处(可借鉴的技术)

1.1 错误也是观察,不是异常

妙在哪: agent 的每一次工具失败,如果直接抛异常,整个任务就断了。 OpenManus 把所有失败都转成字符串返回给模型:工具名不认识、参数不是合法 JSON、 工具内部抛异常,三种情况都变成一句 Error: …(app/agent/toolcall.py:207-216)。

模型看到错误信息,下一轮就有机会自己改参数重试。这是 agent 容错的第一原则, 代码里只有十几行,但没有它 agent 根本跑不完一个复杂任务。

同一原则在 ToolCollection.execute 里也出现了一次:ToolError 被转成 ToolFailure 而不是往上抛(app/tool/tool_collection.py:34-35)。

1.2 从 JSON Schema 反向合成 Python 函数签名

妙在哪: FastMCP 靠函数的类型注解生成工具描述,而 OpenManus 的工具把描述 写在 parameters 字典里。两者对不上。

解法不是给每个工具手写一个适配函数,而是造一个通吃的 **kwargs 函数, 再伪造它的 __signature____doc__(app/mcp/server.py:56-58):

tool_method.__name__ = tool_name
tool_method.__doc__ = self._build_docstring(tool_function)
tool_method.__signature__ = self._build_signature(tool_function)

_build_signature(app/mcp/server.py:98-134)把 string→strarray→list 这类映射反着走一遍,造出 inspect.Parameter 列表。 任何 BaseTool 都能零改动变成 MCP 工具。

这个技巧在任何「schema 驱动 + 需要真实函数签名」的场景都能复用。

1.3 用 Protocol 让「在哪跑」变成配置

妙在哪: StrReplaceEditor 有 432 行,里面没有一行 open()、没有一行 subprocess。 所有 I/O 都走 operator,而 operator 在运行时按配置选 (app/tool/str_replace_editor.py:106-112)。

于是「文件改在本机还是改在容器里」变成一个布尔配置,工具代码完全不知情。

对比一下:如果当初在工具里写死 Path(path).read_text(), 后来要加沙箱就得把每个工具改一遍。

1.4 MCP 客户端直接继承工具表

妙在哪: 一行代码就把「远程工具」和「本地工具」统一了 (app/tool/mcp.py:49):

class MCPClients(ToolCollection):

因为 MCPClients 就是一张 ToolCollection,ToolCallAgent 遍历工具表时 完全分不出哪个是远程的。Manus 只需要把新工具 add_tools 进去 (app/agent/manus.py:152-156)。

架构含义: 「加能力」从「写代码」降级成「改配置」。浏览器能力就是这么从 内置工具变成 MCP 服务器的——app/tool/ 下已经没有 browser_use_tool.py 了。

1.5 图片单独开一条 user 消息

妙在哪: OpenAI 的 tool 角色消息不支持多模态内容块。 工具(比如截图)返回的图片如果硬塞进 tool 消息就会被拒。

OpenManus 的做法是:tool 消息只放文字,图片另起一条 user 消息 (app/agent/toolcall.py:162-168),内容写「Image returned by <工具名>:」。 模型照样能把图和工具对上号。

1.6 改计划时按「同位置同文本」保留进度

妙在哪: 允许模型中途修订计划,但已完成的步骤不会被误重置 (app/tool/planning.py:192-199)。这是「可变计划」这类设计的必备细节—— 少了它,模型每改一次计划,进度就清零一次。


2. 边界与局限(诚实清单)

2.1 设计上刻意不做的

不做什么证据
记忆压缩/摘要Memory 只有一个 100 条的滑窗(app/schema.py:161-168)
上下文管理唯一手段是按字符数截断工具输出(app/agent/toolcall.py:148-149)
任务持久化计划存在实例字典里(app/tool/planning.py:69),进程退出即失
智能体间直接通信PlanningFlow 只做串行派活,agent 之间不交换消息
模糊代码编辑str_replace 要求唯一精确匹配(app/tool/str_replace_editor.py:297-314)
并行工具调用act() 里是 for 循环顺序执行(app/agent/toolcall.py:142-169)

2.2 代码里真实存在的缺口

下面每一条都能在源码里核对,按影响排序:

run() 返回后状态恒为 IDLE state_contextfinally 无条件还原状态(app/agent/base.py:81-82), 所以 FINISHEDERROR 都不会活着离开 run()。 直接后果:PlanningFlowapp/flow/planning.py:128 那个 「执行者想提前收工就跳出」的判断不会命中。

② 默认模型不在多模态名单里,图片被静默丢弃。 MULTIMODAL_MODELS(app/llm.py:35-42)是硬编码的旧名单, 而示例配置默认用 claude-3-7-sonnet-20250219(config/config.example.toml:2)。 于是 format_messagesdel message["base64_image"] 分支 (app/llm.py:337-339),没有任何日志。浏览器截图对模型来说等于不存在。

③ 重试装饰器的注释与行为不符。 注释写「Don't retry TokenLimitExceeded」,但 retry_if_exception_type 里包含 Exception(app/llm.py:354-360),而 TokenLimitExceededException 的子类 (app/exceptions.py:8-14)。所以它会被重试 6 次、退避最长每次 60 秒, 之后包成 RetryError 才被上层识别(app/agent/toolcall.py:61)。

④ 计划步骤无条件被标成完成。 只要 executor.run() 没抛异常就打勾(app/flow/planning.py:296-301), 哪怕智能体跑满步数一事无成。blocked 状态没有任何代码会写入。

⑤ 步数预算跨 run() 累加。 current_step 只在撞 max_steps 时归零(app/agent/base.py:149-151), 所以 PlanningFlow 里越靠后的步骤可用步数越少。

python_execute 没有真正的隔离。 所谓 safe_globals 是完整的内置命名空间副本 (app/tool/python_execute.py:57-60),open__import__ 全可用, 而且不走沙箱。工具描述里的 "safety restrictions" 实际只有超时。

⑦ 沙箱模式下退出码和 stderr 是假的。 SandboxFileOperator.run_command 恒返回 (0, stdout, "") (app/tool/file_operators.py:148-152)。

⑧ A2A 协议入口在本 commit 是坏的。 protocol/a2a/app/main.py:13 仍然 from app.tool.browser_use_tool import _BROWSER_DESCRIPTION, 但 app/tool/browser_use_tool.py 已经不存在(浏览器能力迁到了 MCP)。 这个入口目前 import 就会失败。

⑨ 三个工具被导出但没接线。 WebSearchCrawl4aiToolComputerUseTool 都在 app/tool/__init__.py 里导出,却不在任何智能体的 available_tools 中。

SandboxManager 写好了没人用。 313 行的多容器管理器(app/sandbox/core/manager.py)只被测试引用, 生产路径走的是单例 SANDBOX_CLIENT

2.3 会在哪儿崩

场景现象
没装 uvx / 没网Browser Use MCP 连接失败,只打一条 error 日志继续跑(app/agent/manus.py:98-99),于是没有浏览器能力
记忆滑窗切在 tool_calls 与 tool 消息之间服务端拒绝请求(inferred,app/schema.py:163-175 无保护)
ask_human 被触发但不在交互终端input() 阻塞整个事件循环(app/tool/ask_human.py:21)
未装 Docker 却开了 use_sandboxdocker.from_env() 抛错(app/sandbox/core/sandbox.py:45)

3. 横向对比

3.1 和同书架的兄弟项目

项目核心抽象和 OpenManus 的关键差异
MetaGPTRole + 共享环境消息总线同一个团队出品。MetaGPT 靠发布订阅让角色互相触发;OpenManus 只有一条 ReAct 循环,多智能体是外层串行派活
OpenHandsEvent Stream + Runtime 沙箱OpenHands 的沙箱是一等公民(所有动作都在容器里);OpenManus 沙箱是可选开关,且 python_execute 绕过它
Suna服务化 agent + Daytona 沙箱两者都用 Daytona;Suna 是产品化的服务端形态,OpenManus 的 SandboxManus 更像一个实验分支
AgentScope显式的消息传递与编排图AgentScope 把「多智能体拓扑」做成一等概念;OpenManus 只有一种线性计划流
CAMEL角色扮演对话CAMEL 研究「两个 agent 对话涌现什么」;OpenManus 关心「一个 agent 怎么把活干完」

3.2 一个具体维度的对比:代码编辑策略

项目匹配策略失败时怎么办
OpenManus精确唯一匹配(app/tool/str_replace_editor.py:297-314)报错,并把重复出现的行号告诉模型
Aider多级模糊降级(缩进容忍、相似块)尽力猜出用户意图

取舍的本质: OpenManus 选了「宁可失败也不猜」,把纠错责任交给模型的下一轮; Aider 选了「尽量帮用户改成」。前者代码简单、行为可预测, 代价是模型要多花几轮才改对。

3.3 该学它什么、不该学什么

该学不该学
四层继承链的职责切分用继承承载「智能体类型」(工具表其实是组合关系)
错误当观察的容错纪律记忆只有定长滑窗
MCP 作为能力插槽MULTIMODAL_MODELS 这类清单硬编码
Protocol 抽象让环境可切换用「注释描述意图」代替代码约束(重试那段)

4. 总代码地图

4.1 入口

主题文件路径符号名
单智能体main.pymain
计划流run_flow.pyrun_flow
MCP 客户端run_mcp.pyMCPRunner
MCP 服务端run_mcp_server.pyMCPServer
云沙箱sandbox_main.pymain
A2A 协议服务(本 commit 有坏 import)protocol/a2a/app/main.pyrun_serverA2AManus

4.2 智能体

主题文件路径符号名
循环 / 状态 / 记忆app/agent/base.pyBaseAgentrunstate_contextis_stuck
ReAct 分工app/agent/react.pyReActAgent
工具调用app/agent/toolcall.pyToolCallAgentthinkactexecute_tool
通用智能体app/agent/manus.pyManuscreateinitialize_mcp_servers
MCP 智能体app/agent/mcp.pyMCPAgent_refresh_tools
浏览器智能体app/agent/browser.pyBrowserAgentBrowserContextHelper
编程智能体app/agent/swe.pySWEAgent
数据分析智能体app/agent/data_analysis.pyDataAnalysis
云沙箱智能体app/agent/sandbox_agent.pySandboxManus

4.3 工具与 MCP

主题文件路径符号名
工具基类 / 结果app/tool/base.pyBaseToolToolResultto_param
工具表app/tool/tool_collection.pyToolCollection
文件编辑app/tool/str_replace_editor.pyStrReplaceEditor
常驻 shellapp/tool/bash.pyBash_BashSession
Python 执行app/tool/python_execute.pyPythonExecute
计划工具app/tool/planning.pyPlanningTool
收工 / 反问app/tool/terminate.pyapp/tool/ask_human.pyTerminateAskHuman
多引擎搜索(未接线)app/tool/web_search.pyWebSearch
MCP 客户端app/tool/mcp.pyMCPClientsMCPClientTool
MCP 服务端app/mcp/server.pyMCPServer_build_signature

4.4 基础设施

主题文件路径符号名
模型封装app/llm.pyLLMask_toolTokenCounter
Bedrock 适配app/bedrock.pyBedrockClient
配置单例app/config.pyConfigAppConfigconfig
消息 / 记忆 / 状态app/schema.pyMessageMemoryAgentStateToolChoice
异常app/exceptions.pyToolErrorTokenLimitExceeded
日志app/logger.pyapp/utils/logger.pydefine_log_level(loguru)、structlog 配置
Docker 沙箱app/sandbox/core/sandbox.pyDockerSandbox
容器终端app/sandbox/core/terminal.pyAsyncDockerizedTerminal
沙箱客户端app/sandbox/client.pySANDBOX_CLIENT
文件操作抽象app/tool/file_operators.pyFileOperatorLocalFileOperatorSandboxFileOperator
计划流app/flow/planning.pyPlanningFlow

4.5 提示词在哪儿

所有系统提示与每步提示集中在 app/prompt/:

文件给谁用
app/prompt/manus.pyManusSandboxManus
app/prompt/toolcall.pyToolCallAgent 默认
app/prompt/mcp.pyMCPAgent
app/prompt/browser.pyBrowserContextHelper 拼接的浏览器状态提示
app/prompt/swe.pySWEAgent
app/prompt/planning.py计划相关(注意:PlanningFlow 自己内联了另一套提示)
app/prompt/visualization.pyDataAnalysis