☰
ClaudeCode 源码深度剖析:Agent 架构拆解与 MVP 最小骨架落地
2026/9/29 3:41:13 网站建设 项目流程

1. 为什么我要把 ClaudeCode 的 Agent 架构拆开看

ClaudeCode 是 Anthropic 官方推出的代码智能 Agent,它能在终端里读文件、跑命令、改代码,背后是一套相当克制的 Agent 架构。很多人第一次接触它,会直接去翻源码,结果陷进零散的函数调用里,看完还是不知道一个请求从输入到工具执行到底经过了哪些层。我试过按「架构先行 → 链路贯通 → 源码落地」的顺序来读,效率高很多。

这篇内容聚焦三件事:把 ClaudeCode 的分层架构讲清楚,把一次文件读取的完整链路走通,最后给出一套可运行的 MVP 最小骨架,包含目录结构、核心模块配置和本地启动验证步骤。适合有 TypeScript 基础、想从零理解 Agent 闭环的开发者。读完你能自己跑通一个「用户输入 → 模型决策 → 工具执行 → 结果汇总」的最小 Agent,并知道每一层为什么这样切。

需要的前置知识不多:TypeScript 的联合类型、async/await、异步生成器,Node.js 的 fs/promises 和 readline/promises,再加上对 LLM 多轮对话的基本认知就够了。不需要提前熟悉 ClaudeCode 源码,也不需要真实 API Key 就能跑通骨架。

2. 六层架构与四条硬约束

ClaudeCode 的架构核心是分层加单向依赖。整套系统拆成六层,每层职责单一,只和相邻层通信,避免跨层依赖和循环依赖。

层级核心文件职责
入口层index.ts终端输入输出、事件渲染、维护会话消息队列
编排层query.ts全局状态机,管控多轮循环、模型调度、工具触发
模型层modelClient.ts / fakeModel.ts / config.ts统一模型调用入口,支持模拟与真实 API 切换
工具层tools.ts工具执行入口,权限校验、IO 操作、结果封装
消息协议层message.ts定义全量消息类型与工厂函数
持久化层compress.ts / session.ts历史压缩、会话保存恢复(可选)

真正支撑稳定性的是四条约束。第一,编排层和模型实现完全解耦,query.ts 只认通用响应类型,不依赖具体模型。第二,modelClient.ts 是唯一的模型切换入口,换服务商只改这一个文件。第三,工具层独立可复用,不反向调用编排层。第四,消息协议全局唯一,message.ts 被所有模块依赖,自身不依赖任何业务代码。

注意:如果你在编排层直接 import 了具体模型实现,解耦就破了;如果工具层反向调用 query.ts,就会形成循环依赖。这两条是最容易踩的坑。

3. 一次文件读取的完整链路

静态架构看懂后,用「读取 package.json」这个场景把模块串起来。整个过程分三个阶段。

第一阶段是模型决策工具调用。用户在终端输入read package.json,入口层通过消息工厂生成标准用户消息,推入消息队列,触发编排层。编排层调用模型路由入口,模型识别出「读取文件」意图,不返回文本,而是输出结构化的 tool_use 指令,指定工具名和文件路径参数。

第二阶段是工具执行。编排层拿到指令后触发 executeToolUse,先做权限询问,校验通过后用 Node.js 原生 API 读取文件,再把结果封装成 tool_result 消息推回队列。

第三阶段是结果汇总。模型接收包含工具结果的完整消息队列,解析内容,生成自然语言回复。编排层收到最终回复后终止循环,入口层渲染到终端。

这里有个设计亮点值得单独说:整个 Agent 没有全局状态变量,所有决策和状态流转完全依赖消息队列。模型不输出自由文本,而是结构化指令驱动工具执行,可控性强。工具执行的成功、权限拒绝、读取失败、未知工具,全部走统一的结果封装逻辑,没有散乱的异常消息。

4. 可复制的 MVP 最小骨架

官方 examples/mvp 剔除了持久化、历史压缩、多工具池等扩展能力,只保留 6 个核心文件,完整复刻原版架构。下面给出目录结构和每个文件的核心配置骨架。

claudecode-mvp/ ├── index.ts └── src/ ├── message.ts ├── modelClient.ts ├── fakeModel.ts ├── tools.ts └── query.ts

4.1 消息协议层 message.ts

这是全局数据地基,定义四类消息和工厂函数,自身无业务依赖。

export type UserMessage = { role: 'user'; content: string } export type AssistantMessage = { role: 'assistant'; content: string } export type ToolUseMessage = { role: 'tool_use'; id: string; toolName: string; input: Record<string, unknown> } export type ToolResultMessage = { role: 'tool_result'; toolUseId: string; toolName: string content: string; isError?: boolean } export type Message = UserMessage | AssistantMessage | ToolUseMessage | ToolResultMessage let nextToolUseNumber = 1 export function createUserMessage(content: string): UserMessage { return { role: 'user', content } } export function createAssistantMessage(content: string): AssistantMessage { return { role: 'assistant', content } } export function createToolUseMessage( toolName: string, input: Record<string, unknown>, ): ToolUseMessage { return { role: 'tool_use', id: `toolu_${nextToolUseNumber++}`, toolName, input } } export function createToolResultMessage( toolUse: ToolUseMessage, content: string, isError = false, ): ToolResultMessage { return { role: 'tool_result', toolUseId: toolUse.id, toolName: toolUse.toolName, content, ...(isError ? { isError } : {}), } } export function lastMessage(messages: Message[]): Message | undefined { return messages.at(-1) }

