ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Claude Code源码深度拆解:从CLI入口到Agent循环的工程实践

Claude Code源码深度拆解:从CLI入口到Agent循环的工程实践

简介:Claude Code 的完整源码以 zip 压缩包形式提供,适合希望深入理解 AI 编程助手实现原理的开发者、研究者以及正在进阶 TypeScript 工程化的学员参考。资源共包含 1903 个文件,主体为 1332 个 ts 文件与 552 个 tsx 文件,另有少量 js 脚本和 md 文档,压缩后体积约 9.43MB,便于快速下载和本地阅读。源码在结构上严格遵循模块化与面向对象设计,核心算法、数据处理逻辑与工具函数拆分清晰,并通过大量注释、API 文档和测试用例保证可读性与可维护性。与此同时,项目依托 Git 管理代码变更,开发者指南和用户手册等材料也为后续二次开发提供了明确指引。目前已有 289 人学习下载,适合作为学习大型开源项目架构、代码组织方式和工程实践的学习样本。

1. Claude Code源码:这不是又一个AI壳子,而是一套端到端的coding agent工程

先说个反直觉的结论:Claude Code在终端里跑起来只是命令行工具,但真正拉开差距的是它那套渗透到源码层面的代号为“harness”的调度机制。很多人以为装个npm包就要排队等Claude的API,实际用下来你会发现,它能听懂你在编辑器里选中了哪一段代码,能在你翻车的时候自动做unit test循环修复,这才是源码级工程该有的样子。

这份源码包适合谁?两类人最赚:一是被各种GUI套壳工具折磨过的前端和全栈开发者,想从命令行拿到上下文感知的AI辅助;二是想研究“agent循环”——也就是让模型自己感知、计划、执行、验证的工程师。你拿到的不只是代码,而是一整套能跑起来的agent框架。我的建议是别当普通工具安装,直接去读它的entry point和bash executor相关源码,比翻十篇博客都涨功力。

以下内容来自我对这套源码实拆后的记录,从架构到部署、从参数到避坑,全部是实操踩过的路径。项目本体是基于TypeScript的harness实现,下载源码包后按下面的步骤走,十分钟内能跑起来,之后慢慢拆。

2. 拆源码先看骨架:从CLI入口到agent循环的调用链

2.1 顶层目录:一眼认出这条链路里谁在干什么

Claude Code的源码工程解压后,第一件事不是跑npm install,而是先把目录结构摸了。它的核心分四块:

  • cli/:面向用户的所有命令入口,也就是你在终端里输入的claude命令对应的逻辑。
  • harness/:agent循环的核心,负责把模型输出转成行动。包括plan、exec、observation三个环节的衔接。
  • integrations/:和外部工具通信的适配层,比如编辑器插件(VSCode)、Git、终端命令执行。
  • tools/:模型允许调用的工具集合,比如读文件、写文件、执行bash命令、搜索代码。

还有一个容易被忽略的bash-server/目录,这是专门在隔离环境里执行shell命令用的,项目通过它规避注入风险。这个设计直接关系到后面避坑章节里“命令执行权限”的问题,先留个印象。

2.2 入口文件拆解:命令行参数是怎么变成agent动作的

找到cli/entrypoint.js(部分版本里叫cli/entrypoint.ts),这是整个程序的起点。它不是简单读取参数然后调函数,而是用了一个类事件循环的机制。核心逻辑伪码如下:

// 简化后的入口逻辑 async function main() { const options = parseCommandLineArgs(); const conversation = new Conversation(options); // 会话管理,保留上下文 const runtime = new HarnessRuntime({ model: options.model, maxTurns: options.maxTurns }); // 首次用户输入进来后,进入agent循环 const userTask = options.prompt || await readStdin(); const result = await runtime.run(userTask, { cwd: options.cwd, allowedTools: options.allowedTools, onStepComplete: (step) => printStep(step, options.verbose), }); if (options.outputFormat === 'json') { console.log(JSON.stringify(result, null, 2)); } }

这里有几个参数要说明:

  • allowedTools决定模型能调用哪些工具,默认是全量允许,但生产环境一般要白名单收敛。
  • maxTurns是agent循环的最大轮数,默认50,我一般会调到80,因为复杂任务经常要“尝试-失败-再试”好几轮。
  • onStepComplete是回调钩子,你可以在这里做日志、状态上报、甚至做自定义的门禁校验。

这个入口的设计思路是:把命令行参数解析和真正的agent逻辑解耦。参数只是一堆配置,真正打架的活全在HarnessRuntime里,也就是下一节要拆的循环。

2.3 HarnessRuntime调度细节:工具调用的“解释-执行-验证”三步

