【Claude code 源码启示录】工具是受管执行接口
想象这样一个场景:你让 AI 去删除一个文件夹。三秒钟之后,一个包含了两年数据的项目目录消失了。没有恢复按钮,没有回收站,就是没了。
这一刻,问题的性质从”AI 说错了什么”变成了”AI 做错了什么”。说错话,充其量尴尬一下。但如果 AI 能「调用工具、执行指令、碰到真实世界」,那它的每一个动作都会带上后果。能力增强了,代价也增强了。
很多人还在把工具理解成 Agent 的”能力延伸”——好像就是把模型的触手拉长了一点,让它能动动文件、调调 API。但如果你真的看过一个生产级 Agent 系统怎么处理工具调用,你就会明白:「工具不是能力延伸,是受管执行接口」。系统从那一刻起,就不再是一个”执行机器”,而变成了一个”调度机构”。
并发安全性是第一个检查站
在 Claude Code 的 runTools() 函数里,有一个细节很能说明问题。工具列表到手之后,系统做的第一件事不是执行,而是「分批」。
通过 partitionToolCalls() 和 isConcurrencySafe() 判断哪些工具调用可以并发执行,哪些必须排队。为什么?因为并发执行可能导致顺序混乱。比如你同时删除一个文件,又同时读取它,结果就不确定了。
但这还不是最有意思的部分。即使工具要并发执行,系统也会先把所有的 context modifier 缓存起来,然后「按原始顺序回放」。翻译一下:在执行层面允许并发,但在语义层面保持确定顺序。换句话说,系统承诺:无论你怎么并发,最终的上下文演化路径都是确定的、可重现的。
这是什么意思?意思是系统优先级很清楚——「一致性压倒性能」。
执行前已经发生了太多事
当系统真正执行一个工具的时候,看起来就像:调用工具函数 → 获得结果 → 继续。但实际上呢?
在 runToolUse() 里,系统已经接进来了一整套防护机制:
-
「Permission Check」 —— 权限判定,决定这个工具这次是否被允许执行 -
「Pre-execution Hooks」 —— 执行前钩子,可以在最后时刻发出警告或拒绝 -
「Telemetry & Logging」 —— 完整的追踪和日志,记录每一个工具调用的时间、参数、结果 -
「Synthetic Error Materialization」 —— 把执行失败转换成结构化的错误消息,补齐缺失的结果 -
「Post-execution Hooks」 —— 执行后钩子,可以对结果进行二次验证或修改 -
「Failure Compensation」 —— 失败补偿,如果出了问题,系统自动清理现场
工具执行在这套系统里,不是”裸调用”。它被包裹在一个完整的「前-中-后流程」里。中断、失败、权限问题,系统都见过,都有套路。
权限系统:第三种决策
有一个很关键的函数叫 CanUseToolFn。它决定了一个工具在某一刻是否允许被执行。返回值分成三种:
-
「Allow」 —— 允许,继续执行 -
「Deny」 —— 拒绝,停止执行,返回错误 -
「Ask」 —— 询问,系统自己也不确定,把决定权交给人
看到这个”Ask”了吗?这就是系统的深刻洞察:「某些决定不应该由模型单方面做,也不应该由系统单方面做,而应该由人来做」。
这意味着什么?意味着系统承认自己的局限。它没有那么聪明,足以替用户决定”你真的想删除这个文件吗”。所以干脆让人来决定。
这就是为什么很多生产级 Agent 系统都有一个”需要人工批准”的流程。不是因为系统不信任自己,而是因为系统知道:「权限不是技术问题,是治理问题」。
中断语义:StreamingToolExecutor 的设计
想象一个场景:系统发出了五个并行的工具调用。其中两个已经在执行了,还有三个排队中。这时用户突然按了 Ctrl+C。
系统怎么办?简单粗暴地中止?不对。系统会:
-
消费 StreamingToolExecutor 的剩余缓冲(确保已经发出的部分数据不丢失) -
对那两个正在执行的工具调用生成「合成的 tool_result」(告诉模型”这个没完成,被打断了”) -
对那三个排队的调用,根据它们的 interruptBehavior设置,决定是直接取消还是让它们继续等待
代码里有一个概念叫 synthetic error message。它用来区分三种中断原因:
-
「Sibling error」 —— 同时执行的另一个工具失败了,连带影响 -
「User interrupted」 —— 用户手动中止了 -
「Streaming fallback」 —— 流式获取失败,系统降级处理
每一种原因对应不同的恢复策略。系统不会笼统地说”都失败了”,而会说”第二个工具因为第一个工具出错而没执行”或”用户在第三秒按了中止”。
这就叫「中断是一等语义」。不是”出了问题所以中止”,而是”中断本身是一种系统状态,需要被妥善处理”。
Bash 为什么最可疑
在 Claude Code 里,Bash 这个工具有特别待遇——专门针对它写了一大段操作规约。为什么?
因为 Bash 几乎不受任何领域边界的约束。它可以:
-
直接读写文件系统的任何地方 -
启动、中止、监控任意进程 -
发起网络请求、下载、上传 -
修改 Git 仓库的历史 -
执行管道、重定向、后台运行
还有复杂的 shell 语义(&&、||、|、重定向符等),一个命令就能做到别的工具需要五步的事。
正因为这样,Claude Code 对 Bash 有一套专门的防御:
-
明确列出了危险命令的黑名单(比如 rm -rf /、git push --force) -
限制 subcommand 的数量,防止复合命令导致检查失控 -
对 Bash 的 hooks 做了特殊处理,确保每一步都能被追踪 -
对 Git 操作做了完整的审计,防止无可挽回的改动
这不是说 Bash 不能用。而是说,「权力越大、风险越高,约束就越细」。系统对待 Bash 的态度就像对待一个精神不太稳定的天才——很想用它的智力,但必须给它套上足够的防护装置。
工具系统保护的,不只是用户
这里有个很有意思的反转。很多人以为工具的权限管理是为了”保护用户”。但实际上,系统也在「保护自己」。
为什么?因为工具的执行结果会影响系统的状态。如果一个工具调用留下了不完整的 tool_result,系统就无法继续工作。如果执行顺序出了问题,上下文演化就失序了。如果中断处理不当,就会留下孤立的工具调用,导致系统的逻辑图断裂。
权限系统、调度机制、中断处理,这些看起来像是”防御措施”的东西,其实是「系统自己的器官」。没有它们,系统根本活不了。
核心观点
一旦模型开始调用工具,Agent 就不再是一个”执行机器”了。
它变成了一个「调度机构」——需要在工具之间协调、在权限和能力之间平衡、在安全性和效率之间找到平衡点。工具的权限管理不是锦上添花,而是基础设施。
这就是为什么真正的 Agent 系统,从来不会让模型”想用什么工具就用什么工具”。系统会说:”你想用什么?让我看看,权限检查一下,调度安排好了,再准许你用。执行过程中如果出问题,我来处理。”
权限不是限制,是秩序。秩序让 AI 和人类能在同一个系统里共存。
夜雨聆风
