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"]

这六层可以进一步概括为三个核心问题:

  1. Tool 长什么样:定义层——如何描述一个工具的能力、输入、执行逻辑和渲染方式
  2. Tool 怎么进 prompt:注册+注入层——如何把当前会话可用的工具集合,变成模型能消费的 schema
  3. Tool 怎么执行和回写:路由+执行+回写层——模型发起调用后,如何安全执行并把结果喂回上下文

3. 主流项目的 Tool System 设计

3.1 Codex CLI2:按 Turn 动态装配的工具平台

Codex CLI 的 Tool System 最像一个平台级的能力装配系统。它定义了四层Tool体系如下:

  • 描述层ToolSpec 负责把能力描述成模型可见的统一规格。它有五种形态:

    • 普通 Function:JSON Schema function tool
    • ToolSearch:客户端工具发现
    • LocalShell:原生 shell
    • ImageGeneration:图像生成
    • 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
  • 它接收什么输入:inputSchemainputJSONSchema
  • 它如何执行:call(...)
  • 它如何描述自己:description(...) / prompt(...)
  • 它是否只读、是否可并发、是否 destructive
  • 它如何参与权限系统:checkPermissions(...)
  • 它执行结果如何映射给模型:mapToolResultToToolResultBlockParam(...)
  • 它在 UI 里怎么渲染:renderToolUseMessage / renderToolResultMessage

这意味着 Claude Code 里的 tool 同时承担四个角色:模型侧的 function schema、运行时的可执行单元、权限系统的判定对象、UI/transcript 的渲染对象。这种"一个对象服务四方"的设计优点是统一,但导致了 Tool 接口很大。

Claude Code 的 Tool 装配分为三步:

  1. getTools(permissionContext):按当前模式、deny rules、REPL 模式过滤内建工具
  2. assembleToolPool(permissionContext, mcpTools):把 built-in 和 MCP tools 合并,按名称去重,排序保持稳定以尽量维持 prompt cache
  3. mergeAndFilterTools(...):再按 coordinator mode 等约束做最终过滤

最终注入给模型的是 filteredTools,而不是 getAllBaseTools()。因此 getAllBaseTools() 是"定义全集",filteredTools 才是"本轮 API 实际注入集"。

Claude Code 最值得深入分析的设计是 ToolSearch 机制——一套为动态 tool pool 设计的 schema 按需暴露机制。它的核心闭环如下:

  1. 当 ToolSearch 启用时,deferred tools 以 defer_loading: true 的形式出现在 API 请求中,但不携带完整的 input schema
  2. 模型先调用 ToolSearchTool(它本身是一个普通内建 tool,负责"取回被 defer 的工具定义")
  3. ToolSearchTool 的返回值不是文本,而是结构化的 tool_reference blocks——通知 API “请把这些工具的完整 schema 纳入后续上下文”
  4. 下一轮构建 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 骨架对象补齐所有缺失字段——namemcpInfoisMcpinputJSONSchemaprompt()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 适配"的统一能力单元,通过 ToolRegistryAgentRunner 把这些单元编排成可迭代的 agent runtime。

从学术角度看,nanobot 的 Tool System 可以用来参考学习:schema、cast、validate、execute 的职责分层清晰;内置 tool 和 MCP tool 共享完全同一条调用链。但从工程角度来看,它缺少 ToolSearch 式的按需加载、缺少细粒度的并发控制、也缺少 hook 扩展点,所以实用度不高。

3.4 pi5:definition-first 与会话管控

