ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenManus 项目深度解析:BaseAgent 与 MCP 工具链的协作机制

OpenManus 项目深度解析:BaseAgent 与 MCP 工具链的协作机制 1. OpenManus 里 BaseAgent 到底在调度什么一次多步任务的执行循环拆解OpenManus 是一个开源的通用 AI 智能体框架核心目标是把「大模型推理」和「真实工具执行」串成一个能跑多步任务的闭环。它最值得研究的部分不是 UI而是 BaseAgent 这个抽象基类——它定义了智能体的状态机、步骤上限、记忆读写以及最关键的 Think-Act 循环。你可以把它理解成一个「任务调度中枢」LLM 负责想下一步做什么BaseAgent 负责把这一步翻译成工具调用、执行、把结果写回记忆然后决定是否继续。适合谁看如果你正在做 Agent 方向的应用开发或者想把 MCP 工具链接进自己的自动化流程又或者你只是好奇 Manus 这类产品背后的执行机制这篇拆解都能直接跟做。我会从 BaseAgent 的字段定义讲到 MCP 工具注册再给出一份可复制的配置片段最后用一个真实任务演示从输入到工具调用的完整链路。先说清楚一个容易混淆的点OpenManus 里的 BaseAgent 不是「一个能聊天的机器人」它是一个带生命周期管理的执行器。它的 run() 方法内部是一个 while 循环每轮做三件事——think调 LLM 拿决策、act执行工具、update写记忆并判断终止。这个循环的边界由 max_steps 控制默认 10 步超过就强制终止避免死循环烧 token。我试过把一个「读取本地 CSV 并生成统计摘要」的任务丢进去观察它的执行轨迹第 1 步 LLM 决定调用 PythonExecute 读取文件第 2 步根据返回的行数决定调用 PythonExecute 做聚合第 3 步调用 Terminate 结束。整个过程 BaseAgent 本身不写业务逻辑它只负责把 LLM 的 tool_calls 映射到 ToolCollection 里的具体工具实例。这就是它和普通 ChatBot 的本质区别ChatBot 输出文本就结束了BaseAgent 输出的是「动作」并且会等动作结果回来再继续推理。理解这一点之后MCP 工具链的接入就顺理成章了。MCPModel Context Protocol本质上是一套标准化的工具描述与调用协议让 BaseAgent 可以在运行时动态发现外部工具服务器提供的能力而不需要把每个工具都硬编码进 ToolCollection。下面几节我会把配置、注册、验证、排障逐层拆开。2. TaoToken 前置给 BaseAgent 配一个稳定的 LLM 出口与 MCP 工具接入示例BaseAgent 的 think 阶段完全依赖 LLM 返回结构化的 tool_calls所以 LLM 接口的稳定性直接决定整个执行循环能不能跑通。OpenManus 的 LLM 层做了抽象支持 OpenAI、Anthropic、Azure、Ollama 等多种提供商配置集中在 config/config.toml 里。这里我用 TaoToken 作为统一出口来演示因为它同时提供 OpenAI 兼容接口和 Anthropic 兼容接口切换模型时不用改业务代码。先拿 Key。打开 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 创建一个 API Key复制下来。注意这个 Key 只在创建时完整显示一次丢了就得重建。拿到之后Base URL 用 https://taotoken.net/api 不要带任何多余路径OpenManus 的 LLM 客户端会自动拼接 /v1/chat/completions 或 /v1/messages。接下来是模型选择。BaseAgent 的 think 阶段需要模型支持 function calling / tool use否则 LLM.ask_tool() 拿不到结构化的 tool_calls循环第一步就会卡住。实测下来Claude 系列和 GPT 系列在工具调用上的表现都比较稳。你可以在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 看到当前可用的模型列表选一个支持 tool use 的即可。配置写进 config/config.toml 的 [llm] 段。这里有个坑OpenManus 不同版本的字段名略有差异有的用 api_type有的用 provider。下面这份是当前主流版本的写法我按 OpenAI 兼容模式配置# config/config.toml [llm] model claude-3-5-sonnet-20241022 base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 max_tokens 4096 temperature 0.0 api_type openai [llm.vision] model claude-3-5-sonnet-20241022 base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 [runflow] use_data_analysis_agent false [mcp] servers []temperature 设 0.0 是有意的。BaseAgent 的决策需要可复现温度高了会导致同一任务每次走的工具路径都不一样排障时很难定位。max_tokens 给 4096 是因为 tool_calls 的 JSON 结构本身占 token太小会被截断导致解析失败。如果你要用 Anthropic 原生协议而不是 OpenAI 兼容模式把 api_type 改成 anthropicbase_url 保持 https://taotoken.net/api 不变模型名换成 claude-3-5-sonnet-20241022 这类 Anthropic 命名即可。两种模式在 OpenManus 内部走的是不同的 LLM 子类但 BaseAgent 的调用方式完全一致。MCP 工具服务器的配置也在同一个文件里[mcp] 段的 servers 数组。每个 server 是一个对象包含 type、command、args 等字段。这部分我放到下一节和工具注册一起讲因为 MCP 的接入方式和本地工具注册是两套逻辑混在一起容易乱。配好之后先别急着跑任务用一行命令验证 LLM 出口是否通curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的TaoToken密钥 \ -H Content-Type: application/json \ -d {model:claude-3-5-sonnet-20241022,messages:[{role:user,content:ping}],max_tokens:16}返回里有 choices[0].message.content 就说明出口正常。这一步能省掉后面 80% 的「LLM 调用失败」类排障时间。3. 可复制配置BaseAgent 字段定义与 MCP 工具注册的完整 settings 片段这一节是全文的技术核心。BaseAgent 的调度能力来自它的字段设计而 MCP 工具链的接入则依赖 ToolCollection 的动态加载。我把两者拆开讲最后给一份能直接跑的完整配置。先看 BaseAgent 的字段。它继承自 Pydantic 的 BaseModel 和 ABC所以既有数据校验又有抽象方法约束class BaseAgent(BaseModel, ABC): name: str Field(..., descriptionUnique name of the agent) llm: LLM Field(default_factoryLLM, descriptionLanguage model instance) memory: Memory Field(default_factoryMemory, descriptionAgents memory store) state: AgentState Field(defaultAgentState.IDLE, descriptionCurrent agent state) max_steps: int Field(default10, descriptionMaximum steps before termination)name 是智能体标识多智能体协作时用来区分角色。llm 是 LLM 实例默认从 config.toml 读取配置构造。memory 是记忆存储管理 user/assistant/tool/system 四类消息。state 是状态机取值包括 IDLE、RUNNING、FINISHED、ERROR。max_steps 是循环上限前面说过默认 10。真正干活的是 ToolCallAgent它继承 BaseAgent 并实现了 think 和 actclass ToolCallAgent(BaseAgent): available_tools: ToolCollection Field(default_factoryToolCollection) async def think(self) - bool: # 调 LLM.ask_tool()把 memory 里的消息和工具 schema 一起发出去 # 返回的 tool_calls 存到 self.tool_calls ... async def act(self) - str: # 遍历 self.tool_calls逐个调 available_tools.execute() # 结果写回 memory ...Manus 是 ToolCallAgent 的具体实现它在 available_tools 里预置了本地工具class Manus(ToolCallAgent): available_tools: ToolCollection Field( default_factorylambda: ToolCollection( PythonExecute(), BrowserUseTool(), StrReplaceEditor(), AskHuman(), Terminate(), ) )这里就是 MCP 工具链的接入点。ToolCollection 支持在初始化后动态追加工具MCP 服务器提供的工具会在运行时被包装成 BaseTool 的子类实例塞进 available_tools。所以 BaseAgent 的 think 阶段看到的工具 schema 是「本地工具 MCP 远程工具」的并集LLM 不需要知道某个工具是本地还是远程它只按 schema 决策。MCP 服务器的配置写在 config/config.toml[mcp] servers [ { type stdio, command npx, args [-y, modelcontextprotocol/server-filesystem, /tmp/openmanus-workspace], env {} }, { type sse, url https://your-mcp-server.example.com/sse, headers { Authorization Bearer your-token } } ]stdio 类型适合本地进程command args 指定启动命令。sse 类型适合远程服务url 指向 SSE 端点。两种类型在 OpenManus 内部都会被 MCPClient 接管工具列表通过 list_tools 拉取调用通过 call_tool 转发。如果你用的是 Cline 或 Claude Code 这类支持 MCP 的编辑器配置格式略有不同但三件套是一样的Base URL、Key、Model ID。以 Cline 的 MCP settings 为例{ mcpServers: { filesystem: { command: npx, args: [-y, modelcontextprotocol/server-filesystem, /tmp/openmanus-workspace] } } }注意 Cline 的 MCP 配置只管工具服务器LLM 出口在它自己的 API 配置里单独设。如果你在 Cline 里用 TaoTokenBase URL 填 https://taotoken.net/api Key 填你的密钥Model ID 填 claude-3-5-sonnet-20241022。这三件套缺一不可少一个就会报 401 或 model not found。回到 OpenManus。配好 MCP 之后启动 run_mcp.py 模式python run_mcp.py这个入口和 main.py 的区别在于它会在构造 Manus 实例之前先初始化 MCPClient把远程工具注册进 ToolCollection。如果你直接跑 main.pyMCP 工具不会加载LLM 也就看不到它们。4. 验证请求一次任务从输入到工具调用的完整执行链路配置写完必须验证否则你不知道是 LLM 没返回 tool_calls还是工具注册失败还是执行结果没写回记忆。这一节我用一个具体任务走完整链路。任务描述「在当前目录创建一个 hello.txt写入三行文本然后读取并返回文件行数。」启动 run_mcp.py 后输入这个任务。BaseAgent 的执行循环会这样走第 1 轮 thinkLLM 收到 system prompt 用户任务 工具 schema 列表。它决定调用 StrReplaceEditor 创建文件。返回的 tool_calls 结构大致是{ id: call_abc123, type: function, function: { name: str_replace_editor, arguments: {\command\:\create\,\path\:\hello.txt\,\file_text\:\line1\\nline2\\nline3\\n\} } }第 1 轮 actBaseAgent 把 tool_calls 里的 name 映射到 available_tools 里的实例调 execute()。StrReplaceEditor 执行文件创建返回 File created successfully at hello.txt。第 1 轮 update工具结果作为 tool message 写进 memory。state 保持 RUNNING。第 2 轮 thinkLLM 看到文件已创建决定调用 PythonExecute 读取文件并计数。tool_calls 的 arguments 是 {code: print(len(open(hello.txt).readlines()))}。第 2 轮 actPythonExecute 执行代码返回 3。第 2 轮 update结果写回 memory。第 3 轮 thinkLLM 判断任务完成调用 Terminate 工具。Terminate 的 execute 返回终止信号BaseAgent 把 state 置为 FINISHED跳出循环。整个过程你能在终端看到每一步的日志。如果你想更细粒度地验证可以在 think 和 act 里加日志import logging logger logging.getLogger(__name__) async def think(self) - bool: logger.info(f[{self.name}] thinking, step{self.current_step}) result await super().think() logger.info(f[{self.name}] tool_calls{self.tool_calls}) return result跑一次之后日志里应该能看到 tool_calls 非空。如果 tool_calls 是空列表说明 LLM 没决定调工具可能是模型不支持 function calling或者 system prompt 里的工具描述没传对。验证 MCP 工具是否注册成功可以在启动后打印 available_tools 的 name 列表print([tool.name for tool in agent.available_tools.tools])如果列表里只有 python_execute、browser_use、str_replace_editor 这几个本地工具没有 MCP 服务器提供的工具说明 MCPClient 初始化失败或者 servers 配置没被读取。这时候去检查 config.toml 的 [mcp] 段是否在正确的 section 下以及 npx 命令是否在 PATH 里。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 逐条对照排障的核心是看报错发生在哪个阶段。BaseAgent 的执行链路分三段LLM 调用、工具执行、记忆更新。不同阶段的报错特征完全不同。401 Unauthorized 出现在 LLM 调用阶段。原因通常是 api_key 没填、填错、或者 Key 被禁用。检查 config.toml 里 [llm] 段的 api_key 是否和你在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 创建的一致。注意不要有多余空格TOML 里字符串两端的空格会被保留。如果用的是环境变量注入确认变量名和代码里读取的一致。local proxy failed 这个报错比较有迷惑性。它通常出现在 LLM 客户端初始化阶段本质是 base_url 配置有问题。OpenManus 的 LLM 子类会根据 api_type 拼接路径如果你把 base_url 写成 https://taotoken.net/api/v1 它再拼一次 /v1/chat/completions 就变成了 /api/v1/v1/chat/completions直接 404。正确写法是 base_url https://taotoken.net/api 不要带 /v1。另外确认没有在系统层面配置任何会拦截请求的环境变量这类变量会干扰 httpx 客户端的连接。reading choices 报错出现在 LLM 返回解析阶段。完整报错通常是 KeyError: choices 或 TypeError: NoneType object is not subscriptable。这说明 LLM 返回的 JSON 里没有 choices 字段或者返回体是空的。可能原因有三个一是模型名写错了接口返回了 error 对象而不是正常响应二是 max_tokens 设得太小响应被截断导致 JSON 不完整三是 api_type 和实际接口协议不匹配比如用 OpenAI 模式去调 Anthropic 原生端点。逐个排查先用 curl 确认模型名有效再把 max_tokens 调到 4096最后确认 api_type 和 base_url 的组合正确。OAuth 相关报错出现在 MCP 远程服务器连接阶段。如果你配的是 sse 类型的 MCP server且服务器要求 OAuth 认证headers 里的 token 过期就会报 401 或 invalid_token。解决办法是重新获取 token 并更新 config.toml。stdio 类型的 server 一般不涉及 OAuth但如果 command 指向的进程需要认证也会在 stderr 里输出认证失败信息这些信息会被 MCPClient 捕获并抛到主进程。还有一个不报错但行为异常的情况BaseAgent 循环跑满 max_steps 还没结束。这通常是因为 LLM 反复调用同一个工具但没推进任务。检查 system prompt 里是否明确告诉模型「任务完成后必须调用 Terminate」。OpenManus 默认的 prompt 里有这句但如果你自定义了 prompt 模板可能漏掉了。另一个原因是工具执行结果没有正确写回 memory导致 LLM 每轮看到的上下文都一样自然做不出新决策。在 act 方法里加日志确认 tool result 确实进了 memory。6. 把 BaseAgent 和 MCP 工具链接进你的日常编码流拆完执行循环和工具注册你会发现 OpenManus 的设计其实很克制BaseAgent 只管状态和循环ToolCallAgent 只管 think-act 的翻译具体能力全部下沉到工具层。这种分层让 MCP 工具的接入变得很自然——远程工具和本地工具在 ToolCollection 里是平级的LLM 看到的只是 schema。如果你打算长期用这套机制做编码或 Agent 任务建议把 LLM 出口固定下来避免每次换模型都改配置。TaoToken 的 OpenAI 兼容接口和 Anthropic 兼容接口可以让你在 config.toml 里只改 model 字段就完成切换base_url 和 api_key 保持不变。需要长期跑多步任务的话可以看看 Coding Plan 的额度方案https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它比按量计费更适合高频调用场景。调试阶段想快速验证模型返回的 tool_calls 结构可以直接在模型对话里发一条带工具 schema 的请求https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 对比不同模型的 function calling 输出格式差异。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的完整示例。最后留一个实操建议每次改完 config.toml先跑一遍第 2 节那个 curl 验证 LLM 出口再跑 run_mcp.py 打印工具列表最后才跑完整任务。这三步能把排障范围从「整个链路」缩小到「单个环节」省下来的时间够你多调几个 MCP server 了。
返回列表