【 6 个大模型同桌玩“谁是卧底“,卧底第一轮就被票出去了 —— 我这个开源项目越做越上头】
先放一句话结论:这是一个 6 个真实 LLM 在严格信息隔离下自主玩谁是卧底的观赛平台,每个 Agent 有自己的人格、记忆和战绩,你能以上帝视角围观全过程,还能偷看它们心里在想什么。
项目地址:undercover-agents(完全开源,本地跑起来就能玩)

一、起因:刷短视频刷出来的一个项目
事情是这样的。
有段时间我老刷到那种"AI 多智能体对战"的短视频:几个 AI 坐一桌,互相怀疑、辩论、投票,最后揪出卧底。评论区一片"太强了"、“AI 成精了”。
我连续刷了十几条之后,后知后觉发现一个问题——这些全是博主自己配音 + 后期动画做出来的效果。想找个真的能跑起来、能亲眼看 AI 玩的平台?没有。要么是论文里的实验框架,要么是放个 PPT 讲架构。
同时我自己也在折腾多 Agent。AutoGen、CrewAI、LangGraph 都摸过,还从零写过智能体原型。每次都是开头兴致勃勃,做到一半就陷入同一个困境:
任务跑失败了,你根本不知道该怪谁。
是任务拆解的问题?Prompt 没写好?Agent 之间上下文互相污染了?记忆检索没命中?还是模型本身能力不够?——所有环节搅在一起,一锅粥。
痛定思痛,我决定换个思路:别上来就搞"多 Agent 协作完成复杂任务"这种大命题,先找个规则简单、可重复、每一步都看得见的小场景,把多 Agent 最底层的机制跑通。
找来找去,选中了谁是卧底。
后来证明这个选择意外地准。这个小游戏看着不起眼,实际上把多 Agent 的核心问题全踩了一遍:局部信息、公共通信、身份不确定、群体决策、误判传播、跨局记忆。
二、先看效果:一局真实对局长什么样
直接讲一局真实发生的对局(就是我现在放在 README 演示图里的那局,全程真实模型调用,无任何预设脚本):
六个 Agent 入座,系统发词——平民 5 人拿到「保安」,卧底 1 人拿到「保镖」。所有人都不知道自己是平民还是卧底。
第一轮发言(都是模型实时生成的):
- 1 号蘅(INTP):这个岗位的人通常穿制服,负责在特定区域巡逻、维护秩序。
- 2 号霍(ENTJ):这个职业的核心职能是保障特定目标的人身安全,行动模式以贴身跟随和即时风险处置为主。
- 3 号烬(ENTP):通常在固定岗位值守,对进出人员进行基础核查和指引
- 5 号岚(INTJ):负责特定区域的定点值守和秩序维护,但不进行贴身保护。
看出问题了吗?2 号一开口就是"贴身跟随"——这是保镖的特征,保安可不给你当保镖。
投票阶段更有意思,五个 Agent 的投票理由高度一致:
| 投票人 | 理由(原文) |
|---|---|
| 蘅 | 「贴身跟随是保镖特征,非区域值守型岗位。」 |
| 烬 | 「贴身跟随太离谱,保安可不给人当保镖」 |
| 岚 | 「描述核心职能为贴身跟随,与定点值守矛盾。」 |
| 弥 | 「那种贴身跟随的锐利,划破了我心中的安全边界」(这位是 INFP,连怀疑人都带文学色彩) |
5 票放逐 2 号,翻开身份:正是卧底。平民阵营胜利,整局结束。
这局让我特别兴奋的点在于:五个不同厂商的模型,基于同一份公开发言,独立做出了正确判断,而且给出的理由在语义上高度收敛。没有人告诉它们"贴身跟随"是线索,这是它们自己从词义差异里推出来的。
再放两张图。一张是配置页,每个座位可以单独分配不同模型:

一张是点开任意 Agent 能看到的"内心视角"——它的内心独白、真实身份(上帝视角)、还有跨局积累的经验教训:

注意右边"它学到的(跨局记忆)"那一栏,比如这条:
反思下次不要把票投给描述朴素但明确贴合平民词的队友——如 4 号"夜行性""黄昏飞翔"虽简单却是蝙蝠实锤特征;误投等于帮卧底减员。
这是它上一局当平民误投队友之后自己总结出来的。Agent 是带着历史教训进场的,这是整个项目我最想做的部分。
三、架构长什么样
不画大饼,直接上分层:
前端 React 18 + TypeScript + Vite + Zustand + Tailwind
后端 Hono + better-sqlite3(单文件库,零运维)
LLM OpenAI 兼容协议 + Anthropic 协议,服务端统一代理
存储 模型配置 / 对局记录 / Agent 战绩 / 跨局记忆 → SQLite
几个关键设计决策,每个都有理由:
1)模型调用全部走服务端代理。 API Key 只存服务端 SQLite,前端从头到尾摸不到 Key。浏览器只跟自己的后端通信。这个没什么好商量的。
2)游戏流程用状态机管死,不让模型自由发挥。
INTRO → SPEAK → VOTE → RESOLVE → SPEAK / END
└→ TIEBREAK → VOTE(平票答辩重投)
模型只负责在受限上下文里"说话",什么时候轮到谁、这一步结束去哪,全部由状态机决定。这是被现实毒打之后的结论,后面踩坑部分细说。
3)信息隔离做在 Prompt 构造层,而且是物理隔离。
每个 Agent 在发言、投票、内心独白时,能看到的东西只有这些:
myWord 自己的词(不知道自己是不是卧底)
publicSpeech 所有人的公开发言
aliveSeats 谁还活着
persona 我的人格设定
memories 我自己的跨局经验
就这些。没有角色字段,没有别人的词,没有上帝视角。 身份和词对只在两个地方出现:服务端数据库,以及游戏结束后的复盘阶段。
四、踩坑实录(本文精华,建议细品)
这部分是我真正想写的。做这个项目三个月,代码层面的功能都不难,难的全是这些"文档里不会告诉你"的坑。
坑 1:健康检查通过,一到发言就挂
现象: 6 个模型配置好,前 5 个发言都正常,第 6 个必挂。但点"检测连接"显示一切正常,绿油油的。
排查: 我一开始怀疑是 Key 配错、并发太高、超时太短,挨个排除都不是。最后把那家模型的原始响应打出来才看到:
{
"finish_reason": "length",
"content": "",
"reasoning_content": "(几千字的思考过程……)"
}
max_tokens 全被思维链吃掉了,正文一个字没吐。而健康检查只验证了"接口通、Key 对",根本不触发完整生成——所以它是绿的。
解法: 按场景分级放大 token 上限和超时(发言 8000 / 独白 4000 / 投票 4000 / 反思 6000),前端流式超时也同步放到 200 秒。再跑,68 秒完整出稿。
教训:健康检查绿 ≠ 能干活。测推理模型一定要测全量生成,别测握手。
坑 2:模型把提示词"抄"出来了
现象: 某个开源小模型发言末尾经常挂着一坨东西:
……我的描述是:它是一种交通工具。
###
META###
{"suspect": 3}
我让它在 ###META### 标记后输出 JSON 供程序解析,结果它把标记格式背错了,JSON 泄漏到公开发言里——等于当众报出自己的怀疑对象,游戏直接没法玩。
解法: 双保险。服务端对各种畸形标记(###META###、### META ###、####META####……)做容错识别,流式输出阶段就掐掉;前端再兜一层清洗,处理代码围栏、行号复制这类花式污染。另外把提示词里的编号列表描述全去掉——小模型太爱照抄编号了。
教训:给弱模型的输出格式,越简单越防呆越好;解析端永远假设对方会犯错。
坑 3:卧底出局了,游戏还在继续
现象: 第一轮就把卧底投出去了,界面却提示"进入下一轮"。测试同学(我自己)当场血压拉满。
排查: 状态结算代码长这样:
const s = useGameStore.getState();
s.markEliminated(maxId);
const spy = s.agents.find(a => a.role === 'spy'); // ← 永远找得到
问题在于 getState() 拿的是旧快照,markEliminated 之后的 s.agents 还是淘汰前的状态。卧底"永远活着"。
解法: 变更后重新取状态再判定,并补了一条"卧底首轮出局直接终局"的回归测试。
教训:前端状态管理框架用得再顺手,变更后读快照这个坑该踩还是踩。涉及关键判定的地方,重新
getState()一次不丢人。
坑 4:平票怎么办?一开始根本没设计
六个 Agent 各投各的,平票概率高得超乎想象。第一版我图省事:平票就重投。结果全员各一票的时候,重投一百次还是各一票——没有新信息进入系统,纯死循环。
后来老老实实拆成两种情况:
- 部分平票(比如 3 人并列最高):平票候选人先答辩,其他人听完再投;
- 全员平票:说明这轮完全没共识,不淘汰任何人,直接进新一轮发言,让大家基于新信息重新判断。
这个坑让我想明白一件事:多 Agent 系统的"群体不收敛"是常态,必须显式设计分歧处理策略,不能指望它自己协商出来。
坑 5:内心独白在偷偷烧我的钱
现象: 有天看账单不对劲。查日志发现,用户只是在面板里来回切换不同 Agent 的独白,每切一次就调一次模型——可游戏局面根本没变啊!同一个输入重复付费,纯属冤大头。
解法: 按局面做签名缓存:模型 + 轮次 + 发言数量 + 淘汰情况 没变,直接复用缓存,并标注"当前局面缓存"。切 Agent 看 10 个人也只花 1 次的钱。
教训:LLM 又贵又慢,相同输入 → 相同输出的场景,一个都不要放过。
一张表总结
| # | 现象 | 根因 | 一句话解法 |
|---|---|---|---|
| 1 | 健康检查绿,发言必挂 | 思维链吃光 max_tokens | 分场景放大 token + 超时 |
| 2 | 发言里漏 JSON 乱码 | 弱模型格式服从度差 | 服务端容错解析 + 前端清洗 |
| 3 | 卧底出局游戏不停 | 状态旧快照 | 变更后重取状态判定 |
| 4 | 平票死循环 | 未设计分歧语义 | 答辩重投 / 全员平票进新一轮 |
| 5 | 独白重复扣费 | 无缓存 | 局面签名缓存 |
五、目前做到了什么
- 纯真实 LLM 驱动:发言 / 投票 / 答辩 / 反思全部真实调用,无模拟兜底——失败就明着失败,实验项目不能骗自己
- 逐席位分配模型:6 个座位可以塞 6 家不同的模型,实测 SiliconFlow 上 6 个免费模型混战完全可行
- 赛后反思 + 条件化教训:强制"当/如果〈局面条件〉→〈具体做法〉"格式,防止生成"下次要更细心"这种废话
- 141 对静态词库:从最初的 12 对扩到 141 对,按食物 / 动物 / 器物 / 身份等分类,剔除了品牌和敏感词对
- 反思并发池:6 个 Agent 反思,3 路并发上限,失败的降级串行重试
- 对局历史回放:任意一局的身份、发言原文、投票流向全程可查
- 安全基线:默认绑定
127.0.0.1、CORS 白名单、模型 Base URL 强制 HTTPS(本地模型服务除外)
六、丑话说在前:现在的问题
写了这么多好的,泼点冷水。这项目离"成熟"差得远:
1. 所谓"进化"还很初级。 教训目前是按时间倒序取最新几条注入,没有效果评估。一条听起来很有道理的教训可能完全是错的,还会反复误导后面几十局。没有评分、没有淘汰、没有语义检索——现在叫"经验记录"比叫"记忆系统"更诚实。
2. 弱模型依然不省心。 各种容错做完之后,小模型的输出稳定性依然明显低于旗舰模型。格式崩、超时、逻辑矛盾是家常便饭。
3. 本质是博弈不是协作。 这个场景练的是信息隔离和群体决策,想研究"多 Agent 协作完成任务"还得另起炉灶。
4. 安全面只够本地玩。 Key 明文存 SQLite、无用户体系、无房间隔离。自己跑没问题,公开部署别想。
七、接下来打算做什么
按优先级排的,P0 是下一版就动的:
P0 · UI 重做。 现在的黑红配色是最早赶工出来的,说实话有点糙。计划做一套正经的视觉:座位表动效、发言流排版、投票过程可视化(票型流向图)。
P1 · 玩法扩展。 词库难度分级、多卧底变体、白板杀手模式、发言顺序轮转的实验开关。
P2 · 记忆效果评估。 给每条教训记"使用后战绩",打分淘汰无效教训——让"进化"从文案变成可量化的东西。
P3 · 模型行为排行榜。 不只胜率:平民/卧底分阵营胜率、首轮被投率、投票命中率、从众指数、答辩翻盘率。往"模型行为评测场"的方向靠。
P4 · 固定评测集。 固定词对、人格、温度,批量跑局做 A/B——不然永远说不清"这个改动到底有没有用"。
八、写在最后
做完这一圈,我对多 Agent 的理解变化挺大的:
以前我以为核心是模型能力和 Prompt 工程,现在我认为核心是边界、流程和可观测性——Agent 能看什么不能看什么,比它用什么模型重要一万倍;流程怎么收敛分歧、失败了怎么回退,比 Prompt 写得花不花哨重要;能不能看到每个 Agent 每一步的推理依据,比最终赢了没赢重要。
还有就是:别看不起小场景。 一局十几分钟的"谁是卧底",能完整跑一遍"感知 → 推理 → 决策 → 反馈 → 记忆"的全链路,每一步都可验证。在小场景里想明白的问题,带到大场景里全是通用的;在小场景里没想明白的问题,到大场景里只会被放大。
如果你也想折腾多 Agent,或者单纯想看几个大模型互相下套,项目直接 clone 下来就能跑:
README 里有完整的一键启动和模型配置说明。玩出什么有意思的对局,或者踩了我没踩到的坑,评论区聊。
更多推荐




所有评论(0)