引子:tool 调用到底是什么
如果你只读过教程级别的 agent 框架,“tool call"在你脑子里大概是这样的:
| |
一行代码,函数指针加参数解包。但工业级 agent——这里以 claw-code(Claude Code 的开源对位实现)为参照——把这一行扩成了 14 步 pipeline:JSONSchema 校验、speculative bash 分类、3 类 hook、5 级权限模式、Linux namespaces 沙箱,外加结构化错误回灌。每一步都不是装饰,而是有事故复盘支撑的工程决策。
为什么这么重?因为 tool 调用是 agent 唯一的外部世界接口。模型可以 hallucinate 一段废话,那是文本污染;模型 hallucinate 一次 rm -rf / 并被无脑执行,那是真实损失。本文沿着 claw-code 的源码——src/tools.py、rust/crates/runtime/src/permissions.rs、hooks.rs、sandbox.rs——把这条 pipeline 拆给你看。
文末有一个 <150 LOC 的 stdlib-only Python demo(code/02-tool-and-permission/tool_pipeline.py),把 pipeline 的 5 个核心环节复刻一遍。
1. Tool Registry:工具池是被裁剪的,不是堆出来的
引子
新人写 agent 框架时,registry 通常是个 dict[str, Callable],越长越自豪。但 claw-code 的入口长这样:
| |
注意三个参数:simple_mode、include_mcp、permission_context。registry 不是一个常量,它是一个被运行时上下文裁剪过的视图。
数据点
simple_mode=True时,可见工具收缩到 3 个:BashTool、FileReadTool、FileEditTool。- 默认完整集合在内部架构里 cap 在 15 个左右:
FileRead, FileEdit, FileWrite, Bash, Glob, Grep, TodoWrite, TaskCreate, AskUserQuestion, Skill, Agent, MCPTool, Sleep等。 - 真正的"全集"通过
load_tool_snapshot()从reference_data/tools_snapshot.json加载,而不是硬编码。 permission_context还会进一步剔除当前模式下显然不可用的工具(例如 ReadOnly 模式直接拒发FileWrite)。
Insight
工具数量不是能力指标,而是 context cost line item。每多一个 tool,system prompt 就多一段 schema、多几十个 token。模型的"工具选择准确率"也随工具数下降——这是经验性结论,几乎所有做大规模 tool use 评测的团队都验证过。一个常被忽略的现象是:当工具集超过 30 个,模型开始出现"工具混淆”——把 Glob 用成 Grep、把 FileEdit 用成 FileWrite。这不是模型变笨,是 schema 在 attention 上互相挤兑。
所以 claw-code 的设计哲学是:让工具池在每轮请求里都尽可能小。simple_mode 是给 sub-agent 用的,permission_context 是给受限会话用的,MCP 工具默认 lazy-load(见下一节)。这是一个工程师友好的取舍——你拿到的不是"全功能 IDE",而是"按需加载的 IDE"。
另一个值得抄的细节是:get_tools() 返回的不是 list 而是 tuple。这对 Python runtime 几乎没差,但对调用方传递的语义清晰——工具集合在一次请求内不可变。后续任何修改(比如 hook 想动态隐藏工具)必须显式生成新视图。这是把"不可变快照 + 显式重建"做成 API 习惯的小手势。
2. JSONSchema + Deferred Tools:context 经济学
引子
每次模型 emit 一个 tool_use block,runtime 起手做的第一件事不是"调用",而是 schema 校验:
- 用 Zod schema 解析
tool_use.input是否符合声明的 JSON Schema; - 调用 tool 自带的
validateInput()做语义校验(例如 path 是否在 workspace 内)。
任一步失败,pipeline 立刻短路,回灌一个 tengu_tool_use_error 给模型,并在 trace 里打 tool_use_error 事件。模型下一轮看到结构化错误,会自我纠偏。
这是个朴素但容易被略过的细节:没有校验层的 agent,等于把 LLM 的语法错当成自己的责任去 catch。
Deferred tools:把 schema 留在外面
更精彩的设计在这里。如果你看 claw-code 一次会话的 system prompt,会发现一段:
Some tools are deferred and not listed above. When a deferred tool is
surfaced later in the conversation, its full schema appears as a
<function>{...}</function> definition inside a <functions> block.
具体表现是:deferred tool 的名字出现在 <system-reminder> 里,但完整 JSON Schema 不进 system prompt。模型必须先调用 ToolSearch("select:WebFetch") 把 schema 拉进来,才能真正调用 WebFetch。
为什么要这样?因为 MCP 生态里,一个企业用户接 10 个 server、每个 server 暴露 20 个 tool 是常态。200 个 tool 的 schema 全塞 system prompt,光这部分就能吃掉 30k+ token,且每轮都重复支付。Deferred tools = tool schema 的按需分页,把"这个 tool 存在"和"这个 tool 怎么调"解耦。
Insight
工程师视角的 takeaway:schema 是 context,不是免费的元数据。如果你在做自己的 agent 框架,请把"tool 注册"和"tool schema 注入 prompt"显式分开。前者可以海量,后者必须吝啬。
deferred tools 还有一个隐性收益:audit 友好。当一个 tool 必须经过 ToolSearch 才能被调用,trace 里就天然有"模型在某轮请求 schema、下一轮才使用"的两段记录。安全审计想问"模型为什么用了 X tool",不再只能看 system prompt 推断,而是有显式的"申请-发放"事件可查。这是 lazy loading 顺手送的可观测性。
校验链上还有个容易踩的坑:Zod schema parse 和 validateInput() 不是冗余。前者管 JSON 结构("path 是不是 string"),后者管语义("path 是不是在 workspace 内"、"command 是不是空串")。把两者合并到一个大 validator 看似简洁,但会让"结构错误"和"策略错误"在错误回灌时混淆——模型分不清"我写错了 JSON"还是"我请求了不该请求的资源",纠偏方向就乱了。分开两层,错误码不同,模型才能精准修。
3. 权限模型:5 级 + 3 规则类,评估顺序是 Deny → Allow → Ask
引子
权限是 agent 安全里最容易做歪的一环。常见的错法是"一个开关"——要么允许所有,要么 prompt 所有。claw-code 的做法是 5 级模式 × 3 类规则的笛卡尔积,源码在 rust/crates/runtime/src/permissions.rs:
| |
5 级模式从严到宽:ReadOnly(只读)→ WorkspaceWrite(限定 workspace 写)→ Prompt(每次问)→ Allow(默认允许)→ DangerFullAccess(裸奔)。
3 类规则与评估顺序
每个模式下还可以叠加规则。规则结构是这样的:
| |
规则分 Deny / Allow / Ask 三类,评估顺序固定:
- Deny rules first. 命中即拒绝,不可推翻。
- Allow rules. 命中则放行。
- Ask rules. 即使在
Allow模式下,命中 Ask 规则也强制弹窗——这条最关键,意味着"高风险操作可以从宽松模式里被钉出来"。
主语提取也很务实:从 tool_use.input 里抽 command(对 Bash)或 path(对 File*)字段,再跟 matcher 对。matcher 支持 Exact("git status")、Prefix("git ")、Any 三种。
Insight
把权限做成"模式 × 规则"的两维矩阵,本质上是承认:安全策略不是单调的偏序。你既需要"宽松默认 + 个别加锁",也需要"严格默认 + 个别开洞"。Deny→Allow→Ask 的顺序保证了管理员的 deny 规则永远胜过用户的 allow,而 Ask 又能在最宽松的模式下保留人类介入点。
值得一提的是 PermissionPrompter trait 把"决策"和"询问"解耦:
| |
CI 环境可以注入一个永远 Deny 的 prompter,IDE 环境可以注入一个真正弹 UI 的实现——同一套策略层,不同的承载终端。
还有一个工程上很关键但容易被忽略的点:规则的主语提取是 tool-aware 的。Bash 工具的主语是 command 第一个 token(git、curl、rm),File* 工具的主语是 path(且会先 normalize)。如果你天真地用 command 字段对所有 tool 做 prefix match,规则就只能写给 Bash 看;反过来,把 path 当字符串硬匹也会因为 ./foo vs foo 这种归一化差异翻车。claw-code 在 permissions.rs 里把"如何从 input 提主语"做成 per-tool 的小函数,这是把"匹配规则"和"工具语义"耦合恰到好处的写法——既不强迫规则作者懂全部工具内部,又不让规则系统对所有工具一视同仁。
4. Sandbox = Linux Namespaces:用内核原语做隔离
引子
很多人以为 agent 沙箱 = Docker。claw-code 的选择更轻:直接用 unshare 起一组 Linux namespaces,源码在 sandbox.rs:
| |
User / mount / IPC / PID / UTS namespace 默认全开,network namespace 按需。没有 seccomp、没有 chroot、没有 Docker。
三档文件系统隔离
| |
默认 WorkspaceOnly:mount namespace 内只 bind-mount 当前 workspace 为 rw,其余 fs 路径视情况隐藏或只读。AllowList 让你显式追加可写路径,例如 ~/.cache/pip。
CI 友好的探测
启动前还有一段防御:
| |
很多 CI(包括 GitHub Actions 的某些 runner)出于安全把 user namespace 禁了。这个探测函数用 OnceLock 缓存结果,sandbox 在启用前先 probe,否则 fallback 成无沙箱模式并打 warning。
macOS 上则没有对应实现——sandbox 仅 Linux。这也是个诚实的工程取舍:与其做半残的 macOS sandbox-exec 集成,不如告诉用户"在 Linux 容器里跑 agent"。
Insight
namespaces 的好处是启动开销近零(毫秒级)、不需要镜像、不需要 daemon。坏处是不能阻止 syscall 滥用(没 seccomp)。claw-code 的判断是:tool pipeline 上游已经有 schema 校验 + permission 规则把 90% 的恶意意图筛掉,sandbox 是"最后一道防线",目标是控制 blast radius而不是"防御任意攻击者"。这个定位决定了它选择轻量原语而不是重型容器。
这里的设计哲学和"纵深防御"完全一致:每一层都不要求自己能挡住所有攻击,但要求自己挡住一类典型攻击。schema 挡格式畸形,permission 挡显式恶意,sandbox 挡误伤型 blast。重型容器(Docker/Firecracker)能挡更多,但代价是启动时间从毫秒变秒、磁盘从零变 GB——对一个一分钟内可能跑几十次 tool 的交互式 agent 来说,性价比不划算。
5. Hooks:不是观察者,是策略层
引子
Hook 在大多数框架里是"日志钩子"。在 claw-code 里,它是第一公民的策略层。源码 hooks.rs 定义了三类事件:
| |
但真正的精妙在 hook 的输出 schema:
| |
对应的 JSON 字段:
systemMessage/reason→ 注入下一轮的 system context;continue: false或decision: "block"→ 直接 deny;hookSpecificOutput.permissionDecision→ 改写本次权限决策(Allow / Deny / Ask);hookSpecificOutput.permissionDecisionReason→ 解释给模型听;hookSpecificOutput.updatedInput→ 重写 tool 的输入 JSON。
14 步 pipeline 全貌
把上面四节拼起来,一次 tool 调用的完整 pipeline 是这样的(PDF §8 的简化版):
flowchart TD
M[Model emits tool_use] --> V[Zod validate + validateInput]
V -->|fail| Err[tool_result error]
V --> Pre[PreToolUse hooks]
Pre -->|deny| Err
Pre -->|updatedInput| Perm
Pre --> Perm[Permission policy: Deny→Allow→Ask rules]
Perm -->|ask| User[(prompter)]
Perm --> SB[Sandbox: unshare namespaces]
SB --> Call[tool.call]
Call --> Post[PostToolUse hooks]
Call -->|throw| Fail[PostToolUseFailure hooks]
Post --> R[tool_result back to model]
完整 14 步还包括:find tool by name → MCP metadata 拼装 → schema parse → validateInput → speculative bash classifier(专门判断 Bash 命令的副作用类别)→ PreToolUse → permission decide → updatedInput rewrite → sandbox wrap → tool.call → analytics → PostToolUse → structured output 回灌 → 失败时 PostToolUseFailure。
Insight
一个 hook API,五种能力:deny / 改 permission / 改 input / 注入 context / 终止整轮。这是"小 surface 大 power"的范例。对比一下:如果你只允许 hook 返回 bool(拦或不拦),那么"想把 rm -rf / 改成 rm -rf ./build“这种场景就只能在 tool 内部硬编码。给 hook 一个结构化输出 schema,等于把策略和工具实现彻底解耦。
工程上还有个隐藏好处:hook 可以由用户脚本(甚至是另一个进程)实现,输出 JSON 即可。这意味着安全团队能写自己的 hook 二进制,不用改 agent 主代码。
代码 demo:把 pipeline 缩到 150 行
完整代码在 code/02-tool-and-permission/tool_pipeline.py,stdlib only,无外部依赖。核心 5 步:
| |
Demo 注册了两个 tool:
read_file:安全工具,schema 要求path: str;shell:危险工具,pre_hook 检测到rm -rf /会把command重写成echo '[blocked rm -rf]',并在 audit log 里记一条"updatedInput”。
跑 python tool_pipeline.py 你会看到 4 个场景的输出:合法 read、schema 错误、被 hook 改写的 rm -rf、被 deny rule 拦掉的 curl evil.com。每一步都打印谁拦了 / 谁改了什么,一眼能看到 pipeline 的"切片"。
收束
把这一节的几个隐喻凑起来:
- Tool 是 agent 的手——你必须先承认它要碰真实世界。
- Schema 是手套——校验输入、拦截畸形调用,是最便宜也最高 ROI 的一道。
- Permission 是合同——5 级模式 × 3 类规则,给安全团队一个可表达策略的语言。
- Sandbox 是房间——namespaces 控制 blast radius,承认有限但保住关键防线。
- Hook 是策略层——它能 deny、能改、能注入 context,把"工具实现"和"安全策略"解耦。
这五者合起来,就是 claw-code 的 14 步 tool pipeline。每一步都不是装饰,每一步都对应一类生产事故。
下一篇预告
当一个 agent 跑不动复杂任务,怎么并行调度多个 sub-agent 而不污染主 context?claw-code 的答案是 Subagent + Plan + Worktree 三件套:subagent 拿到隔离的工具池和上下文,plan 模式让模型先写方案再执行,worktree 让多个 agent 在 git 层物理隔离。下一篇我们拆这条流水线——以及它如何回避"多 agent 系统"最常见的两个坑:context 串味和写冲突。
引用
src/tools.pyrust/crates/runtime/src/permissions.rsrust/crates/runtime/src/hooks.rsrust/crates/runtime/src/sandbox.rs- claw-code internal architecture PDF §3.4(tool 集合)、§8(toolExecution pipeline)