先放一句话结论:这是一个 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 下来就能跑:

👉 GitHub - undercover-agents

README 里有完整的一键启动和模型配置说明。玩出什么有意思的对局,或者踩了我没踩到的坑,评论区聊。


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