免费POC, 零成本试错
FDE知识库

FDE知识库

学习大模型的前沿技术与行业落地应用


收藏

Claude Code 源码揭秘:为什么不造 100 个工具?一个 Bash 打天下的哲学

发布日期:2026-01-12 22:38:12 浏览次数: 2643
作者:与AI同行之路

微信搜一搜,关注“与AI同行之路”

推荐语

Claude Code 用极简工具集实现强大功能,背后是"让模型直接生成Shell命令"的哲学思考。

核心内容:
1. Claude Code 工具系统的极简设计:仅核心工具+Bash
2. 不造专用工具的原因与跨平台适配难题
3. Bash工具的强大实现与七步执行流水线

杨芳贤
53AI创始人/腾讯云(TVP)最具价值专家

今天来拆工具系统。这么能打的 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 网关。请求进来也是这么一层层过滤——认证、限流、参数校验、路由、执行、响应封装。每一层职责单一,出问题好定位。


权限系统——用户始终在掌控

权限检查是流水线里最关键的一环。工具按危险程度分了四个等级:

级别
工具示例
风险
read
Read, Glob, Grep
只读,最安全
write
Edit, Write
修改文件
execute
Bash
执行任意命令
network
WebFetch
访问网络

权限配置有个层级优先级:


这意味着公司可以设策略禁止某些操作,用户没法绕过。用户可以设默认偏好,项目可以进一步定制。

每个权限有三种动作:

  • • Allow:直接执行,不问
  • • Ask:弹窗问用户
  • • Deny:直接拒绝

当权限是 Ask 的时候,CLI 会显示提示:

四个选项:单次允许、本次会话都允许、单次拒绝、本次会话都拒绝。用户始终知道模型要干什么,始终有否决权。

这个设计我很认可。我们之前做过一个自动化运维平台,一开始为了"智能",很多操作都自动执行。结果出过一次事故——脚本自动清理了不该清的日志。后来改成关键操作必须人工确认,虽然麻烦了点,但安全了很多。


Bash 工具——原力与约束

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
  • • content:"给我看匹配的内容"返回匹配行和上下文
  • • count:"有多普遍?"返回每个文件的匹配数

默认是 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 需要该文件类型配置了运行中的语言服务器。没有的话工具返回错误。能用的时候很强,但不是普遍可用。


Task——委派给子代理

复杂任务可以委派给专门的子代理。

{
  "name"
: "Task",
  "input"
: {
    "description"
: "Find auth code",
    "prompt"
: "Search for authentication-related code and explain the login flow",
    "subagent_type"
: "Explore"
  }
}

有些任务需要大量探索,会让主对话变得杂乱。子代理在隔离环境里工作,完成后返回摘要。主对话保持专注,子代理在几十个文件里挖掘。

专门的代理类型存在是因为不同任务需要不同方法:

  • • Explore:快速、浅层搜索,适合"找 X 在哪定义"
  • • Plan:深度思考架构,适合"设计怎么实现 Y"
  • • code-reviewer:专注发现问题,适合"审查这个 PR"
  • • security-audit:专门找漏洞,适合"检查安全问题"

resume 参数:子代理可以带着之前的上下文恢复。探索被中断或需要继续,不用从头开始。


Web 工具

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 / 怎么办?


53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询

扫码登录
登录即表示您同意《53AI网站服务协议》
服务协议

欢迎您使用【53AI 官方网站】(以下简称“本网站”或“我们”)。本《会员服务协议》(以下简称“本协议”)是您(以下简称“会员”或“用户”)与【深圳市博思协创网络科技有限公司】之间关于注册、登录及使用本网站会员服务所订立的法律协议。

在您注册或登录前,请务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的条款、知识产权条款、争议解决条款等。此类条款将以加粗形式提示您注意。 当您通过微信公众号授权、手机验证码验证或其他方式成功登录本网站时,即视为您已完全理解并同意接受本协议的全部内容。

一、 定义

本网站:指由【深圳市博思协创网络科技有限公司】运营的,域名为【53ai.com】的网站及相关移动端页面。

