OpenManus源码剖析-探索智能体框架的设计与实现

926 阅读30分钟

一、项目基本信息

1.1 背景信息

OpenManus 是基于大语言模型(LLM)构建的智能体框架,致力于打造灵活、可扩展且功能强大的系统,助力 AI 借助各类工具与外界交互,攻克复杂任务。它与传统聊天机器人大相径庭,不局限于文本理解与生成,还具备强大实操技能。既能快速精准搜索信息,又能流畅浏览网页,代码执行、文件保存等操作也不在话下,已然成为真正实用的 “智能助手”。其核心的 “思考 - 行动” 循环独具匠心。智能体接到任务,先审慎分析当下状态与需求,完成 “思考” 步骤;随即挑选并启用恰当工具付诸 “行动”;再依据行动结果展开新一轮思考。如此循环,OpenManus 逐步拆解难题,始终精准把握任务全貌,高效满足用户需求。

1.2 项目主要目录结构

OpenManus 项目的 app 目录下主要包含了与智能体、配置、工具、流程等相关的模块,以下是根据代码片段总结的主要结构及各部分功能:

1.2.1 核心功能模块

agent 目录:包含智能体相关的实现。

  • manus.py:定义了 Manus 类,这是一个通用的多功能智能体,集成了多种工具,如 PythonExecute、BrowserUseTool 等。

  • config.py:负责项目的配置管理。定义了各种配置类,如 LLMSettings(大语言模型设置)、SearchSettings(搜索设置)、BrowserSettings(浏览器设置)、SandboxSettings(沙盒设置)和 MCPSettings(MCP 配置)。

tool 目录:包含各种工具的实现。

  • base.py:定义了工具的基类 BaseTool,所有具体工具类都继承自该类。BaseTool 类包含工具的基本属性(如名称、描述、参数)和执行方法,同时提供了将工具转换为函数调用格式的方法 to_param。

  • planning.py:实现了 PlanningTool 类,用于创建和管理解决复杂任务的计划。该工具支持创建计划、更新计划步骤、跟踪进度等操作。

  • search 目录:包含搜索工具相关的实现。

  • base.py:定义了 WebSearchEngine 基类和 SearchItem 类,WebSearchEngine 类提供了执行搜索的抽象方法,SearchItem 类用于表示单个搜索结果。

1.2.2 辅助功能模块

prompt 目录:包含提示信息的定义。

  • manus.py:定义了 Manus 智能体的系统提示信息 SYSTEM_PROMPT 和下一步提示信息 NEXT_STEP_PROMPT,用于指导智能体的行为。
  • planning.py:定义了规划工具的系统提示信息 PLANNING_SYSTEM_PROMPT 和下一步提示信息 NEXT_STEP_PROMPT,帮助智能体更好地使用规划工具。
  • schema.py:定义了项目中使用的各种数据模型和枚举类型。
    • Role 枚举:定义了消息的角色选项,包括 SYSTEM、USER、ASSISTANT 和 TOOL。
    • ToolChoice 枚举:定义了工具选择的选项,如 NONE、AUTO 和 REQUIRED。
    • AgentState 枚举:定义了智能体的执行状态,如 IDLE、RUNNING、FINISHED 和 ERROR。
    • Message 类:表示对话中的消息,包含角色、内容、工具调用等信息,并提供了消息的创建和转换方法。
    • Memory 类:用于管理智能体的内存,支持添加消息、获取最近消息等操作。 flow 目录:包含执行流程相关的实现。
    • base.py:定义了 BaseFlow 类,作为执行流程的基类,支持多个智能体的协作。该类提供了管理智能体的方法,如获取主智能体、添加智能体等,并定义了执行流程的抽象方法 execute。

1.2.3 其他模块

  • init.py:对 Python 版本进行检查,确保使用的 Python 版本在 3.11 - 3.13 之间。如果版本不兼容,会输出警告信息。

1.3 核心组件介绍

1.3.1 智能体系统

智能体系统作为 OpenManus 的核心,采用层次化设计,使得代码模块化且可扩展性强,各层次专注自身职责:

  • BaseAgent:提供基本的状态管理和执行循环功能。
  • ReActAgent:实现思考-行动循环模式。
  • ToolCallAgent:实现工具调用机制,确保智能体能够根据需求调用合适的工具。
  • Manus:是集成多种工具的具体智能体实现。

1.3.2 工具系统

工具系统为智能体提供与外部世界交互的能力,每个工具都有明确的名称、描述和参数规范,便于大语言模型(LLM)正确选择和使用:

  • BaseTool:所有工具的抽象基类,定义了工具的基本属性和方法。
  • ToolCollection:工具的集合和管理器,负责管理和执行工具调用。
  • 具体工具:如 PythonExecuteGoogleSearchBrowserUseTool 等。在代码中,BrowserUseTool 在 OpenManus/app/agent/manus.py 中被使用,智能体在执行任务时会根据需要调用这些具体工具。

1.3.3 记忆系统

记忆系统使智能体能够在多个步骤中保持上下文连贯性,记录用户输入、LLM 响应和工具执行结果,帮助智能体基于历史信息做出决策:

  • Memory:存储交互历史的容器。在 OpenManus/app/schema.py 中,Memory 类管理智能体的记忆,支持添加消息、获取最近消息和清空消息等操作。
  • Message:表示不同类型消息的结构。Message 类定义了消息的角色(如 SYSTEMUSERASSISTANTTOOL)、内容、工具调用等信息,并提供了消息的创建和转换方法。

