我把 Claude Code 拆成了一间餐厅:从一句话到一次回复,中间到底发生了什么

本文基于公开的 @anthropic-ai/claude-code@2.1.88 源码进行分析。如果你没读过源码、甚至不写代码也没关系------文章前半程不出现任何代码,先用一间餐厅把整件事讲清楚。


开场:先别管代码,看一间餐厅怎么出菜

想象你走进一家很特别的餐厅,点了一道菜。这家餐厅的运作方式,跟你理解的 AI 编程助手,是同一回事。

它特别在哪?这家店的大厨,是个失忆症患者。

这位大厨厨艺天下第一,任何菜他都会做、任何难题他都能判断。但他有个毛病:每做完一件事,挂断电话后,就把刚才的事忘得干干净净。 他甚至不在店里------他在很远的地方,你只能打电话找他。

于是这家店雇了一个接线员 ,坐在店里。接线员不会做菜,一道菜都不会,他唯一的工作就是打电话问大厨,然后照做。店里还有一块大白板,从你进门那一刻起,你说的每句话、每次对话的结果,都被记在上面。

现在,你下单了。看这道菜怎么做出来的:

  1. 接线员先看一眼白板 ,然后拿起电话打给远方的大厨。因为大厨失忆,接线员必须把白板上的内容从头到尾念一遍给他听,不然大厨根本不知道现在是什么情况。念完,问一句:"接下来该干嘛?"

  2. 大厨在电话那头动脑子,给出两种回答之一:

    • 要么说:"菜好了,可以上了。"
    • 要么说:"你先去冰箱第三层拿块牛排、再开火煎一下。"
  3. 如果大厨要求动手 ,接线员就照办------他自己不做菜,但他会去按那些"料理机"的按钮(拿食材、开火、切菜)。做完,把结果写到白板上。

  4. 然后接线员再打一次电话,又把白板上的内容从头念一遍,再问:"接下来呢?"......

  5. 这个"打电话问 → 照做 → 记白板 → 再打电话问"的循环一直转着,直到某一次大厨说"菜好了"。接线员这才把菜端给你。

就这么简单。一道复杂的菜,就是靠接线员反复打电话,一步步做出来的。

这里藏着两个反直觉、但一旦想通就豁然开朗的点:

  • 聪明的不是店里的人,是电话那头的大厨。 接线员从头到尾没出一个主意,他只是个纪律严明的跑腿的。所有"该怎么做"的判断,全在电话那头。
  • 所谓"AI 记得我们之前聊过什么",是个错觉。 大厨啥都不记得。是接线员每次打电话,都把整块白板重新念一遍------是复述,不是记忆。

把这两点记住,你已经理解了 Claude Code 的灵魂。剩下的,全是围绕"这位失忆天才"搭起来的脚手架。


那如果一道菜太复杂,一个大厨忙不过来呢?

真实的活往往没这么简单。比如你点的这道菜,需要先翻遍整个大冷库,把所有能用的食材都找出来------冷库里塞了成百上千样东西,得一格一格翻。

如果接线员亲自去翻、还边翻边往白板上记,会发生什么?白板会被写满。 翻一样记一条、再翻一样再记一条......几百条全堆在白板上。而每打一次电话都要把整块白板念一遍,白板越长,电话打得越慢,最后长到大厨那边根本"听不完"(比如每次打电话只能说1000个字)。

这家餐厅的解法很妙:再开一间隔壁厨房,雇个临时工去干这种脏活。

临时工也有一整套班子------他有自己的接线员、自己的白板、自己电话那头的大厨。他关起门在自己那间厨房里把大冷库翻个底朝天,把自己的白板写得再乱也没关系。干完,他只回主厨一句话:"能用的就这 3 样,在这儿。" 那几百条翻找记录,全留在他自己的白板上,主厨这边的白板干干净净,只多了一句结论。

这就是 subagent(子代理)------它的头号价值不是"分担工作量",而是"分担白板污染"。

