如果你已经写过或读过「单 Agent + 工具循环」,多半遇到过这种尴尬:
- 让模型去读一大段测试、扫全仓库------主对话被工具输出塞满;
- 想「一边规划、一边让人去查文档」------只能串行等 subagent 跑完;
- 后台任务完成了------还要你手动粘贴结果回主对话。
Agent Teams 的思路很直白:Lead 负责统筹;Teammate 在独立线程里跑自己的 loop;彼此用 MessageBus 传话;Lead 通过 inbox 注入把结果接回主上下文。
一句话:
子任务从「同步函数调用」升级成「异步协作」:队友有自己的 messages,Lead 只收结论。
一、和「同步 Subagent」差在哪?
很多教学实现里,subagent 是这样工作的:
text
Lead 调用 task 工具
→ 等 subagent 跑完(阻塞)
→ 整段历史作为 tool_result 塞回 Lead
适合短任务;不适合「Lead 继续想、队友并行查」。
Agent Teams 改成:
text
Lead 调用 spawn_teammate
→ 后台线程启动 teammate_loop(不阻塞)
Lead 继续对话 / 做别的事
→ teammate 完成后 BUS.send → lead 的 inbox
→ queue_processor 发现 inbox → 自动唤醒 Lead 的 agent_loop
Teammate 不是 tool 的返回值,而是一个在跑的工人。
二、三张图看懂架构
1. 三条事件流,共用一个 Lead
text
① 用户打字 → history → agent_loop
② Cron 到点 → _queue → inject → agent_loop
③ Teammate 回信 → inbox → inject → agent_loop
Cron 和 Teammate 都不单独调模型「乱入」,而是变成 外部事件 ,由后台 queue_processor 抢锁后统一交给 Lead 的 agent_loop 消费------这是很多 Agent Runtime 的共同形状。
2. Lead / MessageBus / Teammate
text
Lead agent loop MessageBus Teammate loop
+----------------+ +-----------+ +-------------+
| prompt + tools | --spawn-----> | .jsonl | <---send---- | own history |
| inject inbox | <---send------ | 邮箱 | ---inbox---> | own tools |
+----------------+ +-----------+ +-------------+
3. 关键设计:共享磁盘,隔离上下文
- 共享 :同一工作目录下的
bash/ 读写文件(教学版未做权限隔离); - 隔离 :Lead 与 Teammate 不共享
messages列表。
队友读了哪些文件、执行了哪些命令,留在队友自己的上下文里;Lead 最终只看到 inbox 里那条 result。
三、MessageBus:用文件当邮箱(教学版)
生产里常见 Redis、Kafka、数据库队列表。教学代码用 每个 Agent 一个 JSONL 文件,足够把机制讲清楚。
目录示意:
text
.mailboxes/
lead.jsonl
reviewer.jsonl
send():向目标文件 append 一行 JSON:
json
{
"from": "reviewer",
"to": "lead",
"content": "Parser looks good",
"type": "result",
"ts": 1779780000.0
}
read_inbox():读光并删除文件(消费式),避免同一条消息反复注入。
注意 :当前实现用
threading.Lock保护同一进程内的读写;多进程同时写同一文件需要更强的一致性方案。
四、Lead 多了三个「团队工具」
| 工具 | 作用 |
|---|---|
spawn_teammate |
按 name / role / prompt 启动后台队友 |
send_message |
给某个 agent 发消息(含队友之间) |
check_inbox |
主动读 Lead 邮箱(自动注入时也可不用) |
spawn_teammate 本质是启动守护线程:
python
threading.Thread(
target=teammate_loop,
args=(client, model_name, name, role, prompt),
daemon=True,
).start()
Lead 立刻拿到「已启动」,不会卡在队友的十轮工具循环里。
五、Teammate 怎么跑? deliberately 受限
Teammate 有自己的 system prompt 和 messages,入口任务来自 Lead 的 prompt。
工具白名单(教学版):
- 有:
bash、read_file、write_file、edit_file、send_message - 无:
spawn_teammate、schedule_cron...(避免队友再 spawn 队友,复杂度爆炸)
轮数上限:最多 10 轮 tool loop,防止后台线程无限烧 token。
结束时无论成败,都会:
text
BUS.send(队友名, "lead", 最后一段文本, msg_type="result")
并从 active_teammates 里注销。
六、Inbox Injection:为什么伪装成 user 消息?
Lead 每轮调模型前会执行:
python
inject_inbox_messages(messages) # 读 lead.jsonl → 删文件 → 追加一条 user 消息
注入内容类似:
text
<inbox>
[reviewer:result] queue_processor 会监听外部事件并安全唤醒 Lead......
</inbox>
为什么是 role: user?
对 Agent Loop 来说,inbox、cron 触发、用户打字都是 「外部世界发来的新事件」。模型下一轮统一按「用户侧输入」处理,无需为每种事件单独设计 role。
Cron 触发则用 <scheduled-work> 包一层,逻辑相同。
七、Queue Processor:cron 与 inbox 共用一个唤醒器
第 8 篇里,后台线程只盯 cron 队列。第 9 篇条件变成:
python
if not SCHEDULER.has_queue() and not BUS.has_inbox("lead"):
continue
有定时任务 或 有队友回信 → 尝试非阻塞获取 agent_lock → 跑一轮 agent_loop(history)。
agent_lock 解决的是:用户正在终端输入时,后台不能同时改同一份 history。拿不到锁就 0.2 秒后再试。
八、和定时任务怎么拼在一起?
没有替换 cron,而是并入同一套「外部事件」模型:
text
cron 到点 → tick 入队 → inject_cron_jobs → <scheduled-work>
队友完成 → send 到 lead → inject_inbox_messages → <inbox>
↓
queue_processor → agent_loop → 模型 + 工具
持久化方面:durable=True 的 cron 会写入 .scheduled_tasks.json,重启后 load() 恢复;已到点但未消费的队列仍在内存里,进程挂了就没了(教学边界)。
九、本地怎么跑?一句命令试全队
环境变量(.env):
text
OPENAI_BASE_URL
OPENAI_API_KEY
MODEL_NAME
仓库根目录:
bash
python3 harness_agent/09_agent_teams.py
推荐试一句(复制到终端即可):
text
启动一个 teammate,名字叫 reviewer,角色是 code reviewer。
让它阅读 harness_agent/09_agent_teams.py,用几句话说明 MessageBus 怎么工作。
预期现象:
- Lead 调用
spawn_teammate,终端打印[teammate] reviewer spawned; - 你可以继续输入别的,或等待;
- reviewer 完成后
[bus] reviewer -> lead,[queue processor] delivering external work; - Lead 被自动唤醒,根据 inbox 内容回答你。
十、教学版刻意没做啥(读代码时的预期)
| 简化点 | 真实系统常见补法 |
|---|---|
| 文件 JSONL 邮箱 | MQ / DB / gRPC |
| 单进程锁 | 分布式锁、ack、重试 |
| Teammate 最多 10 轮 | 长期 worker、idle 唤醒 |
| 共享工作目录 | 沙箱、per-agent 权限 |
| 无取消/超时/成本上限 | lifecycle、budget、审计 |
知道边界,才知道下一版该往哪长。
十一、带走三句话
- 多 Agent ≠ 多个模型随便调 ------要有 消息边界 (MessageBus)和 主 loop 串行消费(queue processor + lock)。
- 协作靠异步,隔离靠独立 messages------Lead 不被队友的工具轨迹淹没。
- Cron、用户输入、队友回信------形态不同,本质都是「外部事件注入主 loop」。
从「一个人干活」到「一个小队分工」,这是 Coding Agent 走向工程化协作的第一步;后面还可以叠权限、结构化结果 schema、队友生命周期管理------但 邮箱 + 注入 + 后台唤醒 这条骨架已经够用。