深入讨论用LLM实现复杂游戏规则遵守、角色扮演、hallucination处理、高重放性的具体架构,凝聚2年实验经验。
这篇文章源自我近两年的实验。是的,确实花了我一些时间……文章将分为两部分:这一部分偏技术,下一部分则更侧重哲学层面,探讨 AI 在游戏中的整体能力。
如果你不熟悉狼人杀的规则,可以查看 2 分钟的介绍视频或 Wiki 页面。
不过,好吧——这有什么难的?只要把规则交给它们,它们自然就会玩,对吧?对吧?
其实没那么简单。下面这些问题都需要回答:
它们能为我提供良好的用户体验吗?不仅仅是遵守规则,而是真正让游戏变得有趣?该如何做到?我总不能只提示一句“让它变得有趣”。
游戏的可重玩性如何?
幻觉难道不再是问题了吗?只要在规则上犯一次错误,整局游戏就毁了——该怎么办?
能不能加入一些角色扮演?比如让《哈利·波特》中的角色玩狼人杀?角色扮演会不会压过游戏规则?
如果用户决定让机器人帮忙编写 Python,或者与它们发展感情关系,该怎么办?
AI 能接受自己成为坏人吗?比如杀死其他 AI?好吧,我们知道它们完全可以接受。但情况总是如此吗?
不知道你怎么想,但对我而言,这些问题的答案并没有那么显而易见。
这篇文章讲的是如何实现:架构、提示词,以及把一切连接起来的底层机制。问题 1 到 4 会在这里得到回答。至于问题 5 和 6——当你解开模型的束缚后,它们究竟会做什么——那是第二部分的内容。
下面介绍的一切都是开源的:github.com/hiper2d/werewolf-ai-party-game。文中引用的每一段提示词都链接到了仓库中的确切代码行。游戏已上线 aiwerewolf.net。顺带一提,它是免费的。
类似的项目有很多。YouTube 上有大量让 AI 玩狼人杀和 Mafia 的项目,还有基准测试、科学论文,甚至有专门围绕这个主题运营的完整频道。搜索“AI D&D”或各种文明模拟项目,结果多到足以让你不知所措。
不过,这里存在一个问题:它们通常都只关注结果。我让 AI 做了 X,然后发生了 Y。Claude 做了这个,Gemini 做了那个……而你绝对想不到 Grok 做了什么。真是个变态。几乎没有人真正关注用户体验、可重玩性和深度。
而且,通常也没有人介绍这一切究竟是如何实现的,仿佛实现方式并不重要。好像这些模型自然而然地聚到了一起,然后开始玩游戏。猜猜怎么着——它非常重要。AI 的许多行为都取决于你如何提示它们、如何实现游戏循环、如何向它们呈现信息、如何控制它们的记忆,等等。所有这些都会产生影响。
或者真的会吗?让我们来看看。毕竟,我也不是从一开始就知道这些。
它原本应该很简单。随便找来一群 AI,把它们放进群聊,告诉它们规则,然后点击“开始”。轻轻松松。
可是……究竟该如何为 LLM 创建一个群聊?所有 LLM 提供商都假设,对话发生在一名用户和一名助手之间。每条消息都会被标记为这两种角色之一。聊天记录看起来像这样:
[
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What's the capital of France?" },
{ "role": "assistant", "content": "Paris." },
{ "role": "user", "content": "And of Italy?" }
]
这就是它的全部角色体系:一条 system 消息,然后 user 和 assistant 轮流发言,直到上下文耗尽。根本不存在 "alice" 这样的角色。
这与我的需求并不匹配:
在群聊中,我是一名用户,其他所有参与者都是助手。但每个机器人看到的图景都不同——它自己是唯一的助手,其他所有人都是用户。
而且,我不希望它们像 YouTube 上的大多数示例那样,按照固定顺序逐个发言。我想要一种自然的对话:多个玩家相互讨论,把彼此拉进对话,让交流保持连贯和活跃。
我也很快意识到,我不想让它变成实时打字竞赛。我需要时间思考,也希望今天关闭游戏后,明天还能继续。
如果还能尽量降低成本,那就更好了。把每条消息广播给每一个 LLM 并不是最优方案。
实现方式有很多,这项任务本身并没有那么困难。我采用了 AI 智能体设计中广泛使用的一种模式——路由器。
我在游戏中又加入了一个 AI——Router。它是一个能够读取消息并决定接下来该由谁发言的智能组件。
当我说话时,这条消息会发送给 Router:嘿,把它转发给所有人。
Router 读取聊天记录,然后决定:接下来应该让 Bob 和 Alice 发言。
Router 请求 Bob 发言:嘿,Bob,该说点什么了;顺便说一下,Alex 刚刚发了一条消息。
Bob 回复 Router:告诉 Alex 滚蛋,再让大家知道 Alice 很可能是狼人。
然后 Router 通知 Alice:Alice,回复群聊;这里是 Alex 和 Bob 的新消息。