而这种"另起一套班子"的合作,按照关系的不同,一共有四种玩法。这四种,是本文后半程的重点,我先用一句话让你有个印象:

  • 外包:喊个临时工,干完就地遣散,只回一句结论。(用完即焚)
  • 分身:不喊外人,而是把主厨连记忆一起复制一个出去干。(带记忆的克隆)
  • 协调者-工人:主厨脱下围裙当"包工头",自己不炒菜,只指挥一队工人。
  • 队友:一群平等的大厨,能互相打电话商量着干。(唯一能横向对话的)

到这里,你已经在脑子里有了一整幅画面:一个失忆大厨 + 一个跑腿接线员 + 一块白板;活太脏就外包给隔壁厨房。 接下来,我们把这幅画面一层层落到真实的代码上------从这里开始会出现文件名和行号,但每一处,你都能对应回刚才那间餐厅。


一、把餐厅对回代码:角色表

先认人。上面那间餐厅里的每个角色,在源码里都有对应:

代码里的东西 餐厅角色 职责 会思考吗
模型(远程 LLM) 失忆的大厨 所有"下一步怎么做"的判断 ✅ 唯一的智慧来源
query.ts 的主循环 接线员 传话、按工具、记账、决定再不再转一圈 ❌ 只跑腿
messages 数组 白板 记录整段对话------大厨唯一的记忆 ---
services/api/claude.ts 唯一那条对外专线 把白板打包发给大厨、解析回话 ---
tools/ 工具 后厨的手脚/料理机 真正读文件、跑命令、改代码 ❌ 按一下自动出活
subagent 临时工 / 分身 另起一套完整班子去干脏活 (是另一个大厨)

记住三个关键角色:失忆大厨(模型)、接线员(query.ts)、白板(messages)。整篇文章都绕着它们转。


二、一道菜的旅程:Agent 主循环(落到代码)

开场那个"打电话转圈"的故事,落到代码里,核心就是 query.ts:307 那个 while(true) 循环。我把一整圈拆成几个动作。

动作 A:打电话前,先看白板塞不塞得下

js 复制代码
// query.ts,主循环开头
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]
// ... microcompact / autocompact 在这之后

打电话给大厨之前,接线员先看白板长不长。因为打电话 = 把整块白板从头念一遍给大厨听(大厨失忆,只能这样),白板太长有两个后果:念不完(超 token 上限被拒接)、念一遍贵得离谱。

所以太长了先压缩:

  • microcompact:小清理,只把最占地方的几条旧工具结果删掉/缩短。
  • autocompact:大打包,把一大段历史概括成摘要。

这里藏着理解 AI agent 的第二把钥匙:所谓"AI 有上下文记忆",本质是每次都把整块白板重新念一遍给一个失忆的大厨听。 不是大厨记得,是接线员每次都复述。

动作 B:打电话(query.ts 里的 deps.callModel

接线员拿起唯一那条对外专线,把整块白板念给大厨。大厨的回话是一个字一个字流式传回来的。

有个聪明细节:大厨话没说完,只要冒出一句"去查冰箱",接线员当场就派人去查 ,不等他挂电话(StreamingToolExecutor)。省时间。

动作 C:接线员这辈子唯一做的判断

大厨说完,接线员只问自己一个机械问题query.ts:1062 附近):

js 复制代码
if (!needsFollowUp) {
  // 大厨没要工具 → 这单可能做完了
}

needsFollowUp 是什么?就是"大厨的回话里,有没有冒出工具调用请求(tool_use)"。有就是有,没有就是没有,读个标志位而已。 这不是判断力------它只是检查大厨要不要动手。这恰好印证了开篇那句:接线员不产生一点智慧。

动作 D:分两条路

没要工具 → 这单做完了,走打烊流程(stop hook、token 预算检查),然后 return { reason: 'completed' }。圈结束。

要了工具 → 真正干活:

js 复制代码
// query.ts 尾部:把这轮的话 + 工具结果,全部续到白板末尾,再回到动作 A
const next = {
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
  // ...
}

