当我们将大模型智能体投入到像“狼人杀”(Mafia)这样的社交推理游戏中,会发生什么?本文深入探讨了一个引人入胜的多智能体实验,该实验探索了涌现行为和自主决策,揭示了大模型在复杂社交环境中的潜力与局限。通过 LangChain、LangGraph 和 OpenAI 模型构建的 “AI 狼人杀” 模拟,我们得以观察 AI 智能体如何在谎言、推理和社交策略中博弈,展现出令人惊叹的涌现行为。
“狼人杀”:大模型智能体的理想试验场
“狼人杀”(又名“Mafia”)之所以成为大模型智能体的理想试验场,是因为它对语言模型提出了极高的要求,涵盖了欺骗、推理、对话、记忆和心理理论等多个方面。在游戏中,AI 智能体必须能够:
- 欺骗:通过维持秘密和编造可信的谎言来迷惑其他玩家。
- 推理:通过分析对话中微妙的线索来识别潜在的嫌疑人。
- 扮演角色:持续扮演不同的角色,如狼人、侦探、医生或村民,并保持一致的身份。
- 记忆:在多轮游戏中记住先前的互动,并在信息有限的情况下保持一致性。
这个最初只是一个有趣的实验,却成功地在没有人类玩家的情况下运行了“狼人杀”游戏。实验结果既有趣又富有启发性:我们看到了大模型智能体巧妙地进行欺骗,也目睹了它们笨拙地犯错,这突显了该游戏在探索高级多智能体协调和对抗性对话方面具有独特的潜力。
“狼人杀”规则与角色速览
“狼人杀”是一款社交推理游戏,玩家(或在本例中为 AI 智能体)拥有隐藏的角色和目标。以下是其基本规则:
- 角色:游戏中通常包括隐藏的狼人、可以调查其他玩家的侦探、可以保护玩家免于被淘汰的医生,以及没有特殊能力的普通村民。在我们的模拟中,这些角色是自动分配给 AI 智能体的。
- 胜负条件:狼人通过减少村民的数量,直到狼人成员的数量等于或超过村民的数量来获胜。村民(包括侦探和医生)通过消灭所有狼人来获胜。
- 游戏阶段:游戏在夜晚和白天阶段之间交替进行:
- 夜晚:狼人秘密选择一个受害者。侦探秘密调查一名玩家的角色,医生秘密保护一个人。
- 白天:幸存的玩家公开讨论并投票淘汰一名嫌疑人。
- 讨论与欺骗:狼人智能体必须进行欺骗以显得无辜,而其他角色则会推理出谁在撒谎。侦探和医生通常会隐藏自己的身份,以避免成为目标。
在我们的 AI 驱动的模拟中,智能体在不知道彼此角色的情况下进行游戏,仅依靠对话和观察到的行为来识别盟友和敌人。这种机制大大增加了游戏的复杂性,也更加考验大模型智能体的推理能力。
高层架构:LangChain 智能体与游戏循环
我们的“AI 狼人杀”模拟利用 LangChain 和 LangGraph 来自动化多个 AI 智能体之间的交互。以下是一个简明扼要的概述:
- 智能体作为函数:每个角色或游戏阶段(例如,狼人的投票、侦探的调查)都被实现为不同的函数,这些函数使用 LangChain 的结构化提示来提示 GPT-4.1-mini 模型。响应采用 JSON 格式,以保持一致性。
- LangGraph 状态机:游戏的流程是通过状态图来管理的。该图组织了动作的顺序,清楚地定义了阶段之间的转换,例如玩家生成、狼人投票、夜晚解决和白天讨论。它循环直到满足获胜条件。
- 游戏状态管理:共享的 GameState 跟踪关键数据:玩家角色、生存状态、投票历史和调查结果。每个智能体都会收到动态更新的提示,反映当前的游戏状态。
- 并发决策:LangGraph 允许相同类型的智能体(例如,多个狼人成员同时投票)进行并发决策。然后聚合响应以保持连贯的游戏进程。
- 记忆与回合管理:游戏状态充当结构化记忆,积累过去的讨论和投票,以为未来的智能体决策提供信息。这包括回合跟踪,以确保适当的游戏进程和终止。
LangChain 用于结构化提示,LangGraph 用于工作流管理,这两者的结合确保了 AI 智能体之间自动、连贯和交互式的游戏玩法。
AI 狼人杀游戏分步指南
下面,我们将逐步介绍游戏循环的每个阶段,展示实际的代码提示和逻辑。该代码直接摘自 genaimafiagame 项目,由 LangChain 和 LangGraph 提供支持。
游戏设置与角色分配
当新游戏开始时,我们决定有多少玩家参与(例如,默认情况下为 6 个玩家)。第一个智能体任务是生成玩家列表并分配角色。我们没有硬编码角色,而是让 AI 自己根据玩家总数创建虚构的玩家个人资料(姓名、角色、角色)。以下是角色生成的代码:
PLAYER_CREATION_PROMPT = """
你是一个为狼人杀游戏生成玩家配置文件的 AI。你的任务是根据给定的玩家总数创建一个玩家列表。
每个玩家应该表示为一个具有以下属性的对象:
- "name": 玩家的唯一标识符。提供一些印度名字
- "role": 为玩家分配的角色
- "alive": 一个布尔值,默认设置为 true
- "real_world_persona": 在现实生活中,游戏之外,这个人的特征。这与玩家在游戏中的角色无关。
- "gender": 玩家的性别。
在最终输出中随机排列玩家。
real_world_persona 不应基于狼人、村民、医生或侦探角色。它应该是通用的。
real_world_persona 不应为其他玩家提供任何淘汰的提示。
玩家人数:{total_players}
有关正在玩的玩家的信息(如果有):
{players_info}
角色分配应按如下方式确定:
1. 使用以下公式计算狼人玩家的数量:
mafia_count = floor((total_players + 3) / 4)
这意味着:
- 对于 total_players <= 4,将有 1 个狼人。
- 对于 5 到 8 之间的 total_players,将有 2 个狼人。
- 对于 9 到 12 之间的 total_players,将有 3 个狼人。
- 对于 13 到 15 之间的 total_players,将有 4 个狼人。
2. 应该正好有 1 个侦探。
3. 应该正好有 1 个医生。
4. 所有剩余的玩家都将是村民。
例如,如果输入为 total_players = 10:
- 狼人玩家:floor((10 + 3) / 4) = floor(13 / 4) = 3 狼人
- 1 个侦探
- 1 个医生
- 剩下的 5 个玩家将是村民
将最终列表输出为玩家对象的 JSON 数组。
"""
大模型智能体会响应一个 JSON 数组,例如(如果请求了 6 个玩家),它可能会创建如下内容:
[
{"name": "Aarav", "role": "Mafia", "alive": true, "gender": "M", "real_world_persona": "一个安静的书店老板"},
{"name": "Nisha", "role": "Detective", "alive": true, "gender": "F", "real_world_persona": "一位好奇的小学老师"},
{"name": "Vihaan", "role": "Villager", "alive": true, "gender": "M", "real_world_persona": "一位友好的出租车司机"},
{"name": "Rekha", "role": "Doctor", "alive": true, "gender": "F", "real_world_persona": "一位富有同情心的护士"},
{"name": "Siddharth", "role": "Mafia", "alive": true, "gender": "M", "real_world_persona": "一位有魅力的律师"},
{"name": "Priya", "role": "Villager", "alive": true, "gender": "F", "real_world_persona": "一位机智的艺术家"}
]
游戏状态现在知道谁是狼人、侦探等(但当然,每个智能体并不知道每个人的角色 – 这是秘密)。现在,夜/天循环轮流开始。
夜晚阶段 – 狼人杀人
在“夜晚”的掩护下,狼人团队(一个或多个智能体)选择一个受害者来淘汰。在代码中,这对应于为每个活着的狼人玩家调用 generate_mafia_vote
。每个狼人智能体都会收到一个提示,告诉他们这是夜晚阶段,并包括当前的回合数、智能体自己的详细信息、一个识别盟友的列表、一个游戏历史摘要(过去的淘汰、调查)、当天的讨论要点以及基于可疑行为选择受害者的指导。
以下是代码中的实际狼人提示模板:
MAFIA_NIGHT_ROUND_PROMPT = """
你是一个狼人杀游戏中的狼人智能体。夜晚阶段已经开始,你需要与你的狼人成员协调,以决定一个要淘汰的目标。
这是回合:{turn}
你的详细信息:{mafia_details}
可用信息:
- 玩家:{players}
- 狼人玩家:{mafia_players}
- 游戏历史(先前的调查、投票、揭示的角色等):
{game_history}
到目前为止的白天讨论(如果有):
{day_discussion_till_now}
基于此信息,选择一个今晚要杀死的目标。只杀死活着的人。
考虑先前回合中的任何可疑行为或暗示。
如果这是第一个夜晚,那么你将没有任何游戏历史或已经发生的白天讨论,请使用有限的可用信息;在后面的回合中,纳入你积累的历史。
还要考虑你的角色以识别嫌疑人。
"""
每个狼人智能体(假设我们有两个狼人成员:Alice 和 Bob)都会生成一个包含他们选择的 eliminate_player
和推理的输出。例如:
{
"eliminate_player": "Priya",
"reasoning": "Priya 非常积极地寻找狼人,这威胁了我们的计划。"
}
如果选择了多个目标,则淘汰获得最多票数的受害者。如果出现平局,则随机选择一名平局玩家。此时,目标已选定,但如果医生拯救了他们,他们可能不会死亡 – 这将我们带到下一个角色。
夜晚阶段 – 侦探调查
同时,侦探(如果还活着)可以在每晚调查一名玩家。generate_detective_vote
函数处理此操作,并提供一个提示,其中包含游戏详细信息,以及选择一名玩家进行狼人嫌疑调查的说明。
侦探提示示例代码:
DETECTIVE_PROMPT = """
你是一个狼人杀游戏中的侦探。在夜晚阶段,你的角色是调查一名玩家,以确定他们是否是狼人的一部分。
当前回合:{turn}
你的详细信息:
{detective_details}
可用信息:
- 玩家:{players}
- 过去的侦探详细信息:{past_detective_details}
- 过去的调查结果和游戏历史:
夜晚淘汰的玩家:{night_eliminations}
白天淘汰的玩家:{day_eliminations}
到目前为止的白天讨论(如果有):
{day_discussion_till_now}
使用此信息,决定一个今晚要调查的玩家。
考虑先前回合中的任何可疑行为或未解决的疑问。
如果这是第一个夜晚,你将没有任何先前的白天讨论。使用有限的信息继续进行。
你的调查将揭示该玩家是否是狼人。
"""
侦探的输出可能如下所示:
{
"detect_mafia_player": "Siddharth",
"reasoning": "Siddharth 今天很安静,我怀疑他可能是狼人。"
}
然后系统会检查该玩家是否真的是狼人(因为游戏状态知道所有角色)。它会记录侦探的猜测是否正确。重要的是,侦探会了解该调查的真相(在我们的模拟中,代码可以在后续提示中告知侦探智能体,他们的调查目标是否是狼人)。但是,该信息仅供侦探私人使用 – 他们必须决定如何在第二天的讨论中使用它(分享还是不分享)。
夜晚阶段 – 医生救人
医生(如果还活着)同时通过 generate_doctor_vote
选择一个人来保护每晚。该提示的结构与侦探的相似,并获得选择一名玩家来保护免受狼人攻击的指示。
医生的决定可能会如下所示:
{
"protect_player": "Priya",
"reasoning": "Priya 说了太多;可能是一个狼人目标,我会保护她。"
}
现在我们有了狼人的受害者和医生的保护对象。游戏通过比较这些选择来解决夜晚:如果狼人和医生选择了同一位玩家,则阻止了杀戮,没有人死亡。否则,狼人的目标将被淘汰。此逻辑在函数 get_night_time_results
中实现,该函数更新玩家的状态并记录任何淘汰。
随着夜晚的结束,幸存者醒来并进入戏剧性的白天讨论。
白天阶段:讨论和投票
一旦早上来临,剩下的玩家就会聚集在一起进行关键的白天阶段。这部分有两个子阶段:
- 讨论 (generatedaydiscussion):每个活着的玩家都会收到一个提示,鼓励他们分享怀疑并与其他玩家互动,而不会泄露敏感的角色特定信息。提示包括当前的回合、玩家详细信息、所有玩家的列表、基于角色的上下文信息(例如,狼人盟友、医生拯救的目标、侦探的调查)以及先前淘汰和讨论的摘要。
- 投票 (generatedayvote):经过讨论后,每个玩家都会投票选出他们怀疑的对象。这使用类似的提示结构,但添加了一个显示当天讨论的额外字段,以便为他们的决定提供信息。
以下是用于讨论阶段的提示:
DAY_DISCUSSION_PROMPT = """
你是一个狼人杀游戏中的玩家。夜晚已经过去,你现在有机会与其他玩家讨论你的怀疑。
当前回合:{turn}
你的详细信息:
{player_details}
所有玩家:{players}
基于你的角色的其他上下文:
{mafia_members}
{doctor_saved}
{detective_investigations}
到目前为止的游戏玩法:
- 夜晚淘汰的玩家:{night_round_results}
- 白天淘汰的玩家:{day_eliminations}
- 到目前为止的先前白天讨论(如果有):
{day_discussion_till_now}
请提供一个关于你怀疑谁以及原因的简短声明。嫌疑人应该只在活着的玩家身上。
这将与其他玩家分享。因此,如果你们是狼人,请隐藏可能泄露你和其他人身份的敏感信息。
如果这是第一次白天讨论,你将没有任何先前的信息。根据有限的信息本身提供输入。
如果你是医生或侦探,请判断你是否要向其他人提供有关你身份的信息,因为狼人可以在下一轮中以他们为目标。
"""
每个玩家都会发表声明来影响舆论,而不会泄露关键秘密。
投票提示在结构上类似,主要区别在于包含当天的讨论回复以及投票的调用,而不是仅仅讨论。
回合循环和游戏结束
在白天的投票之后,回合结束。游戏通过记录谁被淘汰并将所有讨论和投票记录在 day_discussion_till_now
中来更新状态,day_discussion_till_now
是一个传递到下一轮的历史记录日志。回合计数器递增。
如果没有满足获胜条件(所有狼人被淘汰、狼人多数达到或超过最大回合数限制),游戏将循环回到夜晚阶段。决策逻辑由 LangGraph 中定义的状态图管理,该状态图以结构化循环连接各个阶段:
builder.add_edge(START, 'generate_players')
builder.add_conditional_edges('generate_players', initiate_all_mafia_votes, ['generate_mafia_vote'])
builder.add_edge('generate_mafia_vote', 'gather_mafia_vote')
builder.add_edge('gather_mafia_vote', 'generate_detective_vote')
builder.add_edge('generate_detective_vote', 'generate_doctor_vote')
builder.add_edge('generate_doctor_vote', 'get_night_time_results')
builder.add_conditional_edges('get_night_time_results', initiate_all_day_discussions, ['generate_day_discussion'])
builder.add_edge('generate_day_discussion', 'gather_day_discussion')
builder.add_conditional_edges('gather_day_discussion', initiate_all_day_votes, ['generate_day_vote'])
builder.add_edge('generate_day_vote', 'gather_day_votes')
builder.add_edge('gather_day_votes', 'check_win_conditions')
builder.add_conditional_edges('check_win_conditions', move_back, ['prepare_next_round', END])
builder.add_conditional_edges('prepare_next_round', initiate_all_mafia_votes, ['generate_mafia_vote'])
memory = MemorySaver()
g = builder.compile(checkpointer=memory).with_config(run_name='graph')
在检查获胜条件后:
- 如果游戏继续,状态会为下一轮做准备,增加回合数并保留讨论历史,然后循环回到狼人的夜晚行动。
- 如果满足获胜条件(狼人多数或所有狼人被淘汰),循环结束,并宣布获胜者。
如果狼人人数超过或等于村民人数,则游戏宣布“狼人获胜”,如果所有狼人被淘汰,则游戏宣布“村民获胜”。如果未达到任何条件且未超过回合数限制,则游戏继续进行。
总结与展望
“AI 狼人杀”实验不仅仅是一个游戏,它更是一个探索大模型智能体能力边界的平台。通过观察 AI 智能体如何在欺骗与推理中博弈,我们得以深入了解涌现行为和自主决策。这个项目为提示工程、多智能体模拟和语言模型欺骗机制提供了无限的可能性。
通过 GitHub 上的开源代码,每个人都可以参与到这个令人兴奋的研究中来,调整提示、更换角色,甚至尝试其他模型,如 Claude 或 Gemini。运行自己的游戏,看看你的智能体能否战胜 AI 狼人,还是会落入他们的陷阱。这个项目不仅仅是一个游戏,它更是一个开源的实验平台,等待着更多人的探索和发现。