大家好。今天,我们将继续探索 Claude Code 的核心源码。
在上一章,我们弄懂了 QueryEngine 是如何管理对话循环的。但你有没有想过,远在云端的 Claude 3.5 模型,究竟是怎么在你的电脑上敲下 npm install、是怎么搜索代码、又是怎么修改文件的?
如果没有“手脚”,再聪明的大脑也只是一台陪聊机。今天,我们就来揭开让 Claude Code 真正动起来的核心秘密——工具系统(Tool System)。
学习完这章后,你会对以下内容有清晰的认识:
- 工具驱动架构:为什么在 Claude Code 里“万物皆工具”?
- 依赖注入与生命周期:工具是怎么安全地跑起来的?
- 深入 BashTool 源码:它怎么处理长输出?怎么防止死循环?
- 动手实践:像官方一样,写一个真正的本地工具。
如果说大模型是“大脑”,那么 src/Tool.ts 就是让大脑长出三头六臂的“基因图谱”。
打开 src/Tool.ts,你会看到一个极其庞大且严谨的 Tool 泛型接口。所有的能力——无论是 BashTool、GlobTool 还是外部的 MCP 服务——全都是这个接口的实现。
为了让大家有个直观的感受,我把 src/Tool.ts 中 Tool 接口的核心定义摘录了出来:
// 摘自 src/Tool.ts export type Tool< Input extends AnyObject = AnyObject, Output = unknown, P extends ToolProgressData = ToolProgressData, > = { // 1. 核心的执行逻辑:注入了大礼包 context call( args: z.infer<Input>, context: ToolUseContext, canUseTool: CanUseToolFn, parentMessage: AssistantMessage, onProgress?: ToolCallProgress , ): Promise< ToolResult< Output>> // 2. 严格的输入校验(基于 Zod) readonly inputSchema: Input // 3. 统一的权限拦截 checkPermissions( input: z. infer< Input>, context: ToolUseContext, ): Promise< PermissionResult> // 4. 统一的 UI 渲染 renderToolUseMessage( input: Partial
infer<
Input>>,
options: {
theme:
ThemeName;
verbose:
boolean; commands?:
Command[] }, ):
React.
ReactNode
// ... 其他诸如 isConcurrencySafe, interruptBehavior 等细节 }
为什么要这么设计?我在源码中看出了三个字:大一统。
- 统一的入参校验:如源码中的
readonly inputSchema: Input。在大模型把 JSON 传过来时,系统第一步就是严格的类型校验,坚决不让非法的参数污染本地环境。 - 统一的权限拦截:每个工具都要实现
checkPermissions。系统不用管你具体是什么工具,只要在这个层面上统一拦截,就能实现auto、plan、ask等多种权限模式。 - 统一的 UI 渲染:
renderToolUseMessage要求返回React.ReactNode,这直接把枯燥的执行日志变成了终端里漂亮的 React Ink 组件。
在传统的代码里,如果一个工具想读取系统状态,可能会写满各种 import store from '...' 的全局单例。但这在复杂的 Agent 系统中是一场灾难。
Claude Code 的做法非常优雅:依赖注入(Dependency Injection)。
每次调用工具的 call() 方法时,系统都会给它塞一个超级大礼包——ToolUseContext。我在源码里给大家提炼出了它的核心骨架:
// 摘自 src/Tool.ts export type ToolUseContext = { options: { tools: Tools // 让工具可以调用其他工具 commands: Command[] // 可用命令 mcpClients: MCPServerConnection[] // MCP 连接 // ... } abortController: AbortController // 极其重要!用来响应用户的 Ctrl+C 打断 getAppState(): AppState // 读取当前大环境状态 setAppState(f: (prev: AppState) => AppState): void // 修改大环境状态 readFileState: FileStateCache // 文件状态缓存 messages: Message[] // 当前的聊天历史记录 // ... }
这意味着什么?意味着一个工具在执行时,完全不需要关心外面的世界是怎么运转的。只要拿到这个 Context,它就能呼风唤雨。它甚至可以通过 setAppState 偷偷在后台起一个新的子任务进度条!
为了让大家更有体感,我们来解剖一下系统里最危险、也最强大的工具:src/tools/BashTool/BashTool.tsx。
让大模型自由执行 Bash 命令,无异于让一个三岁小孩玩核按钮。官方是怎么防患于未然的呢?
当你让模型去排查一个 Bug 时,它可能会疯狂地 grep、cat、ls。如果这些命令的输出全都堆在终端里,你的屏幕早就被刷爆了。
在 BashTool.tsx 的开头,我发现了一个叫做 isSearchOrReadBashCommand 的神奇函数:
// 摘自 src/tools/BashTool/BashTool.tsx const BASH_SEARCH_COMMANDS = new Set(['find', 'grep', 'rg', 'ag', 'ack', 'locate', 'which', 'whereis']); const BASH_READ_COMMANDS = new Set(['cat', 'head', 'tail', 'less', 'more', 'wc', 'stat', 'file', 'jq', 'awk']); const BASH_LIST_COMMANDS = new Set(['ls', 'tree', 'du']); // 在 BashTool 对象里,它会使用这些白名单来做判断 export function isSearchOrReadBashCommand(command: string) { // ... (省略复杂的管道、重定向解析逻辑) const isPartSearch = BASH_SEARCH_COMMANDS.has(baseCommand); const isPartRead = BASH_READ_COMMANDS.has(baseCommand); // ... return { isSearch: hasSearch, isRead: hasRead, isList: hasList }; }
原来,系统维护了一份详细的“无害查询命令”白名单。当发现你在跑 cat file | grep error 时,UI 渲染层会直接把这些中间过程“折叠”起来,让你只看到最终的结论,界面清爽无比!
有时候大模型会犯傻,想等某个文件生成,于是直接敲了个 sleep 10。这会让整个主线程傻等 10 秒!
源码中专门写了一个 detectBlockedSleepPattern 拦截器:
// 摘自 src/tools/BashTool/BashTool.tsx export function detectBlockedSleepPattern(command: string): string | null followed by: \({rest}` : `standalone sleep \){secs}`; }
如果模型手滑,跑了一个 cat package-lock.json,几万行的输出不仅会挤爆终端,还会瞬间耗尽你的 API Token 预算(而且非常贵)。
Claude Code 采用了 EndTruncatingAccumulator。它不会把所有输出都塞进内存,而是像一个两头漏风的管子:保留开头的几百行,保留结尾的报错信息,中间的一大段全部替换为 [… truncated …]。
// 摘自 src/tools/BashTool/BashTool.tsx (工具执行阶段) const stdoutAccumulator = new EndTruncatingAccumulator();
// … 在命令执行完成或流式收集输出时 … stdoutAccumulator.append((result.stdout || “).trimEnd() + EOL);
if (result.code !== 0) { stdoutAccumulator.append(Exit code ${result.code}); }
// 最终将截断后(保留头尾,中间省略)的字符串交给大模型 const stdout = stdoutAccumulator.toString();
这种设计既保证了大模型能看到执行结果的头尾(通常报错都在尾部),又极大地省了钱!
看了这么多源码,手痒了吗?官方提供了一个非常好用的 buildTool 辅助函数。我们来模仿源码的风格,写一个只读的 GitStatusTool,帮大模型快速获取当前仓库的干净程度。
import { z } from ‘zod/v4’; import { buildTool } from ’../Tool.js’; import { exec } from ‘child_process’; import from ‘util’;
const execAsync = promisify(exec);
// 🟢 我们的实战代码:极简版 Git 状态工具 export const GitStatusTool = buildTool({ name: ‘GitStatus’, description: ‘获取当前代码库的 git 状态’,
// 1. 严谨的输入校验 inputSchema: z.object({
directory: z.string().optional().describe('要检查的目录路径,默认当前路径')
}),
// 2. 告诉系统这是绝对安全的只读操作,不需要弹窗警告 isReadOnly: () => true,
// 3. 核心执行逻辑 async call({ directory }, context) {
const cwd = directory || process.cwd(); try { // 真实执行系统命令 const { stdout } = await execAsync('git status --short', { cwd }); return { data: stdout.trim() || '当前分支很干净,没有任何未提交的修改。' }; } catch (error) { return { data: `执行失败: ${error.message}` }; }
} });
只要把这个 Tool 注册进上下文的 tools 数组里,大模型就能随时调用它了!这就是工具驱动架构的魅力——插拔式扩展,毫无违和感。
今天先到这,我们已经深入体会了工具系统作为 Claude Code “改变现实世界”桥梁的精妙之处。
通过一套统一的 Tool 接口,它实现了严格的 Zod 校验、基于 Context 的依赖注入,以及优雅的 UI 渲染折叠。而在看似简单的 BashTool 背后,隐藏着命令分类折叠、死等拦截、长输出截断等无数打磨细节。
Claude Code 源码系列还未完结。敬请期待!一键三连,关注不迷路。
- Zod 官方文档 —— 学习如何在 TypeScript 中写出像魔法一样严谨的运行时类型校验。
- Anthropic Tool Use 官方指南 —— 了解大模型是如何理解你写的
description和inputSchema的。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/259715.html