该文件定义了一个狼人杀游戏中的基础玩家角色类 BasePlayer,它继承自 Role 基类,负责模拟玩家在游戏中的核心行为,包括监听游戏指令、根据指令思考并决定行动(如公开发言或执行特殊技能)、执行行动(调用相应的 Action 类),并在过程中可选地进行反思和利用经验库。该类是构建具体狼人杀角色(如预言家、狼人)的基类。
graph TD
A[BasePlayer 实例化] --> B[初始化: 设置监听、行动能力、特殊技能]
C[游戏回合开始] --> D[Moderator 广播 InstructSpeak 指令]
D --> E[BasePlayer._observe: 接收并过滤消息]
E --> F{玩家是否存活?}
F -- 否 --> G[结束,不参与]
F -- 是 --> H[BasePlayer._think: 解析指令决定行动]
H --> I{指令接收范围?}
I -- 全体 --> J[行动设为 Speak (公开发言)]
I -- 仅自己 --> K[行动设为 special_actions[0] (如 NighttimeWhispers)]
J --> L[BasePlayer._act: 执行行动]
K --> L
L --> M[准备执行参数: 获取记忆、最新指令]
M --> N{是否启用反思?}
N -- 是 --> O[调用 Reflect Action 生成反思文本]
N -- 否 --> P[反思文本为空]
O --> Q{是否启用经验库?}
P --> Q
Q -- 是 --> R[调用 RetrieveExperiences Action 检索相关经验]
Q -- 否 --> S[经验文本为空]
R --> T[根据行动类型调用对应 Action.run]
S --> T
T --> U[生成 WwMessage 作为行动结果]
U --> V[将本次经历记录到 experiences 列表]
V --> W[返回 WwMessage 给环境]
W --> X[回合结束]
Role (MetaGPT 角色基类)
└── BasePlayer (狼人杀玩家基类)玩家的名称,默认为'PlayerXYZ',用于标识游戏中的具体玩家实例。
类型:str
玩家的角色身份描述,默认为'BasePlayer',用于表示玩家在游戏中的角色(如狼人、预言家等)。
类型:str
特殊动作名称列表,用于定义该角色可以执行的特殊技能(如狼人杀人、预言家验人等)。
类型:list[str]
是否启用反思功能,用于在行动前对当前局势和记忆进行思考分析。
类型:bool
是否启用经验检索功能,用于从历史经验中学习并指导当前决策。
类型:bool
是否启用记忆选择功能(当前代码中未使用,保留为未来扩展)。
类型:bool
新经验版本标识符,用于标记当前游戏回合生成的经验记录版本。
类型:str
玩家的游戏状态(如存活、死亡等),用于控制玩家是否参与游戏进程。
类型:RoleState
特殊动作对象列表,根据special_action_names动态加载的具体Action实例。
类型:list[SerializeAsAny[Action]]
角色经验记录列表,存储玩家在游戏中的反思、指令和响应等经验数据。
类型:list[RoleExperience]
该方法用于初始化BasePlayer类的实例,继承自Role类,并设置狼人杀游戏中玩家的基本属性、监听行为、可用动作以及特殊技能。
参数:
kwargs:dict,关键字参数,用于传递给父类Role的初始化方法
返回值:None,无返回值
flowchart TD
A[开始初始化BasePlayer实例] --> B[调用父类Role的__init__方法]
B --> C[监听InstructSpeak动作]
C --> D[根据special_action_names<br>获取特殊动作列表]
D --> E[设置可用动作为Speak加特殊动作]
E --> F[将特殊动作赋值给实例变量special_actions]
F --> G[检查use_reflection和use_experience<br>的逻辑一致性]
G --> H[结束初始化]
def __init__(self, **kwargs):
# 调用父类Role的初始化方法,传入所有关键字参数
super().__init__(**kwargs)
# 技能和监听配置
# 监听Moderator的指令以做行动
self._watch([InstructSpeak])
# 根据实例的special_action_names列表,从全局ACTIONS映射中获取对应的特殊动作类
special_actions = [ACTIONS[action_name] for action_name in self.special_action_names]
# 设置该角色的可用动作:基础发言动作Speak加上所有特殊动作
capable_actions = [Speak] + special_actions
self.set_actions(capable_actions) # 给角色赋予行动技能
# 将特殊动作列表保存到实例变量中
self.special_actions = special_actions
# 逻辑检查:如果未启用反思(use_reflection)但启用了经验(use_experience),则发出警告并禁用经验功能
if not self.use_reflection and self.use_experience:
logger.warning("You must enable use_reflection before using experience")
self.use_experience = False该方法是一个模型验证器,用于在BasePlayer类实例初始化后,自动检查和设置其addresses属性。如果addresses为空,则根据实例的name和profile属性为其设置一个默认的地址集合。
参数:
- 无显式参数。作为
@model_validator(mode="after")装饰的方法,它接收并处理整个模型实例(self)。
返回值:BasePlayer,返回经过验证和可能修改后的模型实例自身。
flowchart TD
Start[开始验证] --> Condition{self.addresses 是否为空?}
Condition -- 是 --> SetAddresses[设置默认地址集合]
SetAddresses --> Return[返回 self]
Condition -- 否 --> Return
@model_validator(mode="after") # 这是一个Pydantic模型验证器,在模型实例化后(所有字段赋值后)运行。
def check_addresses(self):
# 检查实例的`addresses`字段是否为空列表或None。
if not self.addresses:
# 如果`addresses`为空,则根据条件构建一个默认的地址集合。
# `any_to_str(self)` 生成一个代表实例本身的唯一字符串标识。
# 如果`self.name`存在(非空),则将`self.name`和`self.profile`也加入集合。
# 否则,只包含`any_to_str(self)`。
self.addresses = {any_to_str(self), self.name, self.profile} if self.name else {any_to_str(self)}
# 验证器必须返回模型实例(或包含模型实例的字典)。
return selfBasePlayer._observe 方法是狼人杀游戏角色基类中用于处理消息观察的核心逻辑。它负责从消息缓冲区中获取新消息,根据消息的接收范围(restricted_to)过滤出当前角色有权接收的消息,并将其存入角色的记忆(memory)中。同时,它会更新角色的新闻列表(news),该列表包含了当前角色需要响应(_react)的消息,即那些由角色监听(watch)的动作触发或明确发送给该角色的消息。该方法确保了角色只能看到和响应与其相关的游戏信息。
参数:
self:BasePlayer,BasePlayer类的实例,代表当前游戏角色。ignore_memory:bool,指示是否忽略现有记忆进行消息处理。如果为True,则不会从现有记忆中排除已处理过的消息。默认值为False。
返回值:int,返回一个整数,表示经过过滤后,当前角色需要响应的新消息(self.rc.news)的数量。
flowchart TD
A[开始 _observe] --> B{角色状态是否为 ALIVE?}
B -- 否 --> C[返回 0]
B -- 是 --> D[从消息缓冲区弹出所有新消息 news]
D --> E{ignore_memory 为 True?}
E -- 是 --> F[old_messages = 空列表]
E -- 否 --> G[old_messages = 从 memory 获取的历史消息]
F --> H
G --> H
subgraph H [遍历并处理每条新消息]
H1[遍历 news 中的每条消息 m] --> H2{消息 m 的 restricted_to<br>包含当前角色吗?}
H2 -- 是 --> H3[将消息 m 存入 memory]
H2 -- 否 --> H4[跳过此消息]
H3 --> H5[继续下一条消息]
H4 --> H5
end
H --> I[过滤出需要响应的消息<br>(由监听动作触发或发送给本角色)<br>且不在 old_messages 中]
I --> J[将过滤结果赋值给 self.rc.news]
J --> K[返回 self.rc.news 的长度]
async def _observe(self, ignore_memory=False) -> int:
# 检查角色状态,如果角色已死亡(非 ALIVE 状态),则不参与游戏,直接返回 0
if self.status != RoleState.ALIVE:
# 死者不再参与游戏
return 0
# 初始化新闻列表,从角色的消息缓冲区中获取所有待处理的新消息
news = []
if not news:
news = self.rc.msg_buffer.pop_all()
# 根据 ignore_memory 参数决定是否获取旧消息用于去重
old_messages = [] if ignore_memory else self.rc.memory.get()
# 遍历所有新消息,根据接收范围决定是否存入角色记忆
for m in news:
# 如果消息有特定的接收者限制,且当前角色(profile 或 name)不在接收列表中,
# 则跳过此消息,不存入记忆。空列表表示发送给全体。
if len(m.restricted_to) and self.profile not in m.restricted_to and self.name not in m.restricted_to:
# if the msg is not send to the whole audience ("") nor this role (self.profile or self.name),
# then this role should not be able to receive it and record it into its memory
continue
# 符合条件的消息存入角色记忆
self.rc.memory.add(m)
# 更新角色的新闻列表(rc.news):
# 1. 消息的触发动作(cause_by)在角色的监听列表(rc.watch)中,或者
# 2. 消息明确发送给当前角色(self.profile 在 send_to 中)
# 并且该消息不在旧消息列表(old_messages)中,以避免重复处理。
self.rc.news = [
n for n in news if (n.cause_by in self.rc.watch or self.profile in n.send_to) and n not in old_messages
]
# 返回需要响应的新消息数量
return len(self.rc.news)该方法用于根据接收到的消息决定玩家下一步要执行的动作。它检查消息的来源和接收范围,以确定是进行公开发言还是执行特殊动作。
参数:
- 无显式参数,但方法内部使用
self访问实例属性。
返回值:bool,总是返回 True,表示思考过程已完成并成功设置了待执行动作。
flowchart TD
A[开始] --> B[获取最新消息 news]
B --> C{news.cause_by 是否为 InstructSpeak?}
C -- 否 --> D[抛出断言错误]
C -- 是 --> E{news.restricted_to 是否为空?}
E -- 是 --> F[设置待执行动作为 Speak]
E -- 否 --> G{self.profile 是否在<br>news.restricted_to 中?}
G -- 是 --> H[设置待执行动作为<br>第一个特殊动作]
G -- 否 --> I[不设置动作]
F --> J[返回 True]
H --> J
I --> J
async def _think(self):
# 从角色的新闻缓冲区中获取最新的消息
news = self.rc.news[0]
# 断言:确保该消息是由 InstructSpeak 动作触发的,即来自 Moderator 的指令
assert news.cause_by == any_to_str(InstructSpeak) # 消息为来自Moderator的指令时,才去做动作
if not news.restricted_to:
# 情况1:如果消息的接收范围(restricted_to)为空,表示这是发给全体角色的公开消息。
# 玩家需要做出公开发言(例如,投票或讨论)。
self.rc.todo = Speak()
elif self.profile in news.restricted_to:
# 情况2:如果消息的接收范围中包含当前玩家的角色(profile),
# 表示这是 Moderator 加密发送给该玩家的私密指令,要求执行其特殊动作(如狼人杀人、预言家验人等)。
# FIXME: hard code to split, restricted为"Moderator"或"Moderator, 角色profile"
self.rc.todo = self.special_actions[0]()
# 如果消息的接收范围不为空且不包含当前玩家,则玩家不应做出任何反应(此逻辑隐含在代码中,未显式写出)。
# 方法始终返回 True,表示思考步骤已完成。
return TrueBasePlayer._act 方法是狼人杀游戏中玩家角色的核心行动方法。它根据 _think 阶段确定的待执行动作(todo),结合角色的记忆、反思和经验,执行相应的行动(如公开发言或使用夜间技能),并生成一条游戏消息。同时,该方法会记录本次行动的经验,用于后续的学习和优化。
参数:
self:BasePlayer,当前玩家角色的实例。
返回值:WwMessage,返回一个封装了行动结果(如发言内容或技能使用结果)的游戏消息对象,该消息包含了发送者、接收者、触发动作等信息。
flowchart TD
A[开始执行 _act] --> B[获取待执行动作 todo]
B --> C[记录日志]
C --> D[获取全部记忆 memories]
D --> E[获取最新指令 latest_instruction]
E --> F{是否启用反思 use_reflection?}
F -- 是 --> G[运行 Reflect 动作生成反思 reflection]
F -- 否 --> H[reflection 设为空字符串]
G --> I{是否启用经验 use_experience?}
H --> I
I -- 是 --> J[运行 RetrieveExperiences 获取相关经验 experiences]
I -- 否 --> K[experiences 设为空字符串]
J --> L{判断 todo 类型}
K --> L
L -- 是 Speak --> M[运行 Speak 动作<br>生成公开回应 rsp<br>设置接收者 restricted_to 为空]
L -- 是 NighttimeWhispers --> N[运行 NighttimeWhispers 动作<br>生成加密回应 rsp<br>设置接收者为 Moderator 和自己]
M --> O[用 rsp 等信息构造 WwMessage]
N --> O
O --> P[将本次行动记录为 RoleExperience<br>并存入 experiences 列表]
P --> Q[记录回应日志]
Q --> R[返回构造的 WwMessage]
async def _act(self):
# todo为_think时确定的,有两种情况,Speak或Protect
todo = self.rc.todo
logger.info(f"{self._setting}: ready to {str(todo)}")
# 可以用这个函数获取该角色的全部记忆和最新的instruction
memories = self.get_all_memories()
latest_instruction = self.get_latest_instruction()
# 如果启用了反思功能,则运行Reflect动作来生成基于当前记忆和指令的反思文本
reflection = (
await Reflect().run(
profile=self.profile, name=self.name, context=memories, latest_instruction=latest_instruction
)
if self.use_reflection
else ""
)
# 如果启用了经验功能,则根据反思文本检索相关的历史经验
experiences = (
RetrieveExperiences().run(
query=reflection, profile=self.profile, excluded_version=self.new_experience_version
)
if self.use_experience
else ""
)
# 根据自己定义的角色Action,对应地去run,run的入参可能不同
if isinstance(todo, Speak):
# 执行公开发言动作,生成回应内容rsp,并设置消息接收者为全体(空集)
rsp = await todo.run(
profile=self.profile,
name=self.name,
context=memories,
latest_instruction=latest_instruction,
reflection=reflection,
experiences=experiences,
)
restricted_to = set()
elif isinstance(todo, NighttimeWhispers):
# 执行夜间私语(特殊技能)动作,生成加密回应rsp,并设置消息仅对Moderator和自己可见
rsp = await todo.run(
profile=self.profile, name=self.name, context=memories, reflection=reflection, experiences=experiences
)
restricted_to = {RoleType.MODERATOR.value, self.profile} # 给Moderator发送使用特殊技能的加密消息
# 使用行动结果、角色信息等构造一个完整的游戏消息对象
msg = WwMessage(
content=rsp,
role=self.profile,
sent_from=self.name,
cause_by=type(todo),
send_to={},
restricted_to=restricted_to,
)
# 将本次行动(包括反思、指令、回应等)记录为一个经验对象,并添加到经验列表中
self.experiences.append(
RoleExperience(
name=self.name,
profile=self.profile,
reflection=reflection,
instruction=latest_instruction,
response=rsp,
version=self.new_experience_version,
)
)
logger.info(f"{self._setting}: {rsp}")
# 返回构造的消息,该消息将被放入环境或发送给其他角色
return msg该方法用于获取并格式化当前角色(BasePlayer 实例)的所有记忆。它从角色的记忆存储中检索所有消息,移除每条消息内容中的时间戳,并将消息格式化为一个字符串,其中每条消息以“发送者: 内容”的形式呈现。
参数:
- 无
返回值:str,一个字符串,包含所有格式化后的记忆,每条记忆占一行。
flowchart TD
A[开始] --> B[从 self.rc.memory.get() 获取所有记忆]
B --> C[遍历每条记忆]
C --> D[使用正则表达式移除内容中的时间戳]
D --> E[格式化记忆为“发送者: 内容”]
E --> F{是否还有记忆?}
F -- 是 --> C
F -- 否 --> G[将所有格式化记忆用换行符连接]
G --> H[返回格式化后的字符串]
H --> I[结束]
def get_all_memories(self) -> str:
# 从角色的记忆组件中获取所有存储的消息对象列表
memories = self.rc.memory.get()
# 定义用于匹配时间戳的正则表达式模式,例如 "123 | "
time_stamp_pattern = r"[0-9]+ \| "
# 遍历每条消息,移除内容中的时间戳,并格式化为“发送者: 内容”的字符串
# 注意:这里使用 m.sent_from(玩家名)而非 m.role(玩家角色),因为其他角色不知道说话者的真实身份
memories = [f"{m.sent_from}: {re.sub(time_stamp_pattern, '', m.content)}" for m in memories]
# 将所有格式化后的字符串用换行符连接成一个完整的字符串
memories = "\n".join(memories)
# 返回格式化后的记忆字符串
return memories该方法用于获取角色最新接收到的指令内容。它从角色的重要记忆(important_memory)中提取最近一条指令的内容,该指令通常由游戏主持人(Moderator)通过InstructSpeak动作发出。
参数:无
返回值:str,返回最新指令的文本内容。
flowchart TD
A[开始] --> B[访问 self.rc.important_memory[-1]]
B --> C[获取其 content 属性]
C --> D[返回指令内容字符串]
D --> E[结束]
def get_latest_instruction(self) -> str:
# 从角色的重要记忆列表中获取最后一条记录
# self.rc.important_memory[-1] 指向最新的一条重要记忆
# 该记忆的内容(content)即为最新的指令
return self.rc.important_memory[-1].content该方法用于设置玩家的状态,例如将状态从“存活”更改为“死亡”。
参数:
new_status:RoleState,新的角色状态值,例如RoleState.ALIVE或RoleState.DEAD。
返回值:None,此方法不返回任何值。
flowchart TD
A[开始] --> B[接收参数 new_status]
B --> C[将实例的 status 字段更新为 new_status]
C --> D[结束]
def set_status(self, new_status: RoleState):
# 将当前实例的 `status` 字段更新为传入的 `new_status` 参数。
# 这通常用于在游戏过程中改变玩家的状态,例如在狼人杀游戏中,当玩家被淘汰时,将其状态从 `ALIVE` 改为 `DEAD`。
self.status = new_status该方法用于处理并记录玩家在当前游戏回合中的经验。它会筛选出包含有效反思(非空字符串)的经验条目,为这些条目附加回合ID、游戏结果和游戏设置信息,然后通过AddNewExperiences动作将这些处理后的经验持久化存储。
参数:
round_id:str,当前游戏回合的唯一标识符。outcome:str,当前游戏回合的结果(例如,好人阵营胜利、狼人阵营胜利)。game_setup:str,当前游戏的配置信息(例如,玩家角色分布)。
返回值:None,此方法不返回任何值。
flowchart TD
A[开始] --> B[筛选经验列表<br>过滤掉反思内容长度小于等于2的经验]
B --> C{遍历筛选后的<br>每一条经验}
C --> D[为当前经验条目设置<br>round_id, outcome, game_setup]
D --> C
C --> E[遍历结束]
E --> F[调用AddNewExperiences().run<br>持久化处理后的经验列表]
F --> G[结束]
def record_experiences(self, round_id: str, outcome: str, game_setup: str):
# 1. 筛选经验:从self.experiences列表中,只保留那些反思内容长度大于2的经验条目。
# 长度判断用于过滤掉反思内容为空字符串("")或仅包含两个引号('""')的无效经验。
experiences = [exp for exp in self.experiences if len(exp.reflection) > 2] # not "" or not '""'
# 2. 信息附加:遍历筛选后的经验列表,为每一条经验设置本回合的元数据。
for exp in experiences:
exp.round_id = round_id # 设置回合ID
exp.outcome = outcome # 设置游戏结果
exp.game_setup = game_setup # 设置游戏配置
# 3. 持久化存储:调用AddNewExperiences动作的run方法,将处理后的经验列表提交给系统进行存储。
# 这通常涉及将经验写入数据库或外部知识库。
AddNewExperiences().run(experiences)通过 status 字段和 set_status 方法管理角色在游戏中的生存状态(如 ALIVE),并控制其是否参与游戏流程(如在 _observe 方法中,死者不再处理消息)。
通过 special_action_names 和 special_actions 字段定义并初始化角色的特殊技能(如 NighttimeWhispers),并通过 _watch 和 set_actions 方法配置角色监听的事件(如 InstructSpeak)和可执行的动作列表。
通过 rc.memory 和 experiences 字段分别存储角色的对话记忆和游戏经验。get_all_memories 方法用于格式化记忆,而 record_experiences 方法则将本轮经验与游戏结果关联后持久化存储。
通过 use_reflection 和 use_experience 标志控制是否启用反思和经验检索功能。在 _act 方法中,Reflect 动作基于记忆生成反思,RetrieveExperiences 动作则根据反思查询历史经验,以辅助决策。
在 _observe 方法中,根据消息的 restricted_to 字段过滤接收范围,确保角色只能收到发送给全体或自己的消息。在 _act 方法中,根据动作类型(如 Speak 或 NighttimeWhispers)设置消息的接收者,实现公开发言或私密通信。
- 硬编码的
special_actions索引:在_think方法中,当消息接收者为特定角色时,代码直接使用self.special_actions[0]()来获取待执行动作。这假设了special_actions列表至少有一个元素,并且角色只有一个特殊动作。如果special_action_names配置了多个动作或为空,此逻辑将导致IndexError或执行错误的动作。 - 脆弱的
restricted_to解析逻辑:_think方法中的注释提到restricted_to字段的格式为"Moderator"或"Moderator, 角色profile",但代码中仅通过self.profile in news.restricted_to进行成员检查。如果restricted_to是一个集合(如_act方法中创建消息时所设),此检查有效;但如果它是一个用逗号分隔的字符串(如注释所述),此检查将失败,导致逻辑错误。代码中存在不一致的假设。 - 未使用的
use_memory_selection字段:类中定义了use_memory_selection: bool = False字段,但在代码的任何地方都未使用该字段,这可能是未完成的功能或残留的代码,增加了维护成本。 _observe方法中的死代码和潜在逻辑错误:- 方法开头有
if not news:的判断,但此时news刚被初始化为空列表[],因此news = self.rc.msg_buffer.pop_all()这行代码永远会执行,开头的条件判断是多余的。 - 方法中有一段被注释掉的
TODO to delete代码块,调用了super()._observe()并进行了额外的消息过滤。这段注释代码的存在造成了混淆,不清楚是否应该被删除或替代当前逻辑。 - 消息过滤逻辑
self.rc.news = [n for n in news if (n.cause_by in self.rc.watch or self.profile in n.send_to) and n not in old_messages]可能过于宽松。self.profile in n.send_to检查允许发送给任何包含该角色profile的集合的消息通过,这可能与restricted_to的意图冲突。
- 方法开头有
- 经验记录的条件可能过于宽松:在
record_experiences方法中,筛选经验的条件是len(exp.reflection) > 2,旨在过滤掉空字符串或仅包含两个引号的字符串。然而,一个长度为3但无意义的字符串(如"a")也会被保留,而一个长度为2的有效反射(如"no")则会被过滤。这个启发式规则可能不可靠。 check_addresses验证器的潜在问题:@model_validator(mode="after")装饰的check_addresses方法在初始化后运行。如果addresses在父类Role的初始化逻辑中被设置,此方法可能会覆盖它。此外,它依赖于any_to_str(self),如果self尚未完全初始化,可能会产生意外结果。
- 增强
special_actions的调度逻辑:重构_think方法,使其能够根据消息内容或类型从special_actions列表中智能选择或匹配正确的动作,而不是硬编码索引[0]。可以考虑在消息中携带动作标识符。 - 统一并明确
restricted_to的数据契约:明确restricted_to字段应该是Set[str]类型(如WwMessage的创建所示),并在整个代码库中坚持使用。移除关于逗号分隔字符串的注释,更新所有相关逻辑(包括 Moderator 的发送逻辑)以使用集合操作。如果必须支持字符串格式,应添加明确的解析和转换逻辑。 - 清理未使用的字段和代码:移除未使用的
use_memory_selection字段。审查并决定是删除_observe方法中注释掉的TODO代码块,还是用它替换当前逻辑,以消除混淆。 - 重构
_observe方法:- 移除冗余的
if not news:判断和news = []的初始化,直接使用news = self.rc.msg_buffer.pop_all()。 - 重新评估消息过滤逻辑。考虑将过滤条件与
restricted_to字段的检查更紧密地结合,确保只有明确发送给该角色(或全体)的消息才会被加入self.rc.news并触发反应。当前逻辑可能允许角色对非定向消息做出反应。
- 移除冗余的
- 改进经验筛选逻辑:将
record_experiences中的筛选条件从len(exp.reflection) > 2改为更语义化的检查,例如if exp.reflection and exp.reflection.strip():或if exp.reflection not in ('', '""'):,以更准确地识别有意义的反思内容。 - 考虑将经验处理逻辑抽象化:
_act方法中关于反思 (Reflect) 和经验检索 (RetrieveExperiences) 的逻辑较长,且与主要动作执行逻辑交织。考虑将这些步骤提取到单独的辅助方法中,如_prepare_context,以提高可读性和可测试性。 - 添加更健壮的验证和错误处理:在
_think方法中,当news.cause_by不是预期的InstructSpeak时,当前使用assert。在生产环境中,建议使用更明确的错误处理,如抛出带有描述性信息的异常。同时,在访问self.special_actions[0]之前,检查列表是否为空。 - 优化
get_all_memories方法:当前方法使用正则表达式r"[0-9]+ \| "移除时间戳。确保这个模式与所有可能的时间戳格式匹配。考虑将时间戳格式定义为一个常量,或者如果时间戳是消息的独立属性,则直接访问该属性以避免字符串解析。
本模块旨在为“狼人杀”游戏提供一个可扩展的玩家角色基类。其核心设计目标包括:
- 角色行为抽象:将玩家的通用行为(如观察、思考、行动)抽象为可复用的方法,为不同身份(如狼人、预言家)的角色提供基础。
- 技能与监听机制:通过
special_actions和_watch方法,支持为不同角色动态配置专属技能(如狼人杀人、预言家验人)和需要监听的事件。 - 记忆与经验系统:集成记忆管理,并可选地支持基于反思(Reflection)和经验检索(RetrieveExperiences)的增强决策,以提高角色行为的智能性和上下文相关性。
- 状态管理:通过
status字段管理角色的生存状态(如ALIVE,DEAD),确保死亡角色不再参与游戏进程。 - 消息路由与隐私:通过消息(
WwMessage)中的restricted_to字段实现定向通信,支持公开发言与私密行动(如夜间行动仅对主持人可见)。
主要约束包括对pydantic数据验证库的依赖、与metagpt框架中Role和Action组件的强耦合,以及当前经验系统(use_experience)必须依赖于反思系统(use_reflection)开启的强制逻辑。
当前代码中的错误处理相对简单,主要依赖断言和条件警告:
- 输入验证:在
__init__中,若use_experience为True但use_reflection为False,会记录警告日志并强制将use_experience设为False。这是一种防御性编程,但未抛出异常,可能导致调用方困惑。 - 状态断言:在
_think方法中,使用assert语句验证消息的cause_by必须为InstructSpeak。这在调试时有效,但在生产环境中若断言失败会导致程序崩溃,应改为更健壮的异常处理(如ValueError)。 - 潜在异常点:
get_latest_instruction方法直接访问self.rc.important_memory[-1],如果important_memory列表为空,将引发IndexError。_act方法中,当todo为特殊动作时,直接取self.special_actions[0](),如果special_actions列表为空,将引发IndexError。- 整体缺乏对网络I/O(如
Action.run调用)、外部服务(经验存储)故障的异常捕获和处理逻辑。
建议的优化包括:将关键断言替换为具体的异常抛出;在可能失败的操作周围添加try-except块,并定义清晰的异常类型;为异步方法考虑更完善的错误传播机制。
-
核心数据流:
- 输入:角色通过
_observe方法从环境(rc.msg_buffer)接收消息(WwMessage)。消息根据restricted_to字段进行过滤,只有发送给全体("")或本角色(self.profile/self.name)的消息才会被存入记忆并标记为待处理的新闻(self.rc.news)。 - 处理:在
_think中,角色分析news,根据消息的cause_by和restricted_to决定下一步行动(self.rc.todo),是公开发言(Speak)还是执行特殊动作(如NighttimeWhispers)。 - 增强决策:在
_act前,可选地执行Reflect(反思)和RetrieveExperiences(经验检索),利用历史记忆和上下文生成更明智的回应。 - 输出:在
_act中,执行todo行动,生成回应内容(rsp),并封装成新的WwMessage发送出去。同时,将本次交互的反思、指令和回应记录为RoleExperience对象,并可选地持久化。
- 输入:角色通过
-
角色状态机:
- 状态由
status字段(RoleState枚举)表示,初始为ALIVE。 - 在
_observe入口处检查状态,若不为ALIVE则直接返回0,不再处理任何消息,实现了“死亡”状态的隔离。 - 状态转换由外部调用
set_status方法触发(例如,被投票出局或被狼人杀死后由游戏主持人调用)。状态转换逻辑本身未在BasePlayer内定义,属于外部控制。
- 状态由
-
框架依赖:
- 父类:继承自
metagpt.roles.Role,必须遵循其生命周期(_observe,_think,_act)和内部组件(如self.rc中的memory,msg_buffer,watch)的约定。 - 动作系统:依赖
metagpt.actions.action.Action及其子类(如Speak,NighttimeWhispers,Reflect等)。BasePlayer通过set_actions注册可用动作,并通过run方法调用它们,必须满足各个Action子类定义的run方法参数契约。
- 父类:继承自
-
数据模型依赖:
- 消息格式:使用
metagpt.ext.werewolf.schema.WwMessage作为通信载体,依赖其字段(content,role,sent_from,cause_by,send_to,restricted_to)。 - 经验模型:使用
metagpt.ext.werewolf.schema.RoleExperience来结构化和存储单次交互的经验。
- 消息格式:使用
-
服务/组件依赖:
- 经验库:当
use_experience为True时,依赖RetrieveExperiences和AddNewExperiences动作背后的存储与检索服务(具体实现未在代码中展示)。这引入了外部数据源的依赖。 - 配置常量:依赖
metagpt.environment.werewolf.const中定义的RoleState和RoleType枚举。
- 经验库:当
-
隐式契约:
- 消息路由语义:对
WwMessage中restricted_to字段的解析有特定逻辑(如空集表示全体,包含Moderator和自身表示私密行动),这构成了与消息发送方(如主持人)的隐式协议。 - 指令记忆:假设
self.rc.important_memory的最后一条总是最新的主持人指令,这构成了与游戏流程控制者的隐式契约。
- 消息路由语义:对
BasePlayer的配置主要通过其类字段(也是Pydantic模型字段)在初始化时传入:
- 身份标识:
name(玩家名),profile(角色身份,如“Werewolf”)。 - 能力开关:
use_reflection(是否启用反思),use_experience(是否启用经验系统,依赖反思),use_memory_selection(未在代码中使用,预留)。 - 技能配置:
special_action_names(字符串列表),用于从全局ACTIONS映射中查找并初始化角色的特殊动作。 - 状态与版本:
status(初始状态),new_experience_version(为本次生成的经验记录标记版本)。 - 内部初始化:在
__init__中,基类初始化后,会配置监听列表(监听InstructSpeak),根据special_action_names设置可执行动作,并初始化special_actions列表。model_validator确保addresses字段有默认值。
这种设计允许通过参数灵活创建不同行为和能力的玩家实例,但要求调用方清楚各参数间的依赖关系(如经验依赖反思)。
鉴于模块的复杂性和多态性,建议采用分层测试策略:
- 单元测试:
- 工具方法:测试
get_all_memories(字符串处理和正则匹配)、get_latest_instruction(边界情况)、set_status。 - 核心流程方法:模拟
rc内部组件,测试_observe的消息过滤逻辑、_think的决策逻辑、_act中不同todo类型的分支。 - 初始化与验证:测试
__init__中配置逻辑和model_validator。
- 工具方法:测试
- 集成测试:
- 与Action集成:模拟或使用真实的
Action子类(如Speak,Reflect),测试_act方法中run调用的参数传递和结果处理。 - 经验系统流程:当
use_reflection和use_experience开启时,测试从反思到经验检索再到动作执行的完整数据流。
- 与Action集成:模拟或使用真实的
- 场景测试(模拟游戏):
- 创建多个
BasePlayer子类实例,模拟游戏回合,验证消息的正确收发、状态转换、以及经验记录功能。 - 测试死亡角色(
status != ALIVE)是否确实被隔离。
- 创建多个
- 异常与边界测试:
- 测试
special_actions为空时执行特殊动作的异常。 - 测试
important_memory为空时调用get_latest_instruction的异常。 - 测试
use_experience=True但use_reflection=False时的警告行为。 - 测试发送给非本角色
restricted_to消息的正确过滤。
- 测试
- 异步测试:由于主要方法都是异步的,测试框架需要支持
asyncio。