跳到主要内容

工具系统与 MCP:agent 的"手脚"怎么定义、怎么接远程

这章讲什么: agent 靠工具动手。这一章讲三件事——(1) 一个工具长什么样(BaseTool),(2) 工具箱怎么管理它们并转成 LLM 认得的格式(ToolCollection),(3) 怎么把别人机器上的工具(通过 MCP 协议)接进来当本地工具用(MCPClientTool)。


1. 一个工具长什么样:BaseTool

1.1 三要素 + 一个方法

每个工具都继承 BaseToolapp/tool/base.py:78),本质是一个 Pydantic 模型,有三个字段和一个必须实现的方法:

成员作用
name工具名,LLM 用它来指名调用
description给 LLM 看的说明——决定它"知不知道该在什么时候用你"
parametersJSON Schema,描述参数——决定 LLM 怎么填参数
async execute(**kwargs)抽象方法,工具真正干活的地方

最小的工具例子就是 Terminate(见 §02)。再看 AskHumanapp/tool/ask_human.py:4)——execute 就一行 input(),把"问人"也变成一个工具:

# 示意,非源码:一个极简工具
class AskHuman(BaseTool):
name = "ask_human"
description = "Use this tool to ask human for help."
parameters = {"type": "object", "properties": {"inquire": {...}}, "required": ["inquire"]}
async def execute(self, inquire):
return input(f"Bot: {inquire}\nYou: ").strip()

1.2 to_param:把工具翻译成 LLM 的语言

LLM 的 function-calling 接口需要一种固定格式的"工具定义"。BaseTool.to_paramapp/tool/base.py:124)就是这个翻译器:

# 示意,非源码:to_param 输出
{
"type": "function",
"function": {"name": self.name, "description": self.description, "parameters": self.parameters}
}

这正是 §02 里 available_tools.to_params() 喂给 ask_tooltools 参数。工具的三要素直接决定了 LLM 看到的"工具菜单"。

1.3 ToolResult:统一的结果信封

工具执行返回 ToolResultapp/tool/base.py:38),四个可选字段:output(正常输出)、error(错误)、base64_image(截图等多模态)、system(系统级信息)。

两个巧妙的重载:

  • __bool__:只要任一字段有值就为真(app/tool/base.py:49)——方便 if result: 判断。
  • __add__:两个结果可以相加合并(文本拼接、图片二选一)(app/tool/base.py:52),方便把多段输出攒到一起。

2. 工具箱:ToolCollection

ToolCollectionapp/tool/tool_collection.py:9)就是"一堆工具"的容器,做三件事:

  1. 注册/去重add_tool:51)遇到同名工具会跳过并告警——防止 MCP 工具和本地工具重名互相覆盖。
  2. 转格式to_params:22)= 对每个工具调 to_param,得到给 LLM 的完整工具菜单。
  3. 按名执行execute:25)根据 nametool_map 找到工具、传入参数调用;找不到返回 ToolFailure,工具自己抛 ToolError 也被兜住转成失败结果。
# 示意,非源码:按名执行
async def execute(self, name, tool_input):
tool = self.tool_map.get(name)
if not tool:
return ToolFailure(error=f"Tool {name} is invalid")
try:
return await tool(**tool_input) # 调用 __call__ → execute
except ToolError as e:
return ToolFailure(error=e.message)

tool_map(名字→工具实例)是核心索引;tools(元组)是有序列表。两者在增删时同步维护。


3. MCP:把远程工具接成本地工具

3.1 它要解决的小问题

本地能内置的工具有限。如果想让 agent 用别人写好、跑在另一个进程/服务器上的工具(数据库工具、公司内部 API…),怎么接进来而不改 agent 代码?

答案是 MCP(Model Context Protocol)——一个标准协议:服务器"广播"自己有哪些工具,客户端连上去就能像调本地函数一样调它们。

3.2 MCPClientTool:远程工具的"本地替身"

