Claude Code快速窥探:原来内核就是一个 while 循环外面套了八层壳

最近借助codex把Claude Code源码看了一遍,共51 万行 TypeScript,1902 个文件。拆完它的核心,其实就是一个 while 循环:调模型、跑工具、把结果塞回去,再调一次。

真正的活儿全在循环外面。权限、压缩、并发、上下文组装,一层层壳套在外面,它才敢放手让 AI 直接敲你电脑上的 Bash。

整个系统我掰成九块,每块都给你一句能记住的、一个能照着抄的设计。看完你就会知道:它没有魔法,只是把每个螺丝都拧得很认真。

一、先看下全景:这 51 万行都花在哪儿了

拆之前先有张地图。按行数排:

模块 大概行数 干啥的
utils 18 万 权限、Bash 安全、消息处理、git、MCP 这堆基础设施
components 8 万 React 终端 UI(权限弹窗、diff、消息渲染)
services 5.4 万 API 调用、压缩、MCP 客户端、分析、OAuth
tools 5 万 40 多个工具(Bash、FileEdit、Agent、MCP...)
commands 2.6 万 90 多个斜杠命令
ink 2 万 自研的 Ink 分叉(终端里跑 React)

说人话:纯干活的工具只占一成,基础设施和 UI 吃掉了一半多。这点就值得停一下------很多人写 agent 第一反应是「先把工具做全」,看完这个比例你会改主意:安全/UI/上下文才是真正吃工量的地方,工具反而是最不神秘那块。

通读下来,有五条主线贯穿全局,我后面每块都会回到它们:

  1. 工具即能力边界:agent 能干啥,完全由它挂了哪些工具决定,没有后门。想读文件?得用 FileRead。想敲命令?得用 Bash。新增能力 = 新增一个工具,不是写段特殊逻辑。
  2. Fail-closed 默认:一切跟安全沾边的默认值,都往最保守了取。宁可慢一点,也不赌不冲突。
  3. 上下文工程 > 提示词工程:不是写一段「你是谁」塞给模型完事,是每轮对话都精心组装一整套上下文------分段缓存、动态注入、多层压缩。
  4. 可组合:子 agent 直接复用主 agent 的查询函数,MCP 工具复用内部权限检查。一个轮子用到死。
  5. 编译时消除 > 运行时判断 :用 Bun 的 feature() 宏,没启用的功能在打包时整段删掉,bundle 里压根不存在。

你先把这五条记着,它们是后面所有的「为什么」。

二、Agent Loop:心脏就是个双层循环

文件在 QueryEngine.tsquery.ts 这些。我先讲最容易看走眼的一点:它的循环不是一个 while(true),是两个

外层 QueryEngine 管「会话」:多轮状态、对话记录落盘、对外 SDK 协议适配、累计用了多少 token。内层 queryLoop 管「单轮」:调一次 API、执行这轮的工具、错了怎么恢复。两层之间靠一个东西连起来------AsyncGenerator

为什么是生成器不是回调?你想想,模型一轮可能吐回来好几个工具要你跑,跑完又接着吐,这是条消息河。生成器三个好处:

  • 背压 :调用方按需 next(),不会被消息淹没;
  • 中断 :.return() 一调,嵌套的生成器全部级联关闭,取消操作自然就传下去了;
  • 流式组合:子 agent 的执行函数本身也是个生成器,直接嵌进父 agent 的流里,天生的。

内层 queryLoop 这个循环,每次迭代就是「调一次 API + 跑一轮工具」。退出靠两种结果:要么终止(返回退出原因),要么继续(把状态赋上、continue 跳下一轮)。

它没用 enum 做显式状态机,而是拿一个 State 结构体一路传。聪明的地方在注释里写明的动机:用一个完整的 State 赋值,替代一堆零散的变量赋值,逼着每个「继续」的代码点都把所有状态显式声明一遍------怕的就是漏。

整个循环号称有 7 条恢复路径、10 种终止条件。光这两组数字,你就知道它处理过上多少种「模型抽风」的奇葩现场。