1.3.4 LLM 接口

LLM 接口负责与大语言模型(如 OpenAI 的 GPT 模型)通信,将智能体的记忆和工具信息传递给 LLM,并解析 LLM 的响应:

  • LLM:封装了与 LLM API 的交互。代码中虽未详细展示其实现,但可推测它负责与外部大语言模型进行通信,发送请求并接收响应。
  • ToolResponse:表示 LLM 响应的结构,不过在提供的代码片段中未直接体现其具体实现。

1.3.5 流程控制

流程控制组件管理不同类型的执行流程,使 OpenManus 能够支持不同的执行模式,如规划式执行:

  • BaseFlow:所有流程的抽象基类,为具体流程类提供基础框架。在 OpenManus/app/flow/base.py 中,BaseFlow 类支持多个智能体的协作,定义了执行流程的抽象方法 execute
  • PlanningFlow:实现规划和执行的流程。在 OpenManus/app/flow/planning.py 中,PlanningFlow 类根据输入创建初始计划,然后逐步执行计划中的步骤,直到计划完成或出现异常。
  • FlowFactory:创建不同类型流程的工厂。

1.4 技术亮点

OpenManus 具备诸多极为显著的技术亮点:

其一,异步编程特性:它巧妙且广泛地运用 async/await 语法开展异步操作,这一举措极大地提升了输入/输出(I/O)的效率,使得系统在处理各类任务时能够更加流畅地与外部设备及资源交互,避免因等待数据传输而造成的时间浪费。

其二,模块化精巧设计:组件划分明确清晰,各组件间的接口定义精准规范。如此一来,无论是后续的维护工作,还是根据新需求进行功能扩展,开发人员都能依据既定的模块架构迅速定位问题、高效添加新功能,极大地增强了整个系统的可操作性与适应性。

其三,严谨的错误处理机制:构建起多层次的错误捕获及恢复体系,能够全方位、无死角地监测运行过程中可能出现的各类异常情况。一旦错误发生,系统能够迅速响应,有条不紊地按照预设的恢复流程进行处理,确保整体运行的稳定性,有效降低因局部错误导致系统崩溃的风险。

其四,工具抽象统一化:设计了统一的工具接口,这就像是为系统配备了一个万能插座,无论后续需要接入何种新型工具,只需遵循该接口规范,就能轻松实现对接,无缝融入 OpenManus 生态,为系统功能的多样化发展提供了无限可能。

其五,卓越的记忆管理系统:拥有一套完善的记忆机制,尤其在面对需要上下文连贯配合的多步骤复杂任务时,能够精准地存储、调用关键信息,让智能体如同拥有真正的人类记忆一般,时刻清楚任务的前因后果,有条不紊地推进各项任务,展现出极高的任务执行连贯性。 这些独树一帜的特点,合力塑造了 OpenManus 成为一个既强劲有力又灵活多变的智能体框架,使之有足够的实力从容应对各式各样错综复杂的任务场景。

在后续的章节里,下文将会深入到 OpenManus 的各个细微组件以及内部运行机制之中,将其神秘的面纱彻底揭开,全方位展现其背后的工作原理。

综上所述,OpenManus 项目的 app 目录结构清晰,各个模块分工明确,通过合理的设计和组织,实现了智能体、工具、配置和流程的有效管理和协作。

二、智能体的核心设计实现

2.1 BaseAgent:智能体的基座

BaseAgent 是所有智能体的基类,位于 app/agent/base.py中,它是智能体的基座实现。

BaseAgent 类中,核心属性和主要方法如下:

核心属性

  1. name: 字符串类型,表示代理的唯一名称。

    name: str = Field(..., description="Unique name of the agent")
    
  2. description: 可选的字符串类型,表示代理的描述。

    description: Optional[str] = Field(None, description="Optional agent description")
    
  3. system_prompt: 可选的字符串类型,系统级别指令提示。

    system_prompt: Optional[str] = Field(None, description="System-level instruction prompt")
    
  4. next_step_prompt: 可选的字符串类型,用于确定下一步操作的提示。

    next_step_prompt: Optional[str] = Field(None, description="Prompt for determining next action")
    
  5. llm: LLM 类型的实例,默认值为通过 default_factory 创建的 LLM 实例。

    llm: LLM = Field(default_factory=LLM, description="Language model instance")
    
  6. memory: Memory 类型的实例,默认值为通过 default_factory 创建的 Memory 实例。

    memory: Memory = Field(default_factory=Memory, description="Agent's memory store")
    
  7. state: 表示代理当前状态的 AgentState 类型,默认值为 AgentState.IDLE

    state: AgentState = Field(default=AgentState.IDLE, description="Current agent state")
    
  8. max_steps: 代理的最大步骤数,默认为10。

    max_steps: int = Field(default=10, description="Maximum steps before termination")
    
  9. current_step: 代理当前执行的步骤数,默认为0。

    current_step: int = Field(default=0, description="Current step in execution")
    
  10. duplicate_threshold: 检测重复状态的阈值,默认为2。

    duplicate_threshold: int = 2
    

