上一次的学习笔记:Claude Code 学习笔记 | Palind’s Blog。
这一次主要围绕多 agent 的机制。
主要内容:
- 再看 Claude Code 之前泄露的源码,学习 subagent 与 agent 间交流机制。学习 agent team 机制以及实现。见 多 Agent。
- 顺手搓了一个可视化反代,这样方便实验、方便直接观察 LLM 具体流量,看收到的请求与相应内容。反代的仓库放在 Palind-Rome/agent-wirelens: 反向网关实验台。
- 通过反代实验台,截图记录了真实请求,见 多 agent 实验与截图记录。
- 我很好奇:这种流量与请求如何利用起来?真实的 agent 训练得到多 agent 能力是什么样的?从 hf 上看看 agent 数据集与训练方法。见 真实数据集情况。
Claude Code 中的多 agent
概念
主会话 agent 是用户正在交互的 Claude Code session,持有主消息历史、UI 状态、工具集、权限状态、任务状态,运行主 query() 循环。(query() 见 Agent-Loop)
subagent 是主会话中 Agent 工具启动的 worker。有自己的 ID、消息等。目标是完成一项有边界的任务,把结果交回调用者。
Agent Team teammate 是 team roster 中的命名成员,ID 形如 name@team。和 subagent 的区别在于生命周期与通信协议不同,工作完成后进入 idle,等接收下一条消息,与其他 agent 一起长期运行。通过共享 task list 和 mailbox 与 lead/peer 协调。
最新版不再需要预先调用 TeamCreate,TeamCreate 与 TeamDelete 工具已经移除,team_name 输入也被接受但忽略。
实现
各 agent 状态不同,有各自的 ToolUseContext。

单个 agent
LLM 是无状态的。Claude Code 从本地消息状态重新构造一个完整的 Messages API 请求。主循环保存的是本地消息历史 messages,再用 callModel。调用 Anthropic 的 SDK 往服务器发 JSON。具体笔记我记录在 Agent-Loop 这里。
可以通过反代抓取,看一下发出去的请求,如图:

可以看到构造了系统提示:

在我发的“你好”之前,加入了 system-reminder,并且之后立刻附上 Agent 工具类型的系统提示词:

这里 Agent 工具是多 agent 的重要入口,里面附上了各种能用的 agent。如果主 agent 决定使用,就会出现 subagent。除了 agent 工具以外,还有很多别的工具,比如 bash:

测试一下反代,可以看一眼工具调用的闭环,如图:

工具得到结果后,把结果放进一个 role: "user" 的 message 里发回去:

然后正常得到响应。这里不赘述。
多 Agent
Agent definition
源码在 tools/AgentTool/loadAgentsDir.ts:70-98 里定义一个 agent。有一堆字段,比方说 background: true 就是该 agent definition 默认后台运行。其他字段也是类似的定义。
对于 subagent 与 agent team,他们的 agent ID 与 teammate ID 是两个命名空间。
普通 subagent ID 类似
1 | aa3f2c1b4d5e6f7a8 |
team teammate 类似
1 | team-lead@compiler-research |
显然 teammate ID 更方便被发现与恢复。因为 agent team 相对 subagent 而言,更要得长期持久运行。
Agent 工具
Agent 工具有不少参数,这里跳过。AgentTool.call() 对同一份输入执行三条互斥路由:
flowchart TD
A["Agent(input)"] --> B{"存在 team context/team_name
且传入 name?"}
B -->|是| C["spawnTeammate()"]
B -->|否| D{"显式 subagent_type?"}
D -->|是| E["普通自定义/内置 subagent"]
D -->|否| F{"FORK_SUBAGENT gate 开启?"}
F -->|是| G["fork subagent"]
F -->|否| H["general-purpose subagent"]
E --> I{"shouldRunAsync?"}
G --> J["强制 async"]
H --> I
I -->|否| K["同步 Agent tool_result"]
I -->|是| L["后台 task + 后续 notification"]
可见,teammate 是路由优先的。如果解析出了 team name 并且输入中有 name,AgentTool 不会创建普通 subagent,而是调用 spawnTeammate()。
1 | const teamName = resolveTeamName({ team_name }, appState) |
禁止 teammate 再 spawn teammate,因为 roster 是扁平数组,只有一个 lead,没有嵌套来源关系:
1 | if (isTeammate() && teamName && name) { |
teammate 仍可以创建普通 subagent。
如果没有走 teammate 分支,会看看有没有显式指定 subagent_type,或走general-purpose。普通 subagent 不会自动继承父会话完整对话(感觉大多数情况还是会指定,不然怎么维持 cache prefix)。普通 worker 的工具池会按 worker 自己的 permission mode 重新装配。
createSubagentContext() 完成上下文、隔离工作。
runAgent() 做完初始化后调用主架构中的 query()。
通讯
subagent
同步 subagent
同步 subagent 的结果需要回到父 agent。同步 subagent 的父 agent 当前这次 Agent tool call 一直等待。
子 agent 完成后,finalizeAgentTool() 汇总最后 assistant 内容、token、tool use 次数和耗时。mapToolResultToToolResultBlockParam() 再把它变成协议中的 tool_result。最终内容直接成为这次工具调用的 tool_result。主 agent 下一轮 API 请求会看到该 tool_result。
总之,父模型看不到子 agent 的每一步工具调用。子 transcript 另行保存。
后台 subagent
后台 subagent 需要有完成通知与中途 steering。
后台 subagent 实际有两次向父链路提供信息,启动确认一次,会作为当前 Agent tool call 的同步 tool_result;最终通知一次,完成后异步进入全局队列 pending notification queue,也就是说直接本地队列注入。通知内容是 Claude Code 自己构造的 XML:
1 | <task-notification> |
父 agent 不阻塞。
运行中的 subagent 可能会中途 steering。如果 steering 目标是正在运行的 LocalAgentTask,消息会进入它的 pendingMessages。
Fork subagent
Fork subagent 继承完整父上下文。
Agent Team
team 是一个 lead、多个独立 context window 的 teammates,共享 task list。
至于通信,teammate 间直接 messaging,lead 负责协调和综合。
先用 TeamCreate 建立持久状态。创建 lead ID:
1 | const leadAgentId = formatAgentId(TEAM_LEAD_NAME, finalTeamName) |
写入 team file:
1 | const teamFile: TeamFile = { |
team 相关信息会保存在 .claude 的 config.json。
teammate 的普通文本输出不会自动送给 lead。in-process teammate 每轮工作完成后进入 idle,并不会把最后 assistant response 自动当成 lead 的 tool result。teammate idle 之后,通过 mailbox 给 lead 发送 idle notification;等待新 user message、shutdown、lead message、peer message或可 claim task;收到下一项后再次运行 runAgent()。
这里说的 mailbox 就是成员间通信协议。源码在 utils/teammateMailbox.ts。
mailbox
每个成员有一个 JSON inbox:
1 | ~/.claude/teams/<team>/inboxes/<agent-name>.json |
单条 envelope 是:
1 | type TeammateMessage = { |
多个 agent 可能会同时写一个 inbox,因此对 inbox 获取 proper-lockfile 锁。
SendMessage 工具调用会先尝试 subagent 路由。若不是 subagent,再落到 team mailbox。
Shared task list
每个 task 是独立 JSON 文件。字段太多这里就不列举了。
idle teammate 可以主动认领任务。
源码和理论看完了,现在连上 deepseek API,并启动我的反向代理观察 LLM 请求与响应,做几个实验。
多 agent 实验与截图记录
同步 subagent
我提示 Claude Code 启动一个同步 subagent,观察其发给 LLM 的请求,以及在我电脑上调用的工具:

注意到:

所以是同步 agent。
观察此 agent 的上下文,能看到其不包含任何父节点的上下文:

下一轮 agent loop 中,我的电脑向 LLM 返回了工具的结果,于是此 agent 结束:

主 agent 随机收到 subagent 的结果,此结果被视为工具的 result:

于是 Claude Code 基于此结果向我回答。
后台异步 agent
我提示 Claude Code 启动一个后台 subagent:

结果如图:

作为后台 agent 的 LLM 收到的上下文也符合预期:

主 agent 在创建后台 agent 后,立刻得到消息反馈:

主 agent 随后停止。然后,后台 subagent 完成任务,于是主 agent 收到了自动注入的 prompt:

于是主 agent 回复:

具有父 agent 的上下文的 fork subagent,实验与截图
这个 fork subagent 我可以自己手动用 /subtask 命令调用。注意如果要让 claude code 主动 spawn,就要在 .claude/settings.json 里加入 "CLAUDE_CODE_FORK_SUBAGENT": "1"。
可以看到其具有与主 agent 一样完整的上下文。

Agent Team 与 mailbox,实验与截图
首先要配置 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" 才能启用。
我现在要求 Claude Code 在 lead、A、B 三个成员间发送消息,进行观察。
在启用 agent team 后,主 agent 会成为 lead,并新 spawn 一些 teammate。所有通信都通过 SendMessage 工具。底层存储是 mailbox 文件。mailbox JSON 文件在 ~/.claude/teams/{team-name}/inboxes/{agent-name}.json。

各 teammate 收到的请求如下图,均符合预期:








可以看到 teammates agent 共享 task list,并通过 mailbox 进行交流。
通信时序图
同步普通 subagent
sequenceDiagram
participant U as 用户
participant M as 主 query()
participant S as 模型服务
participant A as AgentTool
participant C as 子 runAgent()/query()
U->>M: 用户 prompt
M->>S: POST Messages
system + messages + tools
S-->>M: SSE tool_use: Agent
M->>A: 本地执行 Agent tool
A->>C: 创建 agentId/context/tool pool
C->>S: 子请求 1
S-->>C: tool_use
C->>C: 本地执行子工具
C->>S: 子请求 2
含子 tool_result
S-->>C: 最终 assistant content
C-->>A: finalizeAgentTool()
A-->>M: Agent tool_result
M->>S: 主请求 2
含 Agent tool_result
S-->>M: 面向用户的综合回答
M-->>U: 输出
后台 subagent + 中途 steering + 完成通知
sequenceDiagram
participant M as 主 agent
participant A as AgentTool
participant T as LocalAgentTask
participant C as 后台 subagent
participant Q as pending queues
participant S as 模型服务
M->>A: Agent(run_in_background=true)
A->>T: registerAsyncAgent()
A->>C: detached runAsyncAgentLifecycle()
A-->>M: tool_result: async_launched + agentId
C->>S: 子请求
M->>Q: SendMessage(to=agentId)
Q->>T: pendingMessages.push(message)
S-->>C: tool_use / response
C->>T: 下个工具轮次 drainPendingMessages()
C->>S: 新请求,包含 steering attachment
S-->>C: 最终结果
C->>T: status=completed
C->>Q: enqueue task-notification
Q->>M: 主 query attachment drain
M->>S: 新请求,包含 task-notification
S-->>M: 读取结果后继续处理
Agent Team
sequenceDiagram
participant U as 用户
participant L as team lead
participant CF as team config.json
participant TF as task files
participant MB as mailbox files
participant W1 as teammate A
participant W2 as teammate B
participant S as 模型服务
U->>L: 请求创建 team
L->>CF: TeamCreate 写 lead roster
L->>TF: 初始化共享 task list
L->>W1: Agent(name=A, team_name=...)
L->>W2: Agent(name=B, team_name=...)
W1->>CF: 加入 roster
W2->>CF: 加入 roster
L->>TF: 创建/分配任务
L->>MB: task_assignment / 普通消息
W1->>S: 独立请求链
W2->>S: 独立请求链
W1->>TF: claim/update task
W2->>TF: claim/update task
W1->>MB: SendMessage(to=B)
MB-->>W2: poll 后取得 peer message
W2->>MB: SendMessage(to=team-lead)
MB-->>L: lead 收到结果
W1->>MB: idle_notification
W2->>MB: idle_notification
L->>MB: shutdown_request
MB-->>W1: shutdown prompt
W1->>MB: shutdown_approved
L->>CF: TeamDelete/cleanup
Subagent 与 Agent Team 的对比
| 维度 | 普通 subagent | fork subagent | Agent Team teammate |
|---|---|---|---|
| 主要入口 | Agent(subagent_type=...) |
fork gate 下省略 subagent_type |
2.1.88:team context + Agent(name=...) |
| ID | 随机 a...16hex |
随机 a...16hex |
确定性 name@team |
| 初始父对话 | 不继承完整 message history | 继承完整有效父历史 | 不继承 lead 对话;接收 spawn prompt 与项目上下文 |
| system prompt | agent-specific | 父已渲染 prompt | session prompt + teammate addendum + 可选 agent definition |
| 工具 | 按 definition/权限重建 | exact parent tools | team 必要工具强制可用,其他按 definition |
| thinking | 普通路径默认禁用 | 继承父配置 | 按 teammate/session 配置 |
| 生命周期 | 有边界的一项任务 | 有边界的一项任务 | 长期,多 prompt,idle 后继续等待 |
| 同步结果 | 直接成为 Agent tool result |
强制后台 | 不自动回 lead |
| 后台结果 | task-notification |
task-notification |
显式 SendMessage + idle notification |
| 中途消息 | pendingMessages,下个 tool round 注入 |
同左 | mailbox;in-process UI 还有 pending user queue |
| 持久化 | sidechain transcript + metadata | 同左,含父 context slice | team config + mailbox + shared tasks + 各自 session/transcript |
| agent 间直接通信 | 不以 peer network 形式开放;主要由调用者协调 | 同左 | teammate 可按名字互发 mailbox 消息 |
| 任务协调 | 父 agent 自己安排 | 父 agent 自己安排 | 共享 task list,可自 claim |
| 运行位置 | 当前进程的独立 query lifecycle | 当前进程 detached async | in-process 或独立 pane/process |
| 完成状态 | completed/failed/killed | completed/failed/killed | 通常先 idle,shutdown 才退出 |
总结
Claude Code 的多 agent 机制与通信:
- 普通 subagent steering:AppState 内存队列 + transcript resume;
- teammate messaging:文件 mailbox;
- background completion:pending notification queue;
- 同步 subagent:Agent
tool_result。
真实数据集情况
我很好奇这种流量请求与真实数据集训练间的差距,想扩展看看。
以 SWE-Gym/OpenHands-Sampled-Trajectories · Datasets at Hugging Face 为例,看看真实的 rollout 情况。

其实轨迹基本都是轨迹统一成 OpenAI 风格的 messages。
SWE-Gym/OpenHands-SFT-Trajectories · Datasets at Hugging Face 筛选了成功轨迹,也是 OpenAI 风格的 messages:

不过 LLM 不是直接读取这些 JSON 进行训练的。messages JSON 需要转为 chat template 文本才能进入 tokenizer,然后对部分 token 进行 mask。
具体来说,举个例子,现在有数据:
1 | { |
以 Qwen3 使用的 Hermes 风格工具协议为例,渲染得到:
1 | <|im_start|>system |
SFT 时,通常构造三个张量:
1 | { |
不同的 token 区域里,情况一般是:
| Token 区域 | input_ids |
labels |
|---|---|---|
| system prompt | 真实 token ID | -100 |
| tools 定义 | 真实 token ID | -100 |
| user 消息 | 真实 token ID | -100 |
| assistant 工具调用 | 真实 token ID | 对应 token ID |
| tool 返回值 | 真实 token ID | -100 |
| assistant 最终回答 | 真实 token ID | 对应 token ID |
一个例子:
1 | 内容: |
-100 的位置仍作为上下文进入 Transformer,但不在这些位置计算训练损失。hf 的 TRL 的 SFTTrainer 就是这样的。
一般来说,有三种 mask:
| Mask | 作用 |
|---|---|
attention_mask |
区分真实 token 和 batch padding |
| causal mask | 禁止当前位置看见未来 token |
| label/loss mask | 决定哪些 token 参与 loss,通常用 -100 排除 |
数据与训练的大致情况如上。