内核其实就这么个东西,你照着抄就够起步:

typescript 复制代码
// 一个能跑起来的最小 agent loop,抛掉所有壳
async function* loop(messages, tools) {
  while (true) {
    const reply = await callModel(messages, tools);
    yield reply;                       // 把模型回复交给调用方
    if (reply.stopReason !== 'tool_use') return;  // 不用工具就结束
    const results = await runTools(reply.toolCalls);
    messages = [...messages, reply, ...results];  // 回填结果,下一轮
  }
}

就这么点。外面那八层壳,才是 51 万行的大头。

三、上下文那条管线:从轻到重,能省则省

这是我最想单独拎出来夸的一块。每轮调模型前,历史消息都要过一条多级处理管线。它的设计铁律三字:从轻到重

便宜的操作先做(纯本地、不花 token),贵的操作放最后(要调一次 API 来搞)。顺序大致是:本地裁剪 → 一些轻量摘录 → 最后才轮到 AutoCompact(全量摘要,最贵)。

为啥不直接上来就 AutoCompact?因为那玩意要专门调一次模型生成摘要,贵,而且一压就把细粒度的原始上下文给糊平了。前面的轻量闸门如果已经腾出足够空间,AutoCompact 根本不用触发。

阈值我记了一下:有效窗口 = 模型上下文窗口 − max(最大输出 token, 20000),触发点再往下留 13000 的缓冲。200k 窗口的模型,大概撑到 167k 才动手压。

还有个工程细节很真实。AutoCompact 带断路器:连续失败 3 次就不重试了。注释里直接甩了数据------「全球每天有个 1279 个会话出现 50 次以上连续失败,最猛的一个干了 3272 次,一天浪费约 25 万次 API 调用」。说白了,放大规模部署里,小概率的异常路径也能烧掉一笔巨款。这告诉所有写 agent 的人:异常路径必须有上限,否则它在生产里就是隐形烧钱机器

四、流式跑工具:并发不是「能跑就跑」,是「声明了才跑」

模型一轮里可能同时甩回来俩读文件、一个跑命令。怎么执行?在 Claude Code 里有个默认的高性能模式叫流式执行器:模型边吐、它边跑,不等整段收完。

这里头的并发控制,我读得最仔细,值得单独讲。

每个工具有个方法叫 isConcurrencySafe(input),自报家门:我能不能跟别的工具并行。规则是这样的:

  • 连续几个都声明「我能并行」,它们组成一个并行分区,里头一起跑;
  • 一旦撞上一个「我不能并行」的,前面那个分区收尾,从它这里开个新分区,分区之间串行

你想想这是为啥------FileRead 读只读,俩一起读谁也不碍谁,安全。FileEdit 不行,两个并行编辑同一个文件的不同位置,行号立刻错位、互相覆盖,灾难。所以「能不能并行」不是看心情,是看这工具会不会动共享状态。

一个 fail-closed 的小细节:如果 isConcurrencySafe 这个调用自己抛异常了(比如输入解析失败),默认当它不安全,降级到串行。宁可慢,不冒冲突的险。这就是前面那条「Fail-closed」落到代码里的样子。

五、工具系统:30 多个方法、40 个字段,工具不是纯函数

工具这层,核心接口是一个泛型类型,挂着 30 多个方法,能分成六个功能组(描述、输入校验、权限、执行、并发声明、结果回填这些)。

我挑两个最反直觉的点讲。

第一,默认值是「往死里保守」。 工具用工厂函数构建,给的默认假设是:不能并发(isConcurrencySafe: false)、会写东西(isReadOnly: false)、要更严的权限。但有个微妙的反向操作:权限检查这个默认是放行 (checkPermissions: allow)------因为外头还有好几层权限网兜着,工具自己别越权判断。

你看这平衡:工具自身属性保守(怕出事),权限判断宽松(交给外面)。各司其职。

