Environment 消息路由与多角色协同
学习目标
理解 Environment 如何实现消息路由、角色协同执行,以及 ExtEnv 如何集成外部游戏环境。
项目实践
Environment 核心职责
Environment 是角色运行的容器,负责:
- 角色管理:注册角色、设置环境引用
- 消息路由:发布消息到订阅者
- 并行执行:异步运行所有角色
- 历史记录:记录所有消息用于调试
- 成本追踪:共享 CostManager
消息路由机制
消息路由是 Environment 最核心的功能,涉及三个要素:
| 要素 | 说明 | 定义位置 |
|---|---|---|
send_to | 消息接收者地址集合 | Message.send_to: set[str] |
cause_by | 消息的触发 action | Message.cause_by: str |
addresses | 角色订阅的地址 | Role.addresses: set[str] |
publish_message() 实现:
def publish_message(self, message: Message, peekable: bool = True) -> bool: found = False for role, addrs in self.member_addrs.items(): if is_send_to(message, addrs): # 匹配 send_to 和 addresses role.put_message(message) # 投递到角色的 msg_buffer found = True if not found: logger.warning(f"Message no recipients: {message.dump()}") self.history.add(message) # 记录到历史 return True角色地址
角色默认订阅自己的名称和类型:
@model_validator(mode="after")def check_addresses(self): if not self.addresses: self.addresses = {any_to_str(self), self.name} if self.name else {any_to_str(self)} return self这意味着角色默认接收发给自己的消息和以自身类型标记的消息。
并行执行
Environment.run() 使用 asyncio.gather() 并行执行所有非 idle 角色:
async def run(self, k=1): for _ in range(k): futures = [] for role in self.roles.values(): if role.is_idle: continue future = role.run() futures.append(future) if futures: await asyncio.gather(*futures)关键细节:
is_idle检查:not rc.news and not rc.todo and rc.msg_buffer.empty()- 所有角色在同一轮次并行执行,不是串行
- 一轮中角色的
publish_message()投递的消息会在下一轮被处理
ExtEnv:外部游戏环境
ExtEnv 继承 BaseEnvironment + BaseModel,提供 Gymnasium 风格接口:
class ExtEnv(BaseEnvironment, BaseModel): action_space: spaces.Space[ActType] observation_space: spaces.Space[ObsType]
def reset(self, seed=None, options=None) -> tuple[dict, dict]: ... def observe(self, obs_params=None) -> Any: ... def step(self, action: BaseEnvAction) -> tuple[dict, float, bool, bool, dict]: ...API 注册装饰器:
def mark_as_readable(func): """标记为 ExtEnv 中的可读 API""" env_read_api_registry[func.__name__] = get_function_schema(func) return func
def mark_as_writeable(func): """标记为 ExtEnv 中的可写 API""" env_write_api_registry[func.__name__] = get_function_schema(func) return func角色通过 read_from_api() / write_thru_api() 调用环境能力,而不需要直接引用环境对象。
已实现的 ExtEnv
| 环境 | 用途 | 特殊能力 |
|---|---|---|
| AndroidEnv | Android 操控 | ADB 命令、屏幕截图、点击/滑动 |
| MinecraftEnv | Minecraft 游戏 | Mineflayer JS 集成、方块操作 |
| WerewolfEnv | 狼人杀 | 游戏逻辑、角色分配、投票 |
| StanfordTownEnv | 社交模拟 | Stanford小镇模拟、角色行为 |
问题与规避
消息静默丢弃
- 如果角色没有
set_env(),publish_message()直接 return,消息静默丢弃 - 对策:确保在
run()前调用env.add_roles()或role.set_env()
并行执行的竞态条件
- 多个角色并行执行时,可能同时向同一文件写入
- 对策:通过
cause_by和watch机制确保角色按序触发,避免并行冲突 - 软件公司场景天然串行:PRD → Design → Code 是流水线式的
消息堆积
- 如果角色处理速度慢于消息产生速度,
msg_buffer会持续增长 - 对策:
_observe()中的pop_all()一次性清空缓冲区,max_react_loop限制处理轮次
设计取舍
异步并行 vs 串行执行
- 异步并行提高了吞吐量,但增加了调试复杂度
- 软件公司的角色间依赖关系(PRD → Design → Code)天然串行,并行执行不会导致问题
- 对于强依赖顺序的场景(如 Werewolf 的夜间阶段),需要额外的状态同步
Gymnasium 接口
- 采用 Gymnasium 标准接口使 ExtEnv 兼容强化学习框架
- 代价是增加了抽象层,简单的场景可能不需要 action_space/observation_space
- 好处:可以直接使用 RL 训练 Agent 在环境中行动
参考来源
- 源码验证:
metagpt/environment/base_env.py:124— Environment 类 - 源码验证:
metagpt/environment/base_env.py:175—publish_message()路由实现 - 源码验证:
metagpt/environment/base_env.py:197—run()并行执行 - 源码验证:
metagpt/environment/base_env.py:53— ExtEnv 类与 Gymnasium 接口 - 源码验证:
metagpt/roles/role.py:429—publish_message()角色端实现