AI 指令与宏
AI 负责写故事。故事里夹着一些很短的指令,比如 [health: -15]、[gold: +50],引擎会在玩家看到之前把它们拿掉。玩家读到的是干净的故事,游戏状态在幕后跟着变。
宏的方向正好相反:它是你写在设定里的占位符,比如 {{char}}、{{user}},引擎会在 AI 读到之前把它们填好。
还没看过 变量 的话,先去看那一页。知道了指令改的是什么,指令才好理解。
哪些变量用指令
打开了 精准追踪 的变量(新建的数值和开关变量默认就开着)由追踪小助手在每次回复后去改,不走指令。AI 看它们是只读的,它给这些变量写的指令会被丢掉。AI 能改的其它变量都靠指令来改:没开精准追踪的变量、没填可选值的文字变量,以及列表/表格变量。在 AI 权限 里设成 AI 只读 或 仅引擎(对 AI 隐藏) 的变量,同样不认指令。
为什么要有指令
桌面 RPG 里,主持人说「哥布林砍中你的手臂,扣 15 点血」,玩家就在角色卡上擦掉一个数字重写。在 Yumina 里,AI 是主持人,引擎是那张角色卡。
AI 当然可以直接写「玩家现在有 85 点生命」,但那样引擎就得去解析自然语言,弄清楚是哪个变量变了,再算出新值。这很脆弱,也不可靠。
所以 AI 会在叙述的最后写一个结构化的方括号:
哥布林的爪子划过你的前臂,留下一道火辣辣的疼。
[health: -15]引擎看到 [health: -15],减去 15,按最小值/最大值卡住,再把这条指令从文字里删掉。玩家只看到故事,状态面板也跟着更新。
AI 不用自己算数,也不用记上下限,这两件事都由引擎来做。
指令语法
每条指令都是同一个格式:
[variable-id: operation value]方括号里有三部分:哪个变量、做什么、用什么值。
全部九种操作
| 操作 | 语法 | 示例 | 效果 |
|---|---|---|---|
| Set | [var: set value] 或 [var: value] | [location: set "forest"] | 整个替换变量的值 |
| Add | [var: add N] 或 [var: +N] | [gold: +50] | 给数值变量加 N |
| Subtract | [var: subtract N] 或 [var: -N] | [health: -15] | 给数值变量减 N |
| Multiply | [var: multiply N] 或 [var: *N] | [damage: *2] | 把数值变量乘以 N |
| Toggle | [var: toggle] | [has-key: toggle] | 翻转开关(true 变 false,false 变 true) |
| Append | [var: append "text"] | [log: append ", found key"] | 在文字变量末尾接一段文字 |
| Merge | [var: merge {...}] | [stats: merge {"level": 2}] | 把一个对象浅合并进 JSON 变量 |
| Push | [var: push {...}] | [inventory: push {"name": "Sword"}] | 往 JSON 数组末尾加一个元素 |
| Delete | [var: delete "key"] 或 [var: delete N] | [config: delete "old-field"]、[inventory: delete 0] | 从 JSON 对象里删掉一个键(带引号的字符串),或者按下标从 JSON 数组里删掉一个元素(数字)。delete 后面必须带值,光写 [var: delete] 会悄悄变成一次隐式 set。 |
写进 JSON 变量内部的路径。 [relations.chars.lin: merge {...}] 会写进 JSON 变量内部。键还不存在时,这次写入会把它建出来:merge 建出对象,push 建出只有一项的列表,add/subtract 从 0 开始算。路径经过一个普通值时(hp 是数字却写 hp.max)会跳过,不会覆盖。数组元素写成 list.0.hp 或 list[0].hp。
值的写法
- 数字:直接写,
10、3.5、-7 - 字符串:用双引号,
"forest"、"magic sword" - 布尔值:直接写关键字,
true、false - JSON:直接写对象或数组,
{"level": 2}、[1, 2, 3]
简写的坑
新手最容易在这里搞混:
[health: -10] → 减 10(subtract 的简写)
[health: +10] → 加 10(add 的简写)
[health: *2] → 乘以 2(multiply 的简写)
[health: 10] → 把 health 设成 10(隐式 set,不是加)开头的符号(-、+、*)决定是什么操作。没有符号就是 set。 所以 [health: 50] 不是加 50,是把当前值换成 50。
想明确设一个负数(很少见,但可以),写 [health: set -10]。
用 ID 还是显示名
指令里可以用变量的 ID(player-hp),也可以用 显示名(Player HP)。引擎有一张名字到 ID 的对照表,两种都认。ID 更可靠,尤其是显示名里有空格的时候。
JSON 变量的嵌套路径
JSON 变量有嵌套结构时,AI 可以用点号定位到某个字段:
[relationships.aria.trust: +10]
[game-state.factions.ember-court.affinity: set 75]
[inventory.0.durability: -1]引擎会自己沿着路径找下去,中间的对象不存在就建出来。所以 AI 可以只改复杂对象深处的一个字段,别的都不动。
音频指令
音频用的是类似的方括号语法:[audio: track-id action],动作有 play、stop、crossfade、volume,还能把音轨串起来。完整的音频指令和例子见 音频设计。
JSON Patch 格式
做复杂操作的时候,尤其是从数组里删东西,引擎支持一种用 XML 包起来的 JSON Patch 格式。从 SillyTavern 迁过来的世界用的也是这种格式。
<UpdateVariable target="inventory">
<JSONPatch>
[
{"op": "remove", "path": "/0"},
{"op": "replace", "path": "/1/durability", "value": 5}
]
</JSONPatch>
</UpdateVariable>支持四种 patch 操作:
| 操作 | 作用 | 示例 |
|---|---|---|
replace | 把某个路径设成一个值 | {"op": "replace", "path": "/health", "value": 80} |
delta | 加或减(正数加,负数减) | {"op": "delta", "path": "/gold", "value": -50} |
insert | 加一个新的键值对 | {"op": "insert", "path": "/inventory/torch", "value": 1} |
remove | 删掉一个键或数组元素 | {"op": "remove", "path": "/0"} |
路径用斜杠写(/health、/inventory/0),引擎内部会转成点路径。
什么时候用 JSON Patch,什么时候用方括号指令
大多数时候方括号指令更简单,AI 写得也更稳。JSON Patch 适合两种情况:
按下标删数组元素:
[inventory: delete 0]也能用,但处理数组时,有些模型写 JSON Patch 的{"op": "remove", "path": "/0"}更稳定。一次改同一个变量的好几处:要一次更新一个 JSON 变量的好几个字段时,一个 JSON Patch 块比五条单独的方括号指令清爽。
如果你在给 AI 写行为规则,告诉它用哪种格式。大多数世界只用方括号指令。只有确实要删数组元素、而方括号写法又不够稳的时候,才引入 JSON Patch。
引擎怎么处理 AI 的输出
AI 回复回来之后,引擎会先过一遍解析流程,玩家才看得到。知道这个顺序,指令没被识别的时候好查原因。
第 1 步:去掉思考标签。 有些模型(比如 Gemini)会把内部推理写在 <thinking>...</thinking> 里,引擎先把这些删掉。
第 2 步:提取 JSON Patch 块。 引擎找出 <UpdateVariable> XML 块,转成内部的效果操作。有些模型会把指令包在一个 json 代码块里,这些也在这一步被找出来。
第 3 步:提取音频指令。 所有匹配 [audio: ...] 的内容被拿出来,交给音频系统排队。
第 4 步:提取 JSON 指令。 带 JSON 值的方括号指令(set、merge、push、delete)被拿出来。它们要先处理,因为 JSON 内容里可能有会让简单正则认错的字符。[image: …] 这种嵌图不动。
第 5 步:提取普通指令。 所有匹配 [var: op value] 的内容,也就是 add、subtract、set、toggle 这些简单操作。
第 6 步:清理文字。 拿出来的指令全部删掉,多余的空行合并。得到的是干净的叙述文字,加上一串效果。
之后,回复处理 的规则会在这段干净文字上跑一遍。引擎应用这些效果(连同精准追踪的判断),再检查行为。玩家看到的是干净的文字。
为什么解析顺序重要
JSON 指令要比普通指令先提取,因为像 [stats: merge {"health": 50, "mana": 30}] 这样的指令里有冒号和数字,普通的正则会认错。先处理 JSON,引擎就不会误匹配。
指令没被识别,最常见的原因是 JSON 写坏了:大括号没配对,或者少了引号。引擎遇到解析不了的指令会悄悄跳过,不会崩。
完整参考 → World Spec: Variables
宏:设定里的动态文字
宏是写在设定里的占位符,引擎在发给 AI 之前会把它换成真实的值。长这样:{{char}}、{{user}}、{{turnCount}}。
设定写一次,就能跟着情况变。改了角色名字,所有 {{char}} 自动跟着改。玩家选了一个人设,{{user}} 也跟着变。
常用的宏
| 宏 | 换成 | 示例 |
|---|---|---|
{{char}} | 当前角色的名字 | Luna |
{{user}} | 玩家的名字(或当前人设的名字) | Kai |
{{turnCount}} | 当前回合数 | 42 |
这三个能覆盖大多数情况。{{char}} 和 {{user}} 几乎每条设定都会用到。
随机宏
| 宏 | 行为 | 示例 |
|---|---|---|
{{random::a::b::c}} | 每次随机选一个 | {{random::sunny::cloudy::rainy}} 可能得到 "cloudy" |
{{pick::a::b::c}} | 稳定地选一个,同一回合里结果不变 | {{pick::red::blue::green}} 同一回合总是同一个颜色 |
{{roll::NdS+M}} | 掷骰:N 个 S 面骰,再加修正值 M | {{roll::2d6+3}} 可能得到 11 |
random 和 pick 的区别:random 每展开一次就重新随机,同一条设定展开两次,结果可能不一样。pick 用宏在设定里的位置和当前回合数算一个稳定的哈希,同一回合里每次都一样,下一回合可能变。
要变化就用 random(天气、人群描写)。同一回合里要前后一致就用 pick(角色的衣服被提到两次,得是同一件)。想做 AI 能接着演的骰子机制,就用 roll。
时间日期宏
| 宏 | 换成 | 示例 |
|---|---|---|
{{time}} | 当前时间(HH:MM) | 14:30 |
{{date}} | 当前日期(本地格式) | 2026/5/13 |
{{weekday}} | 星期几 | Tuesday |
{{isodate}} | ISO 格式日期 | 2026-05-13 |
{{isotime}} | ISO 格式时间 | 14:30:00 |
{{idle}} | 距离玩家上一条消息过了多久 | 5 minutes |
这些是现实里的时间,不是游戏里的时间。适合现实时间和虚构时间混着用的世界,或者做挂机检测(「玩家 10 分钟没回,角色发一条担心的消息」)。
上下文宏
| 宏 | 换成 |
|---|---|
{{lastMessage}} | 最后一条消息的全文 |
{{lastUserMessage}} | 玩家的最后一条消息 |
{{lastCharMessage}} | 角色的最后一条消息 |
{{model}} | 正在用的 AI 模型名字 |
玩家人设宏
玩家在 个人主页 → 人设轮播 里创建人设,也就是带外貌、性格、背景故事的身份。玩家用着某个人设时,这些宏从人设里取值:
| 宏 | 换成 |
|---|---|
{{persona_name}} | 当前人设的名字 |
{{persona_appearance}} | 外貌描述 |
{{persona_personality}} | 性格特点 |
{{persona_backstory}} | 经历和来历 |
{{persona}} | 上面四项合在一起,再加上人设里额外写的条目 |
小技巧:在 角色和世界 里放一条叫「关于玩家」的设定,内容写 {{persona}},AI 就一直知道玩家在扮演谁。玩家换了人设,AI 也会自动跟上。
{{user}} 用的是当前人设的名字;没用人设的话,用玩家的用户名。两个都不知道时,用这张卡设的默认玩家名,再没有就是「Player」。
工具宏
| 宏 | 作用 |
|---|---|
{{// comment text}} | 注释。展开后什么都没有,可以在设定里留笔记,不会发给 AI |
{{trim}} | 吃掉前后的空白。宏留下难看的空隙时,用来精细控制格式 |
变量兜底
宏名对不上任何内置宏时,引擎会看它是不是某个变量的 ID。比如有个 ID 是 mood 的变量,当前值是 "suspicious",那 {{mood}} 就会换成 "suspicious"。这只对由字母、数字和下划线组成的 ID 有效:{{player_hp}} 可以,{{player-hp}} 不行。画布上新建的变量 ID 是一串带减号的随机字符,所以想当宏用,先在变量设置的 高级 里给它改一个简单的 ID。
既不是内置宏也不是变量的话,引擎就把 {{xxx}} 原样留着。不报错,也不会崩。
好用的做法
按比例掉血的战斗
定义一个 health(数值,0-100),在行为规则里按武器类型写好伤害范围。AI 写叙述,再在后面带上指令:
强盗的匕首划过你的肋下,伤口不深,但火辣辣地疼。
你踉跄后退,仍然护着要害。
[health: -8]
你抡起巨剑横扫过去。强盗想躲,剑刃还是砍中了他的肩膀。
他惨叫一声。
[enemy-hp: -22]玩家看到的是一场战斗,数字在背后记账。
关系递进加条件设定
拿一个数值变量(aria-trust,0-100)配几条按阈值发送的设定(画布上的条件设定)。AI 用指令更新信任值,设定系统会按阶段自动调整 AI 对这个角色表现的认知:
- 设定「Aria:戒备」(条件:
aria-trust < 30):客气,保持距离 - 设定「Aria:升温」(条件:
aria-trust >= 30 AND < 60):会说自己的想法,偶尔笑一下 - 设定「Aria:亲近」(条件:
aria-trust >= 60):会示弱,护着你,有只有你们懂的梗
AI 用指令推动数值:一次友好的互动之后写 [aria-trust: +5]。(打开了精准追踪的话,改为由追踪小助手来推。)设定系统决定 AI 看到哪个版本的 Aria。两边互相不知道对方存在,靠这个变量来传话。
用宏写会变的场景描写
一条能跟着情况变的设定:
{{user}} 走进房间时,{{char}} 抬起头。现在是 {{time}},
外面的天气{{random::晴朗::阴沉::下着毛毛雨}}。
{{char}} 今天看起来{{mood}}。如果角色是 Luna,玩家是 Kai,现在下午两点,mood 变量是「心事重重」,AI 收到的是:
Kai 走进房间时,Luna 抬起头。现在是 14:00,
外面的天气阴沉。
Luna 今天看起来心事重重。掷骰判定
在一条给旁白的指令设定里写:
{{user}} 尝试有风险的行动时,用掷骰结果判定成败。
掷骰:{{roll::1d20}}。15 以上成功,10-14 部分成功,
10 以下失败。AI 拿到的是一个实际的数字,可以照着往下写,结果由骰子决定,不用 AI 自己拿主意。
常见错误
指令放错地方。 引擎已经告诉 AI 把指令放在回复最后了。如果某个模型还是写在句子中间,解析照样能用,只是查问题更麻烦。
指望 AI 算数。 AI 写 [health: -15],减法由引擎来做。别在行为规则里写「算出新的生命值」这种话,AI 只要写对指令,算数和最小/最大值的限制都由引擎处理。
指令里的 JSON 写错。 大括号没配对或少了引号,引擎会悄悄跳过这条指令。merge 或 push 没生效的话,检查一下 JSON 是否合法。引擎不会崩,只是跳过去。
把 {{random}} 和 {{pick}} 搞混。 如果角色的眼睛颜色每次展开都在变,说明你该用 pick 的地方用了 random。稳定的属性用 pick,故意要变化的用 random。
用 JSON Patch 把事情搞复杂。 除非确实要从数组里删东西,否则就用方括号指令。更简单,AI 写得更稳,也更好查问题。
忘了 {{user}} 会跟着人设变。 玩家换了人设,{{user}} 就变成新人设的名字。通常这正是你想要的,但如果你用 {{user}} 的地方不该在一局中途变,要注意这一点。
另见
指令语法和操作规范 → World Spec: Variables
