news 2026/11/2 16:57:44

Claude Code CLI 源码拆解:TypeScript Agent 循环与工具调用架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Claude Code CLI 源码拆解:TypeScript Agent 循环与工具调用架构

1. 为什么我要啃 Claude Code CLI 的源码

第一次用 Claude Code CLI 的时候,我的直觉是"这不就是个套了壳的对话工具么"。结果用了两天,发现它在终端里能读文件、能改代码、能跑命令、能自己决定下一步干什么,甚至我让它重构一个模块,它会先 grep 一遍调用点,再逐个改,改完还自己跑测试。这就不是"套壳"了,这是一个跑在终端里的 Agent 运行时。

市面上讲 Claude Code 用法的内容已经很多了,安装、配置、接本地模型、在 VS Code 里怎么用,一搜一大把。但讲它内部怎么组织的、一个 CLI 是怎么把大模型变成能干活的 Agent 的,几乎没有。我花了大概两周时间,把它的源码结构、核心模块、工具调用链路、上下文管理这几块拆了一遍,越拆越觉得值得写出来——因为这套架构思路,你完全可以拿去复刻到自己的 Agent 项目里,不管你是做代码助手、运维助手还是数据分析助手。

这篇东西适合三类人看:一是想搞明白 Agent 到底怎么落地的前端/Node 工程师,二是正在做 AI Agent 框架、想参考成熟产品设计的开发者,三是单纯好奇"我敲一行命令,它凭什么能自己改我代码"的技术爱好者。我会尽量把 TypeScript 层面的实现讲清楚,但不会逐行贴源码,重点是架构决策背后的为什么,以及你抄作业时该注意什么。

先说结论:Claude Code CLI 本质上是一个基于 TypeScript 的、以工具调用为核心的 Agent 循环,它的复杂度不在模型调用上,而在上下文压缩、工具权限控制、流式输出解析和会话状态管理这四个地方。把这四块吃透,你就理解了 80%。

2. 整体架构拆解:一个 CLI 是怎么变成 Agent 的

2.1 从"对话工具"到"Agent 循环"的分水岭

普通 CLI 对话工具的逻辑是线性的:读输入 → 调模型 → 打印输出 → 等下一次输入。这个循环里,模型是"被动"的,它只能说话,不能做事。

Claude Code 的分水岭在于它引入了一个agentic loop。你给它一个任务,它不是回你一段话就结束,而是进入一个循环:模型输出 → 解析出工具调用意图 → 执行工具 → 把结果塞回上下文 → 再调模型 → 直到模型认为任务完成。这个循环的终止条件是模型不再请求工具调用,而是输出一段纯文本回复。

用伪代码表示大概是这样:

async function agentLoop(userInput: string, context: Message[]) { context.push({ role: 'user', content: userInput }); while (true) { const response = await callModel(context, tools); if (response.stopReason === 'tool_use') { const toolResults = await executeTools(response.toolCalls); context.push({ role: 'assistant', content: response.content }); context.push({ role: 'user', content: toolResults }); continue; } return response.content; } }

看起来简单,但魔鬼全在细节里。executeTools要考虑权限、要考虑并发、要考虑超时;context会无限膨胀,必须压缩;callModel要处理流式返回,因为工具调用的参数是边生成边解析的。这些才是真正的工作量。

2.2 目录结构透露的设计哲学

我拿到源码后第一件事是看目录结构,因为目录结构基本等于作者的思维导图。Claude Code CLI 的源码大致分成这么几层:

  • 入口层:处理命令行参数解析、启动 REPL 或者单次执行模式。这一层很薄,基本就是路由。
  • 会话层:管理对话历史、上下文窗口、压缩策略。这是最核心也最容易被低估的一层。
  • 模型层:封装对模型 API 的调用,处理流式响应、重试、错误映射。
  • 工具层:每个工具(读文件、写文件、执行命令、搜索)是一个独立模块,有统一的接口定义。
  • 权限层:决定某个工具调用要不要用户确认,这是安全的关键。
  • UI 层:终端渲染,包括流式文本、工具调用状态、diff 展示。

