Team Protocols
s09 让 Agent 能说话,但没说"怎么说话才安全"。
s10 定义了结构化协议——同一个 request_id 模式,覆盖关停和审批两种场景。
s09 缺少什么?两个需要"规矩"的场景
s09 的 Agent 能互相发消息,但消息是自由的——Agent A 发一句话,Agent B 看到了,决定怎么回应。这在日常聊天中没问题。但在以下两个场景下,自由散漫会出事故:
场景一:关机——不能直接杀线程
Lead 说"Alice 你可以下班了"。如果直接杀 Alice 的线程(thread.kill()),Alice 可能正在写文件——文件写了一半就中断了,磁盘上留下一个损坏的 JSON。
正确的做法:Lead 发一个关机请求(shutdown_request)。Alice 收到后,完成当前手头的工作,保存状态,然后发一个关机响应(shutdown_response: approve=true)。Lead 确认响应后,Alice 的线程优雅退出。如果 Alice 正在做一个重要操作,她可以拒绝(approve=false)。
这就像你让同事下班——你不会直接拔他的电脑电源。你说"可以走了",他保存文件、关掉程序,然后告诉你"好了"。
场景二:高风险操作——动手前先过审
Lead 说"重构认证模块"。这是高风险操作——可能影响整个系统的安全性。如果 Alice 直接开始改代码,改完 Lead 才发现方向错了——浪费了时间,可能还引入了 bug。
正确的做法:Alice 在动手之前先提交一个计划(plan_approval)。Lead 审查计划——"方向对吗?有没有遗漏?有没有更好的方案?"——然后 approve 或 reject。Alice 只在收到 approve 后才开始执行。
核心模式:request_id 关联——一个 UUID 串起请求和响应
s10 整个协议系统的基石只有一行代码:每个请求带一个唯一的 request_id。响应时必须引用同一个 request_id。有了这个 ID,请求方知道"这个响应是对应我哪个请求的"。
为什么需要 request_id?
假设没有 request_id。Lead 同时发了两个关机请求——给 Alice 和给 Bob。Alice 和 Bob 各自回了 shutdown_response。Lead 收到两条消息……但哪条对应关机请求 #1,哪条对应 #2?
更糟的情况:Lead 发了关机请求 #1,然后网络延迟(或 Agent 处理慢),响应很久才回来。Lead 期间又发了一个新的关机请求 #2。收到响应时,它不知道这是 #1 的还是 #2 的。
request_id 解决了这个"对号入座"的问题。请求带 ID,响应带同一个 ID。收到响应 → 查 request_id → 找到对应的请求 → 更新状态。
关机协议:从请求到优雅退出的完整流程
关机不只是"发一条消息说再见"。它是一个完整的状态流转。
Lead 端:发起关机请求
Teammate 端:决定接受或拒绝
关键细节:队友的 Agent Loop 在看到 should_exit=True 后,不会立刻退出——它会在当前轮次结束后退出。这保证了当前正在做的工具调用能完成,状态能被保存。
计划审批协议:先想清楚再动手
和关机协议完全相同的结构——request_id + FSM——但方向相反(队友发起,Lead 审批)。
Teammate 端:提交计划
Lead 端:审批计划
Lead 收到 plan_approval_response 消息后,审查计划内容,决定 approve 或 reject:
approve=true 时还可以带 feedback——"计划方向对,但第 3 步可以优化为..."。这样队友收到审批后不仅知道通过了,还知道怎么改进。
两个协议,同一个骨架——学会一个就学会了另一个
结构对比
| 维度 | 关机协议 | 计划审批协议 |
| 发起方 | Lead(领导) | Teammate(队友) |
| 审批方 | Teammate(队友) | Lead(领导) |
| 请求消息 | shutdown_request | plan_approval |
| 响应消息 | shutdown_response | plan_approval_response |
| 共享模式 | request_id 关联 + FSM (pending → approved/rejected) | |
| 结果行为 | thread 优雅退出 | 按计划执行 / 修订计划 |
这个模式可以扩展到任何需要"请求-确认"的场景
审批代码合并?同样模式。申请访问外部 API?同样模式。请求修改共享文件?同样模式。任何"一方发起、另一方确认、双方根据确认采取行动"的交互,都可以用 request_id + FSM 覆盖。
s10 只实现了两个协议来示范这个模式。在你的 Agent 系统中,你可以按同样的骨架添加自己的协议——关键只有三样:① 一个唯一的 request_id;② 一个 tracker 字典记录状态;③ 请求和响应消息类型。
Tracker 状态机:一个极简的 FSM(有限状态机)
两个 tracker 字典,同一个 FSM
两个 tracker 共享同一个有限状态机(FSM):
等待响应
已批准
等待响应
已驳回
为什么只有一个 pending → approved/rejected 状态? 因为这个 FSM 只建模了"请求的审批过程"。它不关心"审批后发生了什么"——关机时线程退出、审批通过后按计划执行,这些是各协议自己的行为,不属于 FSM 的状态。
一个好的 FSM 只建模一个维度——不把"请求的审批状态"和"执行的状态"混在一起。状态机越简单,越不容易出错。
_tracker_lock:为什么两个 tracker 共享一把锁?
多个线程可能同时操作 tracker——Lead 线程在更新 plan_requests,Alice 的线程在更新 shutdown_requests。如果同时写(哪怕写的是不同的 key),Python 字典的内部结构可能损坏。
一把全局锁保护两个字典,简单粗暴但安全。这是 s08 学到的线程安全模式的延续——在操作共享状态时加锁,操作完解锁。代价很小(锁的持有时间极短——只是字典插入/更新),收益很大(绝对不会出现数据竞争)。
🔬 协议设计的三个原则
s10 的两个协议虽然简单,但体现了三个通用的协议设计原则:
① 幂等性(Idempotency):同一条 shutdown_request 如果被发了两次(因为网络延迟),Alice 应该能正确处理——第二次收到的请求带同一个 request_id,Alice 看到已经在 tracker 中有记录,不会重复执行关机逻辑。request_id 是实现幂等性的关键。
② 超时处理:s10 的协议没有超时机制——如果 Alice 永远不回复 shutdown_request,Lead 会一直等。在生产系统中,你需要加超时:Lead 发出请求后启动一个定时器,如果 N 秒内没收到响应,自动认为 timeout(或重试)。这是 s10 的一个有意简化——它展示了协议的核心骨架,让超时作为你自己的扩展练习。
③ 单向依赖:请求方不阻塞等待响应。Lead 发完 shutdown_request 后继续做自己的事。Alice 的响应是通过收件箱异步回来的。这和 HTTP 的同步请求-响应不同——异步协议让双方都不被阻塞,Agent 可以在等待响应的同时做其他事。
📊 协议中的错误处理与边界情况
真实系统中协议最复杂的部分不是"正常流程",而是边界情况:
场景一:收到一个未知的 request_id:Lead 收到一条 shutdown_response,但 request_id="xyz999" 在 shutdown_requests 中找不到。可能是旧请求的延迟响应,或者数据损坏。s10 的处理:返回 error。生产系统应该:记录日志 + 忽略(幂等处理)。
场景二:Agent 在等待审批时被 shut down:Alice 刚提交了计划,正在等 Lead 审批。Lead 发了一条 shutdown_request。Alice 应该怎么办?是先完成审批流程再关机,还是直接回复 shutdown_response?s10 没有规范这个交互——它把决策权交给 LLM("你觉得应该怎么处理?")。
场景三:重复的 request_id:两个不同的请求(一次关机和一次提交计划)使用了相同的 request_id(极低概率,因为 UUID)。s10 使用两个独立的 tracker 字典避免了这个问题——shutdown_requests 和 plan_requests 是分开的,同一个 ID 在两个字典中不会冲突。
这些边界情况的教学意义:s10 不处理它们不是因为不重要——是因为先把核心骨架讲清楚,边界情况是你深入学习后自然会遇到的扩展点。一个好的教学系统让你先理解 80% 的核心逻辑,剩下的 20% 在你的真实项目中自然会遇到和处理。
🏗️ 从 s10 到生产级协议系统——你还需要什么?
s10 是一个教学级的协议系统。如果要用于生产,你需要加上:
① 超时 + 重试:每个请求带一个 timeout 字段,超时后自动标记为 timeout,可以配置重试次数。
② 消息持久化:JSONL 文件依赖文件系统。生产系统可能用 Redis 做消息队列——保证消息不丢、支持多消费者。
③ 协议版本号:消息格式可能会演进——加一个 version: 1 字段让 Agent 能兼容旧格式。
④ 监控和审计:tracker 字典里记录每条请求的时间戳和状态变更历史——方便排查"为什么这个关机请求卡住了?"。
但这些都不影响 s10 的核心洞察:一个 request_id + 一个 FSM + 一个 tracker 字典,就能覆盖所有"请求-确认"交互。加超时、加重试、加监控——这些是在核心骨架上的扩展,不改变基本结构。
代码拆解 & s09 vs s10
两个新协议函数(~30 行)
handle_shutdown_request 和 handle_plan_review。每个都遵循同样的三步:生成 request_id → 记录到 tracker → 通过 BUS 发送。响应时:查 request_id → 更新 tracker → 发回响应。
Teammate Loop 的改动(~6 行)
新增 should_exit 变量 + shutdown_response 工具的审批逻辑 + 循环末尾的优雅退出判断。
Tracker 字典 + 锁(~5 行)
两个全局字典记录所有进行中的请求。一把全局锁保护并发安全。和 s08 的 BackgroundManager 的锁模式完全一致。
| 组件 | s09 | s10 |
|---|---|---|
| 关机 | 自然退出 / 杀线程 | 请求-响应握手 + 优雅退出 |
| 计划审批 | 无 | 提交 → review → approve/reject |
| request_id | 无 | UUID 关联请求和响应 |
| FSM | 无 | pending → approved / rejected |
| 新工具 | 9 | 12 (+shutdown_req +shutdown_resp +plan) |
| 并发安全 | 无锁 | _tracker_lock 保护 tracker 字典 |
从 s10 带走的三个核心认识
协议 = 结构化消息
普通消息是自由的,协议是结构化的。request_id 是协议的基础——它让请求和响应可追踪、可关联、可查询。
一个 FSM 覆盖多个场景
关机、审批、代码合并——同一个 pending→approved/rejected 状态机可以覆盖所有"请求-确认"交互。不要为每个场景写独立的状态机。
优雅退出 > 暴力杀线程
直接杀线程是隐藏的 bug 来源(文件写一半、状态不一致)。请求-响应握手让 Agent 在退出前完成清理。
落地案例:团队协议在真实系统中的应用
📋 Claude Code 的权限审批
Claude Code 在需要执行高风险操作(如 git push、删除文件)时,会请求用户确认。这就是一个"计划审批协议"——Agent 提交计划("我要 push 到 main 分支"),用户审查后 approve 或 reject。
s10 的 plan_approval 是人类审批的自动化版本——如果审批方也是一个 Agent,就是 s10;如果审批方是人类用户,就是 Claude Code 的权限弹窗。同一套协议结构,不同的审批者。
🔄 Kubernetes 的 Graceful Shutdown
Kubernetes 在关闭 Pod 时,先发 SIGTERM 信号(= shutdown_request),给容器一段时间做清理(保存状态、关闭连接),如果超时才 SIGKILL(= 强制杀线程)。s10 的关机协议就是这个模式的 Agent 版本。
🤖 AutoGen 的 Handoff 机制
AutoGen 的 Agent 之间通过 handoff 机制来交接任务——Agent A 完成自己的工作后,通过 handoff 把控制权交给 Agent B。这个 handoff 就是一个"请求-响应"协议——A 发起 handoff_request,B 确认收到并接受。
s10 的通用 request_id + FSM 模式可以直接实现 handoff。你只需要添加一个新的消息类型 handoff_request 和 handoff_response,复用同样的 tracker + FSM 骨架。
结合 s07 的任务图 + s09 的团队通信 + s10 的协议——
一个 Agent 完成任务后,自动扫描任务池,找到可执行的,claim 并开始执行。
不需要 Lead 手动分配——Agent 自己知道该做什么。