Agent 开发进阶(十六):团队协议系统,让智能体协作更加结构化

2 阅读10分钟

Agent 开发进阶(十六):团队协议系统,让智能体协作更加结构化

本文是「从零构建 Coding Agent」系列的第十六篇,适合想让多个 Agent 进行结构化协作的开发者。

先问一个问题

当你的多个 Agent 开始协同工作时,你是否遇到过这样的问题:

  • 队友要关机,如何确保它优雅退出而不是突然消失?
  • 某个高风险操作需要审批,如何确保请求和回复能够准确对应?
  • 多个请求同时存在时,如何知道某条回复对应哪件事?

如果你的答案是肯定的,那么你需要一个团队协议系统。

自由聊天的「协作困境」问题

到了这一阶段,你的 Agent 已经具备了多种能力:

  • 核心循环运行
  • 工具使用与分发
  • 会话内规划(TodoWrite)
  • 子智能体机制(Subagent)
  • 技能加载
  • 上下文压缩
  • 权限系统
  • Hook 系统
  • Memory 系统
  • 系统提示词组装
  • 错误恢复
  • 任务系统
  • 后台任务系统
  • 定时调度系统
  • Agent 团队系统

但当团队成员需要协作时,仅靠自由文本会遇到明显问题:

  • 无法追踪:多个请求同时存在时,很难知道回复对应哪件事
  • 无法审批:某些动作必须明确批准或拒绝,不能只靠模糊回复
  • 无法确认:无法确认对方是否真的收到了请求
  • 无法恢复:系统重启后,协作状态会丢失

虽然 s15 的邮箱系统让队友可以互相发消息,但如果所有事情都只靠自由文本,协作会变得不稳定。

所以到了这个阶段,我们需要一个团队协议系统:

一层结构化协议,让团队成员能够按规则协作。

团队协议系统的核心设计:请求-响应模式与状态追踪

用一个图来表示团队协议系统的工作流程:

某个队友 / lead 发起请求
  ->
写入 RequestRecord
  ->
把 ProtocolEnvelope 投递进对方 inbox
  ->
对方下一轮 drain inbox
  ->
按 request_id 更新请求状态
  ->
必要时再回一条 response
  ->
请求方根据 approved / rejected 继续后续动作

关键点只有三个:

  1. 请求-响应模式:明确的请求和响应
  2. request_id:唯一标识每个请求
  3. 状态追踪:记录请求的状态变化

几个必须搞懂的概念

协议(Protocol)

协议可以简单理解成:

双方提前约定好「消息长什么样、收到以后怎么处理」。

request_id

request_id 就是请求编号。

它的作用是:

  • 某个请求发出去以后有一个唯一身份
  • 之后的批准、拒绝、超时都能准确指向这一个请求

请求-响应模式(Request-Response Pattern)

这个词听起来像高级概念,其实很简单:

请求方:我发起一件事
响应方:我明确回答同意还是不同意

协议消息 vs 普通消息

  • 普通消息:适合讨论、提醒、补充说明
  • 协议消息:适合审批、关机、交接、签收

最小实现

1. Protocol Manager

import os
import json
import threading
import time
import uuid
from pathlib import Path

