Claude Code 源码拆解:从工程结构到 Agent 循环的完整实践
简介:这份资源为网传的 Claude Code 相关源码包,面向希望研究 AI 编程助手内部实现、学习大型前端工程架构的开发者,适合具备一定 TypeScript 与 React 基础的中高级读者参考。压缩包共 1903 个文件,以 1332 个 ts 与 552 个 tsx 为主,辅以 18 个 js 和 1 个 md 说明文档,整体约 9.42MB,代码组织以模块化方式呈现,便于按目录梳理核心逻辑与组件划分。目前已有 393 人学习下载。通过阅读可了解其功能模块划分、数据结构定义、接口调用方式与工程配置思路,对理解同类工具的设计取舍、排查实现细节、借鉴代码组织方式有一定参考价值,也可作为研究 AI 编程工具技术路线的素材。需注意源码版权与许可协议,仅作学习交流使用。
1. 一份被反复转手的 claude code 源码包,到底能拿来干什么
上周有个做后端的朋友甩给我一个压缩包,文件名就叫claude-code-source.zip,解压完 300 多个文件,TypeScript 和 JavaScript 混着放,package.json里依赖列了满满两屏。他问我一句话:这玩意儿据说是 claude code 的源码,能不能跑起来,能不能改。我花了一个周末把它拆开看了一遍,结论是:它不是一个能双击运行的成品,而是一份值得逐层读的工程样本——尤其适合想搞清楚「一个终端里的 AI 编程助手,从敲下回车到命令真正执行,中间到底发生了什么」的人。如果你只是想找个开箱即用的工具,这份源码会让你失望;但如果你想看一个真实 CLI Agent 的骨架长什么样,它比大多数教程都实在。下面我按「它是什么 → 怎么把它跑起来 → 怎么读它的关键模块 → 坑在哪」的顺序,把这次拆包过程完整写出来。
2. 先搞清楚这份源码的工程结构:它为什么不是一个能直接双击运行的项目
拿到一份来源不明的源码,第一步永远不是npm install,而是先看目录。我见过太多人上来就装依赖,结果卡在某个 native 模块编译上,最后连项目是干嘛的都没弄明白。这份包的结构其实挺典型,属于「CLI 工具 + 本地服务 + 模型调用层」三段式,理解了这个分层,后面读代码会顺很多。
2.1 目录分层与入口文件定位
解压后先看根目录,核心就几个东西:package.json、tsconfig.json、src/、dist/(有些包没有)、bin/。bin/里通常放的是可执行入口,package.json的bin字段会指向它。我一般先用两条命令把入口和构建脚本摸清楚:
# 看 package.json 里的入口、脚本和依赖类型 cat package.json | python3 -m json.tool | head -60 # 找 bin 字段指向的可执行文件 grep -A 5 '"bin"' package.json逻辑说明:第一条命令把package.json格式化后看前 60 行,重点看main、bin、scripts、dependencies四块。第二条专门抓bin字段,因为 CLI 工具的真正入口往往不在main而在bin。参数上没什么可调的,head -60只是防止输出太长刷屏,你想看全就把这个去掉。
看完你会发现,scripts里一般有build、dev、start三个。build多半是tsc或esbuild,dev可能是tsx watch之类。这里有个关键判断:如果dependencies里出现了@anthropic-ai/sdk或者类似的模型 SDK,说明它确实是要连真实模型服务的;如果只有axios、node-fetch这种通用 HTTP 库,那它可能是自己封了一层请求。
2.2 依赖树里藏着的技术选型信息
第二步看依赖,这一步比看目录更能说明问题。我习惯把依赖按用途分类,而不是一股脑看版本号:
# 列出直接依赖,按字母排序,方便分类 npm ls --depth=0 2>/dev/null || cat package.json | python3 -c " import json,sys d=json.load(sys.stdin) for k in sorted(d.get('dependencies',{})): print(k, d['dependencies'][k]) "逻辑说明:优先用npm ls --depth=0,但它要求你已经装过依赖;没装过就用后面那段 Python 直接解析package.json。输出的是「包名 + 版本范围」,重点不是版本号,而是包名本身透露的信息。
我在这份包里看到的典型组合是:commander或yargs做命令行解析,chalk或picocolors做终端着色,inquirer做交互式提问,execa或child_process做子进程调用,再加一个模型 SDK。这个组合基本就定义了一个 CLI Agent 的形态——它要在终端里跟你对话,要能执行 shell 命令,要能读写文件。看到execa和child_process同时出现,你就该意识到:这份代码里有「执行任意命令」的能力,读的时候要格外注意它的权限边界在哪。
2.3 从 package.json 的 scripts 反推运行方式
很多人卡在「装完依赖不知道怎么启动」。其实答案就在scripts里。常见的三种情况:
| scripts 字段 | 含义 | 启动方式 |
|---|---|---|
"dev": "tsx src/index.ts" | 直接跑 TS 源码 | npm run dev |
"build": "tsc"+"start": "node dist/index.js" | 先编译再跑 | npm run build && npm start |
"bin"指向dist/cli.js | 全局命令 | npm link后直接用命令名 |
如果是第三种,npm link会在全局建一个软链,之后你在任何目录敲那个命令名都能触发。这一步是很多人忽略的——他们npm install完发现没有可执行命令,就是因为没做npm link。注意npm link需要写权限,Linux 和 macOS 上如果报权限错误,别急着sudo,先检查 npm 的全局前缀是不是配到了用户目录下。
3. 把源码跑起来:环境、模型接入与第一次对话的完整链路
结构看明白之后,才是真正动手。这一章的目标很明确:让这份源码在你机器上跑出第一次响应。我踩过的坑基本都集中在这一步,所以会把每个环节的参数和失败现象都写清楚。
3.1 Node 版本、包管理器与构建工具链的准备
先确认 Node 版本。这类项目通常要求 Node 18 以上,因为用到了原生fetch和较新的 ESM 特性。检查命令很简单:
node -v npm -v # 如果项目用了 pnpm 或 yarn,看 lock 文件 ls | grep -E "pnpm-lock|yarn.lock|package-lock"逻辑说明:node -v低于 18 的话,很多依赖会直接报SyntaxError: Unexpected token。lock 文件决定你该用哪个包管理器——有pnpm-lock.yaml就用pnpm install,有yarn.lock就用yarn,别混用,混用会导致依赖树和 lock 不一致,出现「别人能跑我不能跑」的玄学问题。
装依赖时如果卡在某个 native 模块(比如node-pty、better-sqlite3这类需要编译的),常见原因是缺少构建工具链。Ubuntu 上一般是build-essential和python3,macOS 上是 Xcode Command Line Tools。这一步没有捷径,报错信息里通常会直接告诉你缺什么。
3.2 模型服务接入:base_url、api_key 与模型名的三个必填项
这是整份源码能不能「活」过来的关键。CLI Agent 本身只是个壳,真正干活的是背后的模型服务。源码里一般会有一个配置读取逻辑,常见的是读环境变量或读一个配置文件。我一般先全局搜一下配置相关的关键词:
# 搜配置读取的位置 grep -rn "API_KEY\|BASE_URL\|baseURL\|apiKey" src/ --include="*.ts" | head -20逻辑说明:这条命令在src/下递归搜常见的配置字段名,--include="*.ts"限定只搜 TS 文件,head -20防止输出过多。你会看到它到底读的是哪个环境变量名,是ANTHROPIC_API_KEY还是自定义的CC_API_KEY,这决定了你后面怎么设。
找到之后,配置通常是三个值:服务地址(base_url)、密钥(api_key)、模型名(model)。我一般会写一个.env文件,而不是每次在命令行 export:
# .env 示例,字段名以源码实际读取的为准 API_BASE_URL=https://your-endpoint.example.com API_KEY=sk-xxxxxxxx MODEL_NAME=your-model-name逻辑说明:字段名必须和源码里process.env.XXX读的完全一致,大小写敏感。API_BASE_URL末尾不要多加斜杠,很多 HTTP 客户端拼接路径时会因此产生双斜杠,导致 404。MODEL_NAME要填服务端实际支持的模型标识,填错了通常报的是「model not found」而不是「unauthorized」,这两个错误要分清楚。
提示:如果源码里读的是
ANTHROPIC_API_KEY这类固定名字,而你想接的是别的兼容服务,最稳妥的做法是改源码里的读取逻辑,而不是硬凑环境变量名,否则后面排查会很乱。
3.3 第一次运行:从命令入口到收到响应的排查路径
配置好之后启动。如果是npm run dev,观察终端输出。正常的话会看到它打印一个欢迎信息或者直接进入交互提示符。如果卡住不动,按这个顺序排查:
第一步,确认进程有没有真的起来。另开一个终端ps aux | grep node,看有没有对应进程。没有的话说明启动脚本本身就失败了,回头看报错。
第二步,确认网络请求有没有发出去。最直接的办法是在源码里请求的那一行前后加console.log,或者用DEBUG=* npm run dev打开调试日志(如果它用了debug库)。
第三步,看返回的错误码。401 是密钥问题,403 可能是服务端权限或地区限制,404 多半是 base_url 或路径拼错,429 是限流。这几个码对应的方向完全不同,别一看到报错就改密钥。
# 打开调试日志重跑,观察请求链路 DEBUG=* npm run dev 2>&1 | head -50逻辑说明:DEBUG=*会打开所有使用debug库的模块日志,2>&1把 stderr 合并到 stdout,head -50只看前 50 行。如果你在日志里看到了完整的请求 URL 和响应状态,基本就能定位到是哪一环断了。注意有些项目不用debug库,那这条命令就没输出,得回到加console.log的老办法。
4. 读源码的重点:Agent 循环、工具调用与命令执行这三块怎么啃
跑起来只是第一步,这份源码真正的价值在于它的架构。我读这类 CLI Agent 源码,永远优先看三个地方:主循环、工具注册、命令执行。这三块看懂了,整个项目就没有黑匣子了。
4.1 Agent 主循环:消息数组是怎么被反复喂给模型的
几乎所有 Agent 的核心都是一个while循环:把对话历史发给模型,模型返回要么是纯文本(结束),要么是工具调用请求(继续)。找到这个循环,就找到了心脏。搜索关键词一般是while、messages、tool_calls、stop_reason:
grep -rn "while\|tool_calls\|stop_reason\|finish_reason" src/ --include="*.ts" | head -30逻辑说明:stop_reason和finish_reason是判断模型「说完了」还是「要调工具」的关键字段,不同 SDK 叫法不同。搜到之后重点看循环的退出条件——是stop_reason === 'end_turn'还是别的。这个条件写错,就会出现「模型一直循环调用同一个工具」的死循环,这是最常见的翻车点之一。
读的时候注意消息数组是怎么增长的。每轮工具调用的结果都要作为一条新消息追加回去,格式必须符合模型服务的要求。如果追加格式错了,模型下一轮会「看不见」工具结果,表现为反复问同一个问题。
4.2 工具注册机制:一个工具从定义到被模型调用要经过几层
工具(tool)是 Agent 的手脚。源码里通常会有一个工具注册表,每个工具包含名称、描述、参数 schema 和执行函数。我一般会先列出所有已注册的工具:
# 找工具定义和注册的位置 grep -rn "name:.*description:\|input_schema\|parameters:" src/ --include="*.ts" | head -40逻辑说明:input_schema或parameters是工具参数的 JSON Schema 定义,模型就是靠这个知道该怎么传参的。搜出来之后,你会看到每个工具的描述文本——这段文本直接决定模型会不会在合适的时机调用它。描述写得含糊,模型就乱调或者不调,这是 prompt 工程在代码层的体现。
重点看两件事:一是工具的 schema 定义是否严格(有没有required字段),二是执行函数有没有做参数校验。我见过 schema 写得很松、执行函数又直接拿参数拼命令的代码,这种一旦模型传了奇怪的值,就是安全事故。
4.3 命令执行与文件读写:权限边界到底卡在哪一层
这是最需要警惕的部分。CLI Agent 要能执行 shell 命令、读写文件,就意味着它有相当高的系统权限。源码里执行命令一般走child_process.exec或execa。搜出来看它有没有做白名单或确认机制:
grep -rn "exec\|spawn\|execa\|writeFile\|readFile" src/ --include="*.ts" | head -40逻辑说明:这条命令把所有涉及进程执行和文件操作的地方列出来。重点看exec的调用点——参数是拼接的字符串还是数组。拼接字符串(exec(\ls ${dir}`))有命令注入风险,数组形式(execFile('ls', [dir])`)安全得多。文件写入同理,看它有没有限制写入路径,还是模型给什么路径就写什么路径。
注意:如果这份源码在执行命令前没有任何确认提示,直接就把模型生成的命令跑掉了,那你在自己机器上试的时候一定要放在隔离环境里,别拿主力工作目录冒险。
5. 避坑与常见问题:拆这类来源不明源码时我踩过的五个坑
这一章全是血泪经验。来源不明的源码包,坑往往不在代码逻辑本身,而在环境、依赖和预期管理上。下面五条是我实际遇到过的,每条都按「现象 → 原因 → 解决」写。
5.1 现象:npm install 报一堆 peer dependency 冲突
原因:这份包的依赖版本范围写得比较旧,或者作者本地用的是特定版本的包管理器,lock 文件没一起打包进来。npm 7 以后对 peer dependency 检查变严,就容易报ERESOLVE。
解决:先别急着--force。正确顺序是先看报错里点名的是哪几个包,然后尝试npm install --legacy-peer-deps。如果还不行,换pnpm install,pnpm 对 peer 的处理更宽松。--force是最后手段,因为它会忽略所有冲突,装出来的依赖树可能是坏的。
5.2 现象:编译通过但运行时报「Cannot find module」
原因:TypeScript 项目里tsconfig.json的module和moduleResolution配置跟 Node 版本不匹配,或者package.json里"type": "module"和 CommonJS 的require混用。
解决:先看package.json有没有"type": "module"。有的话,所有.js文件都按 ESM 解析,require会报错,得改成import。再看tsconfig.json的moduleResolution,Node 18 以上建议用"node16"或"bundler"。改完记得删掉dist/重新编译,缓存有时候会骗人。
5.3 现象:模型有响应但工具永远不被调用
原因:工具 schema 定义和模型服务要求的格式不一致。比如模型服务要的是input_schema,代码里写的是parameters,或者type字段的值不对。
解决:把工具定义打印出来,跟模型服务的官方文档逐字段比对。最容易被忽略的是required数组——如果参数没标 required,模型可能一个都不传。另外工具描述里如果没写清楚「什么时候用」,模型也会犹豫。我一般会在描述里加一句明确的触发场景。
5.4 现象:执行命令时终端卡死,没有任何输出
原因:子进程的输出没有被正确消费。child_process的 stdout 有缓冲区上限,如果命令输出量大又不读,缓冲区满了进程就挂住。
解决:检查执行命令的地方有没有监听stdout的data事件,或者用execa时有没有设stdio: 'inherit'。如果是长时间运行的命令,还要考虑超时设置,否则一个tail -f就能把整个 Agent 卡死。
5.5 现象:改了源码但运行结果没变化
原因:跑的是dist/里的编译产物,不是src/里的源码。或者有 watch 进程在后台没重启。
解决:确认启动命令指向的是src还是dist。如果是dist,每次改完要重新npm run build。另外检查有没有残留的node进程,pkill -f node清一下再重启,避免旧进程占着端口或文件句柄。
6. 进阶玩法:把这份源码当成模板,改出一个自己的终端 Agent
跑通、读懂之后,最有价值的用法不是用它本身,而是把它当脚手架,改出一个符合自己需求的终端 Agent。我一般会从三个地方下手,改动量小但效果明显。
第一个改动点是工具集。原版工具通常覆盖了文件读写和命令执行,但你可以加自己的工具。加一个工具只需要三步:定义 schema、写执行函数、注册到工具表。比如加一个「查当前 git 分支」的工具,schema 里不需要任何参数,执行函数跑git rev-parse --abbrev-ref HEAD返回结果。这种无参数工具最容易验证整条链路通不通。
第二个改动点是模型接入层。如果源码把模型调用写死在某个文件里,把它抽成一个独立的 provider 模块,支持通过配置切换不同的服务地址和模型名。这样你就能在不改业务逻辑的前提下换后端。抽的时候注意把「请求构造」和「响应解析」分开,因为不同服务的响应结构差异往往比请求结构更大。
第三个改动点是权限确认。原版如果没有确认机制,加一个简单的拦截:在执行命令和写文件之前,把要执行的内容打印出来,等用户输入y再继续。实现上就是在执行函数前加一个await confirm(),用readline读一行输入即可。这个改动不大,但能让你在试错时睡得踏实。
验证改动是否成功,我有一套固定流程:先跑一个纯文本对话确认模型链路通,再跑一个无参数工具确认工具调用链路通,最后跑一个带参数工具确认 schema 解析没问题。三步都过,基本就稳了。从那以后我每次拆这类源码,都强制先走一遍「纯文本 → 无参工具 → 带参工具」这条验证链,能省掉大量瞎猜的时间。希望帮到你。
本文还有配套的精品资源,点击获取