这一句"续到末尾再回头",就是【转圈】的字面本体。 白板每转一圈变长一截,所以动作 A 才要压缩------不压,白板迟早撑爆。

一张图钉死这个圈:

css 复制代码
   ┌─────────────────────────────────────┐
   │ A. 白板太长?先压缩                    │
   └──────────────────┬──────────────────┘
                      ↓
   ┌─────────────────────────────────────┐
   │ B. 把白板念给大厨(流式回话)            │
   └──────────────────┬──────────────────┘
                      ↓
          ┌───────────────────────┐
          │ C. 大厨要工具吗?        │ ← 接线员唯一的"判断"
          └─────┬───────────┬─────┘
            没要 │           │ 要了
                ↓           ↓
        ┌────────────┐  ┌──────────────────────┐
        │ 打烊 → 收工 │  │ D. 派工具干活          │
        └────────────┘  │    结果续到白板末尾  ───┼──┐
                        └──────────────────────┘  │
                          回到 A,再转一圈 ←────────┘

为什么这个文件要 1700 行?

逻辑就上面这么点,凭什么写 1700 行?我读下来的结论:真正属于"转圈主干"的不到三成,剩下七成全是"临场救火纪律"。

代码里有个 state 结构体,在 7 个不同的 continue 被改写------意思是"接线员决定再转一圈"有七种理由,正常的"用了工具所以继续"只是其一,其余六种全是救火:

  • 大厨电话占线 → 换备用大厨重打(fallback)
  • 大厨话说一半被掐断(输出超长)→ 让他接着说
  • 白板念到一半发现还是太长 → 当场再压缩重打(reactive compact)
  • 用户按了 Ctrl+C → 把半截活干净收尾
  • 转太多圈 → 强制收工(maxTurns)

所以那句话在代码层面完全成立:接线员的全部本事,是在大厨那通电话的两头,把圈转得又稳又不丢信息。智慧全在电话那头,纪律全在这 1700 行里。


三、Subagent:不是"叫个帮手",是"另起一套班子"

理解了单个循环,就能理解 subagent 了。很多人以为 subagent 是主 agent 手边一个跑腿小弟。大错。

关键在 runAgent.ts:748

js 复制代码
// runAgent.ts ------ subagent 启动时
for await (const message of query({ messages: initialMessages, ... })) {
  // ...
}

看到没?它又调了一次 query() ------就是上面那个 while(true) 转圈机。

所以派一个 subagent,等于在隔壁房间凭空支起一套一模一样的电话亭:里面坐着另一个失忆大厨,配自己的白板、自己的专线、自己的工具、自己的身份 ID。

主班子 一个 subagent
一个接线员 自己的接线员(独立 query 循环)
一面白板 自己的白板 (独立 messages
一条专线 自己的专线
一个大厨 另一个失忆大厨(对主对话一无所知)

为什么要这么重地另起一套? 目的只有一个,也是理解全部四种模式的总纲:

保护主白板的干净。 白板是大厨唯一的记忆,越长越贵、越长越容易爆。一个"读 50 个文件找东西"的活会把白板撑爆------那就外包出去,让临时工在他自己的白板上撑爆,主厨只要那一句结论(prompt.ts:257:"return a single message back to you")。

subagent 的头号价值不是"分担工作量",是"分担白板污染"。 记住这句,四种模式全是它的变体。


四、Subagent 的四种关系模式

同样是"另起班子",按关系拓扑 分四种。区别只在三个旋钮:喂什么系统提示、给不给继承上下文、开放哪些工具权限 。底层都是同一套 query(),能力同构。

模式一:外包(主仆)------用完即焚的临时工

最常见。主厨派一个 subagent,它在自己白板上干完,回一句结论就消失

它的一辈子(runAgent.ts):发工牌(:347)→ 搭全新空白板(:373)→ 配一套重新筛过的工具(:500)→ 写专属岗位说明书(:508)→ 开自己的转圈机(:748)→ 回一句话 → finally 里彻底拆台(:816,关服务、撤钩子、杀掉它后台开的进程、删待办)。

三个亮点:

  1. 省 token 省到砍记忆runAgent.ts:386):Explore 这种只读临时工,出生时主动删掉 CLAUDE.md 和最大 40KB 的 git 状态。据该处源码注释,Explore 一周被派约 3400 万次,砍这一刀每周省下约 50~150 亿 token(数字引自注释原文,未独立复核)。
  2. 权限不泄漏runAgent.ts:469):给临时工配权限时清单清空重设,主厨之前点过的"同意"绝不偷传给它。
  3. "别偷看"是铁律prompt.ts:91):后台临时工干活时不许中途偷看它的草稿------一读,那些过程垃圾就又被搬回主厨白板,外包图的"干净"当场白费。