主要方法

  1. initialize_agent: 模型验证器方法,在实例化后为未提供的凭据设置默认值。

    @model_validator(mode="after")
    def initialize_agent(self) -> "BaseAgent":
    
  2. state_context: 状态上下文管理器,用于安全地在代理执行期间切换状态。

    @asynccontextmanager
    async def state_context(self, new_state: AgentState):
    
  3. update_memory: 向代理的存储中添加一条消息。消息的内容可以是文本或图像。

    def update_memory(self, role: ROLE_TYPE, content: str, base64_image: Optional[str] = None, **kwargs):
    
  4. run: 代理的主执行循环,异步执行代理逻辑直到达到最大步骤数或执行完成。

    async def run(self, request: Optional[str] = None) -> str:
    
  5. step: 必须由子类实现的抽象方法,定义了单步代理行为的逻辑。

    @abstractmethod
    async def step(self) -> str:
    
  6. handle_stuck_state: 处理代理由于重复响应陷入停滞状态的情况,通过修改 next_step_prompt 来帮助代理改变策略。

    def handle_stuck_state(self):
    
  7. is_stuck: 用于检测代理是否处于停滞状态,判断依据是最近的消息内容中是否包含多次重复的代理响应。

    def is_stuck(self) -> bool:
    
  8. messages:

    • property 方法,返回代理存储中的消息列表。
      @property
      def messages(self) -> List[Message]:
      
    • setter 方法,允许设置代理存储中的消息列表。
      @messages.setter
      def messages(self, value: List[Message]):
      

这些属性和方法共同构成了 BaseAgent 类的基础功能,允许代理持续地管理其状态和执行循环。 BaseAgent的run方法是智能体执行的核心,它实现了一个循环,在循环中不断调用step方法,直到任务完成或达到最大步骤数:

    async def run(self, request: Optional[str] = None) -> str:
        """Execute the agent's main loop asynchronously.

        Args:
            request: Optional initial user request to process.

        Returns:
            A string summarizing the execution results.

        Raises:
            RuntimeError: If the agent is not in IDLE state at start.
        """
        if self.state != AgentState.IDLE:
            raise RuntimeError(f"Cannot run agent from state: {self.state}")

        if request:
            self.update_memory("user", request)

        results: List[str] = []
        async with self.state_context(AgentState.RUNNING):
            while (
                self.current_step < self.max_steps and self.state != AgentState.FINISHED
            ):
                self.current_step += 1
                logger.info(f"Executing step {self.current_step}/{self.max_steps}")
                step_result = await self.step()

                # Check for stuck state
                if self.is_stuck():
                    self.handle_stuck_state()

                results.append(f"Step {self.current_step}: {step_result}")

            if self.current_step >= self.max_steps:
                self.current_step = 0
                self.state = AgentState.IDLE
                results.append(f"Terminated: Reached max steps ({self.max_steps})")
        await SANDBOX_CLIENT.cleanup()
        return "\n".join(results) if results else "No steps executed"

2.2 ReActAgent: 思考-行动循环模式的视线

ReActAgent继承自BaseAgent,位于app/agent/react.py中。它实现了思考-行动循环模式,这是一种强大的智能体决策框架。

核心方法

1. think 方法
@abstractmethod
async def think(self) -> bool:
    """Process current state and decide next action"""
  • 功能:这是一个抽象方法,用于处理智能体的当前状态并决定下一步是否需要采取行动。由于它是抽象方法,所以具体的实现逻辑需要在子类中完成。
  • 参数:无
  • 返回值:返回一个布尔值。如果返回 True,表示需要采取行动;如果返回 False,表示思考完成,无需采取行动。
2. act 方法
@abstractmethod
async def act(self) -> str:
    """Execute decided actions"""
  • 功能:同样是一个抽象方法,用于执行智能体已经决定好的行动。具体的行动执行逻辑需要在子类中实现。
  • 参数:无
  • 返回值:返回一个字符串,表示行动执行后的结果。
3. step 方法
async def step(self) -> str:
    """Execute a single step: think and act."""
    should_act = await self.think()
    if not should_act:
        return "Thinking complete - no action needed"
    return await self.act()
  • 功能:执行一个完整的步骤,该步骤包含思考和行动两个阶段。首先调用 think 方法决定是否需要采取行动,如果需要则调用 act 方法执行行动。
  • 参数:无
  • 返回值:如果 think 方法返回 False,则返回字符串 "Thinking complete - no action needed";如果 think 方法返回 True,则返回 act 方法的执行结果。

这些方法共同构成了 ReActAgent 类的核心逻辑,通过 think 和 act 方法的组合,实现了智能体的思考和行动循环。子类需要实现 think 和 act 方法来具体定义智能体的行为。

2.3 ToolCallAgent: 工具调用机制剖析

ToolCallAgent继承自ReActAgent,位于app/agent/toolcall.py中。它实现了工具调用机制,使智能体能够使用各种工具来完成任务。 toolcall.py 文件定义了 ToolCallAgent 类,该类继承自 ReActAgent,用于处理工具调用。以下是该类的核心方法和属性介绍:

核心属性

  • name:代理的名称,默认为 "toolcall"
  • description:代理的描述信息,表明该代理可以执行工具调用。
  • system_prompt:系统提示信息,用于指导代理的行为,默认值从 app.prompt.toolcall 模块中获取。
  • next_step_prompt:下一步提示信息,用于引导代理决定下一步的操作,默认值从 app.prompt.toolcall 模块中获取。
  • available_tools:可用工具的集合,初始包含 CreateChatCompletion 和 Terminate 工具。
  • tool_choices:工具选择模式,默认为 ToolChoice.AUTO,表示自动选择工具。
  • special_tool_names:特殊工具名称列表,默认包含 Terminate 工具的名称,执行这些工具可能会导致代理结束任务。
  • tool_calls:待执行的工具调用列表,初始为空。
  • _current_base64_image:当前工具调用返回的 base64 编码图像,初始为 None
  • max_steps:代理执行的最大步数,默认为 30
  • max_observe:观察结果的最大长度,可选类型为整数或布尔值,初始为 None

核心方法

  1. think 方法
async def think(self) -> bool:
    ...
  • 功能:处理当前状态并决定下一步的操作,使用工具进行思考。
  • 步骤
    1. 添加下一步提示信息到消息列表。
    2. 调用语言模型的 ask_tool 方法获取包含工具选项的响应。
    3. 处理可能的异常,如令牌限制错误。
    4. 记录响应信息,包括思考内容、选择的工具和工具参数。
    5. 根据工具选择模式处理响应,创建并添加助手消息到内存。
    6. 返回是否有工具调用需要执行。
  1. act 方法
async def act(self) -> str:
    ...
  • 功能:执行工具调用并处理结果。

  • 步骤

    1. 检查是否有工具调用,如果没有且工具调用是必需的,则抛出异常;否则返回最后一条消息的内容。
    2. 遍历工具调用列表,执行每个工具调用并处理结果。
    3. 记录工具执行结果,并将工具响应添加到内存。
    4. 返回所有工具执行结果的组合字符串。
  1. execute_tool 方法
async def execute_tool(self, command: ToolCall) -> str:
    ...
  • 功能:执行单个工具调用,并进行健壮的错误处理。

  • 步骤

    1. 检查命令格式是否有效,工具名称是否可用。
    2. 解析工具参数并执行工具。
    3. 处理特殊工具调用,可能会导致代理结束任务。
    4. 检查工具执行结果是否包含 base64 编码图像。
    5. 格式化工具执行结果并返回。
    6. 处理可能的异常,如 JSON 解析错误和其他异常。
  1. _handle_special_tool 方法
async def _handle_special_tool(self, name: str, result: Any, **kwargs):
    ...
  • 功能:处理特殊工具调用并更新代理状态。

  • 步骤

    1. 检查工具名称是否为特殊工具。
    2. 如果是特殊工具且满足结束条件,则将代理状态设置为 FINISHED
  1. _should_finish_execution 方法
@staticmethod
def _should_finish_execution(**kwargs) -> bool:
    ...
  • 功能:判断工具执行是否应该结束代理任务,默认返回 True
  1. _is_special_tool 方法
def _is_special_tool(self, name: str) -> bool:
    ...
  • 功能:检查工具名称是否在特殊工具列表中。
  1. cleanup 方法
async def cleanup(self):
    ...
  • 功能:清理代理使用的工具资源。

  • 步骤

    1. 遍历可用工具列表,检查工具是否有 cleanup 方法。
    2. 如果有,则异步调用 cleanup 方法并处理可能的异常。
  1. run 方法
async def run(self, request: Optional[str] = None) -> str:
    ...
  • 功能:运行代理并在完成后进行清理。

  • 步骤

    1. 调用父类的 run 方法执行代理任务。
    2. 无论任务是否成功,最后都调用 cleanup 方法清理资源。

这些方法和属性共同构成了 ToolCallAgent 类的核心功能,使其能够根据系统提示和用户输入,思考并执行工具调用,同时处理异常和清理资源。

2.4 Manus: 智能体集成

manus.py 文件定义了 Manus 类,它继承自 ToolCallAgent,是一个通用的多功能代理,能够使用多种工具解决各种任务。以下是 Manus 类的核心属性和方法介绍:

核心属性

  1. name
    • 类型str
    • 说明:代理的名称,设置为 "Manus"
  2. description
    • 类型str
    • 说明:对代理的描述,表明该代理是一个可以使用多种工具解决各种任务的多功能代理。
  3. system_prompt
    • 类型str
    • 说明:系统提示信息,使用 SYSTEM_PROMPT 格式化工作空间根目录后得到,用于指导代理的行为。
  4. next_step_prompt
    • 类型str
    • 说明:下一步提示信息,用于引导代理决定下一步的操作,默认值从 app.prompt.manus 模块中获取。
  5. max_observe
    • 类型int
    • 说明:观察结果的最大长度,设置为 10000
  6. max_steps
    • 类型int
    • 说明:代理执行的最大步数,设置为 20
  7. available_tools
    • 类型ToolCollection
    • 说明:可用工具的集合,包含 PythonExecuteBrowserUseToolStrReplaceEditor 和 Terminate 工具。
  8. special_tool_names
    • 类型list[str]
    • 说明:特殊工具名称列表,默认包含 Terminate 工具的名称,执行这些工具可能会导致代理结束任务。
  9. browser_context_helper
    • 类型Optional[BrowserContextHelper]
    • 说明:浏览器上下文助手,用于处理与浏览器相关的上下文信息,初始为 None

核心方法

  1. initialize_helper 方法
@model_validator(mode="after")
def initialize_helper(self) -> "Manus":
    self.browser_context_helper = BrowserContextHelper(self)
    return self
  • 功能:在模型验证后初始化 browser_context_helper 属性,确保代理能够处理与浏览器相关的上下文信息。
  • 返回值:返回 Manus 实例本身。
  1. think 方法