真正的核心在harness/runtime.ts,这里实现了agent循环。简单来说它做三件事:把模型返回的文本解释成结构化的工具调用;执行调用;把结果反馈给模型。循环直到模型认为任务完成或触发最大轮数为止。

我摘一段核心的循环代码,这是理解全源码的关键:

async run(userTask: string, ctx: RuntimeContext): Promise<RunResult> { let messages: Message[] = [{ role: 'user', content: userTask }]; for (let turn = 0; turn < ctx.maxTurns; turn++) { const modelResponse = await this.model.complete(messages, { tools: ctx.allowedTools, // 关键参数:强制模型思考后再行动,能大幅减少无效工具调用 thinking: { budget: 2000 }, }); messages.push(modelResponse.message); if (modelResponse.stopReason === 'end_turn') { return { status: 'done', messages }; } // 解析模型输出中的工具调用意图 const toolUse = parseToolUse(modelResponse.text); if (!toolUse) { // 模型回答了但没有调用工具,判定为无效回复,给它一次纠错机会 messages.push({ role: 'assistant', content: `你的回复没有触发工具调用,请重新规划并选择一个工具。`, }); continue; } const toolResult = await this.executeTool(toolUse, ctx); messages.push({ role: 'user', content: toolResultContent(toolResult) }); } return { status: 'max_turns_exceeded', messages }; }

这里值得在源码里重点标注的是thinking.budget这个参数。Claude Code能“想清楚再动手”靠的是让模型在内部生成综合性的中间思维,但如果预算给得太大,每一轮耗时会明显拉长;给得太小,复杂任务容易开局就翻车。经验值是2000到4000之间。

executeTool内部会根据工具名走不同分支:bash类工具会送进bash-server独立进程执行,文件工具直接在项目进程内操作。这个隔离设计是安全底线,源码改了这里就等于改了安全模型,不建议动。

3. 把源码跑起来:从零到能独立改代码的最小可运行路径

3.1 环境准备与依赖安装

源码是TypeScript工程,跑起来需要Node.js环境。我这边测试用的是Node 20,npm 10。版本太老的Node会在编译依赖时直接报错,建议线上跑之前先node -v确认。

# 1. 安装依赖(最好用pnpm,npm有时候会卡在postinstall脚本上) pnpm install # 2. 构建所有包(cli、harness、integrations一次全编译) pnpm build # 3. 把命令行工具软链到全局,方便直接跑 npm link

注意第一行如果报权限错误,检查npm源是不是被修改过;企业内部源经常缺包,切回官方源再试一次。pnpm build在这份源码里不会只编译一个包,而是按依赖顺序构建全部子包,所以输出比较长,看到多个built提示就是成功。

3.2 认证配置:拿到会话密钥并设置模型参数

Claude Code依赖Anthropic的API,但这里的认证方式不是直接把API Key写在配置里,而是走一个登录会话机制。跑claude命令会提示你去浏览器里完成授权,完成后密钥会存在本地的配置目录里。

# 初始化配置,按提示完成登录 claude # 如果你想指定模型和开关,用下面的参数组合 claude --model claude-3-7-sonnet-latest --max-turns 80

--model参数要注意:不同版本源码支持的模型标识符不完全一样,如果跑--help看不到你想要的模型,去model-registry相关源码里查一份常量列表,别凭记忆猜。--max-turns上面提过,控制单次对话最大循环轮数,默认50对大多数任务够用,但你让它一口气修20个编译错误的话,50轮确实会不够。

还有一个非交互模式,适合在CI里跑:

# 非交互模式,直接给任务,输出结果后退出 claude -p "读一下 src/main.ts,找出所有未处理的 Promise rejection 并修复" --output-format json

这个模式配合--output-format json非常实用,你可以在CI脚本里拿到它修改过的文件列表和状态。JSON结构里messages就是完整对话记录,fileEdits是实际改动文件的路径集合,自己写校验脚本时直接用这两个字段。

3.3 自定义工具扩展:加一个“获取PR变更列表”的工具

源码的价值在扩展性。官方没提供“Github PR信息拉取”能力,但通过写一个工具函数并注册进去,模型就能学会用它。工具注册的代码在tools/registry.ts里有明确的位置,直接加进数组即可。

// 在 tools/registry.ts 里追加这个工具 const githubTools = [ { name: 'get_pr_changes', description: '获取指定PR的变更文件列表和每处变更的diff', input_schema: { type: 'object', properties: { prNumber: { type: 'number', description: 'PR编号,比如 342' }, repo: { type: 'string', description: '仓库名,如 owner/repo' }, }, required: ['prNumber', 'repo'], }, async execute({ prNumber, repo }: { prNumber: number; repo: string }) { const response = await fetch(`https://api.github.com/repos/${repo}/pulls/${prNumber}/files`, { headers: { Accept: 'application/vnd.github.v3.diff' }, }); return await response.text(); }, }, ];

注册以后,模型在收到“分析PR #342 的改动影响”这类请求时,就会自主选择调用这个工具。input_schema写不好会导致模型频繁传错参数——两个最典型的错误是required数组里漏字段、或者字段描述太模糊。描述要具体到“仓库名,格式如 owner/repo,不要带 git@ 前缀”这种程度,模型才会正确生成参数。

这种改造方式不用动harness主循环,只要工具上报的返回内容足够简单直接,模型理解成本低,成功率就上去了。

4. 四个必踩的坑:配置、依赖和误判,哪个都能卡你半小时

4.1 模型显示“Not Available in Your Country”

现象:第一次跑claude登录完成后,控制台直接打印一行note: claude code might not be available in your country. check supported countries,然后退出。

原因:源码里内置了一组受支持的国家或地区代码,判断依据是账户资料里的地区设置。这一步在网络正常状态下也会被触发,确实是账户归属地判断,不是终端环境能绕过的。

解决:确认账户归属地。如果你长期使用不支持地区的账户,看下onboarding相关源码,里面对supportedCountries有明确的常量定义。检查你当前用来登录的账户是否匹配。和网络环境本身没有关系,不要试图通过修改网络出口来解决,换个账户或修改账户地区设置是唯一通道。

4.2pnpm install卡在 postinstall 不动

现象:依赖安装时长时间停在@anthropic-ai/claude-code: postinstall或esbuild相关脚本。

原因:这份源码里 esbuild 的二进制包需要在安装时下载对应平台的二进制文件,如果当前 npm 源不包含该包,postinstall 脚本就会一直重试。国内开发者常遇到的另一个情况是环境变量里配置了不兼容的代理,导致下载请求被拦。

解决:先清掉代理环境变量再装:

unset HTTP_PROXY HTTPS_PROXY ALL_PROXY pnpm install --no-frozen-lockfile

注意不要开任何系统级代理工具,让 esbuild 直接走默认通道下载,成功率最高。如果换源,务必选一个 esbuild 二进制完整的镜像源,否则即使安装成功,启动时也会报esbuild: command not found。

4.3 修改代码后pnpm build不重新编译

现象:你改了cli/下的某个源文件,重新跑claude命令,发现改动不生效。

原因:npm link软链到的目录不是你当前源码目录,而是官方包的全局安装目录。

解决:用pnpm build之后直接跑源码目录的入口文件,不要依赖软链:

node cli/entrypoint.js --model claude-3-7-sonnet-latest

如果你想继续用claude命令,必须确认软链指向:

which claude ls -l $(which claude) # 看指向是否是你当前工作目录

软链指向错了就重新npm link,指向对了但还是旧代码,那就是 build 输出目录缓存问题,执行rm -rf dist && pnpm build强制全量构建。

4.4 工具调用陷入死循环:同一段代码反复改

现象:让Claude Code修复一个lint错误,结果它改一个文件,跑一次lint,报错,再改,再跑。有时候同一个位置改了七八次都没法收敛。

原因:这类问题大概率是模型在“修复”和“验证”之间缺少足够的信息传递——它看不到lint的完整输出,或者工具返回里只有摘要,没有具体行列号。循环结构本身没问题,问题在信息密度。

解决:检查工具返回内容是否被截断。在harness/runtime.ts里找toolResultContent这个函数,看它对长输出的截断策略。如果你被截断到只有前500字符,模型当然只能盲猜。把截断阈值调高,并强制保留错误出现的上下文尾部:

function toolResultContent(result: ToolResult): string { const fullOutput = result.text; // 默认只保留头部,修改为保留头部和尾部各1000字符 if (fullOutput.length > 2000) { return `${fullOutput.slice(0, 1000)}\n...(中部省略)...\n${fullOutput.slice(-1000)}`; } return fullOutput; }

这样改完后,模型能同时看到附件的错误开头和收尾信息,对“哪里错了”的判断会准确很多。

5. 让agent更懂你的项目:代码库上下文工程的三个有效手段

5.1 用CLAUDE.md喂项目经验,效果比任何prompt都持久

Claude Code源码里对CLAUDE.md有天然的支持:每个目录下如果有这个文件,它的内容会被自动注入到全局上下文中。这比你在对话开头粘贴一大段说明靠谱得多,因为每次新对话它都会自动加载。

我在项目根目录放的内容一般分三块:

  • 项目技术栈清单(什么语言、什么框架、包管理器是什么)
  • 常见操作约定(测试命令、lint命令、构建命令)
  • 历史踩坑记录(这个项目最容易出错的地方在哪,以及为什么)
# CLAUDE.md - 项目指引 ## 技术栈 - 后端:Node.js 20 + TypeScript - 前端:React 18 + Vite - 包管理器:pnpm ## 常用命令 - 本地开发:pnpm dev - 跑测试:pnpm test -- --runInBand - 构建:pnpm build && pnpm start ## 容易踩坑的点 1. src/db/migrations 下的文件不要手动改,用 `pnpm migration:generate` 生成 2. 修改 API 路由后,需要同时更新 openapi.yaml,否则 CI 会挂 3. 不要直接在 main 分支上跑 --force,先切 feature 分支

模型在遇到相关任务时,会直接从这些条目里找依据。比如你让它改数据库相关代码,它就自己去读migrations目录并意识到“这个项目对迁移文件有特殊约定”。这种上下文注入方式比你在对话里反复强调要稳定很多,而且可以随项目演进持续更新。

5.2 自定义slash command:把高频操作压缩成一条命令

源码支持在.claude/commands/目录下创建自定义命令,格式是markdown文件,文件名就是命令名。比如我写了一个fix命令,用来一键跑“跑测试-修编译错误-检查lint”的triple loop:

--- description: 修复失败的测试或编译错误 argument-hint: 可选的失败范围描述,如"只修复utils模块" --- 我会按顺序执行以下操作: 1. 读取 package.json 中的 test 脚本定义 2. 运行 `pnpm test` 并记录失败用例 3. 针对失败用例逐个定位源码问题 4. 修复后重新运行测试验证 用户输入的范围限制:{{argument_hint}} 如果没有明确范围,默认处理全部失败用例。

{{argument_hint}}是占位符,输入claude /fix 只处理auth模块时,这里会被替换成“只处理auth模块”。这比每次手动敲“请帮我跑一下测试,如果挂了就修,修完再跑”高效十倍。

创建完重启claude会话即可在交互界面里看到新命令。这个方法适合把团队流程固化成模板,让模型遵守统一的代码路径。

5.3 利用会话恢复,把长任务拆成多段跑

源码里有个容易被忽略但很实用的能力:会话持久化。在交互式进程退出后,对话记录会保留在磁盘上。下次运行带--resume或--continue参数,可以接上一次的上下文继续干。

claude --continue -- "继续处理刚才的编译错误,还剩3个"

这意味着你可以把一个大任务拆给不同时段的它去干。比如上午让它扫描代码库并列出需要重构的模块,下午回来直接--continue让它基于上午的分析结果开始改代码,上下文没断。这在处理大型代码库时特别有用,不用一次性把上下文撑爆。

6. 调出“全程广播”模式:用verbose日志和dry-run把黑盒变成白盒

--verbose参数是Claude Code里最被低估的调试工具。默认输出只是简明结果,但带上这个参数以后,harness会把每一步模型思考、工具选择、具体执行参数都打印出来。我第一次打开的时候才真正搞懂“为什么它要改这个文件”——原来是在前一步的错误日志里猜了个根源,而它选工具的依据全在这一层日志里。

我推荐的标准调试流程是这样:

# 1. 先用 dry-run 看计划(如果需要) claude -p "重构 src/utils/date.ts 中的日期格式化函数" --dry-run # 2. 正式跑,但开 verbose 记录完整决策链 claude -p "重构 src/utils/date.ts 中的日期格式化函数" --verbose 2>&1 | tee run.log

--dry-run如果源码版本支持,会在不真正改动文件的情况下展示它打算执行哪些操作。这对评估“改动范围是否符合预期”非常有用——尤其是在批量重构场景下,先看行动计划,确认无误再放行改真实文件。tee run.log是Linux/macOS下的标准操作,Windows PowerShell 用户换成| Tee-Object run.log。

verbose日志里最值得盯的三个模式:

第一,Tool Use段,看模型调了什么工具、传了什么参数。如果它频繁调用Read而不是Grep,说明代码库太大,模型在靠读文件碰运气找内容,性能会明显变慢。这时候最好在CLAUDE.md里明示代码结构。

第二,Observation段,看工具返回结果里模型截取了什么。如果模型总是忽略报错里的文件路径,那属于上下文管理问题,调小返回截断阈值不要调大,逼它用Grep定位。

第三,Thinking段中的“推理预算用量”。看到 “thinking budget exhausted” 字样说明模型在复杂任务中思考超预算被截断,这时候把配置里的thinking.budget参数调大比改prompt更直接。

从这之后,我每次让它干涉及多文件的活,都会先把--verbose日志跑一份存下来,干完再对一下它实际做了哪些改动。遇到意外改动,直接回看日志里的决策段就能定位到是哪一步推理跑偏了。这个习惯帮我省下了解谜的时间,也让我敢把更复杂的活交给它。

希望这套源码拆解和调试流程能帮到你,上手之后记得改一版你自己的CLAUDE.md——我保证,这个动作比任何参数调优都值。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进