什么时候用:大海捞针式搜索、开放式调研、要一个"没被你带偏的第二意见"、一批能并行的独立活、又长又脏只要结论的活。

什么时候别用prompt.ts:235):读某个具体文件、找某个具体函数、2-3 个文件的小范围------自己干,别为芝麻小事搭厨房。

模式二:分身(fork)------带着记忆出门的克隆

特殊的主仆:省略工种时,subagent 不是新雇的陌生人,而是主厨的一个克隆 ------继承整面白板、共享缓存(forkSubagent.ts:60FORK_AGENTmodel: 'inherit'tools: ['*'] + useExactTools)。

分身 vs 临时工,核心不是"贵不贵",是"这次要不要那面白板":

  • 要客观清醒、怕被带偏(求证、找茬) → 临时工(白纸)
  • 要懂内情、省得交代(接着你的思路往下干) → 分身(带记忆)

为什么分身便宜 :靠"让一群分身的白板前缀逐字节相同"来共享提示缓存(forkSubagent.ts:99:"all fork children must produce byte-identical API request prefixes")。构造时连"工具占位结果"都用完全相同的占位词(:93),只有末尾的专属指令各不相同(:163)。

省钱带来的副作用要打补丁 :分身原样抄了主厨说明书,而说明书里写着"优先 fork"------分身照读就会无限套娃。于是两道闸:软闸(:171 守则开头吼"STOP,你是分身,别派下属")+ 硬闸(:78 isInForkChild 扫白板里的分身标记,有就拒绝再 fork)。

一句话:省钱是分身的手段和副产品,不是初衷。 分身和外包的初衷相同(护白板),只是适配"要不要前情"这两种相反需求。

模式三:协调者-工人(coordinator/worker)------退居二线的包工头

开一个开关(coordinatorMode.ts:36 的环境变量),主厨的系统提示被整个换掉(:116):

"You are a coordinator. 你的工作是指挥一队工人去调研、实现、验证,然后综合结果。"

协调者不是新程序,就是被换了岗位说明书的主 agent。 它像个包工头------自己不砌墙,只拆活、写精确施工单、派工人、验收综合。

跟主仆的三个关键区别:

  1. 可持续对话 :能用 SendMessage 给同一个 worker 反复追加指令(靠 worker 身上挂的 pendingMessages 待办队列,LocalAgentTask.tsx:162),这才是真正"继续之前的活"。
  2. 异步通知 :worker 干完,结果被拼成 <task-notification>LocalAgentTask.tsx:252)、**伪装成一条"用户消息"**贴回协调者白板(coordinatorMode.ts:144)。

这个"伪装成用户消息"是全系统最巧的设计之一:

协调者的大厨只认识"用户说的话"。于是系统把 worker 的汇报包装成用户角色塞进它白板。对大厨来说,"下属汇报"和"用户发话"长得一样------它不需要理解"下属"这个概念,只需要会读用户消息。复杂的多 agent 协作,被压扁成了大厨本就会的"读对话"。

  1. 并行是超能力coordinatorMode.ts:213):能同时派的绝不串行。

