三、第十季:给 Agent 团队沟通加"结构化协议"
核心目标
第九季的队友能通信,但沟通是 "无规则的" ------比如领导让队友关机,队友可能直接退出导致文件没写完;队友改代码,没有审批流程直接动手。第十季通过 "结构化协议" 构造一个 Team 来解决这些问题。
python
"""
s10_team_protocols.py - 团队协议
关机协议和计划审批协议,均使用相同的request_id关联模式。基于s09的团队消息功能扩展。
关机状态机(FSM):待处理(pending)→ 批准(approved) | 拒绝(rejected)
领导 队友
+---------------------+ +---------------------+
| 关机请求 | | |
| { | -------> | 接收请求 |
| request_id: abc | | 决策:是否批准? |
| } | | |
+---------------------+ +---------------------+
|
+---------------------+ +-------v-------------+
| 关机响应 | <------- | 关机响应 |
| { | | { |
| request_id: abc | | request_id: abc |
| approve: true | | approve: true |
| } | | } |
+---------------------+ +---------------------+
|
v
状态 -> "关机完成",线程停止
计划审批状态机(FSM):待处理(pending)→ 批准(approved) | 拒绝(rejected)
队友 领导
+---------------------+ +---------------------+
| 计划审批 | | |
| 提交:{plan:"..."}| -------> | 审核计划文本 |
+---------------------+ | 批准/拒绝? |
+---------------------+
|
+---------------------+ +-------v-------------+
| 计划审批响应 | <------- | 计划审批 |
| {approve: true} | | 审核:{req_id, |
+---------------------+ | approve: true} |
+---------------------+
跟踪器:{request_id: {"目标|发起方": 名称, "状态": "待处理|已批准|已拒绝"}}
核心思路:"相同的request_id关联模式,适配两个业务场景。"
"""
核心概念:请求-响应协议 + FSM(有限状态机)
1. 核心思路
所有重要沟通都遵循"请求(带唯一ID)→ 响应(引用同一ID)→ 状态更新"的流程,用FSM(有限状态机)管理请求的状态(pending→approved/rejected)。
2. 协议1:关机协议(Shutdown Protocol)
解决"强制关机导致数据丢失"的问题,要求"领导发关机请求→队友确认(批准/拒绝)→再关机"。
步骤1:lead 发送关机请求(生成唯一request_id)
python
shutdown_requests = {} # 跟踪所有关机请求的状态
def handle_shutdown_request(teammate: str) -> str:
# 生成8位唯一请求ID(避免冲突)
req_id = str(uuid.uuid4())[:8]
# 记录请求状态:待处理
shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
# 发消息给目标队友(消息类型是shutdown_request,带request_id)
BUS.send(
"lead", # 发送者
teammate, # 接收者
"Please shut down gracefully.", # 内容
"shutdown_request", # 消息类型
{"request_id": req_id} # 附加请求ID
)
return f"Shutdown request {req_id} sent (status: pending)"
- 发送内容:
lead向sub_agent会发送任务内容; - 消息类型: 是
任务分配还是消息回执; - Request ID: 任务分配的
ID由Lead发起;如果为消息回执,则ID由sub_agent发起;
步骤2:sub_agent 处理关机请求并响应
python
# 队友收到shutdown_request后,调用shutdown_response工具
if tool_name == "shutdown_response":
req_id = args["request_id"] # 引用领导的请求ID
approve = args["approve"] # 批准/拒绝关机
# 更新请求状态
shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"
# 回复领导(同样带request_id,保证关联)
BUS.send(
sender, # 队友自己
"lead", # 接收者是领导
args.get("reason", ""), # 拒绝理由(可选)
"shutdown_response", # 消息类型
{"request_id": req_id, "approve": approve}
)
- 发送内容: 一般为空;
- 消息类型: 是任务分配类型的消息,还是要求审批的消息;
- Request ID: Lead 分配的任务
ID; - Approve: sub_agent 是否同意任务的接收;
总结: 可以发现,Lead 和 sub_agent 在分配任务下的协议,一般都是通过 Request ID 来进行通信的,标识由分配方(Lead)确定;而消息的流动一般会走消息总线(MessageBus),每个 Agent 都会持久化一个自己的消息中心(.json文件)。sub_agent 拿到任务后可以选择 approve 的状态,并返回给 Lead 的消息中心;
3. 协议2:计划审批协议(Plan Approval Protocol)
解决"高风险任务无审批"的问题,要求"队友提交计划(带ID)→领导审批(引用ID)→队友执行/修改"。
python
plan_requests = {} # 跟踪所有计划请求的状态
# sub_agent 提交计划
def submit_plan(sender, plan_content):
req_id = str(uuid.uuid4())[:8]
plan_requests[req_id] = {
"from": sender,
"plan": plan_content,
"status": "pending"
}
# 发消息给领导请求审批
BUS.send(sender, "lead", plan_content, "plan_request", {"request_id": req_id})
return f"Plan submitted (ID: {req_id}), waiting for approval."
- 发送内容: 审批内容;
- 消息类型: 要求 Lead 审批的消息;
- Request ID: sub_agent 分配的任务
ID; - status: 任务状态;
python
# 领导审批计划
def handle_plan_review(request_id, approve, feedback=""):
req = plan_requests[request_id] # 通过ID找到对应的计划
req["status"] = "approved" if approve else "rejected"
# 回复队友审批结果
BUS.send(
"lead",
req["from"],
feedback, # 审批意见(比如"代码需要加注释")
"plan_approval_response",
{"request_id": request_id, "approve": approve}
)
- 发送内容: Lead 的审批结果;
- 消息类型: 要求 Lead 审批的消息;
- Request ID: sub_agent 分配的任务
ID; - approve: 是否同意审批;
4. 统一的FSM(有限状态机)
两个协议共享同一个状态机逻辑,简化代码:
[pending(待处理)]
↙ ↘
[approved(批准)] [rejected(拒绝)]
不管是关机请求还是计划请求,都只需要维护这三种状态,逻辑统一,容易扩展。
5. sub_agent 的管理
python
# -- TeammateManager with shutdown + plan approval --
class TeammateManager:
"""团队成员管理器:负责创建、管理、销毁团队成员,处理成员的核心逻辑"""
def __init__(self, team_dir: Path):
"""初始化管理器
Args:
team_dir: 团队数据目录
"""
self.dir = team_dir
self.dir.mkdir(exist_ok=True) # 创建团队目录
self.config_path = self.dir / "config.json" # 团队配置文件路径
self.config = self._load_config() # 加载配置(成员列表、团队名称)
self.threads = {} # 存储成员的线程:{成员名: 线程对象}
def _load_config(self) -> dict:
"""加载团队配置文件(JSON格式)"""
if self.config_path.exists():
return json.loads(self.config_path.read_text())
# 配置文件不存在则返回默认配置
return {"team_name": "default", "members": []}
def _save_config(self):
"""保存团队配置到文件(格式化输出,便于阅读)"""
self.config_path.write_text(json.dumps(self.config, indent=2))
def _find_member(self, name: str) -> dict:
"""查找指定名称的成员
Returns:
成员字典(name/role/status)或None
"""
for m in self.config["members"]:
if m["name"] == name:
return m
return None
def spawn(self, name: str, role: str, prompt: str) -> str:
"""创建并启动一个团队成员(以独立线程运行)
Args:
name: 成员名称
role: 成员角色(如"开发工程师")
prompt: 给成员的初始提示词
Returns:
创建结果提示
"""
# 查找成员是否已存在
member = self._find_member(name)
if member:
# 成员已存在且非空闲/关机状态,返回错误
if member["status"] not in ("idle", "shutdown"):
return f"Error: '{name}' is currently {member['status']}"
# 更新成员状态和角色
member["status"] = "working"
member["role"] = role
else:
# 成员不存在则创建新成员
member = {"name": name, "role": role, "status": "working"}
self.config["members"].append(member)
# 保存配置
self._save_config()
# 创建并启动线程(daemon=True:主线程退出时子线程也退出)
thread = threading.Thread(
target=self._teammate_loop, # 成员的核心循环函数
args=(name, role, prompt), # 传递给循环函数的参数
daemon=True,
)
self.threads[name] = thread
thread.start()
return f"Spawned '{name}' (role: {role})"
def _teammate_loop(self, name: str, role: str, prompt: str):
"""团队成员的核心循环:持续读取消息、调用AI、执行工具
Args:
name: 成员名称
role: 成员角色
prompt: 初始提示词
"""
# 构造成员的系统提示词(定义成员的行为准则)
sys_prompt = (
f"You are '{name}', role: {role}, at {WORKDIR}. "
f"Submit plans via plan_approval before major work. "
f"Respond to shutdown_request with shutdown_response."
)
# 初始化AI对话历史
messages = [{"role": "user", "content": prompt}]
# 获取成员可用的工具列表
tools = self._teammate_tools()
should_exit = False # 是否退出循环(关机标志)
# 循环次数限制(防止无限循环)
for _ in range(50):
# 读取并清空收件箱
inbox = BUS.read_inbox(name)
# 将新消息加入对话历史
for msg in inbox:
messages.append({"role": "user", "content": json.dumps(msg)})
# 收到关机批准则退出循环
if should_exit:
break
try:
# 调用Anthropic AI模型获取响应
response = client.messages.create(
model=MODEL,
system=sys_prompt,
messages=messages,
tools=tools,
max_tokens=8000,
)
except Exception:
# 调用失败则退出循环
break
# 将AI响应加入对话历史
messages.append({"role": "assistant", "content": response.content})
# 如果AI没有调用工具,退出循环
if response.stop_reason != "tool_use":
break
# 处理工具调用结果
results = []
for block in response.content:
if block.type == "tool_use":
# 执行工具并获取输出
output = self._exec(name, block.name, block.input)
# 打印工具执行日志(截断过长内容)
print(f" [{name}] {block.name}: {str(output)[:120]}")
# 构造工具结果(供AI后续处理)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
})
# 如果是关机响应且批准关机,设置退出标志
if block.name == "shutdown_response" and block.input.get("approve"):
should_exit = True
# 将工具结果加入对话历史
messages.append({"role": "user", "content": results})
# 更新成员状态(关机/空闲)
member = self._find_member(name)
if member:
member["status"] = "shutdown" if should_exit else "idle"
self._save_config()
def _exec(self, sender: str, tool_name: str, args: dict) -> str:
"""执行成员调用的工具
Args:
sender: 工具调用者(成员名称)
tool_name: 工具名称
args: 工具参数
Returns:
工具执行结果
"""
# 基础工具(与s02版本一致)
if tool_name == "bash":
return _run_bash(args["command"])
if tool_name == "read_file":
return _run_read(args["path"])
if tool_name == "write_file":
return _run_write(args["path"], args["content"])
if tool_name == "edit_file":
return _run_edit(args["path"], args["old_text"], args["new_text"])
if tool_name == "send_message":
return BUS.send(sender, args["to"], args["content"], args.get("msg_type", "message"))
if tool_name == "read_inbox":
return json.dumps(BUS.read_inbox(sender), indent=2)
# 关机响应工具(核心协议)
if tool_name == "shutdown_response":
req_id = args["request_id"]
approve = args["approve"]
# 加锁更新关机请求状态(线程安全)
with _tracker_lock:
if req_id in shutdown_requests:
shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"
# 发送关机响应给负责人
BUS.send(
sender, "lead", args.get("reason", ""),
"shutdown_response", {"request_id": req_id, "approve": approve},
)
return f"Shutdown {'approved' if approve else 'rejected'}"
# 计划审批工具(核心协议)
if tool_name == "plan_approval":
plan_text = args.get("plan", "")
# 生成8位唯一request_id
req_id = str(uuid.uuid4())[:8]
# 加锁记录计划请求(线程安全)
with _tracker_lock:
plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"}
# 发送计划审批请求给负责人
BUS.send(
sender, "lead", plan_text, "plan_approval_response",
{"request_id": req_id, "plan": plan_text},
)
return f"Plan submitted (request_id={req_id}). Waiting for lead approval."
# 未知工具
return f"Unknown tool: {tool_name}"
def _teammate_tools(self) -> list:
"""返回成员可用的工具列表(包含工具描述和参数schema)"""
return [
# 基础工具(与s02版本一致)
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "write_file", "description": "Write content to file.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in file.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "send_message", "description": "Send message to a teammate.",
"input_schema": {"type": "object", "properties": {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string", "enum": list(VALID_MSG_TYPES)}}, "required": ["to", "content"]}},
{"name": "read_inbox", "description": "Read and drain your inbox.",
"input_schema": {"type": "object", "properties": {}}},
# 关机响应工具
{"name": "shutdown_response", "description": "Respond to a shutdown request. Approve to shut down, reject to keep working.",
"input_schema": {"type": "object", "properties": {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "reason": {"type": "string"}}, "required": ["request_id", "approve"]}},
# 计划审批工具
{"name": "plan_approval", "description": "Submit a plan for lead approval. Provide plan text.",
"input_schema": {"type": "object", "properties": {"plan": {"type": "string"}}, "required": ["plan"]}},
]
def list_all(self) -> str:
"""列出所有团队成员及其状态"""
if not self.config["members"]:
return "No teammates."
lines = [f"Team: {self.config['team_name']}"]
for m in self.config["members"]:
lines.append(f" {m['name']} ({m['role']}): {m['status']}")
return "\n".join(lines)
def member_names(self) -> list:
"""返回所有成员名称列表"""
return [m["name"] for m in self.config["members"]]
# 初始化全局团队管理器实例
TEAM = TeammateManager(TEAM_DIR)
- sub_agent 的初始化: 通过
.json文件进行持久化,并创建对应线程,后进行管理,防止重复创建并监控线程状态; - sub_agent 的提示词: 每个 sub_agent 都有统一的
Prompt,和 Lead 的不一样; - sub_agent 的工作逻辑: 如下所示,通过
_teamate_loop执行,由于是 sub_agent,所以首先会从自己的邮箱冲取得消息,然后封装到message列表中,然后执行得到response;其次,判断用什么工具,然后同样将tool_result封装到message列表中;最后流程和agent_loop是一样的没什么区别;
python
thread = threading.Thread(
target=self._teammate_loop, # 成员的核心循环函数
args=(name, role, prompt), # 传递给循环函数的参数
daemon=True,
)
- sub_agent 工作逻辑 _teamate_loop 和 agent_loop 不同之处: (1)执行工具那部分包含了很多协议工具,比如申请复查等等;(2)跳出
loop的条件不再是单单 "有无使用工具了",如果使用了协议工具也需要跳出loop,目的是告诉 Lead "我不同此次任务的分配" 或者 "我想要提出这个解决方案";
6. 主 Agent 的管理
python
def agent_loop(messages: list):
"""负责人的核心循环:读取消息、调用AI、执行工具
Args:
messages: 对话历史列表
"""
while True:
# 读取并处理负责人的收件箱
inbox = BUS.read_inbox("lead")
if inbox:
messages.append({
"role": "user",
"content": f"<inbox>{json.dumps(inbox, indent=2)}</inbox>",
})
messages.append({
"role": "assistant",
"content": "Noted inbox messages.",
})
# 调用AI模型获取响应
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)
# 将AI响应加入对话历史
messages.append({"role": "assistant", "content": response.content})
# 如果AI没有调用工具,退出循环
if response.stop_reason != "tool_use":
return
# 处理工具调用
results = []
for block in response.content:
if block.type == "tool_use":
# 获取工具处理函数
handler = TOOL_HANDLERS.get(block.name)
try:
# 执行工具并获取结果
output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
except Exception as e:
output = f"Error: {e}"
# 打印工具执行日志
print(f"> {block.name}: {str(output)[:200]}")
# 构造工具结果
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
})
# 将工具结果加入对话历史
messages.append({"role": "user", "content": results})
主 Agent Loop: (1)工具变多了,因为工具和 Query 的关系,主 Agent 能够让 Sub_Agent 接任务;(2)先查看邮箱再继续生成;
第十季核心亮点
| 特性 | 解决的问题 | 实现方式 |
|---|---|---|
| 结构化协议 | 沟通无规则,易出错 | 请求-响应模式+唯一request_id |
| 状态跟踪 | 不知道请求的处理结果 | shutdown_requests/plan_requests字典 |
| 优雅关机 | 强制关机导致数据丢失 | 关机请求-响应握手 |
| 计划审批 | 高风险任务无审核直接执行 | 计划提交-审批流程 |
四、两季核心对比(学生视角)
| 维度 | 第九季(基础版) | 第十季(进阶版) |
|---|---|---|
| 核心能力 | 队友能持久运行、异步发消息 | 在第九季基础上,沟通有规则、可追溯 |
| 消息特点 | 无结构化(普通文本) | 结构化(带类型、request_id) |
| 状态管理 | 仅队友的工作状态(working/idle) | 新增请求状态(pending/approved/rejected) |
| 工具数量 | 9个(基础通信、执行) | 12个(+关机请求/响应、计划审批) |
| 协作规范 | 无明确规范 | 有请求-响应的协议规范 |
五、代码运行与验证(实操步骤)
作为学生,你可以按以下步骤跑通代码,理解实际效果:
1. 第九季运行
bash
# 进入代码目录
cd learn-claude-code
# 运行第九季代码
python agents/s09_agent_teams.py
# 交互操作(示例):
# 1. 创建alice(编码员)和bob(测试员)
# 2. 让alice给bob发消息
# 3. 广播"status update: phase 1 complete"给所有队友
# 4. 查看领导的收件箱
# 5. 查看团队名单(/team)和收件箱(/inbox)
2. 第十季运行
bash
cd learn-claude-code
python agents/s10_team_protocols.py
# 交互操作(示例):
# 1. 创建alice(编码员),发送关机请求,查看她的状态
# 2. 创建bob(高风险重构任务),提交计划,领导拒绝
# 3. 创建charlie,提交计划,领导批准
# 4. 查看团队状态(/team)
总结
核心知识点回顾
- 第九季核心 :用
TeammateManager实现队友持久化(线程+配置文件),用MessageBus(JSONL文件)实现异步通信,解决"代理能组队、能发消息"的基础问题; - 第十季核心 :在通信基础上增加"请求-响应协议",通过
request_id关联请求和响应,用状态机管理请求状态,解决"沟通有规则、可追溯、不混乱"的进阶问题; - 关键设计思想:从"无结构"到"结构化"是编程中常见的升级思路,比如先做功能,再做规范,这和真实团队协作的逻辑一致。
这两季的代码核心是"模拟人类团队的协作模式",把编程中的"线程、文件IO、状态管理、协议设计"等知识点结合起来,是非常好的实战案例。