这个分层最值得学的地方是工具层和权限层是分开的。很多自己做 Agent 的人会把权限检查写死在工具实现里,结果就是加一个新工具就要重新想一遍权限逻辑。Claude Code 把"这个工具能做什么"和"这个操作要不要批准"解耦了,工具只负责执行,权限层统一拦截。

2.3 为什么用 TypeScript 而不是 Python

这是个被问烂的问题。我的判断是:Claude Code 的定位是开发者终端工具,它的用户是写代码的人,它的运行环境是开发者的机器。TypeScript/Node 在这个场景下有几个实打实的优势:

第一,分发。Node 生态有 npm,一个npm install -g就装好了,跨平台。Python 的打包分发一直是痛点,尤其是要带一堆原生依赖的时候。

第二,异步 IO 模型。Agent 要同时处理流式模型输出、文件读写、子进程执行,Node 的事件循环天然适合这种 IO 密集场景。Python 的 asyncio 也能做,但生态里同步代码太多,混用容易出问题。

第三,类型系统。工具调用的参数是模型生成的 JSON,需要严格校验。TypeScript 配合 zod 这类库,能在编译期和运行期双重保证参数结构正确。这在工具数量多了以后非常关键。

当然代价也有:Node 的启动开销比编译型语言大,冷启动会慢一点。但对于一个交互式 CLI 来说,这点延迟可以接受。

3. 核心模块深挖:工具调用、上下文与流式解析

3.1 工具定义:模型能"看见"的世界

Agent 的能力边界,完全由它手里的工具决定。Claude Code 的工具集大致是这几类:文件读取、文件写入/编辑、目录列举、内容搜索(grep 类)、命令执行、以及一些辅助工具。

每个工具的定义包含三部分:名称、描述、参数 schema。这三样东西会一起被塞进模型的系统提示里,模型根据描述决定什么时候用哪个工具。