给 worker 配工具:一份全量,过四层筛子agentToolUtils.ts:122 + constants/tools.ts):

复制代码
全量工具池
  → ① 全体 subagent 禁用(Agent 防套娃 / TaskOutput / PlanMode...)
  → ② 自定义 agent 额外禁
  → ③ 异步 worker 只放行白名单(Read/Grep/Bash/Edit... 十几样)  ← 白名单制!
  → ④ 工种自己的黑名单
= worker 实际拿到的工具

注意③是白名单 (默认不给、只给明确安全的),和①②的黑名单逻辑相反------worker 的能力被有意收得比主线窄。 而协调者自己的工具是另一份"只管人不干活"的名单(constants/tools.ts:107:Agent/TaskStop/SendMessage/SyntheticOutput,一个 Bash/Edit 都没有)。

模式四:队友(teammate/swarm)------唯一的横向关系

前三种里,agent 之间要么不说话、要么只跟上级单线联系。队友是唯一能横向对话的------队员 A 能直接给队员 B 发消息,像真人团队。

它是一群长期存活、地位平等的完整 Claude Code 实例 ,各开各的进程(spawnMultiAgent.ts 里 tmux 分屏 / 独立窗口 / 同进程三种后端),靠信箱文件通信。

为什么必须"投文件" :队友是独立进程,内存不共享,A 想跟 B 说话没有共享内存可用,只能靠文件系统当公共黑板(teammateMailbox.ts:4):每个队友一个 .claude/teams/{队名}/inboxes/{名字}.json,别人往里加锁写(:134:165 文件锁防并发),收件人自己轮询读。

系统提示专门追加一段告诉队友(teammatePromptAddendum.ts:15):"光在文本里写回复,队友是看不见的------你必须用 SendMessage 工具。" 因为白板私有,别人读不到,要让队友听见必须显式投信。

但要泼一盆冷水:队友在代码项目里大多是过度设计。

它的设计灵感来自人类团队,可代码项目的协作范式根本不同:

人类团队 代码项目的 agent
信息在哪 各人脑子里,必须开口问 落在文件里,谁都能读
怎么同步 靠对话 靠读同一份代码

人类需要横向对话,是因为知识锁在脑子里;agent 的成果都落在文件系统上、是公开的。 所以"A 问 B 你干了啥"大多能被"A 直接读 B 改的文件"替代------队友的核心卖点被共享文件架空了。它真正不可替代的只剩一个夹缝:协作依赖运行时状态(没法落成文件)、且参与方须长期共存时(比如一个 agent 挂着服务、另一个实时打它)。

这也解释了它至今仍是 EXPERIMENTAL + 带远程 killswitch------适用面窄、成本高(另起进程 + 文件信箱 + 海量维护代码),是四种关系里最拟人、也最少真正用得上的一种。

四种模式速查

模式 谁创造谁 信息方向 子方寿命 能否横向对话
外包(主仆) 主派子 下派↓ + 一次回↑ 用完即焚
分身(fork) 我裂我 下派↓(带全部记忆)+ 一次回↑ 用完即焚
协调者-工人 协调者派工人 下派↓ + 反复差遣 + 异步通知↑ 可反复唤起 否(worker 互不可见)
队友(swarm) 队友拉队友 任意点对点 ↔ + 广播 长期独立存活

五、一条贯穿始终的铁律:白板永不共享

读到这你会发现,四种模式再花哨,都逃不出同一条底层规矩:

每个 agent 都有自己独立的白板(messages),任何两个 agent 都不共享同一面白板。

  • 外包/新雇:白板空白开局。
  • 分身:创建时克隆 主厨白板的快照------注意是拷贝,不是接线(runAgent.ts:373[...contextMessages, ...promptMessages] 新建数组)。克隆完各自生长,主厨后续改动分身看不到buildWorktreeNotice 甚至提醒分身"文件可能已被改过,动手前重读")。
  • 协调者-worker:worker 结果靠"伪装成用户消息"塞回,不是共享白板。
  • 队友:靠"投信箱文件"通信,不是共享白板。

