Claude Code 源码深度研究报告:TypeScript Agent 架构与 Prompt 工程拆解
1. 从一次“底牌走光”说起Claude Code 源码到底能学到什么Claude Code 是 Anthropic 官方推出的终端编程 Agent能在本地仓库里读文件、跑命令、改代码。它适合谁适合已经会用命令行、想搞清楚“一个能落地的 TypeScript Agent 到底怎么搭”的开发者。我这次不是讲怎么用它写业务而是把它的源码结构拆开看Agent 主循环怎么转、工具调用怎么被拦住、Prompt 怎么在运行时拼出来。起因挺戏剧性的。Anthropic 发布 npm 包时.npmignore配置漏了完整 TypeScript 源码被一起打包发到了公开镜像。盘子不小约 51.2 万行、1900 多个文件。跳开 CLI 那层壳往里看会发现它根本不是“聊天框套壳”而是一台带进程管理、内存管理、权限网关和系统调用的微型 Agent OS。这篇按三条链路走Agent 主循环、工具调用管线、Prompt 组装。每条都给可复制的目录速查、关键模块调用链注释以及本地跑通验证的步骤。最后说怎么用统一 Key/API 通道接入自建 Agent 做对照实验把“读源码”变成“能复现”。2. 前置准备统一 Key/API 通道与本地环境读源码最怕只读不跑。要跑对照实验先得有一条稳定的模型调用通道。我用的是 TaoToken 的统一 Key/API 通道官网入口 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。它的作用是把你自建 Agent 的模型请求统一收口方便和 Claude Code 的行为做 A/B 对照。先拿 Key进控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面创建 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。拿到后不要写进代码用环境变量。export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api本地环境建议 Node 20、pnpm 9、Git 2.40。源码分析阶段不需要完整 clone 官方仓库准备一个你自己的 TypeScript 工程即可用来复刻主循环和工具管线。mkdir cc-agent-lab cd cc-agent-lab pnpm init pnpm add -D typescript tsx types/node pnpm add zod npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext注意源码里大量用到 Zod 做工具参数校验这是它“防幻觉”的第一道门后面会重点讲。先把依赖装上别等到写校验时才发现缺包。3. 源码目录速查表与三条链路定位先把顶层结构抽象出来。Claude Code 的目录划分很像操作系统分层下面这张速查表可以直接对照你 clone 下来的源码看。目录类比 OS 概念职责core/内核层调度器、状态机、权限网关memory/内存管理上下文组装、缓存边界、静态规范agents/进程管理主 Agent 与特化子 Agenttools/系统调用文件、终端、MCP 外部工具constants/prompts.ts引导扇区系统提示词动态编排入口三条链路分别落在不同目录先记住对应关系后面逐个拆。Agent 主循环 - core/orchestrator.ts agents/BaseAgent.ts 工具调用管线 - tools/ToolRegistry.ts tools/*/schema.ts Prompt 组装 - constants/prompts.ts constants/prompt-sections/3.1 Agent 主循环fork 语义与上下文隔离主循环的核心不是“调模型”而是“决定要不要派生子进程”。源码里有一个类似 Linuxfork()的机制主 Agent 遇到重活时克隆当前上下文注入子 Agent 专属 Prompt物理移除写权限工具跑完只回传一份摘要。// 概念重构子 Agent 派生 async function forkSubAgent(task: Task, parentContext: Context) { // 1. 克隆父上下文切断指针联系 const childContext ContextManager.clone(parentContext); // 2. 注入子进程专属 Prompt childContext.injectPrompt(EXPLORE_AGENT_PROMPT); // 3. 物理级权限阉割只保留只读工具 const safeTools ToolRegistry.getReadonlyTools(); // 4. 启动子循环 const worker new AgentRunner(childContext, safeTools); const report await worker.execute(task); // 5. 子进程试错废话丢弃只回传干货 return report.summarize(); }这段逻辑的价值在于子 Agent 去几千个文件里乱翻产生的“垃圾 Token”在返回报告后被直接回收主 Agent 的上下文始终干净。这就是它敢在真实仓库里动刀子的底气。3.2 工具调用管线Zod 校验 权限拦截工具层被设计成微服务每个工具目录下有index.ts执行逻辑、schema.tsZod 校验、prompt.ts专属说明书。调用时先过 Zod再过权限拦截器最后才碰物理 API。import { z } from zod; const ReplaceBlockSchema z.object({ filePath: z.string().nonempty(), oldCode: z.string().min(1), newCode: z.string(), startLine: z.number().int().positive(), endLine: z.number().int().positive(), }); class ReplaceBlockTool { async execute(rawArgs: unknown, context: Context) { // 第一关参数格式 const args ReplaceBlockSchema.parse(rawArgs); // 第二关物理文件状态比对防幻觉 const lines (await fs.readFile(args.filePath, utf8)).split(\n); const actual lines.slice(args.startLine - 1, args.endLine).join(\n); if (actual.trim() ! args.oldCode.trim()) { return { status: ERROR, message: oldCode 与物理文件不匹配请重新阅读文件 }; } // 第三关快照后执行支持回滚 await GitTracker.snapshot(args.filePath); await fs.replace(args.filePath, args.oldCode, args.newCode); return { status: SUCCESS }; } }关键点当模型写错路径或传错参数时程序不崩溃而是把错误作为 System Message 扔回模型逼它自我修正。这种基于报错的自愈能力是复杂任务成功率的关键。3.3 Prompt 组装动态边界与缓存压榨constants/prompts.ts里没有静态长文本全是函数拼装。最值钱的是SYSTEM_PROMPT_DYNAMIC_BOUNDARY这个常量它把 Prompt 切成“冷区”和“热区”。function buildOptimizedSystemMessages(context: Context) { const messages []; // 冷区身份、安全基线、工具 Schema全部静态 const staticContent getBaseIdentity() getSecurityRules() getAllToolSchemas(context.tools); messages.push({ type: text, text: staticContent, cache_control: { type: ephemeral }, // 缓存断点 }); // ---- SYSTEM_PROMPT_DYNAMIC_BOUNDARY ---- // 热区Git 状态、报错日志、用户输入每次变 const dynamicContent getCurrentGitState() getLastTerminalError(); messages.push({ type: text, text: dynamicContent }); return messages; }冷区占约 80% Token几乎 100% 命中缓存成本打一折热区每次实时算。这条边界是计费与性能的分水岭也是“长提示词不费钱、变量混进去才费钱”的工程证明。4. 可复制配置把三条链路跑起来光看不够得跑。下面给一份最小可运行的配置把主循环、工具管线、Prompt 组装串起来用 TaoToken 通道发请求。4.1 项目结构与依赖cc-agent-lab/ ├── src/ │ ├── core/orchestrator.ts │ ├── tools/registry.ts │ ├── tools/read-file.ts │ ├── prompts/assemble.ts │ └── index.ts ├── package.json └── tsconfig.json4.2 工具注册表与只读工具// src/tools/registry.ts import { z } from zod; export interface Tool { name: string; schema: z.ZodTypeAny; execute: (args: any) Promisestring; } const readFileSchema z.object({ path: z.string() }); export const readFileTool: Tool { name: read_file, schema: readFileSchema, execute: async ({ path }) { const fs await import(node:fs/promises); return fs.readFile(path, utf8); }, }; export const ToolRegistry { all: [readFileTool], getReadonlyTools: () [readFileTool], };4.3 Prompt 组装器// src/prompts/assemble.ts export function assembleSystemPrompt(tools: { name: string }[]) { const staticPart [ You are a CLI coding agent., Use tools precisely. Do not guess file paths., Available tools: ${tools.map((t) t.name).join(, )}, ].join(\n); return { staticPart, dynamicPart: CWD: ${process.cwd()}, }; }4.4 主循环与模型调用// src/core/orchestrator.ts import { ToolRegistry } from ../tools/registry.js; import { assembleSystemPrompt } from ../prompts/assemble.js; export async function runAgent(userInput: string) { const tools ToolRegistry.getReadonlyTools(); const { staticPart, dynamicPart } assembleSystemPrompt(tools); const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/messages, { method: POST, headers: { content-type: application/json, x-api-key: process.env.TAOTOKEN_API_KEY!, anthropic-version: 2023-06-01, }, body: JSON.stringify({ model: claude-sonnet-4-20250514, max_tokens: 1024, system: [ { type: text, text: staticPart, cache_control: { type: ephemeral } }, { type: text, text: dynamicPart }, ], messages: [{ role: user, content: userInput }], }), }); const data await res.json(); return data; }4.5 入口// src/index.ts import { runAgent } from ./core/orchestrator.js; runAgent(列出当前目录下的文件).then((r) console.log(JSON.stringify(r, null, 2)));跑起来pnpm tsx src/index.ts5. 验证请求与成功结果配置写完得确认请求真的通了、缓存边界真的生效。分两步验证。5.1 验证模型通道curl -s https://taotoken.net/api/v1/messages \ -H content-type: application/json \ -H x-api-key: $TAOTOKEN_API_KEY \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 64, messages: [{role:user,content:回复 OK 两个字母}] }成功时返回 JSONcontent数组里有文本块。如果返回 401检查 Key 是否带sk-前缀返回 404检查 base URL 是否漏了/v1。5.2 验证缓存边界连续发两次相同请求第二次的usage里应出现cache_read_input_tokens大于 0说明冷区命中了缓存。for i in 1 2; do curl -s https://taotoken.net/api/v1/messages \ -H content-type: application/json \ -H x-api-key: $TAOTOKEN_API_KEY \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 32, system: [{type:text,text:You are a CLI agent.,cache_control:{type:ephemeral}}], messages: [{role:user,content:hi}] } | grep -o cache_read_input_tokens:[0-9]* done第二次输出非零说明缓存断点生效。这一步跑通你就复现了 Claude Code 最核心的成本控制机制。5.3 对照实验自建 Agent vs 官方行为把自建 Agent 的 Prompt 组装逻辑和官方源码对照重点看三处静态前缀是否放在最前、动态变量是否全部压在边界之下、工具 Schema 是否随 Agent 角色动态挂载。用同一批任务跑两边记录 Token 消耗和首字延迟差异会很明显。6. 本篇常见错排查读源码和跑实验时下面几个坑我踩过列出来帮你省时间。报错一ZodError: Required at path模型返回的工具参数缺字段。原因通常是 Prompt 里没把工具 Schema 描述清楚。解决在prompt.ts里给每个工具写一段专属说明随 Agent 角色动态挂载别把所有工具说明塞进主 Prompt。报错二oldCode 与物理文件不匹配模型凭记忆写oldCode和硬盘实际内容对不上。这是防幻觉机制在起作用不是 Bug。解决让模型先调read_file拿到真实内容再调替换工具。报错三缓存命中率始终为 0检查动态变量是否越过了边界。常见错误是把current_time或随机 ID 放在静态前缀第一行导致后面几万 Token 缓存全废。解决所有会变的字段一律压在SYSTEM_PROMPT_DYNAMIC_BOUNDARY之下。报错四子 Agent 越权写文件如果子 Agent 能改文件说明工具注册表没做物理隔离。解决getReadonlyTools()里根本不要引入写工具类靠类型系统在编译期就拦住。报错五fetch failed或超时检查TAOTOKEN_BASE_URL是否写成https://taotoken.net/api别漏协议头。网络层问题优先用 curl 单独验证排除代码因素。7. 继续深挖从读源码到自建 Agent把三条链路跑通后下一步可以做对照实验用同一批重构任务分别跑官方 Claude Code 和你的自建 Agent记录成功率、Token 消耗、首字延迟。差异最大的地方往往就是 Prompt 组装和工具校验的细节。如果你想把实验做深建议从模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 先手动验证几轮模型行为再切到 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 做长期编码任务的对照。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的请求示例和参数说明。源码里最值得反复看的是prompts.ts的动态拼装和ToolRegistry的权限分发。前者教你“怎么让模型只看到当下需要的信息”后者教你“怎么用类型系统而不是 Prompt 去约束行为”。这两条想明白自建 Agent 的骨架就立住了。