const ReadFileTool = { name: 'read_file', description: '读取指定路径的文件内容,支持指定行范围', parameters: z.object({ path: z.string().describe('文件绝对路径'), offset: z.number().optional().describe('起始行号'), limit: z.number().optional().describe('读取行数') }), async execute(params) { // 实际读取逻辑 } };

这里有个特别容易被忽略的点:description 的写法直接决定工具调用准确率。我实测过,把"读取文件"改成"读取指定路径的文件内容,支持指定行范围,大文件建议配合 offset 和 limit 使用",模型在读取大文件时主动分段的比例明显上升。工具描述不是文档,是给模型看的 prompt,要写得像在教一个新人怎么用。

参数 schema 用 zod 定义还有个好处:模型生成的参数可能缺字段、类型不对,zod 校验失败时能给出精确的错误信息,这个错误信息可以回传给模型让它重试。这比直接抛异常友好得多。

3.2 上下文压缩:Agent 的"记忆管理"

这是我认为整个项目里最精妙的部分。Agent 跑久了,上下文会爆炸——每一轮工具调用都往历史里塞内容,读一个大文件可能就几万 token。上下文窗口是有限的,塞满了就得压缩。

Claude Code 的压缩策略我观察到几个层次:

第一层是工具结果的截断。读文件不会把整个文件塞回去,超过一定行数会截断并提示"文件还有 N 行未显示"。命令执行的输出同理,超长输出会被裁剪。

第二层是历史消息的摘要。当对话轮次多了,早期的消息会被压缩成摘要。这里的关键是保留什么、丢弃什么。我的理解是:用户的原始意图、已经做出的关键决策、当前任务的状态要保留;中间的探索过程、失败的尝试可以压缩掉。

第三层是系统提示的稳定化。系统提示里包含工具定义、环境信息(当前目录、git 状态等),这部分每次调用都要带上,但内容基本不变,可以命中 prompt cache,成本低很多。

提示:自己做 Agent 时,上下文压缩千万别等到爆了才做。我的经验是设置一个阈值(比如窗口的 70%),到了就主动触发压缩,留出余量给后续的工具结果。

压缩这件事的难点在于信息损失的不可逆性。你把一段历史压成摘要,如果摘要丢了关键信息,模型后面就会做出错误决策。所以摘要的 prompt 要专门设计,明确要求保留"文件路径、函数名、变量名、已确认的事实"这类硬信息。

3.3 流式解析:边生成边执行的工程挑战

模型返回工具调用时,参数是流式生成的。你不能等整个 JSON 生成完再解析,因为那样用户会看到长时间的卡顿。但流式解析 JSON 又很麻烦,因为 JSON 是不完整的。

Claude Code 这里的处理方式是:流式展示文本,但工具调用等完整后再执行。也就是说,模型输出的自然语言部分可以实时渲染给用户看,但工具调用的参数要等 stop 信号到了才解析执行。这个取舍很合理——工具执行本身有副作用,不能执行到一半发现参数还没生成完。

流式解析还有个坑是多工具并发调用。模型可能一次返回多个工具调用,这些调用之间可能有依赖(比如先读文件再改文件),也可能没依赖(比如同时读三个文件)。Claude Code 对无依赖的调用会并发执行,有依赖的串行。判断依赖关系靠的是模型给的顺序和工具语义,这块逻辑不复杂但容易写错。

// 流式事件处理的大致结构 for await (const event of stream) { switch (event.type) { case 'text_delta': ui.appendText(event.text); break; case 'tool_use_start': pendingTools.push({ id: event.id, name: event.name, input: '' }); break; case 'tool_use_delta': pendingTools.at(-1).input += event.partialJson; break; case 'tool_use_stop': // 此时 input 才是完整 JSON,可以解析执行 break; } }

3.4 权限系统:安全的第一道闸

Agent 能执行命令、能改文件,这意味着它有能力搞破坏。权限系统的设计直接决定了这个工具能不能放心用。

Claude Code 的权限模型我理解是分级 + 白名单。有些操作是只读的(读文件、搜索),默认放行;有些操作有副作用(写文件、执行命令),需要确认或者预先授权。用户可以配置哪些命令免确认,比如git status这种只读命令可以放行,rm -rf这种必须拦。

这里的设计精髓是权限判断发生在工具执行之前,而不是之后。很多自己做 Agent 的人会先执行再看结果,这是危险的。正确的顺序是:模型请求工具 → 权限层判断 → 通过则执行,不通过则返回拒绝信息给模型 → 模型调整策略。

拒绝信息回传给模型这点很重要。模型收到"用户拒绝了这个操作"之后,会尝试别的路径,而不是卡死。这比直接报错终止要智能得多。

4. 实操复刻:从零搭一个最小可用的 Agent CLI

4.1 环境准备与依赖选型

如果你想复刻一个类似的 CLI,我建议的技术栈是:Node 18+、TypeScript、zod 做参数校验、commander 或 yargs 做命令行解析、chalk 或 picocolors 做终端着色。模型 SDK 用官方的就行。

npm init -y npm install typescript tsx zod commander chalk npm install -D @types/node npx tsc --init

tsconfig 里重点开这几个:"target": "ES2022"、"module": "ESNext"、"moduleResolution": "bundler"、"strict": true。strict 一定要开,Agent 代码里类型错误往往对应运行时的逻辑漏洞。

4.2 工具接口的统一抽象

先定义工具的接口,这是整个系统的地基:

interface Tool<T extends z.ZodType> { name: string; description: string; schema: T; requiresApproval: boolean; execute(params: z.infer<T>, ctx: ExecContext): Promise<ToolResult>; } interface ToolResult { success: boolean; content: string; truncated?: boolean; }

requiresApproval这个字段就是权限分级的入口。只读工具设 false,写操作设 true。ExecContext里带上工作目录、超时配置、取消信号这些东西。

工具注册表用一个 Map 存,启动时把所有工具注册进去,调用时按 name 查找。这样加新工具只需要写一个文件然后注册,不用改核心逻辑。

4.3 Agent 主循环的实现细节

主循环的骨架前面给过了,这里补充几个实操要点。

超时控制。工具执行必须有超时,尤其是命令执行。我一般设 30 秒,长任务让模型自己决定要不要后台跑。超时后要 kill 子进程,不然会留下僵尸进程。

错误处理。工具执行失败不要直接抛,要包装成 ToolResult 返回给模型。模型看到错误信息会自己调整,比如路径写错了它会重试。只有系统级错误(比如模型 API 挂了)才终止循环。

循环上限。一定要设最大轮次,比如 50 轮。我见过模型陷入死循环反复读同一个文件的情况,没有上限会一直烧 token。

const MAX_TURNS = 50; let turn = 0; while (turn++ < MAX_TURNS) { const response = await callModel(context, tools); if (response.stopReason !== 'tool_use') return response.content; const results = await Promise.all( response.toolCalls.map(async (call) => { const tool = registry.get(call.name); if (!tool) return { success: false, content: `未知工具: ${call.name}` }; const parsed = tool.schema.safeParse(call.input); if (!parsed.success) { return { success: false, content: `参数错误: ${parsed.error.message}` }; } if (tool.requiresApproval && !await askUser(call)) { return { success: false, content: '用户拒绝了此操作' }; } return await withTimeout(tool.execute(parsed.data, ctx), 30000); }) ); context.push({ role: 'assistant', content: response.content }); context.push({ role: 'user', content: formatResults(results) }); }

4.4 上下文管理的落地策略

上下文管理我建议分三步走。第一步先做工具结果截断,这是性价比最高的,几行代码就能防止上下文爆炸。读文件超过 500 行就截断,命令输出超过 2000 字符就截断,截断时明确告诉模型"还有更多内容"。

第二步做token 计数。用 tiktoken 或者简单的字符数估算(中文约 1.5 字符/token,英文约 4 字符/token)。每次往上下文加东西前算一下总量,超过阈值就触发压缩。

第三步做摘要压缩。把最早的一批消息交给模型,让它总结成一段话,替换掉原始消息。摘要 prompt 要明确要求保留路径、函数名、决策结论。

注意:压缩后一定要在上下文里留一条系统消息说明"早期对话已压缩",否则模型会困惑为什么历史突然断了。

5. 踩坑实录与常见问题排查

5.1 工具调用参数解析失败怎么办

这是最高频的问题。模型生成的 JSON 有时候会带 markdown 代码块标记,有时候会多一个逗号,有时候字段名拼错。我的处理顺序是:先尝试直接 JSON.parse,失败则剥离代码块标记再 parse,再失败则用宽松的 JSON 修复库,最后还失败就把原始字符串和错误一起回传给模型让它重新生成。

实测下来,加了"回传错误让模型重试"这一步之后,工具调用成功率从 85% 左右提到了 97% 以上。模型看到自己的错误往往能立刻修正。

5.2 模型陷入死循环的识别与打断

死循环的典型表现是:反复调用同一个工具、参数几乎一样、连续三轮以上。识别方法是记录最近 N 次工具调用的 (name, input) 哈希,如果连续重复就打断,往上下文里插一条提示"你似乎在重复操作,请换一个思路"。

还有一种隐蔽的死循环是"读文件 → 改文件 → 读文件验证 → 发现没改对 → 再改",这个不算严格重复,但也在空转。这种情况靠轮次上限兜底。

5.3 命令执行的安全边界

命令执行工具是最危险的。我的做法是维护一个危险命令黑名单,匹配到就强制要求用户确认,不管用户之前有没有授权过。黑名单包括删除类、权限修改类、网络下载执行类命令。

另外,命令执行的工作目录要锁定在项目根目录内,禁止cd ..逃逸。子进程的环境变量也要清理,不要继承敏感信息。

问题现象可能原因排查方向
工具调用一直失败参数 schema 太严放宽可选字段,检查 description 是否清晰
上下文增长过快工具结果未截断加截断逻辑,检查是否有大文件读取
模型不调用工具工具描述太模糊重写 description,加使用示例
响应卡顿流式解析阻塞检查是否在等完整 JSON 才渲染
权限确认太频繁白名单没配把只读命令加入免确认列表

5.4 流式输出与终端渲染的冲突

终端渲染流式文本时,如果同时要显示工具调用状态,容易出现光标错位、内容覆盖的问题。我的经验是用一个渲染队列,所有输出先入队,由一个渲染器统一刷新,避免多处直接写 stdout。工具调用的 spinner 和文本输出要分开区域,不要混在一起。

6. 从源码里学到的 Agent 设计原则

拆完这套源码,我最大的收获不是某个具体实现,而是几条设计原则,这些原则放到任何 Agent 项目里都成立。

原则一:工具是能力边界,描述是使用说明书。你给模型什么工具,它就能做什么;你怎么描述工具,它就怎么用。花在工具描述上的时间,回报率远高于花在模型调参上。

原则二:上下文是稀缺资源,要像管内存一样管它。主动压缩、及时截断、保留硬信息。别指望模型能记住所有东西,它记不住。

原则三:权限检查前置,拒绝信息回传。安全不能靠模型自觉,要靠系统拦截。拦截之后要告诉模型为什么,让它自己找替代方案。

原则四:循环要有边界,错误要可恢复。最大轮次、超时、重试上限,这些兜底机制一个都不能少。Agent 跑飞了比不跑更可怕。

原则五:流式是体验,完整是正确。展示可以流式,但执行必须等完整。这个边界要划清楚。

我自己后来用这套思路做了一个内部用的运维 Agent,把工具换成查日志、看监控、重启服务,跑了一个多月,稳定性比预期好。核心就是上面这几条,没有玄学。

最后分享一个我踩过的坑:一开始我把所有工具都设成需要确认,结果用户被确认弹窗烦到直接关掉工具。后来改成只读工具默认放行、写操作才确认,使用率立刻上来了。权限设计要在安全和体验之间找平衡,太严和太松都是失败。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 16:49:19

实时数字人交互设计:从语音合成到手持硬件的完整方案解析

说实话,这一轮实时数字人的迭代速度,比我预期的快得多。Meta这次把Muse Realtime Avatar和手持硬件Muse Charm放在一起发布,再加上自家那套Gemini 3.8 Flash TTS做语音底层,基本等于给了业界一套"从声音到表情再到硬件载体"的完整参考设计。以前我们做AI分身,总在纠结…

作者头像 李华
网站建设 2026/11/2 16:48:21

游戏化思维如何重塑职业发展路径

1. 项目概述&#xff1a;当职业发展遇上游戏化思维 第一次看到"工作的未来是游戏"这个说法时&#xff0c;我正在咖啡馆修改简历。邻座两位年轻创业者的对话飘进耳朵&#xff1a;"我们团队上周的OKR完成度用经验值系统展示后&#xff0c;开发效率提升了40%...&qu…

作者头像 李华
网站建设 2026/11/2 16:47:34

AI 日报:多智能体协作跑进生产环境,工程实践成行业主旋律

又到写 AI 日报的时间了。今天整理热搜词的时候&#xff0c;最直观的感受是&#xff1a;Agent 相关的讨论明显比上周更密&#xff0c;而且很多已经不是概念层面的争论&#xff0c;已经有团队把多 Agent 协作跑进了生产环境。与此同时&#xff0c;视频内容侧的“AI 短剧迟早要出…

作者头像 李华
网站建设 2026/11/2 16:47:18

模数共振与智能体工厂:工业AI落地的四大硬标准

1. 什么是“模数共振智能体工厂”&#xff1f;——先别被名字唬住&#xff0c;它解决的是产线里最真实的痛“工业 AI 新风口&#xff1a;模数共振智能体工厂成果验收全套标准”这个标题乍看像一串技术黑话拼贴&#xff0c;但拆开来看&#xff0c;每个词都踩在当下制造业升级的命…

作者头像 李华
网站建设 2026/11/2 16:42:29

源荷储协同优化在主动配电网中的实践与关键技术

1. 项目概述与背景 "源-荷-储"协同优化调度是主动配电网领域的核心研究方向。随着分布式能源渗透率不断提高&#xff0c;传统配电网正经历从"被动消纳"向"主动调控"的范式转变。我们团队在某工业园区微电网项目中&#xff0c;通过构建多时间尺度…

作者头像 李华
网站建设 2026/11/2 16:41:27

MiMo-V3新架构深度解读:DeepSeek技术融合与本地部署实践

昨天还在给 MiMo-V2.6 调 prompt&#xff0c;想着把工具调用的稳定性再往上拉一拉&#xff0c;结果今天模型团队直接扔出了 MiMo-V3 新架构&#xff0c;而且更新日志里明晃晃地写着“引用 DeepSeek 多项成果”。做过本地部署的朋友应该都能理解这种心情&#xff1a;以为终于能喘…

作者头像 李华