async def think(self) -> bool:
    original_prompt = self.next_step_prompt
    recent_messages = self.memory.messages[-3:] if self.memory.messages else []
    browser_in_use = any(
        tc.function.name == BrowserUseTool().name
        for msg in recent_messages
        if msg.tool_calls
        for tc in msg.tool_calls
    )

    if browser_in_use:
        self.next_step_prompt = (
            await self.browser_context_helper.format_next_step_prompt()
        )

    result = await super().think()

    # Restore original prompt
    self.next_step_prompt = original_prompt

    return result
  • 功能:处理当前状态并决定下一步的操作,根据最近的消息判断是否正在使用浏览器工具,如果是,则使用 browser_context_helper 格式化下一步提示信息。
  • 步骤
    1. 保存原始的下一步提示信息。
    2. 获取最近的三条消息。
    3. 检查最近的消息中是否有使用 BrowserUseTool 工具的调用。
    4. 如果正在使用浏览器工具,则使用 browser_context_helper 格式化下一步提示信息。
    5. 调用父类的 think 方法进行思考。
    6. 恢复原始的下一步提示信息。
    7. 返回思考结果。
  1. cleanup 方法
async def cleanup(self):
    if self.browser_context_helper:
        await self.browser_context_helper.cleanup_browser()
  • 功能:清理 Manus 代理使用的资源,主要是清理浏览器上下文。
  • 步骤
    1. 检查 browser_context_helper 是否存在。
    2. 如果存在,则调用 browser_context_helper 的 cleanup_browser 方法清理浏览器资源。

这些属性和方法共同构成了 Manus 类的核心功能,使其能够根据系统提示和用户输入,灵活地使用多种工具解决各种任务,并在需要时处理浏览器相关的上下文信息。同时,代理在结束任务时会清理使用的资源,确保资源的有效利用。

2.5 小结

智能体执行流程

流程分析

image.png

流程图解释

  1. 开始:流程从创建 Manus 实例开始。

  2. 初始化:初始化 browser_context_helper

  3. 接收请求:用户输入请求,调用 run 方法开始执行。

  4. 思考决策:在 think 方法中,判断是否使用浏览器工具,若使用则更新提示信息,否则保持原提示信息,然后调用父类的 think 方法进行思考决策。

  5. 工具执行:根据思考结果调用 act 方法执行工具。

  6. 循环执行:在 run 方法中,循环执行思考决策和工具执行步骤,直到达到最大步数或任务完成。

  7. 资源清理:任务完成后,调用 cleanup 方法清理资源。

  8. 结束:流程结束。

三、工具系统核心设计实现

OpenManus 项目的工具系统设计实现是其核心功能之一,它围绕工具的定义、管理、执行以及与代理的交互展开,旨在让代理能够高效地利用各种工具完成复杂任务。以下是对该工具系统设计实现的详细介绍:

3.1 工具定义与基类

  • BaseTool 类:在 app/tool/base.py 文件中定义了 BaseTool 类,它是所有工具的基类。该类包含抽象方法 execute,用于执行工具并接收参数。同时,提供了 to_param 方法,将工具转换为函数调用格式,方便与 LLM 交互。
@abstractmethod
async def execute(self, **kwargs) -> Any:
    """Execute the tool with given parameters."""

def to_param(self) -> Dict:
    """Convert tool to function call format."""
    return {
        "type": "function",
        "function": {
            "name": self.name,
            "description": self.description,
            "parameters": self.parameters,
        },
    }
  • 工具结果类 ToolResult:同样在 app/tool/base.py 中定义了 ToolResult 类,用于表示工具执行的结果。它包含输出、错误信息、Base64 图像和系统信息等字段,并提供了合并结果和字符串表示的方法。
class ToolResult(BaseModel):
    output: Any = Field(default=None)
    error: Optional[str] = Field(default=None)
    base64_image: Optional[str] = Field(default=None)
    system: Optional[str] = Field(default=None)

    def __bool__(self):
        return any(getattr(self, field) for field in self.__fields__)

    def __add__(self, other: "ToolResult"):
        # 合并结果的逻辑
        pass

    def __str__(self):
        return f"Error: {self.error}" if self.error else self.output

3.2 工具集合管理

  • ToolCollection 类:在 app/tool/tool_collection.py 文件中定义了 ToolCollection 类,用于管理多个工具。它可以初始化工具列表,提供工具的执行、获取和添加等操作。
class ToolCollection:
    def __init__(self, *tools: BaseTool):
        self.tools = tools
        self.tool_map = {tool.name: tool for tool in tools}

    async def execute(self, *, name: str, tool_input: Dict[str, Any] = None) -> ToolResult:
        tool = self.tool_map.get(name)
        if not tool:
            return ToolFailure(error=f"Tool {name} is invalid")
        try:
            result = await tool(**tool_input)
            return result
        except ToolError as e:
            return ToolFailure(error=e.message)

    def get_tool(self, name: str) -> BaseTool:
        return self.tool_map.get(name)

    def add_tool(self, tool: BaseTool):
        self.tools += (tool,)
        self.tool_map[tool.name] = tool
        return self

