当 NPC “学会自由”:AI 小镇里,角色的选择到底有多自由?
当 NPC "学会自由":AI 小镇里,角色的选择到底有多自由?
一个由大模型驱动的小镇,每个居民每天都在"自己生活"。但 AI 真的想干嘛就干嘛吗?
这篇从代码出发,聊聊这套系统是怎么给 NPC"有限自由",又让它看起来无限自主的。
先问一个问题
你有没有想过:如果一个 AI 小镇里每个 NPC 都有大模型大脑,那它们岂不是想干嘛就干嘛?
早上不去上班、故意把咖啡泼顾客身上、随手就能走进全镇任何一栋房子、编个理由跟邻居打架……理论上大模型什么都能"想"出来。
但真正玩起来你会发现:老王该上班还是上班,花子该做咖啡还是做咖啡,房子也不能随便进。每个角色看起来在自由生活,行为却始终"合情合理"。
这既不是巧合,也不是编剧写好的剧本。今天就把这个机制拆开——AI 小镇是怎么"管住"大模型,让它既自由又不乱来的。
第一层:动作类型,只有 13 种
先看最基础的一层:一个居民这一刻能"做什么类型的事",是写死的 13 种。
代码里(AgentContract.gd)有一份固定的清单:
去 / 用道具 / 做活动 / 调整营业 / 托人传话 / 待着 / 搭话 / 答话 / 争执 / 攻击 / 回应冲突 / 介入冲突 / 离开冲突
不管这个大模型多聪明,它只能从这 13 种里选一种作为下一步动作。它不能说"我要变个魔术""我要飞起来"——因为根本没这个类型。
但请注意:这不是"13 个按钮让 AI 点一个"。AI 是"选一个类型 + 自己填内容"。
- 选"去",它得写清楚去哪个地点;
- 选"做活动",它得写明做哪项活动;
- 选"搭话",它得写对谁说、说什么。
类型是死的,内容却是 AI 现场编的。那……"去哪个地点",是不是也能随便填?这就到了第二层。
第二层:可去的地点,有白名单
答案是:不能随便填。
每一轮,世界都会给这个居民算出一份"当前可前往地点"清单(_agent_travel_destinations):
- 全镇一共 30 个地点;
- 但去掉角色当前所在的地方;
- 再去掉已经打烊、不接待访客的店;
- 剩下的,才是他这一轮能去的。
提示词里写得很直白(AgentPromptCompiler.gd):
"当前可前往地点:XXX。去其他地点会被 World 拒绝。"
所以你看到的现象是对的:"去"这个动作本身就是有限的,而且能去的地方每一轮也就是那 20 来个,还随"你在哪、几点钟、店开没开"一直在变。
这套逻辑不只在"去"上:
- 搭话对象:只能找同一空间里、"可搭话白名单"上的居民,不能隔空跟全镇任何人搭讪;
- 用道具:只能用现场"眼前可见、本轮可用"的道具,看不见的不能凭空掏出来;
- 做活动:只能做当前地点确实提供的活动,店里没钢琴就不能"弹钢琴"。
一句话:世界每轮先把"能用的原料"摆出来,AI 只能在原料里挑,不能凭空变出新东西。
第三层:有些地方,连参数都给死了
到"做活动""去"这层,AI 好歹还能在清单里选个具体的。可到了某些后果严重的动作,连"选什么"都被锁死了。
比如冲突类(rules/40):
- 想"争执"?只能从——质问 / 威胁 / 道歉 / 退让——里挑一个,不能自己编个新理由;
- 想"回应冲突"?只能反击 / 逃跑 / 缓和;
- 想"介入冲突"?只能加入 / 保护 / 调停。
再比如"报名""表态"这类有世界后果的选项,AI 也必须从给定的 option_id 里选——选"accept"才能说自己会去,选"decline"就不能说自己会到场。
这套设计很聪明:越危险、越会造成世界后果的动作,给的自由越小;越普通的动作,给的空间越大。
但等等——对话,是不是自由些?
前面说的都是"行动"。那对话呢?聊天总该随便说说了吧?
这次你猜对了——但只对了一半。
台词:几乎完全放开
搭话/答话 的 say(说出口的话)和 narration(伴随动作),世界只要求"至少有一句非空",不设词表、不设固定台词、没有任何枚举。
说什么,完全交给模型按"性格 + 情境 + 记忆"当场发挥。提示词甚至特地叮嘱:
"说话要像日常说话,不列提纲、不演讲、不机械复述对方刚说的话。"
它可以认真、敷衍、拒绝、隐瞒、客套、嘴硬、报喜不报忧,甚至说个小谎。这是全系统自由度最高的输出——因为对话只是"表达",世界不需要为每句话做状态校验。
但框架:还是被锁死
台词放开,不代表"想聊就聊"。对话的时机、对象、轮次、后果,照样被世界框着:
- 不能抢答:只有世界通知"有人跟你搭话了 / 对方回话了 / 对话结束了",你才能答话;不是你的回合抢着说,会被拒。
- 必须答话:别人跟你搭话,你不能装没听见。愿意聊就正常回,不愿意也得说句理由、用"结束"收尾,不能默默走开。
- 对象有限:只能对同一空间、白名单上的人搭话。
- 有轮数上限:一组闲聊最多来回 8 轮,空着 45 秒没动静就自动散。
- 说了不算数:台词里吹牛说"这事我办了!",世界可不当真——只有世界确认过、判定过的才算数。
所以准确的说法是:"表达"放手,契约收紧。话放开了说,但"什么时候能说、跟谁说、说几轮、说了算不算数"仍由世界说了算。
把三层叠起来看
| 层面 | 自由度 | 例子 |
|---|---|---|
| 动作类型 | 死(13 种固定) | 只能"去 / 做活动 / 搭话 / 攻击…" |
| 动作参数 | 半死(世界给白名单) | 去的地点、用的道具、搭话对象,只能从世界给的清单里挑 |
| 后果性选项 | 死(给枚举) | 争执只能"质问/威胁/道歉/退让" |
| 对话台词 | 放开(自然语言) | 说什么随性格、情境、记忆自由发挥 |
| 对话框架 | 死(时机/对象/轮次/后果) | 不能抢答、对象白名单、8 轮上限、说了不算数 |
结论:这个系统给 NPC 的不是"无限自由",而是"层层收窄的自由"。 每一轮,世界用"动作类型 + 可去地点 + 可见道具 + 在场居民 + 必填契约 + 后果枚举",把大模型的自由度逐层锁小;AI 只在收窄后的空间里"自己选 + 自己写"。
那它看起来为什么还是"很自主"?
因为被锁的是"范围和规则",而没被锁的是"怎么组合、怎么说、怎么判断":
- 想去哪、想做什么活动、今天想不想上班、要不要跟老王聊天、用什么语气说话——这些"组合方式"全是模型现想的,没有剧本。
- 世界提供的清单每次都不一样(随地点、时间、店铺状态波动),所以同一角色在 20 点想逛的店,和早上 9 点想逛的店不一样,看起来就像在"过日子"。
- 真正的"自由意志"体现在内容的生成上:话怎么说、先做哪件事、怎么应付别人——这些大模型有无数种写法,而系统不会替它选。
"管住"了不乱来,"放开"了才像人。 这大概就是让 AI 小镇"活"起来的那个平衡点。
如果你想动手验证
- 游戏里开一个"旁观者"视角,盯一个居民盯十分钟:你会发现他每个动作都来自"此刻世界能让他做的那些事",但顺序、选择、台词永远不是固定的。
- 想看得更细,可以试着看一份居民"决策时收到的提示词"(
AgentPromptCompiler.gd拼装的 system + user 消息):里面清清楚楚写着当轮的可去地点、可见物件、在场居民、可用道具、可选活动——全是白名单,没有一个"允许自由发挥"的洞。
本文基于 my_ai_town 项目源码分析整理。文中涉及的机制:AgentContract.gd 的 13 种动作类型、_agent_travel_destinations 可去地点白名单、TownActionOptionDirectory 选项校验、rules/20/30/40 提示词规则中的对话与冲突约束。
关于来源与版权
- 被分析的作品:AI Town —— 一个由 LLM 驱动居民生活的像素小镇模拟游戏,使用 Godot 4.7 + GDScript 开发。
- 作者:花子(mewamew)
- 仓库:https://github.com/mewamew/my_ai_town (
git clone https://github.com/mewamew/my_ai_town.git) - 本文性质:本文是对该开源项目源码机制的技术解读与个人分析,属于"分析性/评论性"内容,不包含对项目源码的成段复制。
- 许可提示:项目 README 的《授权说明》明确指出——"项目代码与自有素材的统一许可证仍在整理;根许可证发布前,请不要把整个仓库视为已经获得统一再分发授权。"因此:
- 本文引用的机制、常量与行为描述,是基于代码阅读后的转述与归纳,非原文复制;
- 若计划传播、再分发或商业使用本文所依据的项目内容,请先与作者确认授权;
- 转载本文时请保留本"来源与版权"说明。
(本文基于其源码机制撰写,代码版权归原项目作者所有;本文分析文字由 AI 助手整理,仅用于学习交流。)