本文是「从零理解 Claude Code:20 个 Agent Harness 机制」系列的第 15 篇。
源码仓库:shareAI-lab/learn-claude-code
对应章节:s15:Agent Teams
设想一个前端项目准备迁移构建工具。需要先梳理现有插件配置,再检查路由构建产物,还要跑一遍测试确认改动范围。让一个 Agent 从头查到尾当然能做,只是它会陆续读进很多配置、日志和代码片段,等它回头处理最开始的问题时,上下文已经堆得像一座山那么高了。
s06 的子 Agent 适合处理一次性调查任务:给它一个问题,它在独立上下文里工作,最后交回总结。s15 继续往前走了一步,主 Agent 可以启动多个队友;队友各自处理任务,通过收件箱向主 Agent 汇报,也能在有限轮次内接收其他消息。
这章新增的重点不在于多开几个线程,而在于补上了队友之间传递结果的通道。
一、一次性子任务为什么无法覆盖协作场景
子 Agent 完成任务后,把一段总结交给主 Agent,生命周期基本结束。对于代码检索、定位问题这类任务,这种方式已经够用。
团队协作会遇到更长的过程。
例如主 Agent 让一个队友检查构建配置,让另一个队友验证测试脚本。检查构建配置的人发现某个别名规则可能影响测试,就需要把这个信息交给主 Agent。主 Agent拿到消息后,可能还要补充新的检查要求。这里需要的是一条可以异步投递和接收信息的通道,而不是只在任务结束时返回一次文本。
s15 把角色分成两类:
| 角色 | 负责的工作 |
|---|---|
| 主 Agent | 拆分任务、启动队友、接收消息、继续决策 |
| 队友 Agent | 在自己的上下文中执行分配任务,并把结果写回收件箱 |
教学代码仍然只有一个主 Agent 的消息历史。每个队友启动后会创建自己的消息列表,主 Agent 不会自动看到队友读过哪些文件、执行过哪些命令。双方靠消息文件交换必要信息,避免把所有中间过程塞进同一段对话。
二、队友启动后拥有一段独立对话
主 Agent 调用 spawn_teammate 后,程序会创建一个守护线程,并为队友准备新的系统提示词、消息列表和工具列表。
下面这段代码位于队友线程启动阶段。需要关注的是,队友没有继承主 Agent 的消息历史:
ini
messages = [{"role": "user", "content": prompt}]
sub_tools = [
bash,
read_file,
write_file,
send_message,
]
for _ in range(10):
inbox = BUS.read_inbox(name)
response = client.messages.create(
model=MODEL,
system=system,
messages=messages[-20:],
tools=sub_tools,
)
队友最开始只知道主 Agent 交给它的任务。后续每一轮调用模型时,程序最多保留它最近的 20 条消息。这个限制让队友的上下文保持相对独立,也限制了它携带历史信息的长度。
工具列表同样被收窄了。队友可以执行命令、读取文件、写入文件和发送消息;它没有创建新队友、创建任务、定时调度等工具,这样可以避免队友继续拉起下一层队友。
不过,角色名称本身只是一段系统提示词。
例如传入 tester 或 frontend developer,程序会把它拼进队友的身份描述。代码没有为不同角色配置不同权限。两个队友都能读取和写入工作目录中的文件,也都能执行 Shell 命令。角色帮助模型聚焦工作内容,不能代替权限控制。
三、队友结果怎样写入收件箱
s15 的消息总线很朴素:每个 Agent 对应一个 JSONL 文件。发送消息时,程序向对方的文件追加一行 JSON;读取消息时,程序读取完整文件,再删除文件。
python
def send(self, from_agent, to_agent, content, msg_type="message"):
inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
with open(inbox, "a") as f:
f.write(json.dumps(msg) + "\n")
def read_inbox(self, agent):
inbox = MAILBOX_DIR / f"{agent}.jsonl"
msgs = [json.loads(line) for line in inbox.read_text().splitlines()]
inbox.unlink()
return msgs
例如名为 config-reviewer 的队友完成检查后,会向主 Agent 的收件箱写入一条结果。主 Agent 的收件箱文件位于 .mailboxes/lead.jsonl,其中每一行都带有发送者、接收者、文本内容、消息类型和时间戳。
这套设计有一个明确的消费动作:读取后删除。主 Agent 一旦读取了收件箱,原始文件就被移除,消息不会在下一轮重复出现。
下面可以帮助大家区分队友自己的上下文、共享工作目录和消息收件箱之间的关系。

