数据截至 (上游 commit 1c4253f6774b)
工具即 MCP 服务:子进程、搜索/抓取/沙箱/推理工具
30 秒导读: 模型能"想",但要真去搜网页、跑代码、抓页面,得有"手脚"。MiroThinker 把每一件手脚都做成一个独立的 MCP 服务(Model Context Protocol,一种标准化的"模型-工具"通信协议)。本章讲这层手脚怎么实现:
ToolManager每调一次工具就起一个子进程会话,把结果统一成{server_name, tool_name, result|error}回给编排循环;并逐个看几个真实工具——搜索、抓取、沙箱、"强模型当工具"、待办清单。
本章讲的是"手脚层"本身怎么落地。相邻章节各管一段:工具怎么被配置进来在 01-config-and-assembly;工具怎么被主循环调度在 02-orchestrator-loop;模型给的调用参数怎么容错修复在 03-robustness-rollback。这里只关心:一次工具调用在底层是怎么跑起来、跑完的,以及每个工具各干什么。
1. 这是什么(零基础也能懂)
先建立一个心智模型
一个"深度研究 agent"的模型本身只会一件事:输出文字。它没法真的去 Google 搜索、没法真的跑一段 Python、没法真的打开一个网页。这些动作必须由外部程序替它执行,再把结果喂回去。这层外部程序,就是"工具"。
MiroThinker 没有把工具写成"函数直接调用",而是把每一类工具做成一个独立的小服务,用 MCP 协议跟主程序通信。
- MCP(Model Context Protocol): 一种约定"模型这边怎么发现工具、怎么调工具、工具怎么回结果"的标准协议。你可以把它想成"工具界的 USB 口":只要按这个口的形状做,主程序就能即插即用地挂上任意工具。
- 服务(server): 一个工具服务 就是一个能独立启动的 Python 程序(比如
search_and_scrape_webpage.py),里面用@mcp.tool()装饰器登记了若干工具函数。
一句话类比
把
ToolManager想成一个外包调度台:主 agent 说"帮我搜一下 X",调度台就临时雇一个工人(启动一个工具子进程)、把活派过去、拿回成果、然后把工人辞退(关掉子进程)。下次再有活,再临时雇一个。唯一的例外是浏览器——那个工人贵、且要记住"我现在停在哪个页面",所以长期留用。
它能做什么(本章覆盖的几个工具)
| 工具(服务) | 干什么 | 源文件 |
|---|---|---|
google_search / sogou_search | 网络搜索(Serper / 腾讯云搜狗) | dev_mcp_servers/search_and_scrape_webpage.py |
create_sandbox / run_python_code … | 在 E2B 云沙箱里跑代码 | mcp_servers/python_mcp_server.py |
reasoning | 把 claude-3-7-sonnet 当一个"推理工具"来调 | mcp_servers/reasoning_mcp_server.py |
add_todo / list_todos / complete_todo | 给 agent 一份可读写的待办清单 | dev_mcp_servers/task_planner.py |
playwright(浏览器) | 常驻会话,操作真实浏览器 | mcp_servers/browser_session.py |
2. 顶层全景(一次工具调用怎么跑)
本节讲"大盘":从主循环喊出"调用某工具",到底层子进程跑完、结果回来,中间经过了什么。
怎么读这张图
从上到下是时间顺序。核心是中间那个虚线框——它对每一次调用都完整走一遍:起子进程 → 建会话 → 调工具 → 关会话。
编排主循环 (02 章)
│ execute_tool_call(server_name, tool_name, arguments)
▼
┌───────────────── ToolManager.execute_tool_call ─────────────────┐
│ ① 查配置:这个 server_name 的启动参数是什么? │
│ ② 是 "playwright" 吗?—— 是 → 走【常驻】PlaywrightSession │
│ 否 → 走【一次性】子进程会话 ↓ │
│ ┌─────────────── 一次性 MCP 会话(每调用一次) ──────────────┐ │
│ │ stdio_client(params) ← 起一个工具子进程 │ │
│ │ └ ClientSession ← 握手 initialize() │ │
│ │ └ call_tool(tool_name, arguments) ← 真正干活 │ │
│ │ └ 取 content[-1].text 当结果 │ │
│ │ (with 语句退出 → 子进程关闭) │ │
│ └────────────────────────────────────────── ────────────────┘ │
│ ③ 防作弊:是在抓 HF 数据集找答案吗?→ 是则替换成警告文本 │
│ ④ 出错兜底:scrape 失败 → 试 MarkItDown fallback │
│ ⑤ 打包成 {server_name, tool_name, result | error} │
└──────────────────────────────────────┬─────────────────────────┘
│ │
▼ ▼
tool_executor 后处理 回到主循环
(DEMO_MODE 截断到 20000 字)
部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
ToolManager | 工具调用的总入口:发现工具、执行工具、收口结果 | manager.py:48 ToolManager |
get_all_tool_definitions | 启动时连上所有 server,列出它们有哪些工具(喂给 prompt) | manager.py:104 |
execute_tool_call | 执行一次工具调用,是本章主角 | manager.py:198 |
PlaywrightSession | 浏览器专用的常驻会话(不每次重启) | browser_session.py:15 PlaywrightSession |
with_timeout | 装饰器:给任意 async 函数套一个超时 | manager.py:19 with_timeout |
ToolExecutor(app 层) | 调用前的参数修复、调用后的结果后处理 | tool_executor.py:30 ToolExecutor |
3. 核心原理
3.1 每次调用起一个子进程会话(MCP 生命周期)
它要解决的小问题: 工具服务是独立程序,主程序怎么跟它对话?MiroThinker 的答案是——不常驻,用完即弃。
思路: MCP 的 stdio 传输(标准输入输出)本质是:启动一个子进程,通过它的 stdin/stdout 收发 JSON-RPC 消息。stdio_client(server_params) 是个异步上下文管理器——进入 with 时起子进程,退出 with 时关子进程。所以"