微信扫码
添加专属顾问
我要投稿
Claude Code 用极简工具集实现强大功能,背后是"让模型直接生成Shell命令"的哲学思考。核心内容: 1. Claude Code 工具系统的极简设计:仅核心工具+Bash 2. 不造专用工具的原因与跨平台适配难题 3. Bash工具的强大实现与七步执行流水线
↑阅读之前记得关注+星标⭐️,😄,每天才能第一时间接收到更新
「Claude Code 源码揭秘」系列的第三篇,上一篇聊了 Claude Code 的消息。但光有消息不够,得有工具才能干活。
今天来拆工具系统。这么能打的 Agent,工具应该很多吧?毕竟功能那么丰富,文件操作、代码搜索、网络请求、进程管理...
结果打开 src/tools/index.ts 一看,核心工具就那么几个:
export function registerAllTools(): void {
// Bash 工具家族
toolRegistry.register(new BashTool());
toolRegistry.register(new KillShellTool());
// 文件工具三剑客
toolRegistry.register(new ReadTool());
toolRegistry.register(new WriteTool());
toolRegistry.register(new EditTool());
// ...
}
就这?没有 GitTool、没有 NpmTool、没有 DockerTool... 你想跑 git status?用 Bash。想 npm install?用 Bash。想 grep 搜代码?还是用 Bash。
我一开始觉得这是偷懒。后来想了想,不对,这是聪明。
说到这个我想起之前在金融科技那个项目里,我们也想过造一堆专用工具。当时团队里有个小伙子特别有热情,说咱们搞一个 GitTool 吧,把常用的 git 操作都封装一下,多方便。我说行,你先调研一下。
结果他搞了两周,来找我说搞不下去了。为啥?光是跨平台适配就把他整崩溃了。Windows 上 Git 可能装在不同位置,PATH 配置五花八门。macOS 可能用 Xcode Command Line Tools 的 Git,也可能是 Homebrew 装的。Linux 各发行版又不一样。然后 git 命令的参数组合几百种,你是穷举还是开放?穷举不现实,开放的话还不如直接让用户敲命令。
Claude Code 的做法就是想明白了这一点:不造专用工具,让模型直接生成 Shell 命令。模型见过的 Shell 命令比任何人写的工具都全。它知道 git log --oneline -10 是啥意思,知道 npm install --save-dev 和 npm install -D 等价。你造 100 个工具,也覆盖不了模型脑子里的知识。
当然,不造专用工具不代表 Bash 工具本身简单。src/tools/bash.ts 有 1089 行代码,处理的事情相当多。
在深入具体工具之前,先说说每个工具调用是怎么跑的。模型发出 tool_use 请求后,不是直接执行,而是要过一道流水线:
任何一步失败(工具不存在、参数不合法、权限拒绝),都会返回 is_error: true 的错误结果。
这套流水线设计让我想起我们之前做的 API 网关。请求进来也是这么一层层过滤——认证、限流、参数校验、路由、执行、响应封装。每一层职责单一,出问题好定位。
权限检查是流水线里最关键的一环。工具按危险程度分了四个等级:
权限配置有个层级优先级:
这意味着公司可以设策略禁止某些操作,用户没法绕过。用户可以设默认偏好,项目可以进一步定制。
每个权限有三种动作:
当权限是 Ask 的时候,CLI 会显示提示:
四个选项:单次允许、本次会话都允许、单次拒绝、本次会话都拒绝。用户始终知道模型要干什么,始终有否决权。
这个设计我很认可。我们之前做过一个自动化运维平台,一开始为了"智能",很多操作都自动执行。结果出过一次事故——脚本自动清理了不该清的日志。后来改成关键操作必须人工确认,虽然麻烦了点,但安全了很多。
Bash 是最强大也最危险的工具。它能做用户 Shell 能做的一切。
{
"name": "Bash",
"input": {
"command": "npm test",
"description": "Run test suite",
"timeout": 60000
}
}
几个关键设计:
输出截断:测试套件、构建日志可能输出几兆内容。全塞进上下文窗口既浪费又混乱。Bash 工具默认截断到 30000 字符,截断时会提示,模型可以按需获取特定部分。
超时控制:默认 2 分钟,最长 10 分钟。大多数命令很快完成,2 分钟能抓住跑飞的进程。真正需要长时间的操作(比如完整构建)可以显式指定更长超时。
description 参数:强制模型用人话说明要干什么。这个会显示在 UI 里,方便用户理解和审批。也是个"三思而后行"的机制——模型得先想清楚再动手。
后台模式:有些操作要跑很久。run_in_background 参数让命令在后台执行,立即返回任务 ID。模型可以继续干别的,稍后用 TaskOutput 查结果。
跨平台适配这块:
function getPlatformShell(): ShellConfig {
if (IS_WINDOWS) {
return { shell: 'powershell.exe', args: [...], isPowerShell: true }
}
return { shell: process.env.SHELL || '/bin/bash', ... }
}
Windows 用 PowerShell,WSL 要特殊处理,Unix 系统用用户的默认 Shell。我之前做运维平台的时候也踩过这个坑,Windows 的命令行兼容性问题能把人搞疯。Claude Code 在这块处理得挺细的。
读、写、改分成了三个独立的工具——Read、Write、Edit。
你可能会问,为什么不搞一个 FileTool 什么操作都能干?
答案是:分开才能加约束。
这个设计让我想起我们之前做云文档系统的时候,也有类似的争论。当时有人说咱们搞一个统一的 DocumentAPI,增删改查都走这一个入口,多简洁。但后来发现不行,因为你没法在统一接口上加细粒度的权限控制。比如我想要求"修改文档之前必须先获取锁",如果增删改查都在一个接口里,这个约束就很难加。
Read——上下文的基础
{
"name": "Read",
"input": {
"file_path": "/project/src/index.ts",
"offset": 1,
"limit": 100
}
}
输出用 cat -n 风格,每行带行号:
1 import { App } from './app';
2
3 const app = new App();
行号不只是给人看的。模型后面用 Edit 工具的时候,需要引用准确位置。行号创建了读和改之间的共享坐标系。
limit 参数强制模型有选择性地读取,而不是把整个大文件塞进上下文。offset 支持分页浏览长文件。
Claude 是多模态的。Read 工具可以加载图片、处理 PDF(逐页提取文本和视觉内容)。这让 Claude Code 不仅仅能处理代码。
Edit——精准优于强力
Edit 工具体现了一个关键原则:让安全的事情简单,让危险的事情困难。
{
"name": "Edit",
"input": {
"file_path": "/project/package.json",
"old_string": "\"version\": \"1.0.0\"",
"new_string": "\"version\": \"1.1.0\""
}
}
早期版本允许基于行号或模式匹配来编辑。问题是:模型读文件和改文件之间,代码可能变了。行号移位了,模式匹配到了错误位置。要求精确、唯一的字符串匹配,文件变了就安全失败,而不是改错地方。
Claude Code 的思路一样。Edit 工具有个硬性前提:必须先读过这个文件才能改。
怎么实现的呢?有个全局单例叫 FileReadTracker:
class FileReadTracker {
private readFiles: Map<string, FileReadRecord> = new Map();
markAsRead(filePath: string, mtime: number): void {
this.readFiles.set(path.resolve(filePath), {
path: filePath,
readTime: Date.now(),
mtime // 记录文件当时的修改时间
});
}
hasBeenRead(filePath: string): boolean {
return this.readFiles.has(path.resolve(filePath));
}
}
Read 工具读文件的时候,会调用 markAsRead 记录下来。Edit 工具改文件之前,会检查 hasBeenRead,没读过就直接报错。而且它还记录了文件的修改时间 mtime,如果你读完之后文件被别人改了,Edit 也会拒绝执行,让你重新读一遍。
这套机制防止的是什么?防止模型盲目编辑。你想想,如果模型可以不看文件就直接改,它会根据自己的"记忆"来改。但模型的记忆可能是过时的,可能是训练数据里别的项目的,可能是它自己编的。强制先读再改,模型必须看到文件的真实内容,才能做出正确的修改决策。
replace_all 逃生舱:有时候你真的需要全局重命名变量。replace_all: true 绕过唯一性检查,但必须显式指定。模型必须主动选择更危险的操作。
还有个细节特别有意思,就是 Edit 工具的智能匹配。
你用 Read 工具看文件,输出是带行号的:
42 foo()
43 bar()
44 }
用户可能直接复制这段作为 old_string 去做替换。正常来说,文件里根本没有 42\t 这个前缀,匹配应该失败对吧?
但 Edit 工具会自动检测并剥离行号:
const LINE_NUMBER_PREFIX_PATTERN = /^(\s*\d+)\t/;
function stripLineNumberPrefixes(str: string): string {
return str.split('\n').map(line => {
const match = line.match(LINE_NUMBER_PREFIX_PATTERN);
return match ? line.substring(match[0].length) : line;
}).join('\n');
}
还有智能引号的问题。你从某些文档或网页复制代码,普通引号可能被自动转成弯引号 ""。Edit 工具也会处理这个,把弯引号转回普通引号再匹配。
这些细节用户基本感知不到,但缺了就会很痛苦。我之前做过一个内部的代码评审工具,就没处理这些边界情况,结果用户天天来问"为什么我复制的代码匹配不上"。后来加了类似的容错处理,投诉一下子就少了。
Write——核武器选项
Write 创建文件或完全覆盖现有文件。
{
"name": "Write",
"input": {
"file_path": "/project/src/utils.ts",
"content": "export function add(a: number, b: number) {\n return a + b;\n}\n"
}
}
系统提示词指示模型在覆盖前先读文件。这不只是礼貌——确保模型看到了它要销毁的内容。工具本身没法强制这一点(它不知道消息历史里有什么),但指令创建了行为护栏。
Write 工具自动创建目录,不用模型每次都先 mkdir -p。减少正常操作的摩擦。
Glob——按名字找文件
知道要什么类型的文件,但不知道在哪?
{
"name": "Glob",
"input": {
"pattern": "src/**/*.tsx",
"path": "/project"
}
}
结果按修改时间排序,最近编辑的在前面。你问"我刚才改的那个组件",排在前面的文件更可能是答案。这个小优化让模型的猜测更准确。
为什么不用 find 命令?Glob 更快,跨平台。用优化过的库实现,而不是调系统命令。大代码库里这个差别很明显。
Grep——搜文件内容
Grep 搜索文件内部,基于 ripgrep 构建,大代码库也很快。
{
"name": "Grep",
"input": {
"pattern": "TODO:",
"path": "src/",
"output_mode": "content",
"-C": 2
}
}
三种输出模式:
默认是 files_with_matches 因为最省 token。模型可以后续针对性地读取。
直接暴露 ripgrep 的参数:-A、-B、-C 上下文参数,大小写敏感,行号等。开发者本来就熟悉这些,不用发明新词汇,工具说的语言和开发者日常用的一样。
LSP——语义理解
Grep 搜文本,LSP 理解代码结构。
{
"name": "LSP",
"input": {
"operation": "goToDefinition",
"filePath": "/project/src/index.ts",
"line": 10,
"character": 15
}
}
正则能找文本,但没法回答"这个函数定义在哪?"或"什么调用了这个方法?"。LSP 操作提供语义导航:跳转定义、查找引用、显示类型信息。这就是 IDE 的工作方式,Claude Code 也能接入同样的基础设施。
局限:LSP 需要该文件类型配置了运行中的语言服务器。没有的话工具返回错误。能用的时候很强,但不是普遍可用。
复杂任务可以委派给专门的子代理。
{
"name": "Task",
"input": {
"description": "Find auth code",
"prompt": "Search for authentication-related code and explain the login flow",
"subagent_type": "Explore"
}
}
有些任务需要大量探索,会让主对话变得杂乱。子代理在隔离环境里工作,完成后返回摘要。主对话保持专注,子代理在几十个文件里挖掘。
专门的代理类型存在是因为不同任务需要不同方法:
resume 参数:子代理可以带着之前的上下文恢复。探索被中断或需要继续,不用从头开始。
WebFetch——把外部世界带进来
有时候答案不在代码库里。
{
"name": "WebFetch",
"input": {
"url": "https://docs.example.com/api",
"prompt": "Extract the authentication endpoints"
}
}
原始 HTML 冗长,充满导航、广告、样板。prompt 参数告诉一个更小更快的模型提取什么。结果是聚焦的摘要,而不是一堆标签。
15 分钟缓存:重复抓取同一个 URL 浪费时间和带宽。缓存确保模型在一个会话里多次引用同一页面时,只抓一次。
WebSearch——发现
不知道 URL?搜索。
{
"name": "WebSearch",
"input": {
"query": "React useEffect cleanup pattern"
}
}
域名过滤:allowed_domains 和 blocked_domains 参数让你聚焦可信来源或排除噪音。只想要官方文档的结果?可以强制。
AskUserQuestion——当模型需要输入时
不是所有事都能自动化。有时候模型需要问。
{
"name": "AskUserQuestion",
"input": {
"questions": [{
"question": "Which database should we use?",
"header": "Database",
"options": [
{ "label": "PostgreSQL (Recommended)", "description": "Full-featured relational DB" },
{ "label": "SQLite", "description": "Lightweight, file-based" }
],
"multiSelect": false
}]
}
}
选项比打字快(点一下 vs 打字),对模型来说也更容易解释。"Other" 逃生舱始终存在,以防选项都不合适。
header 限制 12 字符,因为 UI 把它显示为紧凑的标签。强制简短让界面可扫描。
TodoWrite——可见的进度
模型可以追踪自己的工作。
{
"name": "TodoWrite",
"input": {
"todos": [
{ "content": "Read existing code", "status": "completed", "activeForm": "Reading code" },
{ "content": "Implement feature", "status": "in_progress", "activeForm": "Implementing" },
{ "content": "Write tests", "status": "pending", "activeForm": "Writing tests" }
]
}
}
为什么同时只能有一个 in_progress?这个约束强制顺序专注。模型必须完成或放弃当前任务才能开始另一个。防止待办列表变成一堆半成品。
activeForm 字段:任务的进行时形式,在进行中时显示("Implementing feature" 而不是 "Implement feature")。小 UX 细节,让状态栏感觉更有生命力。
所有工具实现标准接口:
interface Tool {
name: string; // 唯一标识
description: string; // 给模型理解的描述
inputSchema: ZodSchema; // 输入校验
permissionLevel: "read" | "write" | "execute" | "network";
call(input: ToolInput, context: ToolContext): Promise<ToolOutput>;
mapToolResultToToolResultBlockParam(result: ToolOutput): string;
}
这个接口确保所有工具的一致性,无论是内置的还是通过 MCP 提供的。
新增一个工具很简单:继承基类,实现业务逻辑,注册到工具表里,完事。
纵观所有这些工具,模式浮现出来:
安全失败。Edit 要求唯一字符串。Read 有行数限制。Bash 有超时。出问题时,失败模式是保守的。
Token 效率。Grep 默认返回文件路径而不是内容。Read 有限制。输出会截断。每个设计决策都考虑上下文窗口。
用户可见性。Bash 要求描述。危险操作前会提示权限。用户永远不会对模型的行为感到惊讶。
带护栏的力量。工具确实强大,但每个都有约束防止最坏结果。
翻完这部分代码,我总结一下感受。
能让模型干的事,别自己造轮子。Bash 工具就是这个哲学的体现。分离是为了加约束,Read/Write/Edit 分三个不是功能划分,是为了能在 Edit 上加"先读后改"的约束。多轮容错比一次成功更务实,智能引号、行号剥离这些处理,都是在兜用户的底。
好了,工具能跑命令、能改文件了,但你有没有想过一个问题:如果模型生成了一条 rm -rf / 怎么办?
下一篇聊聊 Claude Code 的安全防线。命令注入检测、沙箱隔离、权限拦截... 给 Agent 装安全气囊这件事,比想象中复杂得多。
本文基于 Claude Code 2.0.76 版本源码分析,主要文件:src/tools/bash.ts、src/tools/file.ts、src/tools/base.ts。
最后记得⭐️我,每天都在更新:欢迎点赞转发推荐评论,别忘了关注我
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-01-12
Anthropic官方万字长文:AI Agent评估的系统化方法论
2026-01-12
Anthropic工程实践:AI Agent如何连续工作数天完成复杂项目?
2026-01-12
2026大模型伦理深度观察:理解AI、信任AI、与AI共处
2026-01-12
发现一个比AutoGLM更小的GUI模型,仅4B参数,附实测和部署教程
2026-01-12
阿里云全新发布的 UModel 是什么
2026-01-12
Claude Skills 到底是什么?万字长文深度解析
2026-01-12
Agent Skill 即将统治一切?Claude Code 2.1.3 把斜杠命令"杀"了
2026-01-12
如何用AI表格低门槛手搓一个业务系统?
2025-10-26
2025-11-19
2025-10-20
2025-11-13
2025-10-18
2025-10-21
2025-10-15
2025-11-03
2025-10-23
2025-10-22
2026-01-12
2026-01-12
2026-01-11
2026-01-10
2026-01-10
2026-01-08
2026-01-02
2025-12-31