3.3 工具与代理的交互

  • ToolCallAgent 类:在 app/agent/toolcall.py 文件中定义了 ToolCallAgent 类,它是处理工具调用的基础代理类。该类包含思考和行动两个核心方法。

    • 思考方法 think:根据当前状态和可用工具,向 LLM 请求响应,并根据工具选择模式处理响应。
async def think(self) -> bool:
    response = await self.llm.ask_tool(
        messages=self.messages,
        system_msgs=(
            [Message.system_message(self.system_prompt)]
            if self.system_prompt
            else None
        ),
        tools=self.available_tools.to_params(),
        tool_choice=self.tool_choices,
    )
    self.tool_calls = response.tool_calls if response and response.tool_calls else []
    # 处理不同工具选择模式的逻辑
    pass
  • 行动方法 act:执行工具调用并处理结果,将工具响应添加到内存中。
async def act(self) -> str:
    results = []
    for command in self.tool_calls:
        result = await self.execute_tool(command)
        tool_msg = Message.tool_message(
            content=result,
            tool_call_id=command.id,
            name=command.function.name,
            base64_image=self._current_base64_image,
        )
        self.memory.add_message(tool_msg)
        results.append(result)
    return "\n\n".join(results)
  • 执行工具方法 execute_tool:解析工具调用的参数,执行工具并处理特殊工具。
async def execute_tool(self, command: ToolCall) -> str:
    name = command.function.name
    args = json.loads(command.function.arguments or "{}")
    result = await self.available_tools.execute(name=name, tool_input=args)
    await self._handle_special_tool(name=name, result=result)
    # 格式化结果的逻辑
    pass

3.4 特殊工具处理

  • _handle_special_tool 方法:在 ToolCallAgent 类中定义了 _handle_special_tool 方法,用于处理特殊工具的执行和状态变化。当执行特殊工具且满足结束条件时,将代理状态设置为已完成。
async def _handle_special_tool(self, name: str, result: Any, **kwargs):
    if not self._is_special_tool(name):
        return
    if self._should_finish_execution(name=name, result=result, **kwargs):
        self.state = AgentState.FINISHED

3.5 工具的清理

  • cleanup 方法:在 ToolCallAgent 类中定义了 cleanup 方法,用于清理代理使用的工具资源。它会遍历所有可用工具,调用具有 cleanup 方法的工具进行清理。
async def cleanup(self):
    for tool_name, tool_instance in self.available_tools.tool_map.items():
        if hasattr(tool_instance, "cleanup") and asyncio.iscoroutinefunction(
            tool_instance.cleanup
        ):
            try:
                await tool_instance.cleanup()
            except Exception as e:
                logger.error(
                    f"🚨 Error cleaning up tool '{tool_name}': {e}", exc_info=True
                )

3.6 具体工具示例

  • Manus 代理:在 app/agent/manus.py 文件中定义了 Manus 代理,它继承自 ToolCallAgent 类,并添加了通用工具,如 PythonExecuteBrowserUseTool 等。同时,根据浏览器工具的使用情况,动态调整下一步提示。
class Manus(ToolCallAgent):
    available_tools: ToolCollection = Field(
        default_factory=lambda: ToolCollection(
            PythonExecute(), BrowserUseTool(), StrReplaceEditor(), Terminate()
        )
    )

    async def think(self) -> bool:
        original_prompt = self.next_step_prompt
        browser_in_use = any(
            tc.function.name == BrowserUseTool().name
            for msg in recent_messages
            if msg.tool_calls
            for tc in msg.tool_calls
        )
        if browser_in_use:
            self.next_step_prompt = (
                await self.browser_context_helper.format_next_step_prompt()
            )
        result = await super().think()
        self.next_step_prompt = original_prompt
        return result

3.7 MCP 工具系统

  • MCPClients 类:在 app/tool/mcp.py 文件中定义了 MCPClients 类,它继承自 ToolCollection 类,用于连接 MCP 服务器并管理可用工具。可以通过 SSE 或 stdio 传输方式连接服务器,初始化会话并列出工具。
class MCPClients(ToolCollection):
    async def connect_sse(self, server_url: str) -> None:
        streams_context = sse_client(url=server_url)
        streams = await self.exit_stack.enter_async_context(streams_context)
        self.session = await self.exit_stack.enter_async_context(
            ClientSession(*streams)
        )
        await self._initialize_and_list_tools()

    async def _initialize_and_list_tools(self) -> None:
        await self.session.initialize()
        response = await self.session.list_tools()
        # 清理现有工具并创建新的工具对象
        pass

3.8 总结

OpenManus 项目的工具系统设计实现了工具的定义、管理、执行和与代理的交互,通过工具集合和代理类的协作,使得代理能够根据任务需求选择合适的工具,并处理工具执行的结果。同时,提供了工具清理和特殊工具处理的功能,确保系统的稳定性和可靠性。此外,还支持与 MCP 服务器的连接,扩展了工具的来源和使用场景。

四、错误处理和容错机制

OpenManus 项目通过自定义异常类、超时处理和资源清理等机制,确保在面对各种错误情况时能够稳定运行,减少潜在的风险和损失。以下从异常类定义、超时处理、资源清理和测试用例等方面进行详细介绍:

4.1 自定义异常类

项目中定义了多个自定义异常类,用于处理不同类型的错误:

  • ToolError:当工具执行过程中遇到错误时抛出。例如,在 _BashSession 类的 run 方法中,如果会话未启动、bash 已退出或操作超时,会抛出该异常。