联合类型让 TypeScript 自动收窄,自增 ID 让工具调用和结果精准配对,isError 按需挂载保持消息精简。

4.2 模型路由层 modelClient.ts

对外只暴露一个 callModel,固定输出契约,后续换真实 API 只改这里。

import { fakeModel } from './fakeModel.ts' import { Message, ToolResultMessage, UserMessage } from './message.ts' export type ModelResponse = | { type: 'assistant'; content: string } | { type: 'tool_use'; toolName: string; input: Record<string, unknown> } export type ModelMessage = UserMessage | ToolResultMessage export async function callModel(messages: Message[]): Promise<ModelResponse> { return await fakeModel(messages) }

4.3 模拟模型层 fakeModel.ts

不依赖真实 LLM,用规则模拟「意图识别 → 工具调用 → 结果汇总」,无状态,只看最新消息角色。

import { lastMessage, Message } from './message.ts' import { ModelResponse } from './modelClient.ts' export async function fakeModel(messages: Message[]): Promise<ModelResponse> { const latest = lastMessage(messages) if (latest?.role === 'user') { const lower = latest.content.toLowerCase() if (shouldReadFile(latest.content, lower)) { return { type: 'tool_use', toolName: 'Read', input: { filename: extractPath(latest.content) } } } return { type: 'assistant', content: `普通回答:${latest.content}` } } if (latest?.role === 'tool_result') { if (latest.isError) { return { type: 'assistant', content: `工具 ${latest.toolName} 执行失败:${latest.content}` } } return { type: 'assistant', content: `读取结果:${latest.content}` } } return { type: 'assistant', content: `无法处理:${JSON.stringify(latest)}` } } function shouldReadFile(text: string, lower: string): boolean { return text.includes('读') || text.includes('打开') || lower.includes('read ') || lower.includes('show file') } function extractPath(text: string): string | undefined { const quoted = text.match(/["'`](.+?)["'`]/)?.[1] if (quoted) return quoted const tokens = text.split(/\s+/).filter(Boolean) return tokens.find(t => /[./\\]|\.json$|\.md$|\.ts$|\.txt$/i.test(t)) }

4.4 工具执行层 tools.ts

统一执行入口,整合路径解析、权限校验、IO 执行、结果封装。

import { readFile } from 'fs/promises' import path from 'node:path' import { createToolResultMessage, ToolResultMessage, ToolUseMessage } from './message.ts' import { RuntimeOption } from './query.ts' export async function executeToolUse( toolUse: ToolUseMessage, runtime: RuntimeOption, ): Promise<ToolResultMessage> { if (toolUse.toolName === 'Read') { const readPath = path.resolve(runtime.toolContext.rootDir, toolUse.input.filename as string) const hasAccess = await runtime.askUser(`申请访问:${readPath}`) if (!hasAccess) return createToolResultMessage(toolUse, '访问被拒绝', true) try { const content = await readFile(readPath, 'utf-8') return createToolResultMessage(toolUse, content) } catch (err) { return createToolResultMessage(toolUse, `读取失败:${(err as Error).message}`, true) } } return createToolResultMessage(toolUse, '未知工具调用失败', true) }

4.5 编排调度层 query.ts

核心状态机,用异步生成器实现多轮循环,带最大轮次兜底。

import { createAssistantMessage, createToolUseMessage, Message, } from './message.ts' import { callModel } from './modelClient.ts' import { executeToolUse } from './tools.ts' export type QueryEvent = | { type: 'assistant'; message: Message } | { type: 'tool_use'; message: Message } | { type: 'tool_result'; message: Message } export type RuntimeOption = { toolContext: { rootDir: string } askUser: (p: string) => Promise<boolean> } export async function* query( messages: Message[], runtime: RuntimeOption, ): AsyncIterable<QueryEvent, void> { const maxToolRounds = 5 for (let round = 0; round < maxToolRounds; round++) { const response = await callModel(messages) if (response.type === 'assistant') { yield { type: 'assistant', message: createAssistantMessage(response.content) } return } const toolUse = createToolUseMessage(response.toolName, response.input) messages.push(toolUse) yield { type: 'tool_use', message: toolUse } const toolResult = await executeToolUse(toolUse, runtime) messages.push(toolResult) yield { type: 'tool_result', message: toolResult } } const failed = createAssistantMessage(`工具循环超过 ${maxToolRounds} 轮,已停止。`) messages.push(failed) yield { type: 'assistant', message: failed } }

4.6 入口交互层 index.ts

维护会话状态,消费事件流并渲染,与调度逻辑完全解耦。

import { cwd, stdin, stdout } from 'node:process' import { createInterface } from 'node:readline/promises' import { createUserMessage, Message } from './src/message.ts' import { query, QueryEvent } from './src/query.ts' const rl = createInterface({ input: stdin, output: stdout }) const lineIterator = rl[Symbol.asyncIterator]() const messages: Message[] = [] async function ask(prompt: string): Promise<string | null> { stdout.write(prompt) const next = await lineIterator.next() return next.done ? null : next.value } function renderEvent(event: QueryEvent) { const m = event.message if (m.role === 'assistant') { console.log(`assistant:\n ${m.content}`); return } if (m.role === 'tool_use') { console.log(`tool_use:\n ${JSON.stringify(m.input)}`); return } if (m.role === 'tool_result') { const status = m.isError ? 'error' : 'ok' const preview = m.content.length > 500 ? `${m.content.slice(0, 500)}\n...` : m.content console.log(`tool_result(${status}): ${m.toolName}\n${preview}`) } } while (true) { const answer = await ask('user:\n') if (answer == null) break const input = answer.trim() if (input === '') continue messages.push(createUserMessage(input)) for await (const event of query(messages, { toolContext: { rootDir: cwd() }, askUser: async (q) => { const a = await ask(`${q} [y/N] `) return a?.trim().toLowerCase() === 'y' }, })) { renderEvent(event) } } rl.close()

5. 本地启动与验证请求

骨架跑起来只需要 Node.js 18 以上版本,因为用到了原生 fetch 和 readline/promises。TypeScript 可以直接用 tsx 运行,省去编译步骤。

# 初始化项目 mkdir claudecode-mvp && cd claudecode-mvp npm init -y npm install -D tsx typescript @types/node # 按上面的目录结构创建文件后,启动 npx tsx index.ts

启动后终端会出现user:提示符。输入read package.json,你会看到类似下面的输出:

user: read package.json tool_use: {"filename":"package.json"} 申请访问:/your/path/package.json [y/N] y tool_result(ok): Read { "name": "claudecode-mvp", "version": "1.0.0", ... } assistant: 读取结果:{ ... }

这条链路验证了三件事:模型决策层正确识别了读取意图并输出结构化 tool_use;工具层完成了权限询问和真实文件读取;编排层把结果回传后模型生成了汇总回复。整个闭环没有全局状态变量,全靠消息队列驱动。

如果你想接入真实模型,只需要改 modelClient.ts,把 fakeModel 换成对 Anthropic Messages API 的调用,其他文件一行都不用动。这也是分层解耦带来的直接好处。

6. 本篇常见错排查

报错一:Cannot find module './message.ts'

tsx 默认支持 .ts 后缀导入,但如果你用 tsc 编译,需要在 tsconfig.json 里开启"allowImportingTsExtensions": true并配合"noEmit": true。或者把导入路径改成不带后缀的./message。

报错二:工具执行后循环不结束

检查 fakeModel 里对 tool_result 的处理分支。如果最新消息是 tool_result 但你的判断条件写成了latest.role === 'user',模型会一直返回 tool_use,触发最大轮次兜底。确认分支顺序是先判断 user 再判断 tool_result。

报错三:权限询问一直返回 false

readline 的 asyncIterator 在连续调用时,如果上一次的 next() 没有 await 完成,会拿到 undefined。确保 askUser 里的 ask 调用是 await 的,并且不要在 for await 循环外提前消费迭代器。

报错四:读取文件报 ENOENT

path.resolve 的 rootDir 用的是 cwd(),如果你在子目录启动,相对路径会解析错。要么在项目根目录启动,要么把 rootDir 改成绝对路径。这也是后续加路径白名单沙箱的切入点。

报错五:TypeScript 报input.filename类型错误

toolUse.input 的类型是Record<string, unknown>,直接当 string 用会报错。用as string断言,或者在 createToolUseMessage 时对 input 做更严格的类型约束。

7. 从骨架到可用 Agent 的下一步

这套 6 文件骨架已经跑通了标准 Agent 核心链路,但离生产可用还有距离。扩展方向按优先级排:先接真实 LLM API,在 modelClient.ts 新增路由分支;再加会话持久化,把 messages 数组落盘;然后补路径白名单沙箱,防止越权读取;最后加模型超时和限流容错。

如果你在接入真实模型时想快速验证对话效果,可以直接用 TaoToken 的模型对话功能做对照测试,确认消息格式和工具调用指令是否符合预期。需要生成 API Key 的话,在控制台的 API Keys 页面创建即可,接入文档里有完整的请求示例。对于长期跑编码任务或 Agent 场景,Coding Plan 的额度模型更适合持续调用,不用每次手动续。

骨架代码建议先跑通再改,改的时候守住那四条约束:编排层不 import 具体模型、工具层不反向调用编排层、消息类型不拆分、模型切换只动 modelClient.ts。守住这四条,你后面加多少工具、换多少模型,架构都不会乱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询