会员服务:指本网站向注册会员提供的知识库文章查阅、内容检索及其他相关增值服务。

知识库内容:指本网站发布的包括但不限于文字、图表、数据、研究报告、行业分析等数字化内容资源。

二、 账号注册与登录

登录方式:本网站支持以下登录方式,您可根据实际情况选择:

微信公众号授权登录:您同意将您的微信OpenID信息授权给本网站,用于创建或关联会员账号。

手机验证码登录:您需提供真实有效的手机号码,并通过短信验证码完成身份验证与登录/注册。

账号安全:您的账号仅限您本人使用,禁止赠与、借用、租用、转让或售卖。因您保管不善导致的账号被盗、密码泄露等损失,由您自行承担。

实名认证:根据相关法律法规要求,我们可能要求您在特定功能下完成实名认证。如您拒绝提供,可能无法使用部分或全部服务。

未成年人保护:若您未满18周岁,请在法定监护人的陪同下阅读本协议,并在征得监护人同意后使用本服务。

三、 服务内容与规范

知识库查阅权限:会员登录后,有权按照其会员等级对应的权限范围,在线浏览、检索本网站知识库中的相关文章及内容。

服务变更:我们有权根据业务发展需要,调整、变更或终止部分服务内容,并将以网站公告、公众号消息等方式提前通知。

禁止行为:您在使用服务时不得实施以下行为:

利用技术手段批量爬取、下载、转存知识库内容;

将知识库内容用于商业目的或未经授权地向第三方传播;

干扰本网站正常运行或侵犯其他用户合法权益;

发布违法违规信息或从事违反公序良俗的活动。

四、 知识产权声明

权利归属:本网站知识库中的排版设计、软件代码等内容的知识产权均归【公司全称】或原权利人所有,受《中华人民共和国著作权法》等法律保护。

有限许可:本网站授予会员一项非独占、不可转让、不可转授权的普通许可,仅限于个人学习、研究之目的在线查阅知识库内容。

侵权追责:未经书面许可,任何单位或个人不得以任何形式复制、转载、摘编、镜像、汇编或以其他方式使用上述内容。一经发现,我们保留追究其法律责任的权利。

五、 个人信息保护

我们重视对您个人信息的保护。关于我们如何收集、使用、存储和保护您的个人信息,请单独阅读 《隐私政策》。

您通过微信公众号授权或手机号验证所提供的信息,我们将严格按照《个人信息保护法》的规定处理,仅用于身份识别、服务提供及安全验证等必要用途。

您可以随时通过网站设置或联系客服行使查阅、更正、删除个人信息及撤回授权同意的权利。

六、 免责声明

内容准确性:知识库内容仅供参考,不构成专业建议。我们不对其完整性、准确性、时效性作任何明示或暗示的保证,您应自行判断并承担使用风险。

不可抗力:因自然灾害、政策法规变化、网络故障、第三方平台接口异常(如微信接口维护、运营商短信通道故障)等不可抗力导致的服务中断或延迟,我们不承担违约责任。

第三方链接:本网站可能包含指向第三方网站的链接,该等网站的内容和服务不受我们控制,请您自行甄别风险。

七、 违约责任

如您违反本协议约定,我们有权视情节采取警告、限制功能、暂停服务、注销账号等措施,并保留要求赔偿损失的权利。

如因您的违约行为导致我们遭受行政处罚、第三方索赔或商誉损失,您应承担全部赔偿责任(包括但不限于罚款、赔偿金、律师费、公证费等)。

八、 法律适用与争议解决

本协议的订立、执行和解释均适用中华人民共和国大陆地区法律。

因本协议产生的或与本协议有关的任何争议,双方应友好协商解决;协商不成的,任何一方均可向【公司所在地】有管辖权的人民法院提起诉讼。

九、 其他

本协议构成双方就本服务达成的完整协议,取代此前任何口头或书面约定。

本协议任一条款被认定为无效或不可执行的,不影响其他条款的效力。

我们对本协议享有最终解释权,并在法律允许的范围内保留随时修改的权利。修改后的协议一经公布即生效,继续使用服务即视为同意修订内容。


已查阅