class ToolError(Exception):
    def __init__(self, message):
        self.message = message
  • OpenManusError:作为所有 OpenManus 错误的基类异常。
class OpenManusError(Exception):
    """Base exception for all OpenManus errors"""
  • TokenLimitExceeded:当请求超过令牌限制时抛出。
class TokenLimitExceeded(OpenManusError):
    """Exception raised when the token limit is exceeded"""
  • SandboxError:作为沙箱相关错误的基类异常。
class SandboxError(Exception):
    """Base exception for sandbox-related errors."""
  • SandboxTimeoutError:当沙箱操作超时时抛出。
class SandboxTimeoutError(SandboxError):
    """Exception raised when a sandbox operation times out."""
  • SandboxResourceError:当沙箱遇到资源相关错误时抛出。
class SandboxResourceError(SandboxError):
    """Exception raised for resource-related errors."""

4.2 超时处理

  • _BashSession 类:在 run 方法中,使用 asyncio.timeout 为命令执行设置了超时时间(默认为 120 秒)。如果命令执行时间超过该限制,会抛出 ToolError 异常。
try:
    async with asyncio.timeout(self._timeout):
        while True:
            await asyncio.sleep(self._output_delay)
            output = self._process.stdout._buffer.decode()
            if self._sentinel in output:
                output = output[: output.index(self._sentinel)]
                break
except asyncio.TimeoutError:
    self._timed_out = True
    raise ToolError(
        f"timed out: bash has not returned in {self._timeout} seconds and must be restarted",
    ) from None
  • PythonExecute 类:在 execute 方法中,使用 multiprocessing.Process 执行 Python 代码,并通过 proc.join(timeout) 设置了执行超时时间(默认为 5 秒)。如果超时,会终止进程并返回错误信息。
proc = multiprocessing.Process(
    target=self._run_code, args=(code, result, safe_globals)
)
proc.start()
proc.join(timeout)

if proc.is_alive():
    proc.terminate()
    proc.join(1)
    return {
        "observation": f"Execution timeout after {timeout} seconds",
        "success": False,
    }

4.3 资源清理

  • DockerSandbox 类:在 create 方法中,如果创建沙箱容器时出现异常,会调用 cleanup 方法确保资源被清理,避免资源泄漏。
try:
    # 创建沙箱容器的相关操作
except Exception as e:
    await self.cleanup()  # Ensure resources are cleaned up
    raise RuntimeError(f"Failed to create sandbox: {e}") from e

4.4 错误日志记录

项目使用 loguru 库进行日志记录,在关键操作中记录错误信息,方便后续排查问题。例如,在 PlanningFlow 类的 execute 和 _execute_step 方法中,使用 logger.error 记录执行过程中出现的错误。

try:
    # 执行步骤的相关操作
except Exception as e:
    logger.error(f"Error executing step {self.current_step_index}: {e}")
    return f"Error executing step {self.current_step_index}: {str(e)}"

4.5 测试用例中的错误处理验证

在测试文件中,编写了多个测试用例来验证错误处理机制的有效性:

  • test_local_error_handling:测试本地沙箱的错误处理,尝试读取和复制不存在的文件,验证是否抛出相应的错误。
@pytest.mark.asyncio
async def test_local_error_handling(local_client: LocalSandboxClient):
    await local_client.create()

    with pytest.raises(Exception) as exc:
        await local_client.read_file("/nonexistent.txt")
    assert "not found" in str(exc.value).lower()

    with pytest.raises(Exception) as exc:
        await local_client.copy_from("/nonexistent.txt", "local.txt")
    assert "not found" in str(exc.value).lower()
  • test_sandbox_error_handling:测试使用无效配置创建沙箱时的错误处理,验证是否抛出异常。
@pytest.mark.asyncio
async def test_sandbox_error_handling():
    invalid_config = SandboxSettings(image="nonexistent:latest", work_dir="/invalid")
    sandbox = DockerSandbox(invalid_config)
    with pytest.raises(Exception):
        await sandb

在 OpenManus 项目里,信息的保存、传递与使用机制是保障系统正常运行与功能实现的关键。下面将从信息保存、信息传递和信息使用三个方面详细阐述。

五、消息保存、传递和使用机制

5.1 信息保存机制

1. 内存保存

  • Message 类:定义了聊天消息的结构,包含 role(角色)、content(内容)、tool_calls(工具调用)等字段,可保存聊天过程中的各类消息。
class Message(BaseModel):
    role: ROLE_TYPE = Field(...)
    content: Optional[str] = Field(default=None)
    tool_calls: Optional[List[ToolCall]] = Field(default=None)
    # 其他字段...
  • Memory 类:负责管理消息列表,具备添加消息、添加多条消息、清空消息、获取最近消息以及将消息转换为字典列表等功能。
class Memory(BaseModel):
    messages: List[Message] = Field(default_factory=list)
    max_messages: int = Field(default=100)

    def add_message(self, message: Message) -> None:
        self.messages.append(message)
        # 可选:实现消息数量限制
        if len(self.messages) > self.max_messages:
            self.messages = self.messages[-self.max_messages:]

    # 其他方法...

2. 文件保存

  • FileOperator 协议:定义了文件操作的接口,包含读取文件、写入文件、检查路径是否为目录、检查路径是否存在以及运行 shell 命令等方法,可实现信息在文件中的保存。
