公司动态
Claude Code 源码解析:AI Agent 架构设计与工程实践
1. 项目概述Claude Code 是什么最近在 AI 开发圈里Claude Code 这个名字被讨论得越来越频繁。如果你关注过 Anthropic 这家公司可能知道他们推出了 Claude 3.5 Sonnet 模型而 Claude Code 正是其家族中一个专注于代码生成与理解的“智能体”。但和我们平时在 IDE 里用的代码补全插件不同Claude Code 被设计成一个更独立、更强大的“AI 程序员”代理。它不仅能写代码片段更能理解复杂的项目上下文、执行构建命令、调试错误甚至能根据自然语言指令规划并执行一整套开发任务。简单说它试图成为一个能坐在你电脑旁、理解你意图并帮你干活的“虚拟开发伙伴”。这次我们拿到了 Claude Code 早期版本的部分源码总计约 51 万行。这可不是一个小数目它背后隐藏着 Anthropic 如何构建一个复杂 AI Agent 的工程哲学和技术选型。通过解析这些代码我们不仅能一窥顶级 AI 公司在 Agent 架构上的思考更能为我们自己构建类似的智能体提供宝贵的“参考答案”。无论你是对 AI Agent 开发感兴趣还是想了解大型 TypeScript 项目的组织方式或是好奇 Bun 这个新兴运行时在实战中的表现这份源码都像一座金矿。2. 核心架构与设计哲学拆解面对 51 万行代码直接扎进去看细节无异于大海捞针。我的方法是先“俯瞰”整个项目的结构理解其顶层设计。Claude Code 的核心定位是一个“任务驱动的自主编码代理”。这意味着它的首要目标是接收一个高层次的任务描述比如“为这个 React 组件添加用户身份验证”然后自主地拆解任务、查阅代码库、编写代码、运行测试并循环这个过程直到任务完成或遇到无法解决的问题。2.1 模块化与清晰的职责边界源码的目录结构清晰地反映了这一设计思想。项目主要分为以下几个核心模块core/这是 Agent 的“大脑”。包含了任务规划、决策制定、工具调用编排的核心逻辑。你会看到大量关于状态管理、工作流引擎和决策树的代码。skills/这是 Agent 的“双手”。每一个 Skill 对应一项具体的原子能力例如file_operations/读写文件、遍历目录。code_analysis/语法解析、静态分析、理解代码结构。shell_execution/在安全沙箱中执行终端命令如npm install,git commit。web_search/可能集成外部知识检索。debugging/分析错误日志、设置断点模拟。llm/与大语言模型LLM交互的抽象层。这里定义了如何构造提示词Prompt、如何解析 LLM 的响应尤其是包含工具调用的响应以及如何处理不同的模型提供商如 Claude API的差异。这体现了将 LLM 视为一个“计算单元”而非魔法黑盒的工程思维。memory/短期与长期记忆管理。短期记忆可能跟踪当前会话的上下文而长期记忆则可能涉及向量数据库用于存储和检索过往解决过的问题、项目特定的知识片段以实现“学习”和避免重复劳动。ui/或client/用户界面层。可能是 VS Code 扩展、独立的桌面应用或 Web 前端。这部分负责收集用户指令、展示 Agent 的思考过程和结果。shared/公共类型定义、工具函数和常量。一个大型 TypeScript 项目要保持类型安全清晰的定义是基石。注意这种“大脑Core- 技能Skills- 感知LLM/Memory”的架构模式是目前构建复杂 Agent 的主流范式。它保证了系统的可扩展性——要增加新能力只需开发新的 Skill 并注册到 Core 即可无需改动核心决策逻辑。2.2 技术栈选型背后的考量从热搜词和源码文件扩展名可以看出项目主要采用TypeScript并使用Bun作为运行时。为什么是 TypeScript对于一个旨在理解复杂代码、自身也极其复杂的系统类型安全不是奢侈品而是必需品。TypeScript 的静态类型系统能在编译期捕获大量潜在错误如错误的函数参数、未处理的空值这对于维护一个由 AI 生成和修改的代码库至关重要。此外清晰的类型定义本身就是最好的文档有助于不同模块间的协作和 LLM 对代码结构的理解。为什么是 Bun这是一个非常有意思的选择。相比传统的 Node.jsBun 提供了几个关键优势极速启动与执行Bun 的启动速度远超 Node.js这对于一个需要频繁启动子进程执行命令如运行测试、安装依赖的 Agent 来说能显著减少延迟提升用户体验的流畅度。内置的工具链Bun 自带打包器、测试运行器和包管理器与 npm 兼容。这意味着 Claude Code 项目本身可能利用 Bun 进行构建和测试同时其 Agent 在为用户项目执行bun install或bun test时也能获得原生性能优势。这简化了项目对用户环境的管理。对 TypeScript 和 JSX 的原生支持无需额外的ts-node或转译步骤可以直接运行.ts和.tsx文件简化了开发流程。这个选择透露出团队对开发者体验和最终性能的双重追求。他们不仅希望自己的开发过程高效更希望 Agent 在用户机器上的操作尽可能快。3. 核心工作流与“思考-行动”循环解析Claude Code 的核心是一个循环执行的过程通常被称为“ReAct”Reasoning and Acting模式或其变种。让我们深入core/目录下的主循环引擎代码看看它是如何运作的。3.1 主循环状态机Agent 的核心可能是一个状态机其状态包括IDLE等待任务、PLANNING规划、ACTING执行工具、OBSERVING观察结果、EVALUATING评估、FINISHED/ERROR。主循环的伪代码逻辑如下// 简化版主循环示意 async function agentLoop(initialTask: string, projectContext: Context) { let state State.PLANNING; let plan null; let history []; // 记录思考、行动、观察的步骤 while (state ! State.FINISHED state ! State.ERROR) { switch (state) { case State.PLANNING: // 1. 规划基于任务和当前上下文让 LLM 生成一个计划 const planningPrompt constructPlanningPrompt(initialTask, projectContext, history); const llmResponseForPlan await llmClient.chat(planningPrompt); plan parsePlan(llmResponseForPlan); // 解析出步骤列表如 [“分析现有代码”, “创建auth.ts文件”, ...] state State.ACTING; break; case State.ACTING: // 2. 行动从计划中取出下一步或让 LLM 决定下一步使用哪个工具 const actionPrompt constructActionPrompt(plan, history, projectContext); const llmResponseForAction await llmClient.chat(actionPrompt); const toolCall parseToolCall(llmResponseForAction); // 解析出 {tool: writeFile, args: {path: ..., content: ...}} if (isValidToolCall(toolCall)) { const skill skillRegistry.get(toolCall.tool); const result await skill.execute(toolCall.args); history.push({ type: action, toolCall, result }); state State.OBSERVING; } else { // LLM 产生了无法解析的响应进入错误处理或要求澄清 state State.ERROR; } break; case State.OBSERVING: // 3. 观察将上一步行动的结果整合到上下文 // 这里可能会对结果进行摘要、提取关键信息避免将过长的原始输出如终端日志直接塞给LLM const observation summarizeResult(history.last().result); history.push({ type: observation, observation }); state State.EVALUATING; break; case State.EVALUATING: // 4. 评估判断当前计划是否完成或是否需要调整 const evaluationPrompt constructEvaluationPrompt(initialTask, plan, history); const llmResponseForEval await llmClient.chat(evaluationPrompt); const evaluation parseEvaluation(llmResponseForEval); // 解析出 {isComplete: boolean, nextStep: continue | replan | ask_user} if (evaluation.isComplete) { state State.FINISHED; } else if (evaluation.nextStep replan) { state State.PLANNING; // 回到规划阶段重新制定计划 } else { state State.ACTING; // 继续执行当前计划的下一个动作 } break; } } return { finalState: state, history }; }这个循环是 Agent 自主性的源泉。每一次“行动”都依赖于 LLM 的决策而每一次“观察”又为下一次决策提供了新的信息。3.2 提示词工程的艺术在llm/模块中我们可以看到大量用于构造不同阶段提示词的模板函数。这些提示词的质量直接决定了 Agent 的表现。系统提示词定义了 Agent 的角色、能力和行为准则。例如“你是一个专业的软件开发助手能够通过使用工具来浏览、编辑代码和运行命令。你必须严格遵守安全规范不得执行破坏性操作。你的目标是帮助用户完成编码任务。”规划提示词会注入项目结构如关键文件列表、任务描述和过往历史要求 LLM 输出一个结构化的计划。行动提示词会列出所有可用的工具Skills及其详细描述、参数格式。这是工具使用的关键LLM 需要精确地知道它能调用什么以及如何调用。评估提示词要求 LLM 对比当前状态与目标判断进展。实操心得从源码看Anthropic 的提示词非常详细并且大量使用了XML 标签如plan.../plan,tool_call.../tool_call来结构化 LLM 的输出这比依赖不稳定的自然语言描述要可靠得多。这种“强制结构化输出”是生产级 Agent 的常见做法可以极大地提高响应解析的成功率。4. 关键技能实现深度剖析Agent 的能力最终体现在一个个具体的 Skill 上。我们挑几个最有代表性的 Skill 模块看看它们是如何实现的。4.1 文件操作技能位于skills/file_operations/目录下。这看似简单但实现上需要考虑很多边界情况和安全问题。路径安全所有传入的文件路径都必须进行规范化并检查是否在允许的工作区范围内防止 Agent 意外或被恶意诱导操作系统文件。读写原子性写文件时可能会先写入临时文件然后原子性地移动rename到目标位置防止在写入过程中发生错误导致文件损坏。编码与格式化读取文件时要正确处理不同编码。写入代码时可能会集成 Prettier 或项目自身的 ESLint 配置在保存前自动格式化保证代码风格一致。// 简化的 writeFile skill 实现示例 export class WriteFileSkill implements Skill { name writeFile; description Writes content to a file at the specified path. Will create parent directories if needed.; async execute(args: { path: string; content: string }): PromiseSkillResult { // 1. 安全校验 const safePath pathResolver.resolveWithinWorkspace(args.path); if (!safePath) { return { success: false, error: Path is outside the allowed workspace. }; } // 2. 确保目录存在 await fs.mkdir(path.dirname(safePath), { recursive: true }); // 3. 可选如果是代码文件先格式化 let finalContent args.content; if (isCodeFile(safePath)) { finalContent await codeFormatter.format(args.content, safePath); } // 4. 原子性写入通过临时文件 const tempPath ${safePath}.tmp; await fs.writeFile(tempPath, finalContent, utf-8); await fs.rename(tempPath, safePath); return { success: true, output: File written successfully to ${safePath} }; } }4.2 Shell 执行技能位于skills/shell_execution/。这是最强大也最危险的技能。源码中必然包含一套严格的安全沙箱机制。进程隔离很可能使用类似node:child_process的模块但在独立的、资源受限的环境中运行。可能会限制运行时间、内存和 CPU 使用率。命令白名单/黑名单不是所有命令都能执行。像rm -rf /、format C:这类命令肯定被禁止。同时可能有一个允许的命令列表如npm,git,bun,python,ls,cat等。工作目录限制命令只能在当前项目目录或其子目录下执行。流式输出处理需要实时捕获命令的 stdout 和 stderr并将其流式地返回给 Agent 的“观察”阶段以便 LLM 能及时了解命令执行情况。同时要对过长的输出进行截断或摘要避免超出 LLM 的上下文限制。export class ShellExecuteSkill implements Skill { name executeShell; description Executes a shell command in the project directory. Supports basic commands like npm, git, ls, etc.; async execute(args: { command: string; args?: string[] }): PromiseSkillResult { // 1. 命令验证 if (!this.isCommandAllowed(args.command, args.args)) { return { success: false, error: Command ${args.command} is not allowed or has dangerous arguments. }; } // 2. 准备执行环境 const childProcess spawn(args.command, args.args || [], { cwd: projectWorkspace.rootPath, // 限制工作目录 stdio: [ignore, pipe, pipe], // 忽略 stdin捕获 stdout/stderr timeout: 30000, // 30秒超时 // 可能还有更多的安全选项如 uid/gid, detached: false 等 }); // 3. 收集输出 let stdout ; let stderr ; childProcess.stdout.on(data, (data) { stdout data.toString(); }); childProcess.stderr.on(data, (data) { stderr data.toString(); }); // 4. 等待结束 const exitCode await new Promise((resolve) { childProcess.on(close, resolve); }); // 5. 处理结果 const output Exit Code: ${exitCode}\nStdout:\n${stdout}\nStderr:\n${stderr}; // 对过长输出进行智能摘要 const summarizedOutput this.summarizeIfNeeded(output); return { success: exitCode 0, output: summarizedOutput, metadata: { exitCode, rawStdout: stdout, rawStderr: stderr } }; } }4.3 代码分析技能位于skills/code_analysis/。这个技能让 Agent 能“看懂”代码而不仅仅是当作文本处理。语法树解析利用 TypeScript 编译器 API、Babel 或 Tree-sitter 等工具将代码文件解析成抽象语法树。这使得 Agent 可以回答“这个文件导入了哪些模块”、“这个函数被谁调用了”这类结构化问题。符号导航实现“跳转到定义”、“查找所有引用”等 IDE 常见功能帮助 Agent 理解代码间的关联。静态分析可能集成简单的 linting 规则在编写代码时就发现潜在问题。这个技能的实现通常依赖于现有的、强大的语言服务工具链Claude Code 很可能封装了这些工具为其 LLM 核心提供结构化的代码信息。5. 内存、上下文管理与长程任务处理一个复杂的编码任务可能需要很多步LLM 的上下文窗口是有限的。Claude Code 如何管理漫长的对话和任务历史5.1 分层记忆系统从memory/目录的代码可以看出记忆系统可能是分层的短期记忆/对话历史保存在内存中是最近几次的“思考-行动-观察”循环记录。这部分会直接作为上下文送入 LLM。长期记忆/向量存储当对话变长或者任务被暂停后重新启动时需要从更早的历史中检索相关信息。源码中可能会集成像pinecone或本地向量库如hnswlib的客户端。每次重要的“观察”结果如“成功实现了登录 API”或代码片段的关键摘要会被转换成向量并存储。当 Agent 遇到类似问题时可以快速检索出相关的解决方案。项目上下文索引这可能是一个专门为当前代码库建立的索引包含了所有文件的结构、主要类、函数和它们的文档字符串。它不同于向量存储更像是一个快速查找表用于回答“项目里有没有现成的工具函数”这类问题。5.2 上下文窗口优化策略即使有记忆系统送入 LLM 的当前上下文也需要精心裁剪。源码中可能有专门的模块负责“上下文窗口管理”摘要将一段冗长的终端输出或代码变更总结成一两句话。例如“成功运行了npm test所有 152 个测试通过”比完整的测试日志有用得多。选择性遗忘根据当前任务目标动态决定哪些历史步骤是相关的只保留这些。这通常需要另一个 LLM 调用来做判断。关键信息提取从历史中提取出“事实”如“当前用户模型位于src/models/user.ts”、“已安装的依赖有express和mongoose”将这些结构化事实而非原始对话送入上下文。6. 错误处理、安全与可靠性工程对于一个能自动执行命令和修改文件的 AI 系统鲁棒性和安全性是生命线。51 万行代码中有相当一部分是用于处理各种边缘情况和防御性编程。6.1 全面的错误处理与重试机制在core/或utils/中会有一个统一的错误处理框架。工具调用错误如果writeFile因为权限问题失败Skill 会返回明确的错误信息。主循环会捕获这个错误并将其作为“观察”反馈给 LLM。LLM 可能会因此调整策略比如先检查权限。LLM 响应解析错误如果 LLM 返回的 JSON 或 XML 格式不符合预期解析器会抛出错误。此时Agent 不应直接崩溃而是应该进入一个“修复”状态例如尝试重新提问或者使用更严格的提示词要求 LLM 重试。网络与速率限制与 Claude API 的通信会有重试逻辑如 exponential backoff和速率限制处理。超时控制每一个步骤规划、执行、评估都有超时设置防止 Agent 因某个步骤卡死而“宕机”。6.2 多层安全防护安全是贯穿始终的主题体现在多个层面输入净化与验证所有来自用户或 LLM 的输入文件路径、命令参数都必须经过严格的验证和净化防止路径遍历、命令注入等攻击。资源隔离Shell 命令在沙箱中运行文件操作限制在工作区内。操作确认与回滚对于高风险操作如删除文件、强制推送 gitAgent 可能会向用户请求确认。更高级的实现可能会有操作日志并支持简单的回滚例如通过 git 来管理 Agent 做出的修改。内容安全策略可能会扫描生成的代码防止引入已知的安全漏洞或恶意代码模式。7. 构建、测试与部署基础设施如此庞大的项目必然有一套成熟的 DevOps 流水线。从源码中我们可以窥见其工程化水平。Monorepo 管理项目可能使用 Turborepo、Nx 或 Bun 自带的工作区功能来管理多个包如核心库、VS Code 扩展、独立应用。严格的代码质量门禁.eslintrc,.prettierrc配置文件非常严格。提交代码前可能有 pre-commit hooks 运行 linting 和测试。全面的测试套件测试目录tests/或__tests__/的规模会很大。包含单元测试测试每个独立的 Skill 和工具函数。集成测试测试多个 Skill 的协作例如“规划-写文件-执行命令”的完整流程。端到端测试模拟真实用户场景给 Agent 一个任务看它能否正确完成。这类测试运行成本高但至关重要。Mocking测试中会大量使用 Mock特别是对于 LLM 调用和外部命令执行以保证测试的稳定性和速度。配置管理与特性开关使用config/目录或环境变量来管理不同环境开发、测试、生产的配置。可能还有特性开关用于逐步推出新功能或进行 A/B 测试。8. 从源码中学到的架构启示与避坑指南通读这 51 万行代码与其说是在学怎么写一个 Agent不如说是在学习如何构建一个复杂、可靠、可维护的现代软件系统。以下是我总结的几个关键启示和容易踩的坑8.1 启示一清晰的抽象是应对复杂度的唯一武器Claude Code 通过将系统清晰地划分为 Core、Skill、LLM Adapter、Memory 等模块使得每个部分的职责单一且明确。当你要增加一个“从网页抓取文档”的新能力时你只需要在skills/下新建一个web_scraping/目录实现对应的接口并在 Core 中注册即可完全不用关心任务规划或记忆检索的逻辑。这种架构让系统在变得极其复杂后依然可控。避坑指南不要在 Core 里写具体的工具逻辑。早期为了图快很容易把“执行命令”的代码直接写在主循环里。这会导致 Core 迅速膨胀难以测试和维护。务必坚持“依赖倒置”原则让 Core 依赖于抽象的 Skill 接口。8.2 启示二将 LLM 视为“有才华但不可靠的员工”LLM 能力强大但它的输出是非确定性的可能产生格式错误、逻辑混乱或不符合指令的内容。Claude Code 的代码没有天真地相信 LLM 的输出而是处处设防结构化输出用 XML/JSON 格式强制约束 LLM 的响应。解析验证对解析结果进行严格的模式验证使用 Zod 或类似的库。备选路径当解析失败时有降级策略如请求重试、简化问题。避坑指南永远不要直接JSON.parseLLM 的响应而不做异常处理。一定要假设它可能返回任何东西并用try...catch包裹并设计好重试或向用户求助的流程。8.3 启示三性能与用户体验的权衡使用 Bun 体现了对性能的追求但性能优化不止于此。例如频繁的 LLM 调用是最大的延迟来源。源码中可能实现了Prompt 缓存对于相似的上下文可能缓存构造好的 Prompt。并行工具调用如果任务中的多个步骤互不依赖Agent 可能会尝试并行执行如同时安装多个独立的依赖包。流式响应将 Agent 的“思考过程”实时展示给用户而不是等全部完成再显示这能极大提升感知速度。避坑指南在项目早期就引入性能监控。记录每个规划、行动、评估步骤的耗时。瓶颈往往出现在意想不到的地方比如某个文件读取操作因为未使用缓存而重复进行。8.4 启示四测试策略决定演化速度一个行为由非确定性的 LLM 驱动的系统如何测试Claude Code 的测试策略很可能非常务实Mock LLM在绝大多数单元和集成测试中用一个可预测的 Mock LLM 来替代真实的 API 调用。这个 Mock 会返回预先设定好的、符合格式的响应。黄金文件测试对于端到端测试给定一个固定的任务和 Mock LLM 响应确保 Agent 产生一系列特定的、可预期的工具调用序列和最终结果。将结果与“黄金文件”对比。模糊测试与属性测试对 Prompt 构造器和响应解析器进行模糊测试输入各种边缘案例确保程序不会崩溃。避坑指南不要试图为 LLM 的“智能”本身编写断言如“它应该写出最优的算法”。而应该为 Agent 的确定性行为编写测试例如“当 LLM 返回一个写文件的工具调用时Skill 应该被以正确的参数调用”。把非确定性的部分隔离出去。51 万行代码的 Claude Code 源码向我们展示的不仅仅是一个 AI Agent 的实现更是一个关于软件工程、系统架构和产品思维的完整案例。它证明了将前沿的 AI 能力转化为稳定、可靠、用户友好的产品需要的是极其扎实的工程功底和对细节的深刻把控。对于想要进入 AI Agent 领域的开发者来说这份源码的价值远超过任何一篇教程或论文。它是一张详尽的蓝图告诉你梦想中的“AI 程序员”在现实中究竟是如何一砖一瓦建造起来的。