
先交代背景。我想在酒馆里跑修仙。不是那种穿过去系统激活追妻火葬场的修仙是那种你散修一个灵石掰两半花、路边野店可能黑吃黑、筑基丹要攒三年材料还得看丹阁脸色——的修仙。但你我都知道普通角色卡做不到。AI记不住。昨天你刚筑基今天问它你在什么境界它“你是一名筑基期修士……不对你好像还在炼气等等我再看看——你是什么境界来着”就这种感觉。所以我去看MVU卡了。MagVarUpdate一个让AI能读写变量的架构。然后我花了大概……一天从v0.1干到v0.7搞了37个文件一张PNG。过程中有好几次想摔键盘也有几次觉得自己真牛逼。下面这些都是我还记得的。第一步是设计变量树就这个stat_data ├── 主角 ├── 修炼 ├── 先天气运 ├── 资源 ├── 世界 ├── NPC档案 └── 记忆我一开始觉得这东西没啥好想的。后来AI教我做人了。AI会在NPC档案里写这种东西/NPC档案/柳如烟/好感度一见倾心……不是大哥好感度是0-100的数字 /主角/尾巴长度九尾天狐主角没有尾巴也没有狐妖血统你在写什么 /修炼/修为进度我很快就要筑基了因为我吃了一颗丹药一串完整的汉字你把AI当成一个很聪明但完全没受过编码训练的新人工程师。它理解意图但看不懂类型系统。解决方案是Zod Schema。我不知道你有没有用过Zod。简单说它是一个校验库——你定义修为进度是一个0-100的数字它就会确保修为进度永远是0-100的数字。哪怕AI传了五十、“-100”、“我很快就要筑基了”Schema都能兜住。constpctz.coerce.number().transform(vMath.min(Math.max(v,0),100)).default(0).catch(0);我最喜欢.catch(0)。前面全失败了没关系我们是0。你的变量不会炸。这件事教会我的东西是变量树设计本质是在给AI划跑道。跑道划得越清楚AI开得越稳。跑道划得模糊AI就——飞出去了。后来我还学到一个东西叫封闭对象 vs 字典容器。翻译成人话就是有些变量结构是固定的不能随便增减字段——主角的字段是{姓名, 性别, 境界, ...}你不能今天给它加个尾巴长度明天加个翅膀颜色——这叫封闭对象。有些变量数量是动态的但每个个体结构固定——NPC档案今天有3个明天有30个但每个NPC都长{uid, 姓名, 好感度, ...}——这叫字典容器。这两个一旦不分清楚AI会在/世界/势力格局底下写乱七八糟的东西。别问我怎么知道的。世界书条目的套路变量树搭好了问题变成怎么让AI知道它存在并且会用世界书条目就是干这个的。它们是一些嵌入在角色卡里的指令片段每回合混进prompt喂给AI。基本就四种第一种初始变量。禁用状态。只用来在卡加载时初始化变量树。如果它不小心触发了你玩到一半的角色会被重置到1级。别问我为什么知道要把它禁用。第二种当前变量。用EJS模板把stat_data渲染成JSON插在prompt里让AI看到当前世界状态。private %_ const stat variables?.stat_data || {}; const keep { 主角: { 姓名: stat.主角?.姓名, 境界: stat.修炼?.境界 }, NPC档案: stat.NPC档案 || {}, 记忆: { 摘要: stat.记忆?.摘要 || [] } }; _% status_current_variables %- JSON.stringify(keep, null, 2) % /status_current_variables这段代码运行后AI看到的东西多了一层JSON。但用户看不到——有个正则会在AI输出后把这坨JSON静默移除。AI看到的世界和用户看到的世界不一样。这个事实贯穿了整个项目。第三种更新规则。告诉AI怎么改JSON。路径怎么写UID怎么用哪些操作允许哪些禁止。UpdateVariable JSONPatch [{op: replace, path: /修炼/境界, value: 筑基}] /JSONPatch /UpdateVariable 禁止 - 把JSON放进markdown代码块 - 用姓名做动态key - 整对象覆盖中间对象会吞字段每一条禁止项都对应一个AI真实犯过的错误。真的。第四种剧情指引。这个最轻松。就是告诉AI这个世界是什么样的。必须写 - 资源的现实压力 - 修士的谨慎与试探 - 机缘背后的代价 禁止写 - 主角无代价开挂 - 宗门无脑跪舔 - 好感度高自动服从 - 堕落值高人格崩坏我后来发现一个规律光说应该写什么AI会当耳旁风。同时说禁止写什么效果翻倍。可能是因为AI被训练得太注重不要做你不被允许做的事了。正则才是真正的魔术正则在MVU卡里不是搜索替换工具它是一个两个世界之间的接口。AI看到的东西和用户看到的东西不一样。正则是那个让两边自洽的胶水。隐藏类最简单但最核心。{findRegex:/status_current_variables[\\s\\S]*?\\/status_current_variables/gm,replaceString:,placement:[2]}AI能看到完整JSON。用户看到的是剧情。各自安好。同样的做法用在变量更新标签(UpdateVariable)和选择栏标签(选择)上。AI每轮写正则每轮删。不删的话用户会看到满屏的XML标签在对话里飘。美化类选择栏原本是原始的XML——能读但不好看。我用正则把它替换成一整套带CSS的HTML面板深色半透明背景金色边框七种立场各有配色合理/刚正/仁善/中庸/叛逆/唯我/视角切换。每个选项是一个小卡片有数字编号、立场标签、行动描述。一眼能看出不同路线的区别。做法就是提取7个选项文本和1个行动预估文本拼进一段写好的HTMLCSS模板里。没什么高深的就是正则有坑——我踩了。我后来加了个兜底正则。因为AI偶尔会输出格式不对的东西——七个选项变成六个或者漏了标签。兜底正则匹配得更宽松一点至少保证用户不会看到裸露的XML代码。这个思路是从React Error Boundary偷的——与其让用户看到崩溃的白屏不如优雅地降级。注入类这个最炸裂。有个占位符叫StatusPlaceHolderImpl/。正则会匹配到它然后替换为script// 一段完整的交互式前端// 读取当前对话中的变量数据// 渲染一个状态面板——名字、境界、修为、灵石、伤势// 每轮更新/script等一下正则怎么执行JavaScript它不执行。它把script标签注入到页面上。页面渲染的时候脚本就跑起来了——你在普通网页里写内联脚本也这样。同样的套路用在开局向导GenesisPanelImpl/被替换成一个完整的角色创建表单。玩家填好了点击发送生成【开始游戏】姓名xxx……文本。AI看到文本按世界书里的建档规则创建角色。这里有个关键的设计决策前端不直接写变量。它只生成文本。AI看到文本后再去写stat_data。这样前端和AI的规则是解耦的。你换了前端只要输出格式不变AI那边不用动。v0.7修的那个bug是真有意思从v0.1到v0.6我一直在加功能。到v0.7的时候出了个诡异的bug状态栏显示未命名、“未建档”。但我检查了stat_data——主角.姓名 萧天数据是对的。状态栏为什么读不到查了两小时。结果发现是酒馆不同版本的处理方式不同——有些版本在/stat_data/主角/姓名写有些在/主角/姓名写。AI写的时候路径漂移了状态栏读镜像的时候就乱了。状态栏看到的主角可能是外层空壳{ 姓名: }不是stat_data里的真实数据。修复方案所有JSONPatch如果以/stat_data/开头自动去掉前缀如果外层有数据而stat_data里没有合并到stat_data状态栏不读第一个数据源读所有数据源里评分最高的——有名字的加分有境界的加分分数高的用来渲染第3点我最得意。状态栏维护一个变量快照池每个快照算完整度分数选分最高的那个。这让我意识到一个道理当你控制不了外部系统的行为时你需要的不是更精确的写入而是更容错的读取。酒馆版本升级你是不知道的。你管不了它怎么写。但你可以让你的读取多一层保险。修复完v0.7未命名再也没出现过。装配脚本的诞生到v0.3的时候我已经受不了了。每次改一个部件——比如修了一个世界书条目——我就要手动打开角色卡JSON找到对应的条目替换内容更新版本号检查有没有漏同步的字段大概第五次的时候我写了个Node.js脚本。constbooks[世界书.json,跟随规则.json,映射规则.json].map(readJson);letentries[];for(constbofbooks){entriesentries.concat(convertEntries(b,entries.length1));}脚本把所有部件世界书、脚本、正则读进来拼成一张完整的角色卡JSON。然后另一个脚本把它塞进PNG。验证脚本也一样——跑一次检查所有约束保证你没漏掉什么。每次迭代完跑一次。世界书改了但忘了升版本号脚本抓得到。装配脚本改了格式漏了字段脚本也抓得到。版本迭代的节奏v0.1 搭骨架变量树、Schema v0.2 长血肉前端、NPC跟随 v0.3 修bug v0.4 上保险 v0.5 加交互选项框架 v0.6 扩内容 v0.7 加固核心每个版本只改一件事。加功能和修bug不放在一起。如果v0.5既加了选项框架又修了NPC跟随出了问题你找都找不到。v0.7没加任何新功能没加任何新内容。只修了一个bug。但修完之后整张卡稳定了很多。给修bug留一个单独的版本周期。不要总想着顺便修一下。一些不知道该放哪的话写这篇东西的时候我在想一件事。如果你刚接触制卡不要从变量树开始。先写一个最简单的隐藏正则——匹配什么然后替换为空——跑通它。看到效果。获得正向反馈。然后写EJS读取变量条目。然后写更新规则。变量树放在第二步不是第一步。第一步是跑通一个能工作的最小系统哪怕它只能存一个名字。最后一个事情。我花了一天做了37个文件一张PNG。如果问我值不值得我只能说做这件事的时候我进入了心流状态。我在解决一个个具体的问题不是在做任务。当你做的事情不感觉像在工作的时候——其实也不用问值不值得了。