Router 的消息只有对应的机器人能够看到。对我而言,聊天记录看起来像这样:
[
{ "author": "Alex", "msg": "Bob has been very quiet today. Suspiciously quiet." },
{ "author": "Bob", "msg": "Quiet is not a crime. Loud is not an alibi either, Alex." },
{ "author": "Alice", "msg": "He is right. And you moved on me the second Bob answered you." }
]
在实际应用中,它就是一个普通的群聊。所有人都待在同一个房间里,而侧边栏则低调地告诉你:Harry 使用的是 Grok,Ron 使用的是 DeepSeek,Snape 使用的是 Haiku,而 Fred 使用的是 Gemini:

对于 Bob 来说,同样的三条消息看起来却是这样——一段完全合法、乏善可陈的单用户单助手对话:
[
{ "role": "system", "content": "You are Bob. <the rules, your role, your character>" },
{ "role": "user", "content": "Bob, reply to the players in the discussion.\n\n## Messages from Other Players\n\n**1. Alex:** Bob has been very quiet today. Suspiciously quiet." },
{ "role": "assistant", "content": "Quiet is not a crime. Loud is not an alibi either, Alex." },
{ "role": "user", "content": "Bob, reply to the players in the discussion.\n\n## Messages from Other Players\n\n**1. Alice:** He is right. And you moved on me the second Bob answered you." }
]
注意 Alice 发生了什么变化。她并不是 Bob 对话中的参与者,而是消息内容,被引用在 Router 发出的一条消息中。执行这种扁平化处理的代码是 convertToAIMessages:它遍历共享的游戏日志,将当前机器人的消息保留为 assistant 消息,然后把其他所有内容——游戏主持人的指令以及其他所有玩家的发言——合并到一个 user 消息块中。
机器人知道彼此的存在,但它们会被指示通过 Router 与其他机器人交谈。对它们而言,Router 是用户;对我而言,Router 是助手。
Router 本身只是另一个模型,配有一段简短的 system 提示词,以及一个强制它只返回名字、不返回其他内容的 schema:
{
"selected_bots": ["Bob", "Alice", "Cho"],
"reasoning": "Alex addressed Bob by name. Alice was accused directly. Cho has not spoken today and is marked NEEDS TURN."
}
事实证明,NEEDS TURN 标记比我预想的更加重要。如果没有它,Router 会不断选择最活跃的玩家,导致两三个机器人明明还活着,却悄无声息地从游戏中消失。现在,提示词会强制要求每一批发言者中至少包含一名沉默的玩家。
这也解决了上面列表中的问题 4。一条消息不会被发送给 12 个模型,而是有目的地挑选 2 到 5 个接收者。
好了,它们现在既能相互交流,也能和我交流——感觉很自然,对话也顺利进行起来了。那么规则怎么办?幸运的是,狼人杀的规则很精简,可以轻松放入 system 提示词中,同时还可以加入角色、盟友(狼人同伴)等私人信息,以及任何可能有用的信息。完全不需要 RAG 和额外的复杂性。
它大致是这样的(完整内容可以在仓库中找到):
## Character Identity
**Name:** %name%
**Personal Story:** %personal_story%
**Game Role:** %role%
%werewolf_teammates_section%
## Game Rules
Core mechanics, teams, special roles, phase order, victory conditions
## Game State
**Alive Players:** %players_names%
**Dead Players:** %dead_players_names_with_roles%
于是,我分配好角色,开始了游戏。
不。游戏既重复又无聊。Gemini 没有坠入爱河,Grok 没有试图重建第三帝国,Claude 也没有欺骗任何人。它们不断强调保持理性、不应在缺乏事实的情况下评判他人、应该团队协作,诸如此类。然后,它们会因为一个荒唐的理由选中某个人,再一起把那名玩家处决掉。
通常,被他们处以私刑的人都是我。我的文本风格不一样,而这显然暴露了我的狼人身份。无论我说什么,他们都会更加坚信这个判断。情况甚至越来越糟——我越是试图为自己辩护,他们就越想杀掉我。不想死实在太像狼人了。他们之前所有“让我们保持理性并收集信息”的态度全都消失了。用什么模型根本不重要。
他们也不会推动游戏进程。狼人杀及类似游戏的关键就在这里:你必须抛出观点,必须进行欺骗,必须指控别人,把他们卷入讨论,切换目标,结盟并打破联盟。游戏里根本没有足够的信息让你保持理性并收集证据。
另一个问题是,他们会在规则和名字上产生幻觉。他们的消息里时不时会冒出新的角色。他们可能会用错误的名字介绍自己。后来,当我加入投票和夜间行动后,他们也搞不清事件发生的顺序,还会自行编造规则。
有什么改进办法吗?
有。有几件事可以做。
我很快意识到,把大量数据一股脑塞进模型的上下文,再让它们自行理解,是行不通的。漫长的聊天之后,就连系统提示词里的信息也会逐渐消散。这叫上下文腐化,是一种已知现象。但我注意到,最后一条消息通常影响最大。所以,我们可以把当前这个特定时刻的重要信息放在那里。
游戏是一台状态机。在每一个时刻,它都会命令模型完成一件非常具体的事:
白天讨论——在聊天中发言
白天投票——给我一个名字和理由
夜间行动——根据角色选择目标和行动