第二,工具不是纯函数,它要拿 40 多个字段的运行时上下文。 举两例你就会懂:

  • 文件读取状态缓存------因为「不能编辑没读过的文件」这条规矩要靠它查;
  • 取消信号------Bash 跑条长命令,用户要中断,你得能立刻杀掉。

工具的结果对象里还有个叫 contextModifier 的字段,挺精巧:有些工具跑完要改后续工具的上下文(比如切个工作目录),但又不能直接动全局状态。contextModifier 给了个受控通道做这事,而且只对非并发的工具生效------并行的工具不能互相改上下文,会乱套。

六、BashTool:18 个文件只为安全地敲一行命令

整个工具系统最复杂的就是 Bash,18 个文件全在解一道矛盾:shell 命令的表达力几乎无限,安全约束却必须严丝合缝

提三个我觉得可以直接抄的设计。

复合命令要逐个子命令检查。 cd /tmp && rm -rf stuff,这种 && 连起来的命令,不能只看前面那个 cd 就放行。系统先用 tree-sitter 把命令解析成 AST,拆出每个简单命令,挨个跑权限检查,任何一个被拒,整条命令拒绝。子命令数量还封了顶(50 个),超了直接找你确认------防的就是写个超长命令把权限检查拖死。

只读白名单精确到 flag 的值。 它不光查命令名,还查每个 flag 的值类型。比如 xargs -I-i 这俩 flag 看着像,但 -i 的 GNU 实现参数是可选的,能被人拿来做命令注入。白名单为每个 flag 注册了「允许什么类型的值」,精确到这个粒度才挡得住 flag 注入。说白了,安全这种事,粒度就是壁垒。

沙箱是安全网,不是免死金牌。 沙箱限制了文件系统的读写路径、能连的网络主机、能开的 Unix socket。沙箱内的命令、就算没匹配任何 allow 规则也放行------但 deny 和 ask 仍然优先。意思是它默认「闭着眼也安全」,但你要是明说「这个不许碰」,那它照拦不误。

另外配套的注入检测有 25 项以上,覆盖命令替换 $(...)、反引号、进程替换 <(...)、参数替换 ${...}、Zsh 特有的危险命令、控制字符、Unicode 里的隐形空白字符......能想到的偷渡手段基本都堵了。

七、权限:六挡信任旋钮 + 一条多层评估流水线

工具系统管的是「agent 能干啥」,权限管的是「它被允许干啥」。这是整套系统最该琢磨的地方------怎么在「让 AI 高效干活」和「防止 AI 把你东西搞砸」之间找平衡

七.1 六挡信任旋钮

它不是简单的开/关,是一条从「完全不信任」到「完全信任」的连续谱:

模式 行为 啥时候用
plan 只能规划,一个写操作都不准做 探索、代码审查
default 每个工具调用都弹确认 日常开发(默认就是这个)
acceptEdits 工作目录里的文件编辑自动放,其他还得问 信得过 AI 帮你重构了
auto 分类器自己判断操作安不安全 高信任场景
bypassPermissions 跳过所有权限检查(除了几个硬编码的) 受控环境、紧急修复
dontAsk 把所有「询问」直接当「拒绝」,自己跑、遇阻就跳过 全自动 CI/CD

有意思的是:谁选了 bypass,系统都留着远程刹车的能耐。靠一个特性门控(Statsig 那类),真出了严重安全漏洞,Anthropic 能远程把所有人的 bypass 模式降级。这就是「我相信你,但保留掀桌的权力」。

七.2 拿 rm -rf / 走一遍完整流水线

每次 AI 要调工具,这条权限评估管线按顺序跑一遍。我拿最经典的 rm -rf / 追一遍:

几个设计我读过想拍手的:

  • 你明说的「要问我」永远优先于 bypass。 你配了 ask: ["Bash(npm publish:*)"],就算在 bypass 模式下,跑 npm publish 还是弹确认。哲学很清楚:bypass 是「我信你的大致判断」,ask 是「这个点我要亲自过目」,后者优先级更高。
  • 不管啥模式,有几条线绝不放行。.git/.claude/.vscode/、还有 .bashrc/.zshrc 这类 shell 配置的写操作,就算 bypass 也得弹确认。硬编码,谁也覆盖不了。因为这几个文件一动,整个开发环境的安全基础就晃了。
  • auto 模式防「钻牛角尖」。 分类器连续拒绝 3 次、或累计拒绝 20 次,系统就从「自动拒」降级成「弹框问你」。防的就是 AI 陷入「试了被拒、换种方式再试、再被拒」的死循环。headless 模式下达到上限直接抛异常终止整个 agent。