为什么这条不能破? 因为共享白板 = 两个大厨抢同一个脑子。fork 之后主厨和分身各走各的路,白板各自生长,根本不存在一个"最新版"可以同步。所以唯一干净的做法就是:分家那刻拍张快照,之后互不干涉;要沟通就走外部中介(伪装消息 / 信箱文件)。

跨 agent 沟通必须显式(创建时塞入,或事后投信),正是白板隔绝的必然结果------这不是设计选择,是"大厨各有各的记忆"推导出来的硬约束。


六、最后一把钥匙:谁在思考,谁在干活

全文的收尾,也是最容易被搞混、最值得刻进脑子的一点:Claude Code 是 CLI,不是模型。

  • Claude Code = CLI :就是这些 TypeScript 代码,会转圈、会派 subagent、会拼提示词。它本身不思考。
  • 模型 = 会思考的大脑 :在远程服务器上,通过 services/api/claude.ts 的网络专线被调用。

以"协调者模式"为例,它准确拆成三层:

是什么 扮演
Claude Code(CLI) 这些代码 搭台+执行:决定喂哪份提示词、真正 spawn worker、把结果贴回白板
模型 远程大脑 做判断:读了"你是协调者"提示后,倾向于"派 worker 去干"
系统提示 coordinatorMode.ts:116 的文字 剧本

也就是说:是 CLI 让远程模型"扮演"协调者 (喂它一段提示词),但模型只负责"想说什么/想派谁",真正 spawn worker、加锁写信箱、把结果伪装送达的全是 CLI

想的是模型,做的是 CLI。 连"让模型当协调者"这件事,都不是靠写死代码实现的,而是靠一段提示词说服那个大厨。


结语

把整篇文章压成几句:

  1. 智慧是租来的,工程是自己的 。最核心的 query.ts 不聪明,它是纪律;聪明全在电话那头的模型。
  2. 所谓"上下文",是每次把整块白板念给一个失忆大厨听。白板越长越贵越易爆,于是有了压缩、有了 subagent 外包。
  3. subagent 是"另起一套完整班子",四种模式(外包/分身/协调者-worker/队友)是同一目的(护白板)的四种拓扑变体,区别只在"喂什么提示、给不给记忆、开放什么权限"。
  4. 一条铁律贯穿全部:白板永不共享。所以跨 agent 沟通必须显式。
  5. CLI 负责做,模型负责想,两者是两个东西,任何时候别混。

读源码最大的收获,往往不是记住某个函数,而是看清一个反直觉的设计取向。Claude Code 给我的那一下,就是:做一个强大的 AI agent,难点从来不在"让代码变聪明",而在"如何组织一个不思考的脚手架,让那个聪明但健忘的大脑,稳定地把复杂的活一步步干完"。

相关推荐
Harry技术1 小时前
02 · Codex 核心概念:代理、沙箱、审批和项目说明书
人工智能
阿里云大数据AI技术2 小时前
Agentic Memory Extension 支持对接主流Agent - 适用于 Claude Code、CodeX等
人工智能·agent
我唔知啊2 小时前
不是让 AI 写代码,我是在指挥 AI 干活:一套打磨出来的 AI 编程工作流
人工智能
ZzT2 小时前
在 GitHub 上 @一下 claude,它自己把 issue 改成 PR
人工智能·开源
不加辣椒2 小时前
第15章 上下文窗口管理与长文本策略
人工智能
牛奶3 小时前
AI 能赚钱了——但赚的不是你
人工智能·ai编程·nvidia
凌杰4 小时前
AI 学习笔记:研究方法的演变
人工智能
半盏药香4 小时前
由于jinja2的starlette版本过高引发的问题:500 Server Error TypeError: unhashable type: 'dict'
人工智能
阿里云大数据AI技术4 小时前
MiniMax M3、Kimi K2.7 Code来啦!PAI已支持一键部署,开源前沿触手可及
人工智能·agent