MCPClientToolapp/tool/mcp.py:14)是关键抽象——它继承 BaseTool,所以对 agent 来说和本地工具毫无区别,但它的 execute 不在本地干活,而是转发一次远程调用

# 示意,非源码:远程工具代理
async def execute(self, **kwargs):
if not self.session:
return ToolResult(error="Not connected to MCP server")
result = await self.session.call_tool(self.original_name, kwargs) # 远程调用
text = 拼接(result.content 里的文本片段)
return ToolResult(output=text or "No output returned.")

真实实现见 app/tool/mcp.py:21。这就是代理模式(proxy):本地放个替身,调用被透明转发到远端。

3.3 MCPClients:连接与工具发现

MCPClientsapp/tool/mcp.py:37)继承 ToolCollection,管理与多个 MCP 服务器的连接。支持两种传输:

传输方法适合
stdioconnect_stdio:71本机子进程(启一个命令,用标准输入输出通信)
SSEconnect_sse:50远程 HTTP 服务器(Server-Sent Events)

连上之后 _initialize_and_list_tools:97)向服务器要工具清单,为每个远程工具建一个 MCPClientTool 替身,工具名加前缀 mcp_{server_id}_{原名} 并清洗非法字符(_sanitize_tool_name:128)避免命名冲突。

3.4 挂进 Manus

Manus.connect_mcp_serverapp/agent/manus.py:91)连好服务器后,把只属于该服务器的新工具加进 available_tools:109)。断开时 disconnect_mcp_server:114)重建工具箱、剔除该服务器的工具。整个过程 agent 的循环逻辑一行没变——工具是热插拔的

还有个刷新机制:MCPAgent(专门跑 MCP 的 agent)每隔几步会 _refresh_toolsapp/agent/mcp.py:87)重新拉工具清单,检测服务器端新增/移除/改了 schema 的工具并通过 system 消息通知 LLM。


4. 巧妙之处

  • 统一抽象让远程=本地MCPClientTool 继承 BaseTool,agent 完全不知道某个工具是本地还是远程的——扩展性来自这个统一接口,而非一堆 if/else。
  • 工具菜单即 prompt 工程:LLM 会不会/会不会正确用一个工具,几乎全取决于 descriptionparameters 写得好不好。工具的"智能"一半在这两个字段里。
  • 失败即观察:无论本地还是远程工具失败,都转成 ToolResult(error=...) 喂回 LLM,而非抛异常终止——让 agent 有机会自我纠错。

5. 边界与局限

  • 同名工具静默跳过add_tool 遇重名只告警不覆盖;如果你指望后加的工具替换旧的,会踩坑。
  • MCP 断连的 cancel scope 问题disconnectapp/tool/mcp.py:155)特意捕获并容忍 "cancel scope" 相关的 RuntimeError——这是 anyio/异步上下文栈的已知麻烦,代码是绕过而非根治。
  • 工具结果长度:靠 agent 的 max_observe 硬截断,没有更聪明的摘要/分页。

6. 代码地图

主题文件路径符号名
工具基类app/tool/base.pyBaseTool, BaseTool.to_param
结果信封app/tool/base.pyToolResult, ToolFailure
工具箱app/tool/tool_collection.pyToolCollection.execute, ToolCollection.add_tool, ToolCollection.to_params
远程工具代理app/tool/mcp.pyMCPClientTool.execute
MCP 连接管理app/tool/mcp.pyMCPClients.connect_stdio, MCPClients.connect_sse, MCPClients._initialize_and_list_tools
工具名清洗app/tool/mcp.pyMCPClients._sanitize_tool_name
挂载到 agentapp/agent/manus.pyManus.connect_mcp_server, Manus.initialize_mcp_servers
MCP agent 刷新app/agent/mcp.pyMCPAgent._refresh_tools
极简工具示例app/tool/ask_human.pyAskHuman