资讯详情

Claude Code 源码深度拆解:从构建到自定义工具调用

📅 2026/10/25 16:13:20 | 华诺云谱 👁 阅读
Claude Code 源码深度拆解:从构建到自定义工具调用
简介Claude Code 是一个在技术社区中备受关注的开源项目其完整源码现已打包提供适合前端、Node.js 方向的初中高级开发者以及希望掌握大型项目工程化实践的学习者。资源包为 zip 格式共 1903 个文件压缩后仅约 9.43MB其中包含 1332 个 .ts 模块、552 个 .tsx 组件文件以及 18 个 .js 辅助文件与 1 份 Markdown 文档目录划分清晰便于按需查阅。目前已有 289 人学习下载。源码内部体现了模块化与面向对象设计的优秀范例核心算法、数据处理逻辑、工具类与辅助函数均配有细致注释复杂功能被拆分为职责单一的小模块并配有系统详尽的测试用例展示了如何通过 Git 管理协作与追踪变更。随包附带的开发者指南、API 文档和用户手册还可帮助读者快速完成部署、定制与二次开发并从中获得软件质量保障、代码复用与团队协作的经验。1. Claude Code 源码是什么能读、能改、能换模型的终端编程助手用了一段时间 Claude Code 的人迟早会撞上同一个困惑它每次执行命令前都要问一遍「是否允许」我想让它按我的规则批量跑官方的设置项却不够细又或者我刚在文档里看到某个行为说明真机验证却发现对不上。这时候最直接的解法不是翻文档而是去读 Claude Code 源码。官方早已把源码公开主线就是那个 npm 包anthropic-ai/claude-code的实现。读它你能搞清楚三件事agent 循环怎么转、工具调用链怎么接、权限拦截在哪一步。搞懂这三件事你就能改它的行为接第三方模型甚至把它拆成自己项目里的自动化引擎。这篇文章适合两类人被内置权限模型卡住、想深度定制的从业者以及想搞明白终端 AI 编程助手内部机制、准备二次开发的人。2. 源码仓库结构与入口从 cli.js 到 agent 循环的完整脉络2.1 入口三件套cli 解析、harness 运行时与 agent 会话拿到源码先别急着搜「agent」关键词先把入口理清。Claude Code 的 CLI 实现大体分三层最外层是 cli 入口负责解析命令行参数、读环境变量、加载配置文件中间一层是 harness这是整个运行时的心脏管工具注册、权限判定、消息组装最里层是 agent管跟模型 API 的会话轮转。我见过不少人一上来就翻 tools 目录看完还是一头雾水就是因为没先建立「入口 → 运行时 → 会话」这条主线。cli 入口做的事情非常朴素把你敲的命令行参数映射成配置对象。比如--permission-mode会映射到权限模式--model会映射到模型名-p或--print会切到非交互输出。这些参数解析完才轮到 harness 出场。harness 拿到配置后的第一件事是构建 system prompt把工具列表、工作目录、权限规则全部拼进去然后进入主循环。agent 层则是最容易被误读的部分。很多人以为 agent 就是「调用 API 的那个函数」实际上 agent 更像一个状态机负责维护 messages 数组、判断什么时候把 assistant 消息追加回去、什么时候把 tool_result 回填。源码里这三层职责分得很清cli 不碰业务逻辑harness 不直接发 HTTP 请求agent 不做权限判断。如果你要在源码里动手脚先确定你要改的是哪一层否则很容易改错文件后找不到生效点。2.2 tools 注册表源码里最值钱的一张表工具系统的设计决定了 Claude Code 扩展性的上限。源码里每个工具都是一个独立模块导出固定的四个字段name、description、input_schema、execute。name 是工具名模型通过它来引用description 是给模型的说明书告诉它这个工具什么时候该用input_schema 是 JSON Schema约束参数格式execute 是实际执行函数。harness 启动时会把所有工具扫一遍注册成一个映射表模型请求里的tool_use就是靠这个名字查表找到执行函数的。常见的内置工具大致覆盖这些领域工具类别典型工具用途文件操作Read / Write / Edit读取、覆盖写入、按行编辑文件命令执行Bash在用户机器上执行终端命令代码搜索Glob / Grep按模式找文件、按正则搜内容网络获取WebFetch / WebSearch抓取网页、搜索信息任务分发Task把子任务派给新的 agent 会话看这张表你就明白为什么 Claude Code 在仓库类任务上比纯对话强它不是只会聊天而是把文件系统、命令行、网络全暴露成了模型可调用的函数。你在热词里看到的「Claude Code 如何直接执行终端命令」答案就在 Bash 这个工具的 execute 实现里它用子进程拉起 shell再把 stdout、stderr 和退出码按固定结构返回给模型。理解了这张注册表你就理解了所有终端 AI 编程助手的基本盘。2.3 配置、鉴权与在线升级API key 在源码里怎么流动配置和鉴权是二次开发时绕不开的两个点。Claude Code 的配置分三层用户级、项目级、本地覆盖。用户级配置在~/.claude/settings.json项目级在.claude/settings.json本地覆盖在.claude/settings.local.json。加载顺序是后者覆盖前者这个层级关系在源码里有明确的合并逻辑改配置不生效的坑大多出在这里——你改的是本地覆盖文件但项目级文件里有个同名配置把它压回去了。API key 的流动路径值得单独画一条线优先读环境变量ANTHROPIC_API_KEY读不到就查本机凭证文件再不行才走登录流程。这里有个容易被忽略的细节key 一旦进了环境变量整个进程生命周期内所有请求都会带上它所以你在源码里做请求日志时千万别把 header 完整打出来否则一次 verbose 输出就能把你的 key 泄露到终端历史里。在线升级机制也住在源码里。CLI 启动时会有一次版本检查发现远端版本比本地新就下载替换。这个机制对普通用户很友好但对改源码的人是个隐患——你本地明明改好了下次启动它自动给你换回发布版。后面我会专门讲怎么关掉它。3. 本地跑通 Claude Code 源码最小构建命令与三个启动参数3.1 前置条件与两种拿源码的方式本地跑源码之前先把前置条件列清楚。Node.js 版本要够新建议 18 及以上太低会在构建阶段栽在语法解析上。另外需要本机能正常访问 npm registry因为构建过程要拉一堆依赖包。操作系统不限Windows 上用 Git Bash 或 WSL 都行但后续调试体验 WSL 更顺。拿源码有两条路取决于你想干什么。如果只是想用最新功能直接npm install -g anthropic-ai/claude-code装发布版就行不用碰源码如果你要改代码、调试、加自定义工具那就必须从官方仓库 clone 下来自己构建。区别在于发布版是编译打包后的产物代码被压缩过改起来非常痛苦源码仓库保留完整目录结构和可读的模块文件改完重新构建就能生效。我一般建议后者因为读源码的价值恰恰在「能改」只读发布版那堆压缩代码还不如直接看反编译结果。3.2 从 zero 构建到可执行clone、install、build 一步到底拿到源码后的第一步是装依赖。这个项目依赖不少网络状况不好的时候会等很久属于正常现象。依赖装完再构建构建脚本会把源码里的模块打包成可执行产物。完整的步骤如下git clone 官方仓库地址 claude-code-src cd claude-code-src npm install npm run build node bin/cli.js --helpgit clone把仓库拉到本地地址填官方仓库就行分支建议用默认主线。npm install安装全部依赖这一步如果报错多半是 Node 版本太低或网络问题先解决前置条件再重试。npm run build执行构建产物会输出到指定目录这一步成功说明依赖树没问题。最后用node bin/cli.js --help验证构建结果——能打出帮助信息就说明整条链路通了。这里有个重点构建完一定要用node直接指向仓库里的入口文件来跑而不是敲claude命令。因为你机器上可能已经装了发布版claude命令指向的是全局安装目录跟本地构建产物不是同一个东西。用node bin/cli.js才能确保跑的是你刚构建出来的源码版本。3.3 用源码模式跑通一次对话三条命令与参数逐项说明构建跑通之后先试一次最小对话。我习惯用--print参数做非交互测试因为它不需要启动终端 UI适合自动化和调试。最小命令长这样echo 列出当前目录下所有 js 文件 | node bin/cli.js --print --max-turns 3echo管道把任务文本塞进标准输入CLI 读到输入后进入非交互模式执行完直接退出不会等你敲下一个问题。--print是核心参数等价于命令里的-p它的作用是关闭交互界面、把最终答案直接打到标准输出。--max-turns 3限制对话轮数避免模型陷入无限循环——这在调试时非常有用能防止一个失控的 agent 把你的 shell 搞乱。调试源码时我还会加一个参数echo 解释一下这个仓库的目录结构 | node bin/cli.js --print --verbose --max-turns 5--verbose会把每次请求的模型名、token 数、工具调用记录打到日志里。改源码后跑这条命令你能直接看到请求体里是否带上了你的改动。记住一个调试心法先不加 verbose 跑通功能再加 verbose 看细节。不然第一次就跑 verbose输出会淹没在日志海洋里反而看不清问题在哪。4. 拆解核心机制Agent 主循环、工具调用链与上下文压缩4.1 主循环一次对话在源码里是怎么转起来的把 Claude Code 比作黑匣子那主循环就是这个黑匣子里最值钱的一根轴线。整个 agent 逻辑可以压缩成一小段伪代码messages build_initial_messages(task, system_prompt) for turn in range(max_turns): response call_model(messages) messages.append(response) if response.is_done(): break tool_name extract_tool_use(response) tool_args extract_tool_args(response) if tool_name: result tools_registry[tool_name].execute(tool_args) messages.append(build_tool_result(result)) else: break每次迭代做四件事把当前所有消息发给模型拿到回复后追加进消息数组判断回复里有没有工具调用意图有就查注册表执行对应工具把执行结果以tool_result身份追加回去开启下一轮。这个循环的退出条件有两个模型回复里没有工具调用了或者轮次达到max_turns上限。这个设计里最值得学的一点是模型自身不会执行任何操作它只是输出「我想调用 Read 工具参数是/etc/hosts」这样的结构化内容真正动文件系统的是注册表里的 execute 函数。这也解释了为什么「直接执行终端命令」在源码里如此谨慎——模型只有调用权没有执行权执行权牢牢攥在工具模块手里。你在改源码时只要守住这条边界就能在能力开放和安全性之间找到平衡点。4.2 权限模型哪些命令能自动执行哪些必须问权限模型是 Claude Code 源码里改动需求最密集的地方也是新手最容易改崩的地方。它的核心设计是分级放行default模式每个工具执行前都要问用户acceptEdits模式放行文件编辑类操作bypassPermissions模式全部放行plan模式只允许只读操作。除了模式还有 allowlist 和 denylist 两套规则分别用于精确放行和精确拦截。权限判定发生在工具执行之前也就是说即使模型已经输出了 tool_use也要过一道闸门才能真正执行。源码里有一套独立的规则匹配逻辑先查 denylist命中直接拒绝再查 allowlist命中直接放行都查不到就按当前权限模式的默认策略处理。这个顺序很关键你改配置时如果发现规则不生效八成是 denylist 里有个条目把 allowlist 覆盖了。改权限源码时最常见的翻车是把 bypassPermissions 当成万能钥匙全局一开模型跑什么都自动执行。这在本地个人目录里问题不大但放到 CI 环境里就是灾难——一条rm -rf的命令在自动放行下会直接执行。我见过不止一次这种事故后来养成的习惯是只对特定命令开自动放行比如git status、npm test用 allowlist 精确命中而不是把整个权限系统关掉。4.3 上下文压缩阈值token 逼近上限前源码做了什么跑过几次长任务的人都会发现一个现象对话到后半段模型好像「忘记」了最早的任务描述。这不是玄学是上下文压缩机制在起作用。Claude Code 的会话基于 messages 数组每轮都要把所有历史消息重新发给 APItoken 消耗随轮次线性增长。当总 token 数逼近模型上下文窗口上限时系统会触发压缩。压缩策略在源码里是这么实现的保留最近几轮的关键消息把更早的历史交给模型生成一段摘要用摘要替换掉原来的长消息。这个动作对用户不可见所以你看到的现象是「它好像忘了」实际是原始消息被摘要化了指令细节在摘要过程中丢失了。源码里有个开关配置可以调整压缩行为比如关闭自动压缩、手动触发压缩。调试这个机制有个土办法盯着 verbose 输出里的 token 消耗曲线。如果看到某轮之后 token 数突然掉下去一大截就是压缩被触发了。理解了这一点你就知道长任务应该怎么设计把最核心的指令放在最近的几轮消息里或者分阶段调用别指望模型在 50 轮对话后还记得第一轮里的某个细节。我在源码里加自定义工具时也会刻意让工具把关键状态写回消息流这样压缩时不容易丢。5. 从源码到可用的五个坑构建失效、升级覆盖与第三方模型兼容5.1 构建报错找不到本地包workspace 依赖没装全现象npm install跑完没报错npm run build却报「找不到模块 xxx」而且报错的包名就是你 clone 下来的仓库里的其他目录。原因这个仓库不是单包工程内部模块之间互相引用依赖关系靠 workspace 管理。直接npm install只装了外部依赖内部 workspace 包的软链接没全部建立构建时一引用就炸。解决用npm ci替代npm install或者显式安装 workspace 依赖npm ci npm run buildnpm ci会严格按照 lockfile 重建 node_modules并处理好 workspace 链接比 install 更干净。如果项目里没有 lockfile就先提交一个或在 install 时指定--workspaces。判断是否装全有个快速方法看 node_modules 里有没有指向仓库内部目录的软链接没有就说明 workspace 没建立。5.2 改了源码不生效PATH 里跑的还是发布版现象你改了源码文件重新构建也成功了但运行功能时行为完全没变好像改动被吞了一样。原因你敲的命令是claude它指向的是 npm 全局安装的发布版路径而你改的是本地仓库里的源码。PATH 环境变量里全局安装目录排在前面shell 根本不会去执行你的构建产物。解决运行测试时统一用node bin/cli.js完整路径不要用claude。如果一定要用claude命令名可以用npm link把本地仓库链接到全局但要注意 link 之后可能和已安装的发布版冲突需要先卸载全局包。我的习惯是调试阶段老老实实用完整路径省心。5.3 接入 deepseek/qwen/glm 后工具调用返回格式崩掉现象通过 cc switch 这类切换器把 API 端点换到 deepseek、qwen 或 glm 之后常规对话能跑但模型一调工具就翻车参数序列化错误、类型对不上、或者干脆返回一段文字而不是结构化 tool_use。原因Claude Code 源码里的工具调用链严格按照 Anthropic 消息格式解析。第三方模型的接口虽然兼容但工具调用的 schema 细节不一定一致比如参数嵌套层级不同、required 字段缺失或是在同一次回复里同时给了多个 tool_use。源码解析器拿不到预期结构就直接报错或跳过。解决在工具调用链的入口加一道参数校验和规整层把第三方模型返回的 tool_use 结构转成源码期望的格式。常见的做法是写一个适配函数专门处理参数名映射和类型转换。我的建议是先用--verbose把第三方模型实际返回的 JSON 打出来对照源码里的解析逻辑逐字段查差异比盲改快得多。另一个更省事的路子是只在单轮里允许一个工具调用从根上避开多工具并发导致的解析混乱。5.4 自动批准模式把权限模型打穿命令失控的教训现象开了bypassPermissions模式跑任务模型连续执行了十几条命令其中一条把目录下的构建产物全删了。原因自动放行模式下模型对自己输出的每一条命令都有执行权权限系统形同虚设。模型对工具的理解并不包含「哪些命令后果严重」的价值判断它只按任务目标选择工具和参数。解决用 allowlist 精确放行而不是全局关权限。源码里 allowlist 支持按命令前缀匹配比如只放行git和npm test其余一律询问。我在自动化脚本里跑长任务时会先把任务要用的命令列一遍逐个加进 allowlist跑完之后立刻清空。这个习惯救过我不少次比任何源码改动都管用。5.5 在线升级悄悄覆盖你的本地修改现象昨天还在正常跑的本地分支今天启动时提示版本更新更新完发现你加的自定义工具代码全没了。原因源码里有启动时检查更新的逻辑一旦发现远端版本号更高就会拉取新版本并替换本地产物。如果你直接改的是构建输出目录升级一覆盖就白改了。解决改源码之前先把自动更新关掉在配置文件里禁用更新检查同时把本地构建产物挪出默认安装目录。另一个稳健做法是不要改构建产物而是以「外部工具 注册表扩展」的方式把自定义逻辑挂进 harness这样升级只更新核心部分你的扩展不受影响。除非你要动的是 harness 核心逻辑否则永远优先选扩展而不是改核心。6. 进阶用法把 agent 循环变成你自己的批处理引擎6.1 在 Node 脚本里复用 harness 的两种常见姿势当你啃完了主循环和工具注册表下一步自然是把它集成进自己的项目。最常见的做法是从编译产物里引入 CLI 入口把本来由命令行传入的参数改成在代码里直接构造。我一般保留一个跑批任务的脚本把任务列表写在数组里循环喂给 agentconst { runAgent } require(./build/entry.js); const tasks [ 检查 src 目录下所有 TODO 注释, 为每个 TODO 生成一条 git issue 文本, 输出汇总报告到 report.md ]; for (const task of tasks) { const result await runAgent({ prompt: task, maxTurns: 10, permissionMode: acceptEdits, cwd: process.cwd() }); console.log(result.finalText); }这段代码的关键在runAgent这个导出函数它接收的配置对象和命令行参数一一对应。maxTurns限制每个任务的轮次防止单个任务跑飞permissionMode控制权限模式批处理时一般用acceptEdits而不用全局放行cwd指定工作目录让每个任务在不同的仓库目录里执行。result.finalText拿到的是 agent 最终输出也就是主循环退出时留下的最后一条非工具消息。6.2 改完源码怎么验证三个信号帮你确认改动生效改完源码最怕的就是自我感觉良好实际根本没生效。我有一套三步验证法每次都跑一遍。第一步看 verbose 日志里有没有打到你的改动点比如你改了工具注册表日志里工具列表应该多出或少了一项第二步是故意构造一个触发你改动逻辑的任务确认它走的是新代码分支而不是旧逻辑第三步连续跑三轮对话确认上下文压缩没有把你加的字段洗掉。这套验证法帮我过滤掉了大半「改了但没生效」的无效调试。回头看这段折腾源码的经历最大的教训是别一上来就想着重写核心循环先读、再小改、最后才是重构。工具注册传参这种小改动带来的收益往往比动 agent 循环大得多风险却小一个量级。希望这篇文章能让你在啃 Claude Code 源码时少走几步弯路把终端 AI 助手的控制权真真切切攥到自己手里希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