@runtime_checkable
class FileOperator(Protocol):
    async def read_file(self, path: PathLike) -> str:
        ...

    async def write_file(self, path: PathLike, content: str) -> None:
        ...

    # 其他方法...

5.2 信息传递机制

1. 消息传递

  • BaseAgent 类:通过 update_memory 方法将消息添加到内存中,实现信息在代理内部的传递。
class BaseAgent(BaseModel, ABC):
    def update_memory(self, role: ROLE_TYPE, content: str, base64_image: Optional[str] = None, **kwargs) -> None:
        message_map = {
            "user": Message.user_message,
            "system": Message.system_message,
            "assistant": Message.assistant_message,
            "tool": lambda content, **kw: Message.tool_message(content, **kw),
        }

        if role not in message_map:
            raise ValueError(f"Unsupported message role: {role}")

        kwargs = {"base64_image": base64_image, **(kwargs if role == "tool" else {})}
        self.memory.add_message(message_map[role](content, **kwargs))

2. 工具调用传递

  • MCPAgent 类:连接到 MCP 服务器,将服务器的工具添加到代理的工具接口中,实现工具信息的传递。
class MCPAgent(ToolCallAgent):
    async def initialize(self, connection_type: Optional[str] = None, server_url: Optional[str] = None, command: Optional[str] = None, args: Optional[List[str]] = None) -> None:
        # 连接到 MCP 服务器
        if self.connection_type == "sse":
            await self.mcp_clients.connect_sse(server_url=server_url)
        elif self.connection_type == "stdio":
            await self.mcp_clients.connect_stdio(command=command, args=args or [])

        self.available_tools = self.mcp_clients
        # 存储初始工具模式
        await self._refresh_tools()
        # 添加系统消息和可用工具信息
        tool_names = list(self.mcp_clients.tool_map.keys())
        tools_info = ", ".join(tool_names)
        self.memory.add_message(
            Message.system_message(
                f"{self.system_prompt}\n\nAvailable MCP tools: {tools_info}"
            )
        )

5.3 信息使用机制

1. 消息使用

  • LLM 类:通过 format_messages 方法将消息转换为 OpenAI 消息格式,供大语言模型使用。
class LLM:
    @staticmethod
    def format_messages(messages: List[Union[dict, Message]], supports_images: bool = False) -> List[dict]:
        formatted_messages = []

        for message in messages:
            if isinstance(message, Message):
                message = message.to_dict()

            # 处理消息
            if isinstance(message, dict):
                # 检查消息是否包含必要字段
                if "role" not in message:
                    raise ValueError("Message dict must contain 'role' field")

                # 处理 base64 图像
                if supports_images and message.get("base64_image"):
                    # 初始化或转换内容为适当格式
                    if not message.get("content"):
                        message["content"] = []
                    elif isinstance(message["content"], str):
                        message["content"] = [
                            {"type": "text", "text": message["content"]}
                        ]
                    elif isinstance(message["content"], list):
                        message["content"] = [
                            (
                                {"type": "text", "text": item}
                                if isinstance(item, str)
                                else item
                            )
                            for item in message["content"]
                        ]

                    # 添加图像到内容
                    message["content"].append(
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:image/jpeg;base64,{message['base64_image']}"
                            },
                        }
                    )

                    # 删除 base64_image 字段
                    del message["base64_image"]
                elif not supports_images and message.get("base64_image"):
                    del message["base64_image"]

                if "content" in message or "tool_calls" in message:
                    formatted_messages.append(message)
            else:
                raise TypeError(f"Unsupported message type: {type(message)}")

        # 验证所有消息是否包含必要字段
        for msg in formatted_messages:
            if msg["role"] not in ROLE_VALUES:
                raise ValueError(f"Invalid role: {msg['role']}")

        return formatted_messages

2. 工具使用

  • PlanningFlow 类:根据计划步骤选择合适的代理执行任务,调用工具完成相应操作。
class PlanningFlow(BaseFlow):
    async def execute(self, input_text: str) -> str:
        try:
            if not self.primary_agent:
                raise ValueError("No primary agent available")

            # 创建初始计划
            if input_text:
                await self._create_initial_plan(input_text)

                # 验证计划是否创建成功
                if self.active_plan_id not in self.planning_tool.plans:
                    logger.error(
                        f"Plan creation failed. Plan ID {self.active_plan_id} not found in planning tool."
                    )
                    return f"Failed to create plan for: {input_text}"

            result = ""
            while True:
                # 获取当前要执行的步骤
                self.current_step_index, step_info = await self._get_current_step_info()

                # 如果没有更多步骤或计划完成,则退出
                if self.current_step_index is None:
                    result += await self._finalize_plan()
                    break

                # 使用合适的代理执行当前步骤
                step_type = step_info.get("type") if step_info else None
                executor = self.get_executor(step_type)
                step_result = await self._execute_step(executor, step_info)
                result += step_result + "\n"

                # 检查代理是否要终止
                if hasattr(executor, "state") and executor.state == AgentState.FINISHED:
                    break

            return result
        except Exception as e:
            logger.error(f"Error in PlanningFlow: {str(e)}")
            return f"Execution failed: {str(e)}"

5.4 小结

综上所述,OpenManus 项目通过内存和文件保存信息,利用消息传递和工具调用传递信息,并在大语言模型调用和任务执行中使用信息,形成了一套完整的信息保存、传递与使用机制。