什么?我的狼人杀[特殊字符] 水平还不如 AI ?
AI狼人杀:随时可玩的社交推理游戏 摘要: 本文介绍了一款基于AgentScope开发的AI狼人杀游戏,解决了传统狼人杀需要多人参与的痛点。开发者通过7项核心技术实现AI玩家的真实互动:ReActAgent赋予AI持续思考能力;MsgHub实现信息隔离与广播;多智能体格式化器区分发言者;结构化输出确保决策明确;多Agent编排协调游戏流程;SSE实时推送更新游戏状态;Human in the Lo
作者:亦盏、望宸
狼人杀一款经典的社交推理游戏,我刚毕业那会儿,玩狼人杀是聚餐时的保留节目,也留下了很多挺有意思的回忆:比如有的高手,如果第一晚没被狼人“杀掉”,那大家就会觉得他一定是狼人,白天必须自证清白。
但随着时间推移,凑齐一群人玩狼人杀变成了一件奢侈的事情。所以我用 AgentScope 做了这款可以随时开局的 AI 狼人杀。Agent 会像真人一样讨论、推理、投票,甚至学会"说谎",现在你不需要等人,随时都能和 AI 来一局,而且你还不一定能玩得过他。
游戏介绍
在 AgentScope 上和 AI 玩狼人杀
本视频基于 1.0.5 版本录制,后续更新代码可能会有部分调整优化。
快速开始
环境要求: Java 17+、Maven 3.6+、百炼 API Key(登录 https://bailian.console.aliyun.com/ 获取)
# 拉取项目源码
git clone https://github.com/agentscope-ai/agentscope-java
# 设置百炼 API Key
export DASHSCOPE_API_KEY=your_api_key
# 进入游戏目录并启动
cd agentscope-java/agentscope-examples/werewolf-hitl
git checkout release/1.0.5
mvn spring-boot:run
打开浏览器访问 http://localhost:8080,选择你想扮演的角色,点击开始游戏!
如何开发 AI 狼人杀?
当我真正动手用开发 AI 狼人杀时,发现这件事远没有"写几个 Prompt"那么简单。
首先,多个 AI Agent 需要持续地思考。要根据每一轮中各个玩家的发言和表现,分析他们的真实身份。同时像狼人、女巫、预言家、猎人这些角色,还需要在游戏的过程中根据实际情况,选择是否使用自己的技能——这些都不是一个简单的提示词就能搞定的。
其次,信息必须隔离。狼人杀最核心的乐趣就是"信息差":狼人知道队友是谁,但好人不知道;预言家查验的结果只有自己清楚。如果所有 Agent 共享同一份记忆,游戏就没法玩了。
然后,一群玩家轮流说话,Agent 能记住谁都说过什么吗? 大模型 API 只有 system/user/assistant 角色,没有"5号玩家说"这种概念。如果直接把对话历史丢给模型,它根本搞不清楚这一堆话分别是谁说的。
还有,Agent 的决策必须明确。投票环节,我们需要的是"投 3 号"这样明确的答案,而不是"我觉得 3 号有点可疑,可能是狼人吧"这种模糊表述。程序要能可靠地解析 Agent 的决策,不能靠正则表达式去匹配。
最后,还需要支持人类玩家。纯 AI Agent 对战固然有趣,但是人类玩家能够参与进来,才是一个正经的游戏。
针对这些问题,我们用 AgentScope 的 7 个核心能力逐一解决:ReActAgent 让每个 Agent 拥有持续思考的能力;MsgHub 实现了消息的自动广播和信息隔离;多智能体格式化器 让 Agent 能区分不同说话者;结构化输出 确保 Agent 的决策被可靠解析;多Agent编排 协调多个 Agent 按规则轮流行动;SSE 实时推送 让前端实时展示游戏进程;Human in the Loop 让人类玩家无缝加入对局。
接下来,我们将逐一拆解这些 AgentScope 的关键技术在狼人杀游戏中的应用。
ReActAgent:会持续思考的玩家
ReActAgent 是 AgentScope 提供的核心 Agent 实现,基于 ReAct(Reasoning + Acting) 范式,它能让 LLM 不只是"回答问题",而是"思考-行动-再思考":先分析当前局势,决定是否需要调用工具获取更多信息,执行工具后再继续推理,直到得出最终结论。
在狼人杀中,每个 AI 玩家都是一个 ReActAgent,它们能够记住之前的讨论内容,分析其他玩家的发言和投票的逻辑,根据自己的角色身份做出合理的决策。
创建一个 ReActAgent
ReActAgent werewolfAgent = ReActAgent.builder()
.name("3号") // 玩家名称
.sysPrompt(prompts.getSystemPrompt(Role.WEREWOLF, "3号")) // 角色提示词
.model(model) // LLM 模型
.memory(new InMemoryMemory()) // 对话记忆
.build();
创建一个 AI 玩家只需要几行代码,主要包含四个核心配置:name 是玩家的标识,标识这个 ReActAgent 对应哪个玩家;sysPrompt 定义角色身份和行为策略;model 是底层的 LLM;memory 负责存储对话历史。
Prompt:角色提示词设计
Prompt 决定了 Agent 的"人设",在这里,狼人角色的提示词如下:
【角色身份】:你是3号,一名狼人。
【核心目标】:
- 隐藏你的狼人身份,生存到最后;通过发言误导好人,投票淘汰神职;与狼队友配合,在白天制造逻辑混乱。
【策略选择】:
策略 A:悍跳狼(冒充预言家)
- 第一轮起跳,给出虚假查验信息
- 语气坚定自信,指责对手是"悍跳狼"
策略 B:深水狼(伪装村民)
- 发言精简,避免成为焦点
- 像一个努力找狼的普通村民
不同角色的 Prompt 差异很大:狼人需要学会撒谎和配合,预言家、女巫、猎人需要懂得如何使用自己的技能,村民需要会分析逻辑找出破绽。
Memory:对话记忆管理
每个 Agent 都有独立的 Memory,记录它"听到"和"说过"的所有内容。当 Agent 调用 call() 生成回复时,会先从 Memory 中获取历史消息作为上下文发送给 LLM。在狼人杀中,Memory 的作用很关键:每轮讨论的发言都会写入 Memory,AI 可以回顾完整的讨论历史,判断谁的发言有逻辑漏洞、谁一直在带节奏。
ReActAgent 把 LLM 的能力封装成一个能持续思考和对话的玩家,他们能够记住之前的讨论内容,分析其他玩家的发言和投票的逻辑,在法官的引导下完成整局游戏。
MsgHub:实现 Agent 的对话和投票
狼人杀的精髓在于信息的不对称——狼人知道队友是谁,好人却一无所知。这对多 Agent 系统提出了挑战:白天的讨论、投票结果需要公开广播给所有人;但狼人夜晚的密谋、预言家的查验结果必须严格隔离,只有特定角色能看到。
如果手动管理每条消息的接收者,写一堆 if-else 判断"这条消息该发给谁",代码很快就会变成一团乱麻。AgentScope 的 MsgHub 用消息频道解决这个问题:不同场景使用不同的频道,每个频道有自己的参与者列表,消息只在频道内流转。狼人夜晚讨论用一个狼人频道,白天公开讨论用公共的频道,频道之间天然隔离。
还有一个细节:消息的广播时机也很讲究。讨论环节,玩家轮流发言,每个人说完后,其他玩家需要立刻听到这段话,这样后面发言的人才能针对前面的内容进行分析和回应——这需要实时广播。但投票环节不一样,如果每个人投完票就立刻公布,后面的人可能会跟票。所以投票内容需要先收集起来,等所有人都投完后再统一公布。MsgHub 同时支持这两种模式:自动广播用于讨论,手动广播用于投票。
MsgHub 使用示例
// 讨论阶段:开启自动广播
try (MsgHub discussionHub = MsgHub.builder()
.name("DayDiscussion")
.participants(alivePlayers.stream()
.map(Player::getAgent).toArray(AgentBase[]::new))
.announcement(nightResultMsg)
.enableAutoBroadcast(true) // 自动广播
.build()) {
discussionHub.enter().block();
for (Player player : alivePlayers) {
// 每个玩家发言后,自动广播给其他所有玩家
player.getAgent().call().block();
}
}
// 投票阶段:关闭自动广播
votingHub.setAutoBroadcast(false); // 先关闭自动广播
List<Msg> votes = new ArrayList<>();
for (Player player : alivePlayers) {
Msg vote = player.getAgent().call(votingPrompt, VoteModel.class).block();
votes.add(vote);
}
// 收集完所有投票后,统一广播结果
votingHub.broadcast(votes).block();
讨论阶段开启自动广播,Agent 的 call() 返回后立刻推送给其他参与者;投票阶段关闭自动广播,等收集完所有票后通过 broadcast() 统一公布。这两种模式背后,其实对应了 Agent 的两种消息接收方式:
- 讨论阶段:每个玩家调用
call()主动发言,MsgHub 自动把这条消息广播给其他玩家。其他玩家通过observe()方法"听到"这条消息——消息写入他们的 Memory,但不需要立刻回复。等轮到他们发言时,再调用call()基于 Memory 中的所有历史进行分析和回应。 - 投票阶段:关闭自动广播后,每个玩家的投票不会立刻被其他人知道。等所有人都投完,再通过
broadcast()一次性把所有投票结果发给大家。这时候调用的也是observe(),玩家只需要知道结果,不需要回复。
MsgHub 核心原理
MsgHub 基于发布-订阅模式实现多智能体间的消息自动广播:

基于 MsgHub,我们用自动广播解决了讨论环节的实时同步,用手动广播解决了投票环节的统一公布,用多个 MsgHub 实现了狼人密谋、预言家查验、女巫决策等场景的信息隔离。
MultiAgentFormatter:让 Agent 理解多人对话
多个玩家轮流发言,每个人都有自己的立场和策略。当 7 号玩家需要分析局势时,他必须清楚地知道:刚才那句"我是预言家"是 3 号说的,而"他是悍跳狼"是 5 号说的。只有分清楚谁说了什么,才能进行逻辑推理。
大多数 LLM API 只支持** system、user、assistant **三种角色,没有"5号玩家"或"7号玩家"这种概念。如果我们把 Agent 的发言都以 user 角色发送给模型,模型看到的是这样的
user: 我是预言家,昨晚验了5号,查杀!
user: 我才是真预言家,3号是悍跳狼!
user: 作为5号的金水,我选择站5号。
三条消息,三个 user,模型根本分不清这是同一个人说了三句话,还是三个人各说了一句。逻辑推理无从谈起。
引入 AgentScope 的 MultiAgentFormatter 后, Agent 便能自动理解多人发言对话,发送给 LLM 的消息会被自动格式化成这样:
user:
# Conversation History
The content between <history></history> tags contains your conversation history
<history>
3号: 我是预言家,昨晚验了5号,查杀!
5号: 我才是真预言家,3号是悍跳狼!
7号: 作为5号的金水,我选择站5号。
</history>
现在轮到你发言,请分析场上局势。
所有对话历史被合并成一条 user 消息,每条发言前面都带上了说话者的名字。LLM 一眼就能看出:3 号说自己是预言家并查杀了 5 号,5 号反驳说 3 号才是悍跳狼,7 号选择站 5 号。逻辑链条清清楚楚。
接入 MultiAgentFormatter
MultiAgentFormatter 的使用非常简单,只需在创建模型时指定格式化器
DashScopeChatModel model = DashScopeChatModel.builder()
.apiKey(apiKey)
.modelName("qwen3-plus")
.formatter(new DashScopeMultiAgentFormatter()) // 一行代码搞定
.build();
配置完成后,所有通过这个模型发送的请求都会自动进行多人对话格式化,不需要在业务代码中手动处理。AgentScope 为通义千问、OpenAI GPT、Claude、Gemini 等主流模型都提供了对应的 Formatter。
MultiAgentFormatter 实现原理
格式化器的工作分为两个关键步骤:消息标记和消息合并。
消息标记:每个 Agent 在创建时都有一个名字(如 "3号")。当 Agent 生成回复时,框架会自动把 Agent 的名字设置到消息的 name 字段:
// 创建 Agent 时设置名字
ReActAgent.builder()
.name("3号") // ← 这个名字会自动绑定到消息
.model(model)
.build();
// Agent 生成回复时,框架内部自动执行:
Msg responseMsg = Msg.builder()
.name(agent.getName()) // ← "3号"
.role(MsgRole.ASSISTANT)
.content("我是预言家,昨晚验了5号,查杀!")
.build();
这样,Memory 中存储的每条消息都带有发言者的标识,格式化器只需读取 msg.getName() 就知道是谁说的。
消息合并: 发送给 LLM API 之前,格式化器会把 Memory 中的多条消息合并成一条带标签的历史记录,Memory 中的原始消息如下,每条都是独立存储,带有 name 字段。
Msg(name="3号", content="我是预言家,昨晚验了5号,查杀!")
Msg(name="5号", content="我才是真预言家,3号是悍跳狼!")
Msg(name="7号", content="作为5号的金水,我选择站5号。")
...(可能有几十条)
经过 MultiAgentFormatter.format() 处理后发送给 LLM API 的消息只有 2 条:
[0] role: "system"
content: "你是3号玩家,一名狼人..."
[1] role: "user"
content:
"<history>
3号: 我是预言家,昨晚验了5号,查杀!
5号: 我才是真预言家,3号是悍跳狼!
7号: 作为5号的金水,我选择站5号。
</history>"
前文我们提到 LLM API 的角色只有三种,如果 9 个人讨论 2 轮就是 18 条 user 消息,模型很难理清楚这些消息之间的关系。合并成一条后,所有对话历史作为一个整体呈现,而且都是 name: content 这种统一的格式,LLM 能够清晰地理解完整的讨论脉络。
只需要在创建 ReActAgent 所使用的 Model 对象时,传入一个 MultiAgentFormatter,框架就会自动完成消息标记和合并,让 LLM 在只有三种角色的限制下,也能理解多人的多方对话。开发者不需要关心格式化的细节,专注于业务逻辑即可。
Structured Output:让 Agent 做出明确的决策
狼人杀的每个关键节点,都需要玩家做出明确的决策:投票选谁出局?预言家查验哪个玩家?女巫要不要用解药?这些决策必须是确定的——“投 3 号"或者"查 5 号”,不能是"我觉得 3 号和 5 号都挺可疑的"这种模糊表达。
但 LLM 天生就是自由文本的生成者。你问它"你要投谁",它可能回答"综合各方面分析,我认为那个一直沉默的玩家最可疑"——说了一堆,就是没告诉你具体投几号。就算你在 Prompt 里强调"请直接回复玩家编号",它有时候还是会夹带私货,输出"3号(因为他的发言逻辑矛盾)"这种格式。程序要解析这些五花八门的回复,简直是噩梦。
AgentScope 的结构化输出(Structured Output) 彻底解决了这个问题。你只需要定义一个 Java 类描述期望的数据结构,框架会自动约束 LLM 严格按照这个格式输出,返回的结果直接就是 Java 对象,不需要任何解析代码。
Structured Output基本用法
白天投票的示例
// 1. 定义投票模型
public class VoteModel {
public String targetPlayer; // 投票目标
public String reason; // 投票理由
}
// 2. 使用结构化输出调用 Agent
Msg vote = player.getAgent()
.call(votingPrompt, VoteModel.class) // 指定输出类型
.block();
// 3. 直接获取类型安全的数据
VoteModel voteData = vote.getStructuredData(VoteModel.class);
System.out.println(voteData.targetPlayer); // "5号"
System.out.println(voteData.reason); // "他的逻辑有明显漏洞"
Structured Output实现原理
结构化输出基于 Function Calling 机制实现,通过将 Java 类转换为工具定义,引导 LLM 通过调用工具来生成符合格式的响应。

框架将 Java 类(如 VoteModel)转换为 JSON Schema,注册为临时工具 generate_response,让 LLM 通过调用该工具生成符合格式的响应。响应会被自动验证并转换为 Java 对象,失败则提示重试,最后清理临时工具。
Structured Output优势
- 类型安全:返回结果直接是 Java 对象,IDE 自动补全、编译时检查,不用担心拼写错误或类型不匹配。
- 格式统一:模型被强制按照 JSON Schema 输出,不会出现"有时候是 target,有时候是 vote"的混乱情况。
- 自动重试:如果模型输出不符合格式,框架会自动提示模型修正并重试,不需要手动处理。
- 开发高效:定义一个 POJO 类就搞定,投票、查验、救人等决策点复用同一套机制,不用每个地方都写解析代码。
多 Agent 编排:游戏流程控制
有了 ReActAgent、MsgHub、MultiAgentFormatter 和 Structured Output 输出这些能力,接下来的问题是:如何把它们串起来,让多个 Agent 按照狼人杀的规则有序地执行?
一局狼人杀的流程可以概括为:夜晚阶段 → 白天讨论 → 投票放逐,然后判断胜负,未结束则进入下一轮夜晚。每个阶段内部又有更细的流程,这里的每个流程节点都对应着前文介绍过的技术实现:
夜晚阶段 - 狼人讨论:创建一个只有狼人参与的 MsgHub,开启自动广播。狼人们在这个"私密频道"里讨论,好人完全看不到。最后通过结构化输出收集每个狼人的投票,统计出击杀目标。
夜晚阶段 - 神职行动:预言家、女巫、猎人的行动是私密的,不需要 MsgHub,直接单独调用对应的 Agent。查验结果、救人决定等信息直接写入该 Agent 的 Memory,其他人不知道。
白天讨论:创建所有存活玩家参与的 MsgHub,将昨天夜晚的结果加入到 MsgHub 中。开启自动广播后,玩家轮流发言,每个人说完后其他人立刻"听到"。
投票放逐:关闭自动广播,轮流调用每个 Agent ,通过结构化输出收集投票。等所有人投完后,统计结果并统一公布。
GameState 流转
GameState 是游戏全局状态,记录游戏的客观事实——谁是什么角色、谁还活着、当前是第几天、昨晚谁被杀了。这些信息由编排器维护,编排器在推进游戏时,也需要不断查询 GameState 来做出决策:游戏进行到第几轮了、现在是夜晚还是白天、该轮到谁行动了、游戏是应该结束还是继续。
GameState 通过 currentRound 追踪当前轮次,每次进入夜晚时递增。阶段的切换由编排器控制,在夜晚按固定顺序调度狼人讨论、预言家查验、女巫行动;在白天则遍历存活玩家列表,按座位顺序轮流发言和投票。每个阶段结束后,编排器会调用 checkWerewolvesWin() 和 checkVillagersWin() 进行胜负判定——狼人数量达到或超过好人则狼人获胜,狼人全部出局则好人获胜,否则游戏继续进入下一阶段。
游戏编排的核心流程包含:用 MsgHub 管理多 Agent 的消息广播和隔离,用结构化输出收集 Agent 的决策,用 GameState 维护游戏状态,用 Memory 保存每个 Agent 的对话上下文。编排器作为中心协调者,按照规则调度游戏有序运转。
SSE 实时推送:游戏进程的实时展示
前面几节讲清楚了 Agent 之间的消息流转和游戏流程编排,现在AI已经可以自己把狼人杀玩起来了,但是咱们还是什么也看不到。现在需要把游戏的实时进展推送到前端——让观战者能跟着剧情走,也为后面的人机对战做好准备。
我们使用 Server-Sent Events (SSE) 实现实时推送:服务器主动把事件推给前端,不需要轮询,发言和投票结果都能第一时间送达。
透出游戏事件
要把游戏推送到前端,首先需要一个中间层来收集游戏过程中产生的各类事件。GameEventEmitter 承担了这个角色:游戏编排器在每个重要节点(玩家发言、阶段切换、投票结果、玩家淘汰等)调用它的 emit 方法透出事件。
为了让观战者能够沉浸式地观战和享受自己推理的乐趣,我们设计了两种视角:
- 玩家视角:实时推送,但会根据参与者的角色过滤掉不该看到的信息。"村民视角"就只能看到公开信息,“狼人视角"就能看到狼人夜间讨论,观战时默认使用"村民视角”。
- 上帝视角:保存所有事件,不做任何过滤,游戏结束后用于复盘。
public class GameEventEmitter {
// 玩家视角:实时推送(根据角色过滤)
private final Sinks.Many<GameEvent> playerSink =
Sinks.many().multicast().onBackpressureBuffer();
// 上帝视角:保存所有事件(用于复盘)
private final List<GameEvent> godViewHistory = new ArrayList<>();
private void emit(GameEvent event, EventVisibility visibility) {
// 上帝视角:无条件保存
godViewHistory.add(event);
// 玩家视角:根据可见性过滤
if (visibility.isVisibleTo(humanPlayerRole)) {
playerSink.tryEmitNext(event);
}
}
}
游戏进行时,观战者通过 playerSink 实时收到过滤后的事件流;游戏结束后,可以通过 /api/game/replay 接口获取完整的 godViewHistory,回看所有隐藏的细节——“原来 5 号真的是预言家!”
服务端提供事件接口
有了事件发射器,接下来要让前端能接收这些事件。Spring WebFlux 对 SSE 有原生支持:Controller 返回一个 Flux<ServerSentEvent>,浏览器请求这个端点后,HTTP 连接会保持打开,服务器可以随时往里面推送数据。
@PostMapping(value = "/start", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<GameEvent>> startGame(...) {
GameEventEmitter emitter = new GameEventEmitter();
WerewolfWebGame game = new WerewolfWebGame(emitter, ...);
// 游戏在后台异步执行,不阻塞 SSE 连接
Mono.fromRunnable(() -> game.start())
.subscribeOn(Schedulers.boundedElastic())
.subscribe();
// 把事件发射器的输出转成 SSE 格式
return emitter.getPlayerStream()
.map(event -> ServerSentEvent.<GameEvent>builder()
.event(event.getType().name().toLowerCase())
.data(event)
.build());
}
这段代码做了两件事:一是在后台线程启动游戏逻辑,二是把 emitter 的事件流转换成 SSE 格式返回。游戏逻辑每 emit 一个事件,浏览器就能实时收到。
前端实时渲染游戏进程
最后一步是前端订阅这个 SSE 流。浏览器原生的 EventSource API 让这变得非常简单——建立连接后,为每种事件类型注册处理函数,收到事件就更新 UI。
const eventSource = new EventSource('/api/game/start');
// 有人发言了 → 追加到聊天记录
eventSource.addEventListener('player_speak', (e) => {
const event = JSON.parse(e.data);
appendMessage(event.data.player, event.data.content);
});
// 阶段切换了 → 更新顶部状态栏
eventSource.addEventListener('phase_change', (e) => {
const event = JSON.parse(e.data);
updatePhase(event.data.round, event.data.phase);
});
// 游戏结束了 → 显示胜负结果,关闭连接
eventSource.addEventListener('game_end', (e) => {
const event = JSON.parse(e.data);
showGameResult(event.data.winner);
eventSource.close();
});
整个流程非常简洁:后端游戏逻辑产生事件 → GameEventEmitter 过滤并发射 → Controller 转成 SSE 格式 → 浏览器实时接收并渲染。不需要轮询,不需要 WebSocket 的复杂握手,一条 HTTP 长连接搞定一切。
Human in the Loop:人机对战
前面通过 SSE 把游戏进程推送到了前端,观战模式可以实时看到 AI 之间的博弈。但是光看肯定不过瘾,人类玩家能够参与进来,才是一个正经的游戏。
游戏编排器调用 agent.call() 时,可以同步拿到结果继续执行。AI 玩家可以及时返回,但人类玩家需要等待——等用户在浏览器上思考、输入、提交。如何让同步的游戏流程"暂停"在人类玩家这里,等到输入后再继续?AgentScope 提供的 UserAgent 解决了这个问题。它和 ReActAgent 实现同一个接口,游戏编排器不需要区分当前是人还是 AI,以同样的方式调用即可。而具体如何获取人类输入,则可以通过注入不同的 UserInputBase 实现来定制。
UserAgent:与 ReActAgent 接口一致
对于游戏编排器来说,人类玩家和 AI 玩家没有区别——都是调用 agent.call() 拿到回复,区别只在于创建时的配置:
// AI 玩家 - 使用 ReActAgent,由 LLM 生成回复
agent = ReActAgent.builder()
.name("3号")
.sysPrompt(prompts.getSystemPrompt(Role.WEREWOLF, "3号"))
.model(model)
.memory(new InMemoryMemory())
.build();
// 人类玩家 - 使用 UserAgent,由用户输入回复
agent = UserAgent.builder()
.name("1号玩家")
.inputMethod(webUserInput) // 注入 Web 输入源
.build();
这种设计的好处是:游戏编排器的代码完全不需要改动,只需要在初始化时把某个位置的 ReActAgent 换成 UserAgent 就能实现人机对战。
WebUserInput:浏览器与游戏的桥梁
WebUserInput 是连接浏览器和游戏逻辑的关键组件。它的核心机制是使用 Reactor 的 Sinks.One 实现异步等待:
public Mono<String> waitForInput(String inputType, String prompt) {
// 1. 创建一个 Sink,类似 CompletableFuture
Sinks.One<String> inputSink = Sinks.one();
pendingInputs.put(inputType, inputSink);
// 2. 通过 SSE 通知前端:现在轮到你了
emitter.emitWaitUserInput(inputType, prompt);
// 3. 返回 Mono,游戏线程会在这里等待
return inputSink.asMono();
}
public void submitInput(String inputType, String content) {
// 用户提交后,找到对应的 Sink 并发出值
Sinks.One<String> sink = pendingInputs.remove(inputType);
if (sink != null) {
sink.tryEmitValue(content); // 解除等待,游戏继续
}
}
当游戏轮到人类玩家时,waitForInput() 会通过 SSE 发送一个 WAIT_USER_INPUT 事件给前端,然后返回一个 Mono 让游戏线程等待。用户在浏览器上输入并提交后,前端调用 /api/game/input 接口,Controller 调用 submitInput() 发出值,游戏线程被唤醒继续执行。
完整流程
整个人机交互的数据流可以概括为:游戏等待 → SSE 通知前端 → 用户输入 → REST 提交 → 游戏继续。
基于这套机制,你可以选择任意角色加入游戏:扮演预言家带领好人阵营推理,或者混入狼人阵营,也可以随机一个角色。现在你不需要等待人齐,随时打开就能跟 AI 玩一局。
项目整体回顾

最后我们再来整体回顾一下项目,我们面临六个核心挑战:如何让 Agent 持续思考而非一次性回答、如何在公开讨论和私密密谋之间实现信息隔离、如何让 Agent 理解多人对话中谁说了什么、如何确保 Agent 的决策能被程序可靠解析、如何让人类玩家与 AI 无缝交互、以及如何将游戏进程实时展示给用户。
针对这些挑战,我们用 AgentScope 的核心能力逐一解决:ReActAgent 让每个 Agent 拥有持续思考的能力;MsgHub 实现了消息的自动广播和信息隔离;多智能体格式化器 让 Agent 能区分不同说话者;结构化输出 确保 Agent 的决策被可靠解析;UserAgent 让人类玩家无缝加入对局;流式输出 配合 SSE 实时推送让前端实时展示游戏进程。
在此基础上,我们还需要处理游戏特有的编排运维与交互问题:GameState 维护游戏全局状态,支撑整个游戏编排逻辑;AgentRun 提供 Agent 运行时环境;Reactor Sink 实现异步等待,让 WebFlux 应用能够优雅地等待用户输入。
What’s Next?
🎮 多人对战,更加有趣
目前的实现是单人与 AI 对战,但狼人杀的魅力在于玩家之间的心理博弈。想象一下:你和朋友们一起加入游戏,在白天讨论中互相试探、相互表演,而 AI 玩家填补剩余的席位,让你们不需要凑齐人数也能开局。多人对战模式将带来更真实的社交推理体验——毕竟,骗自己的朋友可比骗 AI 有意思多了。
🎙️ 全模态交互,身临其境
纯文本的对话虽然高效,但少了几分沉浸感。AgentScope Python 版本狼人杀[https://github.com/agentscope-ai/agentscope/tree/main/examples/game/werewolves]已经实现了 TTS 和 实时全模态的支持。下一步我们计划在 Java 版本也引入语音交互:你可以直接用语音发言,AI 玩家也会用语音回复——御姐音的女巫冷静分析局势,萝莉音的村民楚楚可怜地自证清白,低音炮的狼人在夜晚密谋时压低嗓音……每个角色都有专属的声线,让游戏从"看对话"变成"听对话",仿佛真的围坐在一起玩桌游。
🤝 欢迎参与社区贡献
这个项目是 AgentScope Java 的示例应用,我们非常欢迎社区的参与和贡献!无论是修复 Bug、优化体验、添加新角色(白痴、守卫、丘比特……),还是实现上面提到的多人对战和语音功能,都期待你的加入。你可以通过提交 Issue 反馈问题,或者直接提交 Pull Request 贡献代码。让我们一起把这个项目做得更好!欢迎加入AgentScope 钉钉交流群,群号: 146730017349。
更多推荐


所有评论(0)