class ProtocolManager:
    """协议管理器"""
    
    def __init__(self, team_dir=".team"):
        self.team_dir = Path(team_dir)
        self.requests_dir = self.team_dir / "requests"
        self.requests_dir.mkdir(exist_ok=True)
        
        # 请求记录
        self.requests = {}
        self.lock = threading.Lock()
        
        # 加载已存在的请求
        self._load_existing_requests()
    
    def _load_existing_requests(self):
        """加载已存在的请求"""
        for request_file in self.requests_dir.glob("*.json"):
            try:
                request = json.loads(request_file.read_text(encoding="utf-8"))
                self.requests[request["request_id"]] = request
            except Exception as e:
                print(f"加载请求失败 {request_file}: {e}")
    
    def _generate_request_id(self):
        """生成请求 ID"""
        return f"req_{str(uuid.uuid4())[:8]}"
    
    def _save_request(self, request):
        """保存请求"""
        request_file = self.requests_dir / f"{request['request_id']}.json"
        request_file.write_text(
            json.dumps(request, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )
    
    def create_request(self, kind, from_name, to_name, payload=None):
        """创建请求"""
        request_id = self._generate_request_id()
        
        request = {
            "request_id": request_id,
            "kind": kind,
            "from": from_name,
            "to": to_name,
            "status": "pending",
            "payload": payload or {},
            "created_at": time.time(),
        }
        
        with self.lock:
            self.requests[request_id] = request
            self._save_request(request)
        
        return request
    
    def get_request(self, request_id):
        """获取请求"""
        with self.lock:
            return self.requests.get(request_id)
    
    def update_request(self, request_id, status, response_payload=None):
        """更新请求状态"""
        with self.lock:
            request = self.requests.get(request_id)
            if not request:
                return None
            
            request["status"] = status
            if response_payload:
                request["response"] = response_payload
            request["updated_at"] = time.time()
            
            self._save_request(request)
            return request
    
    def list_requests(self, status=None):
        """列出请求"""
        with self.lock:
            requests = list(self.requests.values())
            if status:
                requests = [r for r in requests if r["status"] == status]
            return requests

2. 协议工具

def create_protocol_tools(protocol_manager, team_manager):
    """创建协议相关的工具"""
    
    def request_shutdown(target):
        """请求优雅关机"""
        request = protocol_manager.create_request(
            kind="shutdown",
            from_name="lead",
            to_name=target,
            payload={"reason": "System shutdown"}
        )
        
        # 发送协议消息
        message = {
            "type": "shutdown_request",
            "from": "lead",
            "to": target,
            "request_id": request["request_id"],
            "payload": request["payload"],
            "timestamp": time.time(),
        }
        
        # 这里应该通过 team_manager 发送消息
        # 简化版本直接返回
        return f"关机请求已发送给 {target},请求 ID: {request['request_id']}"
    
    def respond_shutdown(request_id, approve):
        """响应关机请求"""
        request = protocol_manager.update_request(
            request_id,
            status="approved" if approve else "rejected",
            response_payload={"approve": approve}
        )
        
        if not request:
            return f"请求 {request_id} 不存在"
        
        return f"已{'批准' if approve else '拒绝'}关机请求 {request_id}"
    
    def submit_plan(plan_text):
        """提交计划审批"""
        # 假设当前是某个队友
        from_name = "alice"  # 应该从上下文获取
        request = protocol_manager.create_request(
            kind="plan_approval",
            from_name=from_name,
            to_name="lead",
            payload={"plan": plan_text}
        )
        
        return f"计划已提交审批,请求 ID: {request['request_id']}"
    
    def review_plan(request_id, approve, feedback=""):
        """审批计划"""
        request = protocol_manager.update_request(
            request_id,
            status="approved" if approve else "rejected",
            response_payload={"approve": approve, "feedback": feedback}
        )
        
        if not request:
            return f"请求 {request_id} 不存在"
        
        return f"计划已{'批准' if approve else '拒绝'}: {feedback}"
    
    def list_requests(status=None):
        """列出所有请求"""
        requests = protocol_manager.list_requests(status)
        if not requests:
            return "暂无请求"
        
        lines = ["# 协议请求\n"]
        for req in requests:
            lines.append(f"- **#{req['request_id']}** {req['kind']} [{req['status']}]")
            lines.append(f"  从 {req['from']}{req['to']}")
            if req.get('payload'):
                lines.append(f"  内容: {req['payload']}")
        
        return "\n".join(lines)
    
    return {
        "request_shutdown": request_shutdown,
        "respond_shutdown": respond_shutdown,
        "submit_plan": submit_plan,
        "review_plan": review_plan,
        "list_requests": list_requests,
    }

3. 集成到团队系统

class EnhancedTeamManager:
    """增强的团队管理器,支持协议"""
    
    def __init__(self, team_dir=".team"):
        self.team_dir = Path(team_dir)
        self.team_dir.mkdir(exist_ok=True)
        
        # 创建必要的目录
        self.config_dir = self.team_dir
        self.inbox_dir = self.team_dir / "inbox"
        self.inbox_dir.mkdir(exist_ok=True)
        
        # 初始化协议管理器
        self.protocol_manager = ProtocolManager(team_dir)
        
        # 加载配置
        self.config_path = self.config_dir / "config.json"
        self.config = self._load_config()
        
        # 存储队友线程
        self.teammate_threads = {}
    
    def _load_config(self):
        """加载配置"""
        if self.config_path.exists():
            try:
                return json.loads(self.config_path.read_text(encoding="utf-8"))
            except Exception as e:
                print(f"加载配置失败: {e}")
        return {"team_name": "default", "members": []}
    
    def _save_config(self):
        """保存配置"""
        self.config_path.write_text(
            json.dumps(self.config, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )
    
    def send_protocol_message(self, sender, to, message):
        """发送协议消息"""
        inbox_file = self.inbox_dir / f"{to}.jsonl"
        
        with open(inbox_file, "a", encoding="utf-8") as f:
            f.write(json.dumps(message, ensure_ascii=False) + "\n")
        
        return f"协议消息已发送给 {to}"
    
    def _read_inbox(self, name):
        """读取邮箱"""
        inbox_file = self.inbox_dir / f"{name}.jsonl"
        if not inbox_file.exists():
            return []
        
        messages = []
        try:
            with open(inbox_file, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if line:
                        messages.append(json.loads(line))
            
            # 清空邮箱
            inbox_file.write_text("", encoding="utf-8")
        except Exception as e:
            print(f"读取邮箱失败 {name}: {e}")
        
        return messages
    
    def _teammate_loop(self, name, role, prompt):
        """队友循环"""
        print(f"队友 {name} ({role}) 已启动")
        
        messages = [{
            "role": "system",
            "content": f"你是 {name},一个 {role}。请专注于你的职责,完成任务后等待新的指示。"
        }, {
            "role": "user",
            "content": prompt
        }]
        
        while True:
            # 读取邮箱
            inbox = self._read_inbox(name)
            if inbox:
                print(f"队友 {name} 收到 {len(inbox)} 条消息")
                for msg in inbox:
                    # 处理协议消息
                    if msg.get("type") == "shutdown_request":
                        request_id = msg.get("request_id")
                        print(f"队友 {name} 收到关机请求 {request_id}")
                        # 这里应该自动批准或询问用户
                        # 简化版本直接批准
                        self.protocol_manager.update_request(
                            request_id,
                            status="approved",
                            response_payload={"approve": True}
                        )
                        # 退出循环
                        print(f"队友 {name} 正在关机...")
                        return
                    else:
                        # 普通消息
                        messages.append({
                            "role": "user",
                            "content": f"来自 {msg['from']} 的消息: {msg['content']}"
                        })
            
            # 处理任务
            if messages:
                print(f"队友 {name} 正在处理任务...")
                time.sleep(2)
            
            # 等待一段时间再检查邮箱
            time.sleep(5)

核心功能说明

1. 优雅关机协议

请求关机

request = protocol_manager.create_request(
    kind="shutdown",
    from_name="lead",
    to_name="alice",
    payload={"reason": "System shutdown"}
)

响应关机

protocol_manager.update_request(
    request_id,
    status="approved",
    response_payload={"approve": True}
)

2. 计划审批协议

提交计划

request = protocol_manager.create_request(
    kind="plan_approval",
    from_name="alice",
    to_name="lead",
    payload={"plan": "Refactor authentication module"}
)

审批计划

protocol_manager.update_request(
    request_id,
    status="approved",
    response_payload={"approve": True, "feedback": "Good plan, proceed"}
)

3. 请求状态管理

查看请求状态

request = protocol_manager.get_request("req_12345678")
# 返回请求的完整信息,包括状态

列出所有请求

requests = protocol_manager.list_requests(status="pending")
# 返回所有待处理的请求

4. 持久化

请求记录会被保存到 .team/requests/ 目录,每个请求一个 JSON 文件:

.team/requests/
  req_12345678.json
  req_87654321.json

这样即使程序重启,请求状态也不会丢失。

协议消息 vs 普通消息

特性普通消息协议消息
用途讨论、提醒、补充说明审批、关机、交接、签收
结构自由文本结构化数据
标识request_id
状态pending / approved / rejected / expired
追踪完整的状态追踪

使用建议

  • 对于日常交流:使用普通消息
  • 对于需要明确批准或拒绝的操作:使用协议消息
  • 对于需要追踪状态的协作:使用协议消息

各种数据结构的边界

对象回答的问题典型字段
MessageEnvelope谁跟谁说了什么from / to / content
ProtocolEnvelope这是不是一条结构化请求或响应type / request_id / payload
RequestRecord这件协作流程现在走到哪一步kind / status / from / to
TaskRecord真正的工作项是什么、谁在做、还卡着谁subject / status / blockedBy / owner

一定要牢牢记住:

  • 协议请求不是任务本身
  • 请求状态表也不是任务板
  • 协议只负责「协作流程」
  • 任务系统才负责「真正的工作推进」

新手最容易犯的 4 个错

1. 没有 request_id

# ❌ 错误
# 没有请求编号,多个请求会混淆
message = {
    "type": "shutdown_request",
    "from": "lead",
    "to": "alice",
    "content": "Please shut down"
}

# ✅ 正确
# 每个请求都有唯一编号
message = {
    "type": "shutdown_request",
    "from": "lead",
    "to": "alice",
    "request_id": "req_12345678",
    "payload": {"reason": "System shutdown"}
}

2. 收到请求以后只回一句自然语言

# ❌ 错误
# 只回自然语言,系统无法处理
response = "好的,我知道了"

# ✅ 正确
# 回复结构化的批准或拒绝
response = {
    "type": "shutdown_response",
    "request_id": "req_12345678",
    "approve": True
}

3. 没有请求状态表

# ❌ 错误
# 没有状态追踪
def handle_request(request_id):
    # 直接处理,不记录状态
    pass

# ✅ 正确
# 记录请求状态
def handle_request(request_id):
    request = protocol_manager.get_request(request_id)
    request["status"] = "approved"
    protocol_manager._save_request(request)

4. 把协议消息和普通消息混成一种结构

# ❌ 错误
# 没有区分消息类型
message = {
    "content": "Please shut down"
}

# ✅ 正确
# 明确区分消息类型
protocol_message = {
    "type": "shutdown_request",
    "request_id": "req_12345678",
    "payload": {}
}

normal_message = {
    "content": "Hi, how are you?"
}

为什么这很重要

因为一个真正可靠的协作系统,需要明确的协议和状态追踪。

团队协议系统让你能够:

  1. 结构化协作:明确的请求和响应,而不是模糊的自由文本
  2. 状态追踪:知道每个请求当前的状态
  3. 可恢复性:系统重启后,协作状态可以恢复
  4. 可扩展性:基于统一的协议模板,可以轻松添加新的协议类型
  5. 可靠性:避免请求和回复的混淆

推荐的实现步骤

  1. 第一步:实现 ProtocolManager 类,管理请求记录和状态
  2. 第二步:实现基本的协议类型(关机、计划审批)
  3. 第三步:实现协议消息的发送和接收
  4. 第四步:在队友循环中处理协议消息
  5. 第五步:创建协议相关的工具,暴露给模型
  6. 第六步:集成到团队系统,支持协议化协作

团队协议系统与后续章节的关系

  • s16 团队协议:解决团队如何按规则协作的问题
  • s17 自主智能体:会利用协议系统来支持自主认领任务
  • s18 Worktree:会利用协议系统来协调工作区操作

所以团队协议系统是构建高级协作系统的基础组件。

下一章预告

有了团队协议系统,你的多个 Agent 已经能够进行结构化协作。下一章我们将探讨自主智能体系统,让 Agent 能够自主工作、主动认领任务,实现更高的自主性。


一句话总结:普通消息解决「说了什么」,协议消息解决「这件事走到哪一步了」。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。