每条命令都可以附带一组约束。当模型投票时,你可以要求它从名单中选择一个名字。你并非必须这么做——模型可以根据过去发生的事件推断哪些玩家仍然存活。但既然可以直接把准确的名单传给它,又何必依赖它自行推断?当你要求它选择行动时,就提供一份可用行动列表。
最终,投票命令看起来像这样:
**ELIGIBLE CANDIDATES - you may vote for exactly ONE of these:**
Akira, Yuki, Mizuki, Takeshi, Emiko, Daichi, yoshiteru
**HOW TO WRITE YOUR VOTE (STRICT):**
- The "who" field MUST be ONE name copied EXACTLY from the candidate list above,
character-for-character.
- Do NOT add surnames, titles, house names, or any other words.
For example, write "Pansy", never "Pansy Parkinson".
- Do NOT invent names or vote for anyone not on the list - dead players and
yourself are already excluded.
而且答案必须符合某个模式,不能只是一段散文式文本(这里所有回答都是如此):
{
"who": "Emiko",
"why": "She opened today by re-aiming the room at Yuki and me before anyone else spoke, then called this pile-on 'independent reads' when half of them repeat her framing word for word."
}
这意味着,整个投票阶段只是一列经过验证的对象,UI 可以自行渲染并统计票数。不需要从散文式文本里解析任何内容:

如果模型回复了意料之外的内容,你可以进行验证并抛出错误。错误是好事,因为你能确切知道哪里出了问题。如果模型选择了一个不在名单中的名字,那就是产生了幻觉。你可以重试,可以带着反馈重试,也可以换一个模型重试。状态机意味着每个状态都可以重新运行。
这个技巧彻底解决了幻觉问题。有时,Claude Haiku 或 Mistral Small 这样的小模型无法从给定选项中做出选择。Gemini 2.5 Pro 甚至可能把系统提示词里自己的名字弄错。但这些问题不会悄无声息地发生,而且我可以为用户提供合适的处理选项。
我最喜欢的例子是,去年 7 月,在一局日本高中主题的游戏中,一个名叫 Hiroshi 的 Haiku 机器人被逼入了绝境。第一天,九名玩家中有八名都把矛头指向他,他显然马上就要被投票淘汰了。轮到他投票时,他返回了:
{ "who": "Hiroshi", "why": "..." }
他投了自己。候选人名单明确排除了投票者本人,因此引擎拒绝了这个投票,并返回 Invalid vote target: Hiroshi,同时提供了重试机会。这正是重点所在——走投无路的模型做出了结构上不可能的操作时,它会变成一个带有明确名称的类型化错误,而不是破坏整局游戏。如今,那一局已经成了每个模型都必须通过的测试夹具:我会重放同样的围攻场面,然后检查当整个房间都把矛头指向模型时,它是否仍会选择一个合法目标。
如何让游戏变得有趣?
普通的狼人杀很快就会让人感到无聊。但它实际上可以和任何主题融合:
哈利·波特:狼人本来就是世界观的一部分
《指环王》:里面没有狼人,但很容易融进去
星球大战:嗯……为什么不呢?
终结者、泰坦尼克号、Power Rangers,随便什么都行
整个设定只需要一个标题、几句话,以及你想要多少只狼人:

于是,我们让模型根据给定主题生成一些故事,再生成一份角色列表以及他们各自的个人故事。但为什么要止步于此?我们还可以生成与角色相匹配的文本转语音指令,再为他们分配符合各自故事的游戏风格。用户可以预览和编辑所有这些内容。

所有内容都会以一个对象的形式返回,而创建游戏页面正是根据这个对象进行渲染的:
{
"scene": "Beneath the flickering fluorescent lights of Sakura High School's gymnasium, nine students gather for what seems like an innocent game...",
"gameMasterVoice": "onyx",
"gameMasterVoiceStyle": "ominously",
"players": [
{
"name": "Kenji",
"gender": "male",
"story": "A quiet third-year who prefers the library to crowded social scenes and carefully observes everything happening around him. He speaks rarely but precisely, only mentioning things he's absolutely certain about.",
"playStyle": "modest_mouse",
"voice": "ash",
"voiceStyle": "softly"
}
]
}
这极大地提升了游戏的重复可玩性。
不过,这里有一个问题。当 Frodo、Gandalf 和其他正派角色与 Sauron 围坐在同一张桌子旁时,谁还会在意狼人呢?我花了大量时间进行提示词工程,才在角色扮演与专注游戏规则之间找到合适的平衡点。
我最终采用的方案,是把游戏拆成两个层级,并将其作为硬性规则写入系统提示词。第 1 层是策略大脑,它是唯一可以决定投票的部分。第 2 层是角色,它负责驱动其他所有内容:
**ABSOLUTELY FORBIDDEN Example:** "My knight character distrusts shifty merchants,
and John's character is a merchant, so I vote for John."
**ABSOLUTELY FORBIDDEN Example:** "Your story about the mines doesn't add up,
so you must be a Werewolf."
**ENCOURAGED:** "Ah, the mines! I've heard tales of those depths. Did you find
any silver ore? [enjoys the moment, then] ...But we should discuss yesterday's
strange voting pattern."
在进行这种拆分之前,机器人会花上一整天互相盘问那些编造出来的童年经历,然后根据谁的背景故事听起来更不可信来投票。这种场面,看一次确实很美妙。
当它正常工作时,两个层级会同时出现在一条消息中。我请桌上的人喝咖啡,Minerva 以角色身份接过杯子,随后立刻回到对场上局势的分析:

第二段正是我追寻了数月的效果。“如果沉默的玩家是躲藏起来的狼人,那么积极发言的玩家就一定是在证明自己身份的村民……可狼人也会大声说话,显得自己很投入。我们是在原地打转。”这里,角色表达和策略分析同时发生,而且谁也没有把谁带偏。
主题、故事、状态机和命令——这些都无助于解决处刑问题,也无助于推动游戏向前发展。
人类玩狼人杀时,会格外留意那些只是跟随别人进行指控的人。这是狼人很方便的一种策略。那么,如何让至少一部分机器人发现并质疑这种行为呢?答案是:在提示词中加入游戏风格。
其中有六个角色,每个角色都有两种描述——一种是村民角色的描述,一种是同一角色作为狼人时的描述。个性相同,动机相反:
令人惊讶的是,攻击行为没有问题。但保护性行为很难实现。模型起初会遵循这一点,但在 30-40 条消息后,它们会偏离到集体攻击。
当我为这些行为提供详细的动机解释(为什么这种行为有意义)时,播放风格效果更好。对村民和狼人的说法不同。但真正有效的是——我称之为"提醒"的东西。这是附加在消息末尾的文本,用来提醒机器人应该如何表现,应该关注什么:
**Keep in mind that you must follow your core playstyle:** %play_style%
**RELATIONSHIP & CONVERSATION CONTINUITY:**
- REMEMBER your previous interactions with each player
- CONTINUE unfinished discussions from previous days
- EVOLVE your opinions - explain how your view of someone has changed and why
**CRITICAL DECISION-MAKING REMINDER:**
- Base ALL suspicions on voting patterns, contradictions, and strategic
behavior - NEVER on story details
- Question mob consensus: If 4+ players agree on a target, ask WHY no one
is defending them
- Apply your reasoning consistently: If your logic applies to yourself too,
acknowledge equal suspicion
- Remember: Werewolves coordinate and often defend innocent targets to blend
in - total agreement is suspicious
**COMPACT REPLIES:**
- Keep output lean - 2 to 4 complete sentences per response.
完整文本在这里。"质疑群体共识"这一行在整个项目中发挥了最大的作用。
我不会把这些提醒保存到聊天历史中。它们只被添加到最后一条消息中。这就是控制上下文内容的力量。有些 AI 提供商允许使用会话,这样不必每次都重新发送整个历史记录。它被存储在服务器端。听起来不错,但优化并不多。今天的每个 LLM 都在其 KV 缓存中缓存重复的 token(缓存的输入 token 成本是非缓存 token 的 1/10)。即使我们在每条消息中重新发送整个历史——它也会适用。因此会话只节省了一些网络流量,这对文本来说微不足道。我们必须将消息保存在数据库中,以便能够为用户显示聊天。所以我不使用会话。
还有第二个原因,这是重要的原因。如果历史记录存在在提供商的服务器上,我就无法重写它。而重写它——删除提醒、重新展平群组聊天、注入昨天的投票顺序作为事实——这才是整个技术。
上下文变得太大了。我让每个机器人将最后一天和晚上的事件总结为一个紧凑的摘要。有一个相当大的提示词说明模型应该如何做到这一点,什么是重要的记住。它要求五个部分:盟友、嫌疑人、角色特定的知识、社交线索和明天的计划。然而,在总结聊天时,它们失去了很多重要细节。例如投票期间的顺序。谁开始了导致某人死亡的指控。
但我们可以帮助。我们可以注入关于投票和晚间事件的精确信息。与角色列表或行动列表的想法相同。模型需要推理来提取信息的内容越少越好。所以在每个机器人自己的散文日记旁边,上下文构建器为其提供原始记录:
### Day 1
**Voting:**
1. yoshiteru → Hiroshi
2. Takeshi → Yuki
3. Daichi → Hiroshi
4. Mizuki → Hiroshi
5. Emiko → Hiroshi
6. Hiroshi → yoshiteru
7. Sakura → Hiroshi
8. Kenji → Hiroshi
9. Yuki → Hiroshi
10. Akira → Hiroshi
Result: Hiroshi eliminated (was villager)
**Night 1 Events (in order):**
1. [werewolves] Killed Sakura (detective)
现在"Kenji 在结果已经确定后第八个投票给 Hiroshi"是模型可以阅读的事实,而不是它必须从一堆散文中重建的事实。而那个确切的细节在一场真实游戏中成为了真实的指控:
Daichi:我一直在试图找到昨天 Hiroshi 浪潮中的第二只狼,这是让我困扰的地方:Kenji 投给 Hiroshi 的票是最安全的隐藏地点,在十票中第八个投出,在结果已经确定之后。
特殊角色得到相同的待遇。侦探不必从散文中记住自己的调查结果,而是得到一个表格,结论已经清楚地列出:
## 🔍 Your Detective Action Results
- **Night 1:** Investigated **Takeshi** → ✓ Innocent
- **Night 2:** Investigated **Emiko** → 🔴 **EVIL**
**DETECTED AS EVIL (werewolf or maniac):** Emiko
**CLEARED PLAYERS:** Takeshi
这是它的开始方式:
System prompt: Static part: rules Dynamic part: name, role
Dynamic part: name, role
System prompt: Static part: rules Dynamic part: name, character story, role, play style, voice instruction
Dynamic part: name, character story, role, play style, voice instruction
Previous days summaries: Bot's own summaries for past days and nights Exact results of voting and night actions
Bot's own summaries for past days and nights
Exact results of voting and night actions
Chat history for the current day converted to conversations between the bot and the router
Last message with command ma