图中最容易混淆的地方是工作目录。队友的对话上下文互相独立,但读写工具仍然指向同一个工作目录。消息通过收件箱传递,文件修改则直接发生在同一份项目代码里。
四、主 Agent 为什么能在没有用户输入时收到结果
收件箱里有新消息,并不会自动进入模型上下文。本章在主程序中增加了一个轮询线程,每秒检查一次主 Agent 的收件箱是否存在未读内容。
轮询线程只做检测,不会立即读取文件:
scss
if BUS.peek("lead") or has_pending_background():
events.put(("wake", None))
主程序把用户输入和异步通知都放进同一个事件队列。收到 wake 事件后,程序读取主 Agent 的收件箱,把消息拼成一段新的用户消息,再启动一轮 Agent Loop:
bash
inbox = BUS.read_inbox("lead")
history.append({
"role": "user",
"content": "[Inbox]\n" + inbox_text,
})
agent_loop(history, context)
这样设计的好处是,主 Agent 不会同时运行两轮对话。用户输入、队友汇报和后台任务完成通知,都会排队进入同一个事件循环。
假设主 Agent 正在回答用户的问题,此时队友完成了构建配置检查。轮询线程会把唤醒事件放入队列,当前这轮回答结束后,主程序再读取队友结果,开始下一轮模型调用。主 Agent 不需要等用户继续输入,能及时根据队友消息调整后续工作。
当前代码对收件箱内容做了截断处理,每条消息只保留前 200 个字符后写入消息历史。队友发来一段较长的测试结论时,主 Agent 只能看到开头;原始收件箱文件又已经在读取后删除,后续没有读取完整消息的工具。这种处理适合演示异步唤醒流程,信息完整性仍然需要补强。
五、共享工作目录会带来哪些协作风险
队友的文件工具复用了主 Agent 的读写实现,工作目录也相同。消息隔离不等于文件隔离。
假设两个队友都在处理 vite.config.ts:
- 配置队友准备增加路径别名;
- 测试队友为了临时验证构建结果,也修改了同一个配置文件;
- 两个写入操作先后发生,后写入的内容可能覆盖前一次修改。
教学代码没有为文件写入建立锁,也没有给每个队友分配独立分支或工作目录。主 Agent 收到结果时,可能看到两个队友都说已经完成,但磁盘上只留下最后一次写入的版本。
任务系统在这一章仍然存在,不过队友拿到的简化工具列表里没有创建、认领和完成任务的能力。主 Agent可以管理任务文件,队友只能通过消息汇报进度。对于需要精确分工的场景,主 Agent 还要自行维护谁在做什么、哪项工作已经完成,以及哪项结果需要复查。
六、教学实现还缺少哪些团队协议
本章已经具备了多 Agent 协作的最小过程:主 Agent 启动队友,队友在独立上下文中执行工具,结果通过收件箱进入主 Agent 的下一轮对话。
但收件箱和线程生命周期仍然比较简化。
| 场景 | 当前教学代码的处理 | 可能产生的问题 |
|---|---|---|
| 队友运行轮数 | 最多执行 10 轮 | 长任务可能在完成前结束 |
| 队友结束条件 | 模型不再请求工具时退出 | 无法像常驻协作者一样等待后续指令 |
| 主 Agent 收件箱 | 读取后删除文件 | 长消息被截断后无法重新读取 |
| 并发读写收件箱 | 文件追加、读取、删除,没有文件锁 | 读写重叠时可能丢失消息 |
| 队友执行失败 | 捕获异常后退出,仍会尝试发送总结 | 主 Agent 可能只收到默认的 Done. |
| 文件修改 | 多个队友共享同一目录 | 同一文件可能互相覆盖 |
| 主进程退出 | 队友线程是守护线程 | 未完成队友会被直接结束 |
仓库 README 中的源码分析提到了更完整的团队设计方向,例如带锁的收件箱、空闲等待机制、关闭请求和权限审批消息。它们没有出现在 s15 的教学代码中。
因此,这一章更适合用来理解团队协作最基本的三件事:独立上下文、异步消息和主 Agent 的结果回收。任务分配、权限审批、可靠关闭和文件隔离,还需要额外的协议和状态管理。
七、小结
s15 让主 Agent 可以把工作分给多个队友。每个队友从一段独立对话开始,使用有限的工具完成任务,再通过文件收件箱把结果交回主 Agent。主程序轮询主收件箱,有消息时自动开启新一轮对话,因此用户不必手动提醒主 Agent 去查看队友的进度。
这套实现把协作消息传递跑通了,但它还没有处理可靠交付。收件箱读取后会删除,长消息会被截断,线程退出后也没有恢复机制;多个队友同时改同一个文件时,代码同样没有协调手段。
下一个章节会继续处理团队里的消息约定。队友开始协作后,主 Agent 需要知道什么时候可以让队友退出,队友也需要有机会收尾并确认关闭。否则任务结束时,最容易留下的往往是写到一半的文件和一封没人再读的消息。