pi 的 Tool System 最强调 definition-firstsession-controlled。它的设计可以概括为三条主线:

  1. ToolDefinition 不只是可调用函数,还同时携带 LLM 元信息(namedescriptionparameters)、prompt 元信息(promptSnippetpromptGuidelines)、UI 元信息(labelrenderCallrenderResultrenderShell)、执行元信息(prepareArgumentsexecutionModeexecute(...)
  2. AgentSession 统一管理 tool 的可见性、active 集合和 prompt 注入,维护两份 tool 状态——_toolDefinitions(定义注册表)和 _toolRegistry(执行注册表)
  3. agent-loop 把 tool 调用拆成三阶段:prepareexecutefinalize

pi 的标准 tool 集合非常精简,只有 7 个:

  • read:读取文本或图片文件
  • bash:在当前 cwd 执行 shell 命令,支持超时和流式输出
  • edit:对单文件做精确文本替换
  • write:新建或整文件覆盖
  • grep:用 rg 搜索文件内容,尊重 .gitignore
  • find:用 fd 或自定义 glob 找文件
  • ls:列目录内容

默认激活的只有 readbasheditwrite 四个,其他三个是"内置只读增强工具"。这种精简策略本身就表达了pi项目的设计哲学:工具不是越多越好,模型需要的是正交、可靠、可组合的基础能力。这种哲学也是我比较欣赏的。

pi 的工具注入链路非常清晰,先应用 allowedToolNames / excludedToolNames 做总开关过滤,再把 built-in tool 放入 definitionRegistry(source 标为 builtin),之后 extension 和 SDK 自定义 tool 按名字覆盖写入——同名自定义 tool 会覆盖 built-in definition。随后从 definition 中提取 promptSnippetpromptGuidelines 做 prompt 重建。新注册的 tool 默认补进 active 集合。

pi 对 tool 注入 prompt 的方式也值得注意:tool 对模型的影响有两层——结构化 schema(在真正请求模型时作为 Context.tools)和非结构化提示(在 system prompt 里作为 Available toolsGuidelines)。这两层是解耦的,但都依赖 active tool 集合。

在三阶段执行中,pi 的 hook 机制设计得很简洁:

  • prepare:找 tool → 运行 prepareArguments() → schema 校验 → 调 beforeToolCall hook(extension 的 tool_call 事件在此触发)
  • execute:真正执行,tool 内部通过 onUpdate() 产生 tool_execution_update 事件(bash 等工具借此流式刷输出)
  • finalize:调 afterToolCall hook(extension 的 tool_result 事件在此触发),允许 hook 覆盖 contentdetailsisError、甚至写 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_permissionsrequest_user_input 通过 oneshot channel 实现阻塞式问答,不是简单文本插入
MCP 与外部能力MCP tools(动态), list_mcp_resources, read_mcp_resource, tool_search, tool_suggesttool_search 搜索 deferred app/connector tool;tool_suggest 建议用户安装新插件
Responses 原生web_search, image_generation不是本地 handler 模拟,而是交给上游 Responses API 原生执行
客户端协作dynamic_tools, js_repl, code_mode, waitdynamic_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, NotebookEditEdit 做精确字符串替换而非整文件重写,和第一篇中 Turn 的"副作用"建模互相对应
ShellBash单一 shell 工具,但内部区分平台(bash/powershell)
检索Glob, Grep, WebFetch, WebSearchGlob 匹配文件名,Grep 搜索文件内容,两者正交
Agent 编排Agent, TaskOutput, TaskStopAgent 是子 Agent 的唯一入口,复用同一个 query() 内核
计划与交互EnterPlanMode, ExitPlanMode, AskUserQuestion, TodoWriteAskUserQuestion 复用权限队列做 HIL;TodoWrite 让模型自管理任务列表
扩展生态Skill, ListMcpResources, ReadMcpResource, ToolSearchSkill 加载领域知识;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 基类,共享统一的路径解析和越界校验
Shellexectools.exec.enable 配置控制,可全局禁用
Webweb_search, web_fetch共同特点是将外部内容明确标记为不可信数据,web_fetch 返回文本前插入 untrusted banner
通信与调度message, spawn, cronmessage 面向 channel 投递;spawn 交给 SubagentManager 后台执行;cron 只在挂载了 CronService 时才注册

其中 execcron 都有激活条件——不是"定义了就一定会暴露"。文件工具在 restrictToWorkspace=true 时会自动收紧到 workspace 范围。

3.5.4 pi:极致精简的七工具哲学

pi 的标准工具是四个项目中最少的——只有 7 个:

类别工具说明
文件读取read文本按行数/字节数截断;图片转成 message attachment
Shellbash支持超时、流式输出、截断后落临时文件
文件编辑edit, writeedit 做精确文本替换(输入是 edits[],返回 details.diff);write 新建或整文件覆盖
只读增强grep, find, lsgreprg 搜索内容;findfd 找文件;ls 列目录——三者默认不激活

默认激活的只有 readbasheditwrite 四个。grepfindls 被归类为"内置只读增强工具"——它们不是必须的(模型可以用 bash 调用 rg/fd/ls 命令来替代),但作为显式工具暴露可以减少模型写出不可靠 shell 命令的概率。

3.5.5 内置工具对比表

能力域Codex CLIClaude Codenanobotpi
文件读写apply_patch, view_imageRead, Writeread_file, write_fileread, write
文件编辑通过 shell/patchEdit, NotebookEditedit_fileedit
文件搜索list_dirGlob, Greplist_dirgrep, find, ls
Shellshell 等 5 个变体Bashexecbash
Webweb_search, tool_suggestWebFetch, WebSearchweb_search, web_fetch无(通过 Extension)
子 Agent8 个生命周期工具Agent, TaskOutput, TaskStopspawn无(通过 Extension)
人机交互request_user_input, request_permissionsAskUserQuestion, TodoWritemessage无(通过 Extension)
计划与模式update_planEnterPlanMode, ExitPlanMode无(通过 Extension)
扩展生态dynamic_tools, tool_search, tool_suggestSkill, ToolSearch, ListMcpResources, ReadMcpResourcesMCP 动态包装pi.registerTool()
定时调度CronCreate, CronDelete, CronListcron(条件注册)
工作区隔离EnterWorktree, ExitWorktree
总计(约)30+20+(含条件启用 35+)107(默认激活 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 CLIClaude Codenanobotpi
核心抽象ToolSpec + ToolRouterTool 统一接口 (buildTool)Tool 基类 + schema 自声明ToolDefinition definition-first
工具数量30+ 标准工具 + MCP/动态20+ 内建 + MCP~10 内置 + MCP7 个标准工具(默认 4 个激活)
注入策略按 Turn 动态构建ToolSearch 按需加载启动时注册 + 运行时 MCP 懒加载Session 统一管理 + Registry 合并
执行模型Router dispatch → handlerrunTools → runToolUseAgentRunner → 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 CLIToolSpec(模型可见)+ ToolRouter(装配)+ ToolRegistry(执行),三件套各司其职
最小抽象nanobotTool 只管 schema + 校验 + 执行,prompt 文本通过 TOOLS.md 单独注入

从工程实践来看,Codex 的适度分离最具参考价值:描述、路由、执行三者解耦,每层可以独立演化。Claude Code 和 pi 的统一接口虽然让代码更"整齐",但 Tool 接口膨胀到包含了太多职责——这是典型的前期设计不足、后期不断堆叠的结果。

但无论采用哪种抽象策略,工具的描述和 schema 最终都会被直接加载到模型的上下文中。这意味着工具描述本身就是 prompt engineering。Anthropic 的建议是:

  • 把工具描述当成在给团队里的新员工介绍这个工具
  • 将隐式背景显性化(明确特殊的查询格式、术语定义、资源关系)
  • 消除参数歧义(用 user_id 而不是 user,用 start_date 而不是 date1

这也是为什么 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 流水线

项目执行模型特点
CodexRouter dispatch → handler统一入口,handler 内部处理复杂逻辑
Claude CoderunToolUse() 统一包装外层治理重:schema 校验 → 权限 → hook → 执行 → hook → 回写
nanobotregistry.execute(name, args)最简模型:查找 → cast → validate → execute
piprepare / 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 的安全性是一个多层次问题,四个项目的策略各有侧重:

  1. Codex CLI:依赖 config gating(connector 显式启用、app tool 按 mention 暴露)和审批机制(request_permissions tool 主动请求批准)
  2. Claude Code:四层权限体系——tool 自身的 checkPermissions() → 通用权限规则 hasPermissionsToUseTool() → 交互式 handler → hook 扩展层。这四层是串联关系,不是二选一。权限判定返回的不是布尔值,而是一个可以继续演化的决策状态机(allow / deny / ask)
  3. nanobot:安全边界下沉到 tool 内部。exec 内建 deny pattern、路径穿越检测、内网 URL 检测;文件工具统一做路径越界校验。不依赖 prompt 约束
  4. pi:通过 beforeToolCall hook 在 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/call RPC 执行

统一抽象派的好处是执行层代码不分支,代价是适配层要承担很多规范化工作(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_searchasana_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 CLIrequest_user_input 通过 oneshot channel 实现阻塞式用户问答——handler 发出 EventMsg::RequestUserInput,UI 回 Op::UserInputAnswer,结果作为 FunctionCallOutput 回到下一轮模型上下文
  • Claude CodeAskUserQuestion 借用权限队列完成交互——tool 的 checkPermissions() 返回 ask,进入 permission queue,在权限批准阶段收集用户答案并回填到 updatedInputcall() 只是轻量封装
  • nanobotmessage tool 通过 OutboundMessage 主动推送消息给用户
  • pi:通过 extension 的 tool_call/tool_result 事件机制让外部代码介入

Claude Code 的做法比较偷懒:它没有为"向用户提问"单独造一套通道,而是复用了统一的权限交互框架。AskUserQuestion 虽然语义上是"提问",但框架层面它是一个标准的 tool 调用——只不过它的"核心逻辑"发生在 permission UI 阶段,而不是 call() 里。这种复用降低了系统复杂度,也意味着 HIL tool 天然继承了权限系统的竞争决策、超时处理和状态追踪能力。

5. 设计共识

综合四个项目的 Tool System 架构和 Anthropic 的工具设计实践1,可以提炼出一组工程共识:

  1. 重新理解工具的本质:工具是确定性系统与非确定性智能体之间的契约。为 Agent 写工具不能像写普通 API 那样——要面向一个"聪明但会犯糊涂、上下文有限、需要引导"的调用者来设计。
  2. Tool 不是裸函数,而是携带 schema、描述、执行逻辑、权限策略的完整能力单元。其中,工具描述本身就是 prompt engineering——隐式背景显性化、消除参数歧义,直接影响模型的调用成功率。
  3. 内置工具和外部工具(MCP)应在中层收敛到统一抽象,执行层不应区分来源。MCP server 作者应在源头做好命名空间规划(asana_search vs jira_search),而不是只靠 runtime 做名称包装。
  4. 工具不是越多越好,用少量高价值工具替代大量低价值工具。将低级 API 整合为高级工具(如 schedule_event 替代 list_users+list_events+create_event),让工具输出"结果"而不是"原材料"。
  5. 工具注入是动态的,不是静态清单——工具可见性应根据权限、模式、上下文实时变化。当工具数量多时,按需加载(ToolSearch / defer_loading)是必要的。
  6. 工具调用是完整流水线,包含校验、权限、hook、结果映射。结果格式应面向模型优化——返回语义化字段而非技术标识符,甚至可以让模型自主选择返回详细程度(CONCISE vs DETAILED)。
  7. 安全约束下沉到代码层(硬阻断),而不是依赖 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、对话历史、工具结果和长期记忆。

参考文献