八、多 Agent:不是一窝蜂乱撞,分层分得很清楚

任务太复杂------比如「重构这个模块并顺手把测试写了」------一个 agent 在读码、改文件、跑测试之间来回切,上下文窗口很快就塞满。多 Agent 就是干这个的:把任务拆了,并行跑。

它分了三层,边界很清楚:

  • Subagent:最轻。父 agent 派个儿子去做「帮我搜一下」这种小事。
  • Team / Swarm:成员之间能互相通信,有 leader / teammate 角色之分,适合「前后端一起开发」。
  • Coordinator:纯编排。coord 自己不碰文件,所有真活儿全甩给 worker,适合大规模并行。

统一入口也讲究:不管哪种协作,全靠同一个 Agent 工具触发,靠参数组合切不同模式。模型只用学会一个工具,认知负担小。

几个我读完觉得有意思的:

Explore 这种内置子 agent,专门薅 token 羊毛。 它用最便宜的模型干搜索活,还刻意省掉 CLAUDE.md(搜索 agent 不需要那套提交/PR/lint 规矩)和 git 状态(只读 agent 要啥 git 状态)。注释提了一句:每周 3400 万次调用,这两个剪裁省下约 5--15 G tokens/周。大规模部署里,一次省几千 token 累积起来就是个天文数字。

Verification 子 agent 是对着 LLM 弱点设计的。 它的 system prompt 特别长,明确列出 LLM 常见的「假装验证过了」的套话------「代码看起来对」「测试都过了」------逼着每条检查都得有实际跑过的命令和输出。默认后台异步跑,不堵主 agent。说白了,这是拿工程手段治 LLM「自欺欺人」的毛病。

Fork 子 agent 是为 prompt cache 量身定做的。 fork 继承父 agent 的完整历史,只为给 cache 续命。它构建消息时:把父的完整 assistant 消息保留,给每个已用工具生成一模一样的占位结果,只在最后追加一条「这是给这个儿子的专属指令」。这么一来,前面 N 条字节完全相同,几个 fork 并行启动共享同一份 cache 前缀,命中率高到飞起。防递归也做了两重检查(身份标记 + 消息里扫标签)。

Team 两种后端,按场景挑。 一种是基于 tmux / iTerm2 面板的(独立进程,隔离强、每个 agent 自己一个终端面板,崩溃互不影响,但贵);一种是进程内的(共用一个进程,便宜,但一个崩可能连累一片)。交互式开发用前者,SDK / headless 强制用后者。上层通信统一成一个 TeammateExecutor 接口,底层是文件邮箱还是内存通信,上层不操心。

Coordinator 有句话必须记下:「永远不要委派理解」。 错的写法是把任务囫囵转给 worker:「根据你的发现修掉这个 bug」。对的写法是 coord 自己先把发现综合成精确指令:「修掉 src/auth/validate.ts:42 那个空指针,Session 过期时 user 字段是 undefined......」。因为 worker 是从零开始的,没有 coord 的对话上下文。coord 的核心价值就是「综合」------把多个 worker 的发现捏成一条精确执行指令。

九、System Prompt:不是写一段话,是动态拼一台上下文

Claude Code 的 prompt 工程,不是写段「你是谁」塞给模型完事,是个精密的动态组装系统。核心心法还是那四个字:上下文工程

九.1 分段缓存:prompt 也做 memoization

System prompt 不是一个字符串,是个 string[] 数组,每段是独立的一块。这么干图的是 prompt cache------API 能对 system prompt 前缀做缓存,免得每轮都重算。

