1. 前言
在上一篇文章中,我们深入剖析了 Agent Loop 的核心原理——Agent 如何在"思考-行动-观察"的循环中持续迭代,直到完成任务。如果说 Agent Loop 是 Agent 系统的"心跳",那么 Tool System(工具系统)就是 Agent 的"双手"——它决定了 Agent 能做什么、怎么做、以及做的过程中如何保证安全和可控。
在2025年以前,大模型应用流行的形态还是Chatbot,这类应用只能说不能做:它可以给你建议,但无法读你的代码、改你的文件、执行你的命令、搜索你的文档。Tool System 把 LLM 从一个"聊天机器"升级为"能干活的行者"。但这也带来了新的工程挑战:工具怎么定义,才能既让模型理解又让运行时高效执行?工具怎么注入,才能既全面又不撑爆上下文窗口?工具怎么执行,才能既灵活又安全?外部工具(MCP)怎么接入,才能和内建工具一样丝滑?
本文是 Agent 实现原理系列的第二篇,继续参考 Codex CLI、Claude Code、nanobot、pi 四个主流开源项目的实现,从工程角度总结 Tool System 的设计原则和关键权衡。
2. Tool System 的本质
2.1 从"函数调用"到"Tool Runtime"
在深入架构之前,先要澄清一个根本问题:工具到底是什么?
传统软件开发是在两个确定性系统之间建立契约——调用 getWeather("NYC") 每次都会以完全相同的方式获取天气。但 Agent 是非确定性的:同一个用户问题,它可能选择调用天气工具,也可能凭通用知识回答,甚至可能产生幻觉或误解工具用法。Anthropic 在其工具设计最佳实践文章中给出了一个精辟的定义1:工具是确定性系统与非确定性智能体之间的契约。
这个定义意味着,为 Agent 编写工具需要从根本上转变思维——不能再像为其他开发者写 API 那样设计工具,而必须面向一个"聪明但偶尔会犯糊涂、上下文有限、需要引导"的调用者来设计。如果你的工具让模型感到困惑、给了它太多无关信息、或者参数命名模糊,那不是模型的问题,是契约设计的问题。带着这个视角来看后面的 Tool System 架构,很多设计取舍的原因就清晰了。
一个最简单的 tool 实现,可以写成这样:
def execute_tool(name, args):
tool = tools[name]
return tool(args)
这种naïve的实现距离一个"能在真实产品中安全运行的 Tool Runtime"还有很大差距。真实场景需要回答的问题包括:
- 模型传入了非法参数(字符串当数字、数组当对象),如何优雅容错并让模型重试?
- 工具执行可能产生不可逆的副作用(删文件、发 HTTP 请求、修改数据库),如何在执行前做权限判定?
- 大量工具(几十个 MCP server、上百个 tool)如何按需注入,而不是全量撑爆上下文窗口?
- 工具的 schema、描述、执行逻辑、UI 渲染、权限策略,如何统一建模而不是分散在各处?
- 内部工具和外部 MCP 工具,如何在运行时走同一条调用链?
这些问题的本质是:Tool System 不是"函数的简单注册表",而是一套覆盖定义、注入、执行、回写全链路的运行时协议。
2.2 Tool System 的通用分层
Tool System 一般遵循如下分层设计:
flowchart TD
A["Tool 定义层:schema + description + call"] --> B["Tool 注册层:registry + filter + merge"]
B --> C["Tool 注入层:prompt assembly + defer loading"]
C --> D["LLM 调用"]
D --> E["Tool 路由层:识别 tool_call + 查找 handler"]
E --> F["Tool 执行层:validate → permission → execute → result"]
F --> G["Tool 回写层:tool_result → message → 下一轮 LLM"]
这六层可以进一步概括为三个核心问题:
- Tool 长什么样:定义层——如何描述一个工具的能力、输入、执行逻辑和渲染方式
- Tool 怎么进 prompt:注册+注入层——如何把当前会话可用的工具集合,变成模型能消费的 schema
- Tool 怎么执行和回写:路由+执行+回写层——模型发起调用后,如何安全执行并把结果喂回上下文
3. 主流项目的 Tool System 设计
3.1 Codex CLI2:按 Turn 动态装配的工具平台
Codex CLI 的 Tool System 最像一个平台级的能力装配系统。它定义了四层Tool体系如下:
描述层:
ToolSpec负责把能力描述成模型可见的统一规格。它有五种形态:- 普通
Function:JSON Schema function tool ToolSearch:客户端工具发现LocalShell:原生 shellImageGeneration:图像生成WebSearch:网络搜索Freeform:自由文本自定义工具 这些工具,有些是 Codex 自己实现的,有些直接复用 OpenAI Responses API 的原生 tool 类型,在路由和回写阶段统一进入同一条 agent loop。
- 普通
路由层:
ToolRouter内部维护两份东西——specs(完整的工具描述与并行能力配置)和registry(tool name 到 handler 的映射)。这意味着"模型看到什么"和"本地由谁处理"是一次性同时决定的。执行层:模型产出
ResponseItem后,先由ToolRouter::build_tool_call()识别是否为 tool call,再交给ToolRegistry::dispatch_any()统一执行。执行阶段会做 handler 查找、payload 类型校验、pre-tool-use hook、telemetry 统计。回写层:执行结果通过
to_response_item()回写为统一的FunctionCallOutput/ToolSearchOutput/McpToolCallOutput,写入会话历史,参与下一轮 prompt。
综上,Codex的核心特点可以概括为:
以
ToolSpec为模型契约、以ToolRouter为装配枢纽、以ToolRegistry为执行入口,按 turn 动态构建完整的工具暴露面和处理链。
Codex 最突出的设计是 tool 集合按 Turn 动态构建。每次 run_sampling_request() 前,built_tools() 会综合当前 turn 的 ToolsConfig、可见 MCP servers、已启用 plugins、用户显式提到的 connector、dynamic_tools、skill 解析结果等,重新构造 ToolRouter。这意味着同一个 session 的不同 turn,模型看到的工具集合可能完全不同——一个 skill 的激活、一个 connector 的连接,都会实时改变工具的暴露面。
此外,Codex 的 connector/app tool 有显式的 gating 机制:app tools 默认不全量塞给模型,而是只有当前 turn 允许的 connector 才会暴露。允许集合来自用户消息里的 app mention、skill 注入内容里的 app mention、线程里已记住的 connector selection、以及 config 中的 enabled 配置。这是一层非常实用的过滤——既控制了 prompt 大小,也避免了模型在无关工具上"乱用",浪费上下文长度。
3.2 Claude Code3:统一 Tool 协议与按需加载
Claude Code 的 Tool System 最强调统一抽象和按需加载。它把内建工具和 MCP 工具在中层强制收敛到同一个 Tool 接口,然后用 ToolSearch 机制解决"工具太多不能全量注入"的问题。
Claude Code 的核心 Tool 接口(buildTool() 构造)要求每个工具至少回答这些问题:
- 它叫什么:
name/aliases - 它接收什么输入:
inputSchema或inputJSONSchema - 它如何执行:
call(...) - 它如何描述自己:
description(...)/prompt(...) - 它是否只读、是否可并发、是否 destructive
- 它如何参与权限系统:
checkPermissions(...) - 它执行结果如何映射给模型:
mapToolResultToToolResultBlockParam(...) - 它在 UI 里怎么渲染:
renderToolUseMessage/renderToolResultMessage
这意味着 Claude Code 里的 tool 同时承担四个角色:模型侧的 function schema、运行时的可执行单元、权限系统的判定对象、UI/transcript 的渲染对象。这种"一个对象服务四方"的设计优点是统一,但导致了 Tool 接口很大。
Claude Code 的 Tool 装配分为三步:
getTools(permissionContext):按当前模式、deny rules、REPL 模式过滤内建工具assembleToolPool(permissionContext, mcpTools):把 built-in 和 MCP tools 合并,按名称去重,排序保持稳定以尽量维持 prompt cachemergeAndFilterTools(...):再按 coordinator mode 等约束做最终过滤
最终注入给模型的是 filteredTools,而不是 getAllBaseTools()。因此 getAllBaseTools() 是"定义全集",filteredTools 才是"本轮 API 实际注入集"。
Claude Code 最值得深入分析的设计是 ToolSearch 机制——一套为动态 tool pool 设计的 schema 按需暴露机制。它的核心闭环如下:
- 当 ToolSearch 启用时,deferred tools 以
defer_loading: true的形式出现在 API 请求中,但不携带完整的 input schema - 模型先调用
ToolSearchTool(它本身是一个普通内建 tool,负责"取回被 defer 的工具定义") ToolSearchTool的返回值不是文本,而是结构化的tool_referenceblocks——通知 API “请把这些工具的完整 schema 纳入后续上下文”- 下一轮构建 API 请求时,从消息历史里提取已被 ToolSearch 发现过的工具名,那些工具的完整 schema 才真正进入
filteredTools
这里"加载 deferred tool",本质上不是修改全局 tools 列表,而是通过历史消息里的 tool_reference,改变下一轮 API 看到的 filteredTools。这套机制主要解决两个问题:MCP tool 和 feature-gated tool 数量太多,首轮全量注入会撑爆上下文;tool pool 动态变化,全量注入会让 prompt cache 命中率很差。
另外值得关注的是 Claude Code 的 MCP 适配策略。MCP tool 进入系统后会基于 MCPTool 骨架对象补齐所有缺失字段——name、mcpInfo、isMcp、inputJSONSchema、prompt()、description()、isReadOnly() 等。完成适配后,运行时后续绝大多数逻辑不需要区分"这是内建还是 MCP",因为它们都已经变成统一的 Tool 了。MCP tool 的 call() 内部最终落到 callMCPTool() 发出标准 MCP tools/call,但外层仍然走统一的 runToolUse() 链路。这个设计非常巧妙,不用区分mcp和普通tool了。
3.3 nanobot4:声明式 schema + 受控执行器
nanobot 的 Tool System 最简单,最学术化,可以作为教科书参考。它的 Tool 体系是三层拼装:
Tool抽象:定义单个能力的 schema、参数校验和执行入口。每个 tool 自己声明 schema(JSON Schema),基类统一做cast_params()和validate_params()ToolRegistry:注册全部可用能力,向 provider 暴露标准化的 function schema,统一处理 tool 查找、参数 cast、参数校验、异常包装AgentRunner:执行LLM → tool_calls → ToolRegistry.execute() → tool result → LLM的循环
它的核心可以概括为:
把 tool 视为"声明式 schema + 受控执行器 + provider function-calling 适配"的统一能力单元,通过
ToolRegistry和AgentRunner把这些单元编排成可迭代的 agent runtime。
从学术角度看,nanobot 的 Tool System 可以用来参考学习:schema、cast、validate、execute 的职责分层清晰;内置 tool 和 MCP tool 共享完全同一条调用链。但从工程角度来看,它缺少 ToolSearch 式的按需加载、缺少细粒度的并发控制、也缺少 hook 扩展点,所以实用度不高。
3.4 pi5:definition-first 与会话管控
pi 的 Tool System 最强调 definition-first 和 session-controlled。它的设计可以概括为三条主线:
ToolDefinition不只是可调用函数,还同时携带 LLM 元信息(name、description、parameters)、prompt 元信息(promptSnippet、promptGuidelines)、UI 元信息(label、renderCall、renderResult、renderShell)、执行元信息(prepareArguments、executionMode、execute(...))AgentSession统一管理 tool 的可见性、active 集合和 prompt 注入,维护两份 tool 状态——_toolDefinitions(定义注册表)和_toolRegistry(执行注册表)- agent-loop 把 tool 调用拆成三阶段:
prepare→execute→finalize
pi 的标准 tool 集合非常精简,只有 7 个:
read:读取文本或图片文件bash:在当前 cwd 执行 shell 命令,支持超时和流式输出edit:对单文件做精确文本替换write:新建或整文件覆盖grep:用rg搜索文件内容,尊重.gitignorefind:用fd或自定义 glob 找文件ls:列目录内容
默认激活的只有 read、bash、edit、write 四个,其他三个是"内置只读增强工具"。这种精简策略本身就表达了pi项目的设计哲学:工具不是越多越好,模型需要的是正交、可靠、可组合的基础能力。这种哲学也是我比较欣赏的。
pi 的工具注入链路非常清晰,先应用 allowedToolNames / excludedToolNames 做总开关过滤,再把 built-in tool 放入 definitionRegistry(source 标为 builtin),之后 extension 和 SDK 自定义 tool 按名字覆盖写入——同名自定义 tool 会覆盖 built-in definition。随后从 definition 中提取 promptSnippet 和 promptGuidelines 做 prompt 重建。新注册的 tool 默认补进 active 集合。
pi 对 tool 注入 prompt 的方式也值得注意:tool 对模型的影响有两层——结构化 schema(在真正请求模型时作为 Context.tools)和非结构化提示(在 system prompt 里作为 Available tools 和 Guidelines)。这两层是解耦的,但都依赖 active tool 集合。
在三阶段执行中,pi 的 hook 机制设计得很简洁:
- prepare:找 tool → 运行
prepareArguments()→ schema 校验 → 调beforeToolCallhook(extension 的tool_call事件在此触发) - execute:真正执行,tool 内部通过
onUpdate()产生tool_execution_update事件(bash 等工具借此流式刷输出) - finalize:调
afterToolCallhook(extension 的tool_result事件在此触发),允许 hook 覆盖content、details、isError、甚至写terminate
pi 的并发策略也很务实:默认并行(prepare 逐个做,execute 允许并发,toolResultMessage 按 assistant 源顺序写回上下文),但如果 config.toolExecution === "sequential" 或 batch 中任一 tool 的 executionMode === "sequential",则退回串行。这套设计让 schema 校验和 hook 保持确定性顺序,真正耗时的 I/O 并发跑,需要共享状态的 tool 仍能强制串行。
3.5 各项目内置标准工具概览
每个项目都内置了一批标准工具,这些工具的种类、数量和粒度直接反映了该项目对 Agent 能力边界的设计判断。下面逐一盘点。
3.5.1 Codex CLI:大而全的平台级工具集
Codex 的内置工具按功能域可以分成七大类:
| 类别 | 工具 | 说明 |
|---|---|---|
| 基础执行 | shell, local_shell, exec_command, write_stdin, shell_command, apply_patch, view_image | 多个 shell 变体覆盖新旧 API 兼容;apply_patch 同时支持 function 和 freeform 两种形态 |
| 计划与人工交互 | update_plan, request_user_input, request_permissions | request_user_input 通过 oneshot channel 实现阻塞式问答,不是简单文本插入 |
| MCP 与外部能力 | MCP tools(动态), list_mcp_resources, read_mcp_resource, tool_search, tool_suggest | tool_search 搜索 deferred app/connector tool;tool_suggest 建议用户安装新插件 |
| Responses 原生 | web_search, image_generation | 不是本地 handler 模拟,而是交给上游 Responses API 原生执行 |
| 客户端协作 | dynamic_tools, js_repl, code_mode, wait | dynamic_tools 通过 app-server 往返,让桌面端/浏览器端能力也能被 agent loop 调用 |
| 多 Agent 编排 | spawn_agent, send_input, resume_agent, wait_agent, close_agent, send_message, assign_task, list_agents | 两组 API(v1 与 v2 变体),覆盖完整的子 Agent 生命周期管理 |
| 测试与实验 | list_dir, test_sync_tool, spawn_agents_on_csv, report_agent_job_result | 实验性质,多数受 feature flag 控制 |
3.5.2 Claude Code:基础能力 + 深度编排
Claude Code 的标准工具按功能域可以分成六大类:
| 类别 | 工具 | 说明 |
|---|---|---|
| 文件系统 | Read, Edit, Write, NotebookEdit | Edit 做精确字符串替换而非整文件重写,和第一篇中 Turn 的"副作用"建模互相对应 |
| Shell | Bash | 单一 shell 工具,但内部区分平台(bash/powershell) |
| 检索 | Glob, Grep, WebFetch, WebSearch | Glob 匹配文件名,Grep 搜索文件内容,两者正交 |
| Agent 编排 | Agent, TaskOutput, TaskStop | Agent 是子 Agent 的唯一入口,复用同一个 query() 内核 |
| 计划与交互 | EnterPlanMode, ExitPlanMode, AskUserQuestion, TodoWrite | AskUserQuestion 复用权限队列做 HIL;TodoWrite 让模型自管理任务列表 |
| 扩展生态 | Skill, ListMcpResources, ReadMcpResource, ToolSearch | Skill 加载领域知识;MCP 资源读写独立于 MCP tool 调用 |
此外还有大量条件启用的工具:EnterWorktreeTool/ExitWorktreeTool(Git worktree 隔离)、CronCreate/CronDelete/CronList(定时任务)、WorkflowTool(多 Agent 编排脚本)、PushNotification(桌面通知)、LSPTool(语言服务器)等。
3.5.3 nanobot:少即是多的最小能力集
nanobot 的内置工具总共只有 10 个,按功能域分四类:
| 类别 | 工具 | 说明 |
|---|---|---|
| 文件系统 | read_file, write_file, edit_file, list_dir | 都继承自 _FsTool 基类,共享统一的路径解析和越界校验 |
| Shell | exec | 受 tools.exec.enable 配置控制,可全局禁用 |
| Web | web_search, web_fetch | 共同特点是将外部内容明确标记为不可信数据,web_fetch 返回文本前插入 untrusted banner |
| 通信与调度 | message, spawn, cron | message 面向 channel 投递;spawn 交给 SubagentManager 后台执行;cron 只在挂载了 CronService 时才注册 |
其中 exec 和 cron 都有激活条件——不是"定义了就一定会暴露"。文件工具在 restrictToWorkspace=true 时会自动收紧到 workspace 范围。
3.5.4 pi:极致精简的七工具哲学
pi 的标准工具是四个项目中最少的——只有 7 个:
| 类别 | 工具 | 说明 |
|---|---|---|
| 文件读取 | read | 文本按行数/字节数截断;图片转成 message attachment |
| Shell | bash | 支持超时、流式输出、截断后落临时文件 |
| 文件编辑 | edit, write | edit 做精确文本替换(输入是 edits[],返回 details.diff);write 新建或整文件覆盖 |
| 只读增强 | grep, find, ls | grep 用 rg 搜索内容;find 用 fd 找文件;ls 列目录——三者默认不激活 |
默认激活的只有 read、bash、edit、write 四个。grep、find、ls 被归类为"内置只读增强工具"——它们不是必须的(模型可以用 bash 调用 rg/fd/ls 命令来替代),但作为显式工具暴露可以减少模型写出不可靠 shell 命令的概率。
3.5.5 内置工具对比表
| 能力域 | Codex CLI | Claude Code | nanobot | pi |
|---|---|---|---|---|
| 文件读写 | apply_patch, view_image | Read, Write | read_file, write_file | read, write |
| 文件编辑 | 通过 shell/patch | Edit, NotebookEdit | edit_file | edit |
| 文件搜索 | list_dir | Glob, Grep | list_dir | grep, find, ls |
| Shell | shell 等 5 个变体 | Bash | exec | bash |
| Web | web_search, tool_suggest | WebFetch, WebSearch | web_search, web_fetch | 无(通过 Extension) |
| 子 Agent | 8 个生命周期工具 | Agent, TaskOutput, TaskStop | spawn | 无(通过 Extension) |
| 人机交互 | request_user_input, request_permissions | AskUserQuestion, TodoWrite | message | 无(通过 Extension) |
| 计划与模式 | update_plan | EnterPlanMode, ExitPlanMode | 无 | 无(通过 Extension) |
| 扩展生态 | dynamic_tools, tool_search, tool_suggest | Skill, ToolSearch, ListMcpResources, ReadMcpResources | MCP 动态包装 | pi.registerTool() |
| 定时调度 | 无 | CronCreate, CronDelete, CronList | cron(条件注册) | 无 |
| 工作区隔离 | 无 | EnterWorktree, ExitWorktree | 无 | 无 |
| 总计(约) | 30+ | 20+(含条件启用 35+) | 10 | 7(默认激活 4) |
从数量上看,可以看到两种不同的默认工具设计风格:
- “胖工具层”(Codex、Claude Code):把复杂能力封装为显式工具,让模型通过 function calling 直接调度,减少对 shell 的依赖。优点是模型更容易正确使用(有 schema 约束和 description 引导),缺点是工具数量膨胀,注入和 prompt cache 面临压力。
- “瘦工具层”(nanobot、pi):只提供最基础、最不可替代的能力,高级功能通过组合基础工具或外部扩展实现。优点是核心简洁、prompt 干净,缺点是对模型的组合能力和 Extension 生态有更高要求。
其实不管胖瘦,最终的原则是趋同的:核心工具集保持精简,扩展能力按需加载。 Anthropic 在实践中也印证了这一点——他们指出一个常见错误是直接把现有 API 原封不动包装成工具,而不考虑是否适合智能体使用。正确的做法是整合与提炼:将 list_users + list_events + create_event 合并为一个 schedule_event 工具,将 read_logs 替换为 search_logs(只返回相关日志行及上下文)。换句话说,用少量高价值工具替代大量低价值工具,让工具输出"结果"而不是"原材料"1。
3.6 四个项目 Tool System 总体横向对比
| 维度 | Codex CLI | Claude Code | nanobot | pi |
|---|---|---|---|---|
| 核心抽象 | ToolSpec + ToolRouter | Tool 统一接口 (buildTool) | Tool 基类 + schema 自声明 | ToolDefinition definition-first |
| 工具数量 | 30+ 标准工具 + MCP/动态 | 20+ 内建 + MCP | ~10 内置 + MCP | 7 个标准工具(默认 4 个激活) |
| 注入策略 | 按 Turn 动态构建 | ToolSearch 按需加载 | 启动时注册 + 运行时 MCP 懒加载 | Session 统一管理 + Registry 合并 |
| 执行模型 | Router dispatch → handler | runTools → runToolUse | AgentRunner → registry.execute | 三阶段:prepare/execute/finalize |
| 安全模型 | 审批 + config gating | 四层权限(tool check + 规则 + handler + hook) | 安全边界下沉到 tool 内部 | beforeToolCall hook 前置拦截 |
| MCP 策略 | 原生适配到 ToolSpec,统一路由 | 骨架对象补齐字段,统一 Tool 接口 | MCPToolWrapper 伪装成本地 Tool | 通过 Extension 机制接入 |
| 并发策略 | 按 tool spec 配置 | 只读并发 + 上下文提交保序 | 无显式并发控制 | 默认并行,可退化为串行 |
4. Tool System 的关键设计维度
通过对比四个项目,我们可以提炼出 Tool System 的六个关键设计维度。
4.1 Tool 抽象:多角色统一还是职责分离
四个项目对"一个 Tool 对象承担多少角色"有不同选择:
| 策略 | 代表项目 | 特点 |
|---|---|---|
| 多角色统一 | Claude Code、pi | 一个对象同时承载 schema、执行、权限、UI 渲染、prompt 描述,优点是统一,缺点是接口大 |
| 适度分离 | Codex CLI | ToolSpec(模型可见)+ ToolRouter(装配)+ ToolRegistry(执行),三件套各司其职 |
| 最小抽象 | nanobot | Tool 只管 schema + 校验 + 执行,prompt 文本通过 TOOLS.md 单独注入 |
从工程实践来看,Codex 的适度分离最具参考价值:描述、路由、执行三者解耦,每层可以独立演化。Claude Code 和 pi 的统一接口虽然让代码更"整齐",但 Tool 接口膨胀到包含了太多职责——这是典型的前期设计不足、后期不断堆叠的结果。
但无论采用哪种抽象策略,工具的描述和 schema 最终都会被直接加载到模型的上下文中。这意味着工具描述本身就是 prompt engineering。Anthropic 的建议是:
- 把工具描述当成在给团队里的新员工介绍这个工具
- 将隐式背景显性化(明确特殊的查询格式、术语定义、资源关系)
- 消除参数歧义(用
user_id而不是user,用start_date而不是date)1。
这也是为什么 Claude Code 中 Tool 接口同时包含 description()(偏运行时文案)和 prompt()(偏模型能力描述)两个方法——给模型的描述和给人的描述需要不同的语气和信息密度。
Anthropic 在对 Claude Sonnet 3.5 进行精细化工具描述调整后,SWE-bench Verified 得分取得了显著的增量提升,这说明工具描述的质量直接影响 Agent 的整体能力。
4.2 工具注入:静态注册 vs 动态装配 vs 按需加载
四个项目代表了三种不同的注入策略:
- Codex CLI:按 Turn 动态装配。 每次模型调用前重新计算工具集,受 skills、connectors、MCP 连接状态、用户显式 mention 等多因素影响。优点是工具暴露面精准,缺点是 prompt cache 稳定性更难保证。
- Claude Code:ToolSearch 按需加载。 首轮只注入必要工具的完整 schema,deferred 工具先以名字形式存在,模型通过 ToolSearch 按需"拉取"完整定义。优点是解决了大量 MCP tool 的注入问题,缺点是增加了模型决策链路的一跳延迟。
- nanobot / pi:启动时注册 + 运行时过滤。 工具在构建 agent 时注册,用白名单/黑名单/activeToolNames 做过滤。优点是简单可控,缺点是不适合工具数量大的场景。
这三种策略并不互斥。一个成熟的 Tool System 可以同时使用:核心工具首轮注入 + 扩展工具按 ToolSearch 延迟 + 每轮根据上下文做过滤。
4.3 工具执行:单步调用 vs 流水线
| 项目 | 执行模型 | 特点 |
|---|---|---|
| Codex | Router dispatch → handler | 统一入口,handler 内部处理复杂逻辑 |
| Claude Code | runToolUse() 统一包装 | 外层治理重:schema 校验 → 权限 → hook → 执行 → hook → 回写 |
| nanobot | registry.execute(name, args) | 最简模型:查找 → cast → validate → execute |
| pi | prepare / execute / finalize | 三阶段 + hook 在前后两端介入 |
这里最值得借鉴的是 Claude Code 和 pi 的共同模式:工具调用不是简单的 tool(input) → output,而是有前置校验、权限判定、hook 介入、结果映射的完整流水线。但要注意过度设计。pi 的三阶段和 Claude Code 的多层权限,本质上都是在简单执行前后加了治理层。
在结果映射这一环,Anthropic 还提出了一个巧妙的设计:在工具中暴露 ResponseFormat 枚举参数(例如 CONCISE vs DETAILED),让模型根据当前需求自主选择返回详细程度,实测可节省约 1/3 的 Token1。这与 Claude Code 的 mapToolResultToToolResultBlockParam() 思路一致——每种工具可以自定义最适合模型消费的结果格式,而不是假设"模型总是需要全部信息"。
4.4 安全模型:prompt 约束 vs 代码强制
Tool System 的安全性是一个多层次问题,四个项目的策略各有侧重:
- Codex CLI:依赖 config gating(connector 显式启用、app tool 按 mention 暴露)和审批机制(
request_permissionstool 主动请求批准) - Claude Code:四层权限体系——tool 自身的
checkPermissions()→ 通用权限规则hasPermissionsToUseTool()→ 交互式 handler → hook 扩展层。这四层是串联关系,不是二选一。权限判定返回的不是布尔值,而是一个可以继续演化的决策状态机(allow / deny / ask) - nanobot:安全边界下沉到 tool 内部。
exec内建 deny pattern、路径穿越检测、内网 URL 检测;文件工具统一做路径越界校验。不依赖 prompt 约束 - pi:通过
beforeToolCallhook 在 prepare 阶段做前置拦截
这里有一条明确的设计原则:安全约束应该尽量下沉到代码层,而不是依赖 prompt 约束。prompt 是软约束,模型可能不遵守;代码是硬约束,可以在执行前阻断。如 nanobot 那样把路径越界检测、危险命令拦截直接写在 tool 内部,比在 system prompt 里写"不要删除重要文件"可靠得多。
Claude Code 的权限系统还有一个值得学习的细节:权限批准后,真正传给 tool 的 input 可能已被改写——permission rule/hook 返回了 updatedInput,或者用户在审批 UI 中补充了信息。这对于 Human-in-the-Loop tool(如 AskUserQuestion)特别关键。
关于权限系统,本系列也会有专门一篇来介绍。
4.5 MCP 集成:协议适配 vs 统一抽象
四个项目对 MCP(Model Context Protocol)工具的处理,体现了两种策略:
- 统一抽象派(Claude Code、nanobot):将 MCP tool 通过适配层补齐所有字段,转换为内部
Tool接口的实例。此后运行时不再需要区分"这是内建还是 MCP" - 协议原生派(Codex CLI、pi):MCP tool 作为独立来源参与装配,但在路由层保留其特殊性——MCP tool 的路由会转到专门的 MCP handler,通过标准
tools/callRPC 执行
统一抽象派的好处是执行层代码不分支,代价是适配层要承担很多规范化工作(schema 格式转换、只读/并发属性推导、超时策略映射)。协议原生派的好处是 MCP 语义不丢失,代价是需要维护两条调用链。
需要注意的是,Claude Code 的设计可以表明,一旦 MCP tool 完成了到内部 Tool 的适配,后续的校验、权限、hook、结果回写都能无差别处理。适配层的复杂度是固定的、集中的,而执行层如果没有收敛,复杂度会扩散到所有调用方。
此外,MCP 生态天然带来了一个在单项目内不太明显的问题:命名空间冲突。当多个 MCP server 各自提供工具时,同名工具(比如两个 server 都有 search)几乎是必然的。Claude Code 和 nanobot 的 mcp_<server>_<tool> 名称包装是一种事后补救。Anthropic 的建议是在源头做规划1:用通用前缀对相关工具分组(asana_search vs jira_search),甚至按资源类型细分(asana_projects_search、asana_users_search),帮助模型在正确的时间选择正确的工具。MCP server 作者在源头做命名空间规划,远比靠 runtime 打补丁更可靠。
4.6 并发与并行
当模型在一次响应中返回多个 tool_use 时,如何执行这些调用?
| 项目 | 并发策略 |
|---|---|
| Codex | 按 tool spec 配置并行能力 |
| Claude Code | 只读/并发安全工具并发跑,非安全工具串行,上下文提交始终保序 |
| nanobot | 逐个串行执行(无显式并发控制) |
| pi | 默认并行(prepare 逐个,execute 并发),可退化为全串行 |
Claude Code 和 pi 的策略其实是一致的:“并发 side-effect production,顺序 state commit”。schema 校验、hook、阻断逻辑保持确定顺序,真正耗时的 I/O 可以并发跑,需要共享状态的 tool 仍能强制串行。
4.7 Human-in-the-Loop(HIL)Tool
四个项目都支持人与 Agent 交互的 tool,但实现方式差异很大:
- Codex CLI:
request_user_input通过oneshotchannel 实现阻塞式用户问答——handler 发出EventMsg::RequestUserInput,UI 回Op::UserInputAnswer,结果作为FunctionCallOutput回到下一轮模型上下文 - Claude Code:
AskUserQuestion借用权限队列完成交互——tool 的checkPermissions()返回ask,进入 permission queue,在权限批准阶段收集用户答案并回填到updatedInput,call()只是轻量封装 - nanobot:
messagetool 通过OutboundMessage主动推送消息给用户 - pi:通过 extension 的
tool_call/tool_result事件机制让外部代码介入
Claude Code 的做法比较偷懒:它没有为"向用户提问"单独造一套通道,而是复用了统一的权限交互框架。AskUserQuestion 虽然语义上是"提问",但框架层面它是一个标准的 tool 调用——只不过它的"核心逻辑"发生在 permission UI 阶段,而不是 call() 里。这种复用降低了系统复杂度,也意味着 HIL tool 天然继承了权限系统的竞争决策、超时处理和状态追踪能力。
5. 设计共识
综合四个项目的 Tool System 架构和 Anthropic 的工具设计实践1,可以提炼出一组工程共识:
- 重新理解工具的本质:工具是确定性系统与非确定性智能体之间的契约。为 Agent 写工具不能像写普通 API 那样——要面向一个"聪明但会犯糊涂、上下文有限、需要引导"的调用者来设计。
- Tool 不是裸函数,而是携带 schema、描述、执行逻辑、权限策略的完整能力单元。其中,工具描述本身就是 prompt engineering——隐式背景显性化、消除参数歧义,直接影响模型的调用成功率。
- 内置工具和外部工具(MCP)应在中层收敛到统一抽象,执行层不应区分来源。MCP server 作者应在源头做好命名空间规划(
asana_searchvsjira_search),而不是只靠 runtime 做名称包装。 - 工具不是越多越好,用少量高价值工具替代大量低价值工具。将低级 API 整合为高级工具(如
schedule_event替代list_users+list_events+create_event),让工具输出"结果"而不是"原材料"。 - 工具注入是动态的,不是静态清单——工具可见性应根据权限、模式、上下文实时变化。当工具数量多时,按需加载(ToolSearch / defer_loading)是必要的。
- 工具调用是完整流水线,包含校验、权限、hook、结果映射。结果格式应面向模型优化——返回语义化字段而非技术标识符,甚至可以让模型自主选择返回详细程度(
CONCISEvsDETAILED)。 - 安全约束下沉到代码层(硬阻断),而不是依赖 prompt 层(软建议)。错误应作为可恢复文本回灌,且错误信息要给出具体可操作的改进建议,让模型知道"下一次该怎么调"。
这些共识本质上在回答同一个问题:如何让一个非确定性的智能体,安全、高效、可靠地使用一组确定性的能力。
6. 总结
Tool System 是 Agent 从"能说"到"能做"的关键桥梁。从最朴素的 function call,到覆盖定义、注入、执行、回写全链路的 Tool Runtime,再到面向智能体认知特征优化每个工具的描述和返回值,Tool System 的演进反映了 Agent 工程化的核心挑战:在灵活性和安全性之间找平衡,在全面性和效率之间做取舍。
四个项目的 Tool System 各有侧重:
- Codex CLI 设计重心是:平台级 Tool Runtime 如何按 Turn 动态装配和路由工具
- Claude Code 的核心贡献是:统一 Tool 协议 + ToolSearch 按需加载 + 多层权限体系
- nanobot 从学术上定义:声明式 schema + 受控执行器 + 错误可恢复的最小化 Tool Kernel
- pi 从实践角度实现:definition-first + session-controlled + 三阶段 hookable 执行的 Tool 管理框架
而 Anthropic 的实践经验1从工具个体设计的维度补充了完整图景:工具不仅是一个运行时概念,更是一个设计对象——它的命名、描述、返回值格式、Token 效率、错误信息的质量,都直接影响 Agent 的整体表现。工具设计的最强辅助,恰恰是使用这些工具的 Agent 本身。
Tool System 的设计没有银弹,但显而易见的工程原则是:架构上让"定义"和"执行"解耦,个体设计上面向智能体优化,安全在代码层保证,扩展通过统一协议接入。 下一篇,我们将深入探讨 Agent 的 上下文管理(Context Management)——Agent 如何在有限的上下文窗口中,高效组织 system prompt、对话历史、工具结果和长期记忆。
参考文献
Anthropic, “Writing effective tools for agents,” 2025, https://www.anthropic.com/engineering/writing-tools-for-agents ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
OpenAI, “Codex CLI,” https://github.com/openai/codex ↩︎
Anthropic, “Claude Code,” https://code.claude.com/docs ↩︎
nanobot, https://github.com/HKUDS/nanobot ↩︎