WeClaw_64_角色化多Agent协作:从并行隔离到Explorer_Planner_Coder_Reviewer分工编排
第四季系列文章第 3 篇(总第 64 篇) - Multi-Agent · 角色分工 · RoleOrchestrator · EventBus 隔离 · Token 预算控制
📚 专栏信息
《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏 · 第四季
专栏定位:面向开发者和技术决策者的实战专栏,用真实案例和完整代码带你理解如何构建生产级 AI 应用
本文深入探讨 Harness Engineering 七大差距中的 G2——角色化多 Agent 协作。WeClaw 已有会话级 Agent 实例池,但当前是"并行隔离"模式——每个会话独立运行,没有角色分工。本文设计 Explorer/Planner/Coder/Reviewer 四角色协作体系,讲解 EventBus 隔离策略、SharedMemory 共享记忆、TokenBudgetController 预算控制,以及如何在不破坏现有 AgentPool 向后兼容性的前提下引入角色化。
👨💻 作者与项目
作者简介:翁勇刚 WENG YONGGANG 新概念龙虾-WeClaw 开发团队负责人,一群专注于跨平台 AI 应用的实践者 理念:"再复杂的技术,也能用代码讲清楚"
- 💻 项目地址:https://github.com/wyg5208/weclaw.git
- 🌐 官网地址:https://weclaw.link
- 📝 作者 CSDN:https://blog.csdn.net/yweng18
- ⭐ 欢迎 Star⭐、Fork🍴、贡献代码🤝
📝 摘要
本文结构概览: 从 WeClaw 当前的"增强型单 Agent + 并行会话隔离"模式出发,分析为什么复杂任务需要角色分工——一个 Agent 同时负责分析需求、制定计划、编写代码和审查质量,就像一个人同时做产品经理、架构师、程序员和 QA。然后设计 RoleOrchestrator 编排器,引入 Explorer/Planner/Coder/Reviewer 四角色,讲解每个角色的工具权限、推理预算和协作流程。重点解决三个工程难题:EventBus 事件隔离、AgentPool 向后兼容、Token 预算控制。
核心问题: 当一个任务需要"先调研、再规划、再执行、最后审查"时,单个 Agent 需要在同一个 ReAct 循环中切换四种思维模式。这不仅是效率问题,更是认知负荷问题——模型在一次对话中难以同时保持"探索者"的开放性和"审查者"的批判性。
关键成果:
- 设计了四角色协作体系,每个角色有独立的工具集、推理预算和 System Prompt
- 通过独立 EventBus 实例 + SharedMemory 实现角色间安全通信
- TokenBudgetController 硬上限 1.5x,防止成本失控
适合读者:构建多 Agent 系统的开发者、对 Agent 编排感兴趣的架构师
阅读时长:约 18 分钟
关键词:Multi-Agent、角色分工、RoleOrchestrator、EventBus 隔离、Token 预算、SharedMemory
一、为什么单个 Agent 不够?
1.1 认知负荷问题
考虑一个复杂任务:"帮我分析项目中的安全漏洞,然后修复它们,最后验证修复效果"。
单个 Agent 需要在同一个 ReAct 循环中完成:
Step 1-5: 探索阶段 — 搜索代码、理解结构(需要开放性思维)
Step 6-8: 规划阶段 — 制定修复方案(需要系统性思维)
Step 9-20: 执行阶段 — 编写修复代码(需要精确性思维)
Step 21-25: 审查阶段 — 验证修复效果(需要批判性思维)
问题在于:模型在一次对话中难以同时保持"探索者"的开放性和"审查者"的批判性。 当 Agent 进入"执行者"模式后,它倾向于继续执行而非质疑——这导致审查阶段的深度不足。
1.2 上下文污染问题
更隐蔽的问题是上下文污染:
探索阶段的上下文:"我发现 shell.py 有命令注入风险..."
规划阶段的上下文:"修复方案是在 execute() 中添加参数过滤..."
执行阶段的上下文:"已修改 shell.py 第 45 行..."
审查阶段的上下文:"让我检查修改是否正确..."
到审查阶段时,Agent 已经在上下文中积累了大量"探索-规划-执行"的信息。这些信息会形成确认偏误——Agent 倾向于认为自己的执行是正确的,因为"我刚才做的"。
1.3 工具权限问题
单 Agent 模式下,所有工具对所有阶段可见。但理想情况下:
- 探索阶段不应该有
shell.execute权限(防止意外修改) - 审查阶段不应该有
file.write权限(防止审查中修改代码)
角色分工的本质是:通过限制每个阶段的工具权限和上下文,降低认知负荷,提高每个阶段的执行质量。
二、四角色体系设计
2.1 角色定义
class AgentRole(str, Enum):
GENERAL = "general" # 默认角色,向后兼容
EXPLORER = "explorer" # 探索者
PLANNER = "planner" # 规划者
CODER = "coder" # 执行者
REVIEWER = "reviewer" # 审查者
@dataclass
class RoleConfig:
"""角色配置。"""
role: AgentRole
allowed_tools: set[str] # 允许使用的工具集
system_prompt_suffix: str # 角色专属 Prompt
max_steps: int # 最大推理步数
preferred_model: str # 首选模型
max_tokens_budget: int # Token 预算
allowed_intents: list[str] # 允许的意图类型
2.2 角色矩阵
| 角色 | 职责 | 允许的工具 | 最大步数 | 思维特征 |
|---|---|---|---|---|
| Explorer | 分析需求、收集信息 | search, browser, file(只读), codebase_search | 10 | 开放、发散 |
| Planner | 分解任务、制定方案 | file(只读), codebase_search, search | 8 | 系统、结构化 |
| Coder | 编写代码、修改文件 | shell, file, screen, coding_assistant | 30 | 精确、专注 |
| Reviewer | 验证质量、检查错误 | shell, file(只读), screen, log_viewer | 10 | 批判、严格 |
2.3 协作流程
用户输入
│
▼
RoleOrchestrator.handle_task()
│
├─ 1. 分析任务复杂度
│ 简单任务 → 单 Agent 快速路径(role=GENERAL)
│ 复杂任务 → 角色序列
│
├─ 2. Explorer 阶段
│ 创建独立 Agent(独立 EventBus + 独立 session)
│ 执行探索 → 输出上下文摘要
│
├─ 3. Planner 阶段
│ 接收 Explorer 的摘要(SharedMemory)
│ 执行规划 → 输出执行计划
│
├─ 4. Coder 阶段
│ 接收 Planner 的计划
│ 执行编码 → 输出产物列表
│
└─ 5. Reviewer 阶段
接收 Coder 的产物
执行审查 → 输出审查报告
(如发现问题 → 回到 Coder 阶段,最多 1 次返工)
三、EventBus 隔离 —— 代码级评审的关键发现
3.1 问题:EventBus 不支持子实例
V2 方案假设可以为每个角色 Agent 创建 "EventBus 子实例"。但代码级评审发现,WeClaw 的 EventBus 是一个简单的 pub-sub 实现,不支持子实例或独立通道。
# event_bus.py — 现有实现
class EventBus:
def __init__(self):
self._subscribers: dict[str, list[Callable]] = {}
async def emit(self, event_type: str, data: Any = None) -> int:
# 遍历该事件类型的所有订阅者并调用
...
# ❌ 没有 create_sub_instance() 或 create_channel() 方法
如果所有角色 Agent 共享同一个 EventBus,Explorer 的事件会被 Coder 接收到——这会导致角色间的事件污染。
3.2 解决方案:独立 EventBus 实例
最简单也最安全的方案:为每个角色 Agent 创建全新的 EventBus() 实例。
class RoleOrchestrator:
async def handle_task(self, user_input: str, parent_session_id: str) -> str:
# Explorer 阶段
explorer_bus = EventBus() # ⚫ 独立实例
explorer_agent = await self._agent_pool.get_or_create(
session_id=f"{parent_session_id}_explorer",
role=AgentRole.EXPLORER,
role_config=self._role_configs[AgentRole.EXPLORER],
event_bus=explorer_bus, # 独立 EventBus
)
# ...
3.3 关键事件转发
角色完成后,RoleOrchestrator 将关键摘要事件转发到全局 EventBus:
async def handle_task(self, user_input: str, parent_session_id: str) -> str:
# ... Explorer 执行完毕 ...
# 转发关键事件到全局 EventBus
await self._global_event_bus.emit(
EventType.TASK_PHASE_CHANGE,
TaskPhaseEvent(
session_id=parent_session_id,
phase="exploration_complete",
role="explorer",
)
)
# 将 Explorer 的摘要存入 SharedMemory
shared_memory = SharedMemory(
parent_session_summary=explorer_result,
artifacts=[],
decisions=[],
errors=[],
)
# ... 进入 Planner 阶段 ...
四、SharedMemory —— 角色间的安全通信
4.1 设计
角色之间不直接交换消息,而是通过 SharedMemory 传递只读上下文摘要:
@dataclass
class SharedMemory:
"""角色间共享的只读上下文摘要。"""
parent_session_summary: str # 上一角色的执行摘要
artifacts: list[str] # 产生的文件/产物列表
decisions: list[str] # 关键决策记录
errors: list[str] # 遇到的错误
4.2 为什么不直接传递完整上下文?
直接传递完整上下文有两个问题:
- Token 爆炸:Explorer 阶段可能消耗 5000+ token 的上下文,全部传给 Planner 会导致预算超限
- 认知污染:Planner 不需要知道 Explorer 的每一步搜索细节,只需要知道"发现了什么"
SharedMemory 的本质是信息压缩——每个角色只接收上一个角色的"结论摘要",而非"过程日志"。
4.3 传递方式
# Planner 接收 Explorer 的摘要
planner_prompt = f"""
## 上一阶段(探索)的结果摘要
{shared_memory.parent_session_summary}
## 发现的产物
{chr(10).join(f'- {a}' for a in shared_memory.artifacts)}
## 请基于以上信息制定执行计划。
"""
五、AgentPool 向后兼容 —— 不破坏现有系统
5.1 兼容性挑战
WeClaw 的 AgentPool.get_or_create() 方法已有稳定的调用方(桌面端、PWA 远程端)。任何修改都不能影响现有行为。
5.2 解决方案:默认值兼容
async def get_or_create(
self,
session_id: str,
source: str = "desktop",
metadata: Optional[dict] = None,
# ↓ 新增参数,全部有默认值 ↓
role: AgentRole = AgentRole.GENERAL,
role_config: Optional[RoleConfig] = None,
event_bus: Optional[EventBus] = None,
) -> Agent:
# ... 现有逻辑 ...
agent = Agent(
model_registry=self._model_registry,
tool_registry=self._tool_registry,
event_bus=event_bus or self._event_bus, # 角色用独立,默认用共享
# ...
max_steps=role_config.max_steps if role_config else self._default_max_steps,
)
# 角色工具过滤(仅 role != GENERAL 时生效)
if role_config and role != AgentRole.GENERAL:
agent.tool_exposure.set_role_filter(role_config.allowed_tools)
agent.system_prompt += f"\n\n{role_config.system_prompt_suffix}"
关键设计:当 role=GENERAL(默认值)时,不传任何额外参数,行为与现有完全一致。现有调用方无需任何修改。
5.3 验证策略
# 向后兼容测试
def test_general_role_backward_compatible():
"""GENERAL 角色行为必须与无角色参数完全一致。"""
pool = AgentPool(...)
# 旧方式(不传 role 参数)
agent_old = await pool.get_or_create("session_1", "desktop")
# 新方式(显式传 role=GENERAL)
agent_new = await pool.get_or_create("session_2", "desktop",
role=AgentRole.GENERAL)
# 两者行为一致
assert agent_old.max_steps == agent_new.max_steps
assert agent_old.event_bus is agent_new.event_bus # 都用共享 EventBus
assert not hasattr(agent_old, '_role_filter')
assert not hasattr(agent_new, '_role_filter')
六、TokenBudgetController —— 成本控制
6.1 问题:多 Agent 的成本爆炸
四角色协作意味着 4 个 Agent 各自消耗 token。如果不控制,总成本可能是单 Agent 的 4 倍。
6.2 解决方案:1.5 倍硬上限
class TokenBudgetController:
"""多 Agent 协作场景的 token 预算控制。
总预算不超过单 Agent 的 1.5 倍。
"""
def __init__(self, single_agent_budget: int, multiplier: float = 1.5):
self._total_budget = int(single_agent_budget * multiplier)
self._consumed = 0
def consume(self, tokens: int, role: AgentRole) -> bool:
"""消耗 token 预算,返回是否还在预算内。"""
self._consumed += tokens
if self._consumed > self._total_budget:
logger.warning(
"Token 预算超限: %d/%d (role=%s)",
self._consumed, self._total_budget, role.value
)
return False
return True
def get_remaining(self) -> int:
return max(0, self._total_budget - self._consumed)
6.3 超预算降级
当 token 接近上限时,自动降级到更便宜的模型:
# 超过 80% 预算时,后续角色使用经济模型
if budget_controller.get_remaining() < total_budget * 0.2:
model_key = "qwen-turbo" # 经济模型
else:
model_key = role_config.preferred_model # 首选模型
6.4 为什么是 1.5 倍而不是 4 倍?
四角色不等于 4 倍 token,因为:
- SharedMemory 压缩:每个角色只接收上一个角色的摘要,不是完整上下文
- 工具权限收窄:角色只能用少量工具,减少了工具描述的 token 开销
- 步数限制:Explorer 最多 10 步、Planner 最多 8 步,远少于单 Agent 的 30 步
1.5 倍是一个经验值——足以完成复杂任务,又不会让成本失控。
七、简单任务的快速路径
7.1 不是所有任务都需要四角色
用户:"今天天气怎么样?"
→ 不需要 Explorer/Planner/Coder/Reviewer
→ 走单 Agent 快速路径
7.2 复杂度判断
async def handle_task(self, user_input: str, parent_session_id: str) -> str:
# 1. 分析任务复杂度
complexity = await self._analyze_complexity(user_input)
if complexity < self._complexity_threshold:
# 简单任务:单 Agent 快速路径
return await self._single_agent_path(user_input, parent_session_id)
else:
# 复杂任务:角色化路径
return await self._role_based_path(user_input, parent_session_id)
复杂度判断可以基于:
- 输入长度(>100 字符可能是复杂任务)
- 意图分类(code_modify 类意图更可能需要角色分工)
- 历史模式(类似任务过去的工具调用步数)
八、核心教训
8.1 EventBus 隔离不是可选项
V2 方案假设可以"在共享 EventBus 上创建子通道",代码级评审证明这不可行。如果所有角色共享一个 EventBus,事件会跨角色泄漏——Explorer 的搜索事件被 Coder 接收,Coder 以为有新的搜索任务要执行。
教训:在 pub-sub 架构中,隔离的唯一可靠方式是独立实例。
8.2 向后兼容是设计约束,不是事后补丁
我们一开始就为 get_or_create() 的新参数设置了默认值(role=GENERAL),确保现有调用方零改动。如果等到角色化功能开发完再考虑兼容性,很可能需要重构调用方——那就不是"灰度上线"了。
8.3 Token 预算比功能更难控制
四角色的功能设计相对直观——每个角色做什么很清楚。但 token 预算控制是一个持续的工程挑战:SharedMemory 压缩到什么程度?摘要由谁生成(LLM 还是规则)?超预算时降级到什么模型?这些问题没有标准答案,需要在实际运行中持续调整。
📖 相关文章
本文是 WeClaw 专栏第四季的第 3 篇(总第 64 篇)。如果这篇文章对你有帮助,欢迎给项目点个 Star ⭐