一个 DYNAMIC_BOUNDARY 标记把 prompt 劈两半:前半静态区用「全局缓存域」(跨所有用户共享),后半动态区不能跨用户缓存(装的是会话特有的东西)。大规模部署里,全局缓存意味着所有用户第一轮对话都能命中同一份缓存的静态前缀------直接砍 API 成本和首响延迟。

还有个骚操作:叫 DANGEROUS_uncachedSystemPromptSection,名字里带 DANGEROUS 是故意的,逼你每次用都得填个理由参数解释为啥必须破缓存。函数签名自己就是一道审查关。「代码即文档」玩到这份上。

九.2 静态区立的是规矩,专治 LLM 的臭毛病

静态区定的是 agent 的核心行为规范,这里头的规则看着普通,实则条条对着 LLM 的已知臭毛病:

  • 最小化原则:反复强调「别过度」------别加没要求的功能、别给假想的未来设计、别造一次性抽象。原话大意:三行相似代码好过一个过早的抽象。这是治 LLM 「过度工程化」倾向的药。
  • 授权不传递 :用户批了一次操作,不代表他以后在所有场景下都批。许可一次 git push,不等于以后 git push 都自动放行。治的是模型「过度泛化」。
  • 优先用专用工具 :能用 Read 就别用 cat,能用 Edit 别用 sed。不光是体验,更是安全------专用工具自带权限检查,Bash 的权限检查复杂一万倍。

九.3 动态区:会话特有的东西往里塞

CLAUDE.md 的收集就很讲究:从项目目录往上层层遍历,把每一级的 CLAUDE.md 都捡上、合并、注入。--bare 模式跳过自动发现,但还是认 --add-dir 你显式指定的------它的语义是「bare 意味着跳过我没要求的东西,不是忽略我要求的」。

git 状态是会话开始时并行抓五样东西塞进去:当前分支、主分支、status、最近 5 条 commit、用户名。status 输出超 2000 字符就截断,顺手提醒模型「用 Bash 拿完整版」。给够初始判断的料,但不浪费 token 装可能用不上的细节。

九.4 压缩:四层,从轻到重

对话一长,窗口就紧。它用四层压缩,还是老规矩------从轻到重

最贵那层 AutoCompact 生成摘要时,被明确要求保留九类信息。最值得记的一条:「所有用户消息」必须原文保留(不是工具结果的那些)。这是治 LLM 压缩时容易丢用户反馈的毛病------要是压缩时把「用户说了别用 Redux」这条丢了,后面模型大概率又把 Redux 引回来。你看,Claude Code 这种「反遗忘」设计,全是这么一条条拿 LLM 弱点怼出来的。

十、终端 UI:它在自己 fork 一套 React,在终端里跑

这块很多人会跳过,我觉得反而该看一眼------Claude Code 的终端界面是个完整的 React 应用 。用 <Box><Text> 描述界面,框架负责算布局、吐 ANSI、diff 优化输出。它用的是 Ink 的深度分叉,核心子系统几乎全重写了。

挑两个我觉得学到了的:

纯 TS 重写了 Yoga 布局,替换掉原版的 WASM。 好处没想得那么简单:没 WASM 加载延迟(原版得 await loadYoga()),没有线性内存只增不减的问题,调试还更容易。性能跟够了,可维护性翻倍。很多时候「去掉 WASM」反而是更聪明的工程选择。

渲染做了 Blit 优化。 一个节点如果没变脏、位置/尺寸也没动,直接从上一帧的屏幕缓存整块拷过来,整棵子树都不用遍历。结果就是:稳态帧(就是那种转圈圈的 spinner、跳动的时钟)的渲染成本,只跟「变化那块」成正比,跟屏幕多大无关。

这套终端 UI 大概 2 万行、90 个文件,做得极其认真。光为了少闪一下,就上了同步更新包(BSU/ESU)、双缓冲、行缓存。这是那种「我把每个螺丝都拧紧」的工程范儿。

十一、MCP:外部工具的标准接入口

MCP 那块相对标准------四层架构,把外部服务挂进来当工具用。几个决策点:

  • 配置从 6 个来源合并,企业策略层能限制哪些 MCP 服务能用;
  • 每个 MCP 服务的工具被包成内部 Tool,名字统一 mcp__<服务名>__<工具名>;
  • 工具发现结果缓存在应用状态里,每轮迭代都刷新,新连上的 MCP 服务下一轮就能用,不用重启会话;
  • MCP 服务器返回认证错误时,有个 McpAuthTool 让模型直接在对话里引导用户走 OAuth,然后重试。闭环。

十二、拆完之后的几句实话

说几句我不那么认同、或者该警惕的。

全局状态是真的大。 那个全局状态对象挂着 200 多个字段,注释里直接写了「别再往这加状态了」。团队知道这是债,但还没找到更好的替代。51 万行的项目,没引入依赖注入或者模块化状态管理,长期看会越来越难维护。

权限系统的认知门槛不低。 八种来源、五---六种模式、三种 shell 匹配模式、多层评估流水线......普通用户想自己配明白,得啃一阵。不过话又说回来,安全这个词天生就贵,这种复杂度大概是要付的代价**。规则遮蔽检测**(你配了互相打架的规则时给你警告)算是把体验往回拽了拽。

BashTool 的安全职责堆太重。 18 个文件、8 层检查、500 行只读白名单全堆在一个工具里。我更愿意看到它把安全检查抽成一个独立的「安全策略引擎」,工具只管跑命令,安不安全交给策略层统一判。现在这写法,BashTool 一个文件出 bug,安全就漏风。

如果让我重新设计,会动两个地方:一个是权限改声明式策略 (类似 OPA / Rego 那种,而不是现在的 if-else 链),更好审计;一个是上下文管理走渐进式淘汰(最近的消息留原文、早一些的留摘要、再早的只留关键事实),而不是现在「全量摘要」和「不压」的二选一。有意思的是,它的 Context Collapse 功能已经在往这个方向挪了。


拆到这儿,你回过头看开头那个 while 循环:调模型、跑工具、塞回去。52 万行代码,围着这五行转。每一层壳------权限、压缩、并发、缓存、UI------都不是炫技,是在回答一个具体的「如果......怎么办」。

所以我最后想说的一句是:

好 agent 不是 prompt 写得好,是工程兜底兜得勤。 每一个「不可能发生」的边界,Claude Code 都配了断路器、降级路径或默认拒绝。这才是它敢让你敲 Bash 的真正底气------不是模型聪明,是那 51 万行壳没偷懒。


学习自 Claude Code v2.1.88 源码,并参考了 learn.shareai.run 学习站,原文链接:learn.shareai.run/zh/

相关推荐
还不秃顶的计科生1 小时前
具身智能论文学习10:π0: A Vision-Language-Action Flow Model for General Robot Control
人工智能·深度学习·算法·机器学习·语言模型·vla·vlm
AIyy8661 小时前
定制化企业网盘深度解析:技术能力、落地场景与产品选型指南
人工智能
微学AI1 小时前
把时间序列真正用起来:TimechoAI 使用与时序分析实战
数据库·人工智能·大模型
GEO_youxuan1 小时前
AI财务分析软件到底是“自动出表“还是“决策推演“?从自动出表到决策推演的能力分层与选型逻辑
大数据·人工智能
IT_陈寒1 小时前
Vite的热更新突然失效,原来我忽略了这个配置
前端·人工智能·后端
怪奇云呼军1 小时前
闪电智能VoiceAgent 如何管理呼入、接听、桥接和挂断状态?
java·前端·网络·数据库·人工智能
云浪1 小时前
Milvus + RAG 实战:《红楼梦》问答助手
前端·人工智能·后端
空堂与归1 小时前
机器学习如何入门?AI/ML/DL概念与建模流程全景
人工智能·机器学习
花花鱼1 小时前
TinyML Agent:MCU 上的微型智能体,AI 智能体下沉的底层实现
人工智能·单片机·嵌入式硬件