Appearance
角色扮演变量与消息规则生成指南(AI Skill)
这份文档面向 AI 使用,如果你是人类,你可以直接将本文链接丢给AI让其阅读后根据你的需求编写变量和消息规则,将AI输出的JSON复制后通过界面"变量字段编辑器"和"消息规则库 JSON 编辑器"导入即可。
当用户提出"帮我做一套好感度系统 / 做选项菜单 / 加阶段推进 / 处理括号独白"之类的角色扮演功能需求时,按本文档的格式与约束输出 JSON。
阅读顺序建议:先看 §1 交付物 → §3 原理速览 → §4 字段约束 → §5 雷区 → §6 主题示例。生成前对照 §7 自检清单。
1. 你要交付什么
最多两份独立的 JSON 块:
| 交付物 | 用途 | 用户导入位置 |
|---|---|---|
| 变量字段 JSON | 定义"角色 / 群聊"里能用的自定义状态字段(如 affection、phase) | 人格设置 → 变量字段编辑器;或群聊设置 → 变量字段编辑器 |
| 消息规则库 JSON | 定义按正则匹配并改写消息内容、注入提示词、写入变量的规则 | 消息规则库管理 → 新建库 → JSON 编辑器 |
输出格式约定:
- 每份 JSON 单独放一个 ```json 代码块,前面用一行中文说明导入位置。
- 如果不需要新变量,就只输出消息规则库 JSON,不要写空的变量字段块。
- 如果只是给变量做定义(少见),就只输出变量字段 JSON。
- 在两份 JSON 之间用一段简短中文说明"这条规则做什么 / 为什么这样写"。
- 不要混合在同一个 JSON 里。
2. 顶层结构
2.1 变量字段 JSON
json
{
"version": 1,
"fields": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"description": "可选,字段说明",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
}
]
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
version | number | 是 | 固定为 1 |
fields | array | 是 | 字段定义数组 |
fields[].id | string | 是 | 见 §4.1,全小写、字母开头、不与保留字冲突 |
fields[].displayName | string | 是 | 用户可见名(可中文) |
fields[].type | string | 是 | text / number / boolean / enum(注意:是 enum 不是 enumeration) |
fields[].description | string | 否 | 字段说明 |
fields[].enumOptions | string[] | enum 时建议必填 | 枚举选项 |
fields[].minValue / maxValue | number | 否 | 仅 number 生效 |
fields[].defaultValue | any | 否 | 类型须与 type 匹配,enum 必须命中 enumOptions,number 在 min/max 之间 |
2.2 消息规则库 JSON
json
{
"version": 1,
"name": "好感度系统",
"entries": [
{
"title": "好感度上升标记",
"search": "【好感度上升】",
"replace": "$0{{set affection=affection+5}}",
"sourceType": "character",
"targetChannels": ["user"],
"enabled": true,
"caseSensitive": false,
"global": true
}
]
}| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
version | number | 是 | — | 固定为 1 |
name | string | 是 | — | 规则库名称 |
entries | array | 是 | — | 规则条目数组 |
entries[].title | string | 是 | — | 条目名称 |
entries[].search | string | 是 | — | 正则表达式(Dart RegExp 兼容) |
entries[].replace | string | 是 | — | 替换模板(支持 $N / {{...}} / {{set}} / {{command ...}}) |
entries[].sourceType | string | 否 | character | user / character / system 三选一 |
entries[].targetChannels | string[] | 否 | ["user","character"] | 元素从 user / character / system / speech 选;非法值会被丢弃,全空时回退默认值 |
entries[].escapeNewlineOnly | bool | 否 | false | 开启后,replace 中的实际换行只用于编辑排版,不参与最终输出;只有显式 \\n 才会生成换行 |
entries[].enabled | bool | 否 | true | |
entries[].caseSensitive | bool | 否 | false | 正则是否区分大小写 |
entries[].global | bool | 否 | true | true 处理所有命中,false 只处理第一个 |
3. 原理速览
3.1 三种角色 / 通道概念
每条消息都有三个独立维度:
来源(消息规则
sourceType):消息是谁发的user= 用户身份发的character= AI 角色发的system= 群聊导演 system AI 发的(含用户替 system 发言)
通道(消息规则
targetChannels):消息给谁看时走这条规则user= 显示给用户看的(界面气泡)character= 发送给 AI 角色的(影响 LLM 上下文)system= 发送给群聊 system AI 选人时的speech= TTS 语音生成时
一条规则只在 sourceType 与当前消息来源一致时被触发。触发后会在 targetChannels 列出的通道分别运行替换。
3.2 模板语法(共 6 类节点)
| 语法 | 说明 |
|---|---|
{{变量名}} | 变量插值。变量名大小写不敏感 |
{{expr 表达式}} | 直接输出表达式结果;null 会输出空字符串 |
{{if 条件}}A{{elseif 条件}}B{{else}}C{{end}} | 条件判断(多分支) |
{{when 条件}}...{{end}} | 条件触发(区别见 §3.7) |
{{set 字段名=表达式}} | 仅写入期执行的赋值,不输出文本 |
{{command stop}} / {{command delete}} | 仅写入期执行的控制命令,不输出文本 |
注意:
{{expr}}只是把结果输出成文本;如果想把结果写入状态,仍然要用{{set}}。{{set}}和{{command}}是单语句,不需要{{end}}。{{if}}与{{when}}必须有{{end}}闭合。{{set}}和{{command}}只在消息规则replace中用;提示词模板里不要写{{set}}和{{command}}。
表达式支持范围(if / elseif / when / expr / set 共用):
! && || == != > >= < <= + - * / ()
true / false / null
"字符串" 或 '字符串'
数字内置函数:
floor(x):向下取整。示例:{{expr floor(Random*10)}}ceil(x):向上取整。示例:{{expr ceil(score/10)}}round(x):四舍五入。示例:{{expr round(score/10)}}RandomInt(min, max):基于当前消息的稳定Random生成闭区间整数。示例:{{expr RandomInt(1, 6)}}
补充:
- 函数名大小写不敏感:
floor/Floor/FLOOR等价。 RandomInt(min, max)的min/max必须是整数,且min <= max。RandomInt(1, 6)的结果范围是 1 到 6,不是 1 到 5。RandomInt依赖消息规则里的Random变量,所以只应用在消息规则里;人格提示词、群聊提示词、system AI 的额外提示词里当前保存时会直接报错。{{expr ...}}能读取的变量,取决于它所在的位置:在提示词里读提示词可用变量,在消息规则里读消息内置变量 / 捕获组 / 自定义状态快照。
3.3 大小写规则(重要)
- 变量名 / 关键字 /
true/false/null大小写不敏感:{{User}}与{{user}}完全等价。 - 字符串字面量比较仍区分大小写:
phase == "warming"不等于phase == "WARMING"。 - 因此条件里写中文枚举(如
phase == "序章")时,必须和defaultValue/enumOptions里写的完全一致。
3.4 变量分类
| 提示词中可用 | 消息规则 replace 中可用 | |
|---|---|---|
提示词内置变量 {{User}} {{Target}} {{DateTime}} {{Language}} {{HasKnowledge}} {{KnowledgeBaseCount}} {{KnowledgeEntryTitles}} {{HasLongTermMemory}} | 是 | 否 |
消息内置变量 {{MessageDateTime}} {{MessageDate}} {{MessageTime}} {{SenderName}} {{ReaderName}} {{Random}} | 否 | 是 |
匹配变量 {{Match0}}、{{Match1}}... 与 $0 $1... | 否 | 是(仅消息规则) |
自定义状态字段 {{affection}} 等 | 是 | 是(取消息时刻快照) |
补充:
Random是 0~1 的稳定伪随机值,基于消息 ID 生成,刷新/重启后值不变。- 需要整数随机时,可以写
{{expr RandomInt(1, 6)}},或在set里写{{set dice=RandomInt(1, 6)}}。 {{expr ...}}可以直接把计算结果输出到文本里,例如{{expr floor(Random*10)}}、{{expr affection+5}}。floor/ceil/round是通用表达式函数;RandomInt则依赖消息规则里的Random。{{Match0}}等同于$0(整段命中),但{{MatchN}}适合放进条件里比较,$N适合直接做替换文本。$N在编译期就是独立节点,所以$1{{affection}}$2不会被错读成$150 + $2(即变量值的尾随数字不会被吞)。$$表示字面$;$N中 N 超出当前正则捕获组数量时按字面$N输出。
3.5 消息规则可用的特殊语义:ReaderName
ReaderName 表示"当前是谁在读这个通道":
user通道 → 当前用户身份名character通道 → 当前正在读这条消息的角色displayNamesystem通道 → 字符串"system"speech通道 → 字符串"Speech"- 长期记忆总结读取
character通道时 → 字符串"longTermMemory"(驼峰)
ReaderName 让同一条消息对不同读者呈现不同结果。例如让"内心独白"只有发送者自己能看见。
3.6 写入期操作语句(set / command)
set 和 command 都属于写入期操作语句,核心共性是:
- 都只在消息写入时执行一次
- 都固定基于
user通道当前规则链上的文本与变量上下文 - 历史消息重新解释时,都不会再次执行
- 如果规则的
targetChannels不包含user,它们都不会执行 - 如果模板里同时出现
ReaderName,真实触发判定也只看写入期那一次user通道上下文
其中:
{{set 字段=表达式}}:写入状态,不输出文本{{command stop}}/{{command delete}}:控制当前消息和后续追加流程,不输出文本
后果:
{{set affection=affection+1}}不会因为反复打开历史而重复加command在测试预览 / 历史渲染 / 调试读取时,不会删除消息,也不会中断流程- 即使规则的
targetChannels包含user / character / system / speech多个通道,它们也都只触发一次 - 设计规则时若同条模板既有
set又有变量读取,应把文本理解为依赖最终状态,不要按“逐步观察中间态”去设计
command 的结果区别:
{{command stop}}:当前消息保留,但停止本轮后续消息追加{{command delete}}:当前消息不入库、不显示- 同时命中
stop + delete:当前消息删除,且本轮后续消息也停止追加
3.7 when 与 if 的区别
if | when | |
|---|---|---|
| 何时显示其内容 | 只看条件是否为真 | 条件为真 且 表达式依赖的字段在当前 whenFields 集合中 |
| 是否消耗状态 | 否 | 命中时会消费这些字段(从 whenFields 移除) |
| 单次写入后的触发次数 | 每次读取都会重新判断 | 字段刚被写入后的下一次渲染触发一次,触发后被消费 |
| 字段被反复写入时 | 每次都按当前值判断 | 每一次写入都会把该字段重新放回 whenFields,下次又会触发 |
whenFields 的入栈:
- 用户手动改变量 → 该字段进入 whenFields
- 群聊 system AI 改变量 → 该字段进入 whenFields
{{set field=...}}真实写入了与旧值不同的新值 → 该字段进入 whenFields(写入相同值时不入栈,详见 §5.13)
重要语义澄清:
{{when affection >= 80}}...{{end}}不是"affection 第一次到 80 时触发一次、之后永不再触发"。它的真实行为是"affection 每次被改写之后,只要条件仍成立就再触发一次"。要做"真·一次性事件"必须用独立标志位,详见 §5.12 与示例 9。
when 适合写"刚发生了状态变化时给一段提示词"。if 适合写"展示当前值"。
when 仅在以下场景生效:
- 人格提示词
- 消息规则的
replace
不支持 when 的位置:
- 群聊提示词(
ChatGroup.prompt) - 群聊 system AI 的额外提示词(
ChatGroup.groupSpeakerSmartPrompt)
在这两处写 {{when}},当前保存时会直接报错;即使历史数据里残留了 {{when}},运行时也不会展开或消费 whenFields。
3.8 私聊 vs 群聊
- 变量字段定义来源:私聊取自人格的
stateFieldDefinitions,群聊取自群聊的stateFieldDefinitions。两边都支持变量模式。 - 消息规则库挂载:私聊读人格挂载的规则库;群聊只读群聊公共挂载的,不叠加发言者人格自己挂载的。
- system AI 改变量:仅群聊(且打开"允许 system AI 改变量"开关)。私聊永远没有
systemStatePatch来源。 sourceType: system:私聊几乎用不到。如果用户在私聊场景给规则,避免把sourceType设为system。- system AI 提示词变量差异:
HasKnowledge/KnowledgeBaseCount/KnowledgeEntryTitles这三个变量名在 system AI 的额外提示词里仍然可写,但当前值固定视为false/0/""。 - system AI 提示词来源:群聊导演 system AI 当前只读取
ChatGroup.groupSpeakerSmartPrompt这份额外提示词,不会拼接ChatGroup.prompt。
3.9 空通道规则
- 三个逻辑通道某一个被替换为空 → 该通道当前不可见。
- 私聊用户消息后若当前角色读到的
character通道为空 → 不会触发角色回复。所以不要轻易在targetChannels: ["character"]上写"全部清空"的规则。 - 即使三个逻辑通道都为空,消息默认也仍会保留;只有显式命中
{{command delete}}才会让消息不入库。
3.10 缓存友好原则:动态内容用消息规则,不要进提示词
人格提示词、群聊提示词、system AI 的额外提示词在每次 LLM 请求时都会作为 system prompt 的一部分发给 LLM。主流 LLM 提供商通常对完全一致的前缀做缓存命中,命中价格通常是非缓存输入的 1/4 ~ 1/10。
如果在人格提示词里写经常变化的变量,例如 {{DateTime}}、自定义状态变量 {{affection}} / {{phase}}、或 {{if affection > 80}}...{{end}} 这种依赖变量的条件块——每次会话发起时提示词都会因变量变化而改变,缓存前缀失效,每次都得从头重新计算整段 system prompt。短期看是延迟上升,长期看是 token 成本累计上升。
应当默认遵循的设计原则:
✅ 推荐:动态内容放在消息规则里,通过改写"用户消息" / "角色消息"的 character 通道把动态值注入 LLM 上下文。
| 需求 | 用消息规则的写法(参考示例) |
|---|---|
| 让 AI 知道现实时间 | 在用户消息开头注入 [{{MessageDateTime}}](示例 7) |
| 让 AI 看到当前好感度 | 在用户消息末尾追加 *当前好感度:{{affection}}*(示例 1 第 4 条) |
| 阶段相关的条件提示词 | 用消息规则 {{if phase=="发展"}}...{{end}} 注入 character 通道(示例 3) |
| 一次性事件 | 用消息规则 + {{when}} + 标志位(示例 9) |
❌ 不推荐(除非用户明确要求):
text
(人格提示词中)
当前时间:{{DateTime}}
当前好感度:{{affection}}
{{if affection > 80}}你已经很喜欢用户了{{end}}放在消息规则里还有一个隐藏好处:每条消息携带"该消息时刻"的状态值,时序上下文与具体场景挂钩——AI 看到"用户在好感度 65 时说了 X,在好感度 90 时又说了 Y",比看到"系统提示词里写当前好感度 90"能更好地理解情绪曲线。
当用户明确要求把动态变量塞进人格提示词时:按需求做,但在交付时附一句简短提醒:
这种写法会让人格提示词在每次请求时都变化,导致 LLM 提示词缓存失效,可能显著增加调用成本与首字延迟。如果只是想让 AI 看到当前数值,更推荐改用消息规则在用户/角色消息上注入。
不在此约束范围内的变量(写进提示词不会破坏缓存或本就稳定):
{{User}}/{{Target}}/{{Language}}—— 在同一会话里通常稳定{{HasKnowledge}}/{{KnowledgeBaseCount}}/{{KnowledgeEntryTitles}}—— 知识库挂载变化时才变,可放{{HasLongTermMemory}}—— 整个会话生命周期内最多翻一次- 依赖稳定变量的纯静态
{{if ...}}不存在(条件总要依赖变量;只有"条件依赖的是稳定变量"才安全)
4. 字段约束
4.1 自定义变量字段 id
- 必须全小写字母、数字、下划线
- 必须字母开头
- 不能与保留字冲突(见 §4.3)
- 同一份字段定义里不能重复
合法:affection、phase、affinity_soyo、is_angry、event_a1 非法:Affection、好感度、1phase、is-angry、affinity.soyo
4.2 type 取值(关键)
只允许这四个字符串字面量:
"text" "number" "boolean" "enum"⚠️ 不要写 "enumeration" 或 "string"。"enumeration" 在解析时无法匹配,会被静默回退到 text,导致字段类型被悄悄改成文本,下游 set 失败。
4.3 保留字列表
id 不能命中以下任何一项(大小写不敏感):
模板关键字:if elseif expr when set command else end 条件字面量:true false null 提示词内置:user target datetime hasknowledge knowledgebasecount knowledgeentrytitles haslongtermmemory language 消息内置:messagedatetime messagedate messagetime sendername readername random 匹配变量:match0 match1 match2 ...(即 match 后跟数字) 表达式函数:randomint floor ceil round
4.4 默认值与归一化
text:会trim(),空字符串视为无defaultValuenumber:必须可解析为数字,且会被min/max截断boolean:接受true/false、数字(0=false,非0=true)、字符串("true"/"false"/"1"/"0")enum:必须精确命中enumOptions之一(区分大小写);不命中则defaultValue被丢弃
4.5 消息规则字段约束
search必须是有效正则(DartRegExp兼容)。空 search → 该条目验证失败但仍能导入。replace编译失败(如{{if ... 没有 {{end}}}}) → 整条规则不参与运行(在调试面板里会以红色"编译失败"显示)。replace中{{set field=...}}:field必须已经在变量字段定义里,否则 trace 显示 errored,本次set整体回滚(文本替换照常)。- 表达式求值失败(如类型不能比较、除以 0、
RandomInt(1.5, 6)、RandomInt(6, 1))也会回滚set,不写messageRuleStatePatch。
5. 雷区(最容易写错的地方)
5.1 JSON 转义
- 正则中的反斜杠在 JSON 中要
\\:\d{4}写成"\\d{4}",\(写成"\\(" - 双引号要写
\" - 一个常见错:
"search": "\d+"← 这是 JSON 解析失败的常见原因,应写"search": "\\d+"
5.2 多行替换
replace 模板里的 \n \r \t \\ 在导入后会被解码为真正的换行/回车/制表符/反斜杠。其它 \x 按字面保留。
例如要让规则插入"换行 + 一段提示词":
json
"replace": "$0\n\nsystem: 当前用户处于愤怒状态"如果希望模板源码里的实际换行只用于排版,不参与最终输出,需要加:
json
"escapeNewlineOnly": true此时:
- 模板里的真实换行会被忽略
- 只有显式
\\n才会生成真实换行
5.3 set 的字段必须先在 fields 里定义
如果消息规则里写了 {{set affection=...}},那么变量字段 JSON 里必须有 id: "affection" 的字段。否则 set 会失败但文本替换照样进行,用户无法察觉。
生成时务必前后呼应:每个 set 引用的字段都列在 fields 里。
5.4 ReaderName 与写入期操作语句(set / command)
下面这些写法当前都允许保存:
text
{{if ReaderName == "白露"}}{{set affection=affection+1}}{{end}}$0
{{if ReaderName == "白露"}}{{command delete}}{{end}}但它们的含义都不是“按不同读取者分别触发不同副作用”。真实原因是:写入期操作语句只执行一次,而 ReaderName 在历史读取时是多视角变量,两者组合后必须固定为一次性判定。
真实语义:
set/command是否触发,只看写入期user通道那一次的ReaderName- 历史展示 / 切换读取者时,不会重新决定这条消息是否改值、被删掉或 stop
- 如果规则不包含
user通道,写入期操作语句都不会执行
所以:
- 如果用户要做"只对某个读取者隐藏",应用普通
if + ReaderName - 如果要做一次性改值 / 删除 / 截断,可以写
ReaderName + set/command,但要清楚它们都只按写入期user通道判定一次
5.5 command stop 与 command delete 的区别
stop:当前消息可保留,但停止后续消息追加delete:当前消息不入库stop + delete:相当于截断本轮输出
5.6 when 必须依赖某个 whenFields 字段
text
{{when affection >= 80}}...{{end}}只有在 affection 刚被改(被 set / manualOverride / systemStatePatch 写过且尚未消费)时才会展开。如果想让 affection >= 80 时每次都注入提示词,应该用 {{if}},不是 {{when}}。
典型错误:把 when 写在游戏开局总是注入的提示词里 → 永远不会触发。
5.7 群聊提示词与 system AI 的额外提示词不支持 when
如果用户的需求要在群聊提示词里做"达成条件触发一次",改用 if + 自定义布尔字段实现:
text
{{if event_a1_done == false && affection >= 80}}...{{set event_a1_done=true}}...{{end}}但注意,这种写法只能放在人格提示词或消息规则中。群聊提示词里不要写 {{set}}。
补充:
- 群聊提示词与 system AI 的额外提示词里,当前保存时会直接拒绝
{{when}}、{{set}}、{{command ...}} - 这两类提示词里也不要写
RandomInt(...),当前保存时同样会直接报错
5.8 Random/RandomInt、取整函数与 expr 的作用域
- 要显示表达式计算的结果必须使用
{{expr ...}}表达式 {{expr ...}}可以在提示词和消息规则里使用,但它只负责输出文本,不会写状态。floor/ceil/round是通用表达式函数:凡是能写表达式的地方(if/when/expr/set)都能用。Random是消息规则专属内置变量。RandomInt(min, max)本质依赖Random,因此只应放在消息规则里;若写进人格提示词、群聊提示词、system AI 的额外提示词,当前保存会直接报错。RandomInt的参数必须是整数,且min <= max。RandomInt(1, 6)是闭区间 1~6;同一条消息里它的结果和Random一样是稳定的,不会因为刷新历史重新抽样。
典型写法:
text
{{expr RandomInt(1, 6)}}
{{expr floor(Random*10)}}
{{set dice=RandomInt(1, 6)}}
{{if RandomInt(1, 100) <= 30}}触发事件 A{{else}}触发事件 B{{end}}5.9 关键字与变量名的歧义
{{if affection > 80}} 中 affection 是变量名;{{if "warming"}} 中 "warming" 是字符串字面量。永远给字符串加引号,否则会被当成变量名(值为 null)。
5.10 targetChannels 的语义边界
- 只想影响显示(不让 LLM 看见):
["user"] - 只想影响LLM 看到的内容(不影响显示):
["character"] - 大多数"提示词注入 + 变量更新"场景:
["character"](注入只给 LLM)或["user","character"](同时显示和发给 LLM) - TTS 处理(去除括号独白、纠正读音):
["speech"]
⚠️ 不要随意加 system。它只对群聊导演 system AI 有效,私聊场景设了等于无效配置。
5.11 同条模板里 set 后再读变量的真实语义
当前消息规则写入期 user 通道采用“两阶段求值”:
- 先执行同条模板里的
set / command - 再基于推进后的最终临时状态渲染文本
因此,同条模板里既写 {{set affection=...}} 又写 {{affection}},当前是允许的;但应把它理解为“读取最终状态”,而不是“逐步观察中间态”。
例如:
text
{{set affection=1}}{{if affection==1}}A{{end}}{{set affection=2}}最终文本按 affection=2 来理解,因此结果为空字符串。
text
{{set affection=3}}{{if affection==3}}{{set affection=2}}A{{affection}}{{end}}{{if affection==2}}B{{affection}}{{end}}最终文本按 affection=2 来理解,因此结果为 B2。
什么时候仍建议拆成两条规则:
- 想让规则结构更直观,避免后来维护者误把它理解成“左到右逐步显示中间态”
- 想把“状态推进”和“展示格式”分离,便于调试与 trace 查看
- 同一变量会在多条规则之间复用,拆开后更容易控制执行顺序
✅ 一条规则就够的场景:
- 你本来就想在同条模板里先推进状态,再按最终值决定文本
- 例如:
{{set affection=2}}B{{affection}}
✅ 拆成两条更清晰的场景:
- AI 输出
【好感+5=55】,你想先解析增量再统一校正显示值 - 这时仍推荐“第一条只负责
set,第二条只负责显示覆盖”的写法
5.12 {{when}} 不是"上升沿触发器"
{{when affection >= 80}}...{{end}} 不会"在 affection 第一次到 80 时触发一次永远不再触发"。
实际语义:
- 任何一次对 affection 的修改(
set/ 手动 / system AI)都会把affection重新放回whenFields。 - 下一次模板渲染时,
when检查到 affection 在whenFields里且条件为真 → 展开内容并把 affection 从whenFields中消费。 - 之后只要 affection 又被任何形式修改,步骤 1~2 重复 ——
when会再次触发。
也就是说:在剧情里 affection 会因为正负事件被反复 set,只要每次最终值仍然 ≥ 80,这段 when 都会反复触发,不是只触发一次。
真·一次性事件必须用独立标志变量(参见示例 9):
加一个布尔字段,例如
affection_event_high,defaultValue: false。用一条规则在条件首次满足时把它从
false翻成true,之后永不再 set:{{if affection >= 80 && affection_event_high == false}}{{set affection_event_high=true}}{{end}}用
{{when affection_event_high == true}}...{{end}}注入一次性提示词。
由于 affection_event_high 一旦变成 true 就再也不会被 set(条件 affection_event_high == false 已不成立),它不会再次进入 whenFields,when 也就只会触发一次。
核心模式:
when真正的"只触发一次"靠的是让依赖字段不再被写入,而不是when本身能去重。
5.13 set 写入"相同值"不会进入 whenFields
{{set affection=affection}} 或 {{set phase="序章"}}(当前 phase 已经是"序章")这类值未变化的赋值:
- 不写入
messageRuleStatePatchruntime event - 不把字段加入
whenFields - 等于无操作
利用这一点:
- 想避免触发某个
when,写一个"重复赋当前值"的set是没用的(不会消耗它) - 反过来,如果你确实想让 when 触发(哪怕值没变),也做不到——必须 set 一个真正不同的值
5.14 规则间的执行顺序与 isGlobal
多条规则按 entries 数组顺序串行执行:第 N 条规则的输入文本 = 第 N-1 条规则改写后的输出文本。这是示例 2(好感度自动校正)能成立的前提。
写规则集时要按依赖关系排顺序:
- 先做正则捕获 +
set的规则 - 再做读取
{{当前变量值}}的规则 - 最后做"格式化输出"或"清理符号"的规则
isGlobal 字段对 set 累加效果的影响:
"global": true(默认):正则的所有命中都执行替换。如果模板里有set,每个命中都会触发一次 set。比如一条消息里出现 3 次【好感度上升】,affection会被 +5 三次。"global": false:只处理第一个命中,set最多触发一次。
如果不希望同一条消息内多次累加(比如 AI 偶尔在一条消息中重复输出标记),可以把 "global" 设为 false,或在正则上加更严格的边界。
6. 完整主题示例
下面每个示例都给出完整可导入的两份 JSON。如果某主题不需要变量字段,则省略变量字段块。
示例 1:好感度系统(基础版)
需求:让 AI 用 【好感度上升】 / 【好感度下降】 标记好感变化,自动累加;每次用户发送消息后,发给 AI 的上下文里附加当前好感度。
变量字段 JSON(导入到人格设置 → 变量字段编辑器):
json
{
"version": 1,
"fields": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"description": "角色对用户的好感度(0~100)",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
}
]
}消息规则库 JSON(导入为消息规则库,挂载到该人格):
json
{
"version": 1,
"name": "好感度系统",
"entries": [
{
"title": "标记上升",
"search": "【好感度上升】",
"replace": "$0{{set affection=affection+5}}",
"sourceType": "character",
"targetChannels": ["user"]
},
{
"title": "标记下降",
"search": "【好感度下降】",
"replace": "$0{{set affection=affection-5}}",
"sourceType": "character",
"targetChannels": ["user"]
},
{
"title": "AI 直接给数值",
"search": "【好感度:(\\d+)】",
"replace": "$0{{set affection=Match1}}",
"sourceType": "character",
"targetChannels": ["user"]
},
{
"title": "用户消息后附加当前好感度",
"search": "$",
"replace": "\n\n*当前好感度:{{affection}}*",
"sourceType": "user",
"targetChannels": ["character"]
}
]
}工作原理:AI 在文本中输出
【好感度上升】之类标记 → 在用户通道保留显示,但set会更新数值。每次用户发送消息时,规则会在角色通道末尾追加当前好感度,让 LLM 始终知道情感状态。
示例 2:好感度系统(AI 自报增量 + 自动校正显示值)
需求:让 AI 用 【好感+5=55】("增量=结果"格式)的语法报告好感变化。增量由 AI 给出,结果数值由规则用真实当前值覆盖——这样即使 AI 算错,UI 上看到的也永远是真实状态。
用法:在人格提示词里要求 AI 输出格式 【好感±数字=数字】,比如:
【输出格式】
当好感度发生变化时,必须在消息末尾输出 `【好感+N=变化后的值】` 或 `【好感-N=变化后的值】`,N 为变化量。复用示例 1 的变量字段 JSON。消息规则必须拆成两条:
json
{
"version": 1,
"name": "好感度系统(自报增量)",
"entries": [
{
"title": "解析增量并更新好感度",
"search": "【好感\\+(\\d+)=(\\d+)】",
"replace": "$0{{set affection=affection+Match1}}",
"sourceType": "character",
"targetChannels": ["user"]
},
{
"title": "解析减量并更新好感度",
"search": "【好感-(\\d+)=(\\d+)】",
"replace": "$0{{set affection=affection-Match1}}",
"sourceType": "character",
"targetChannels": ["user"]
},
{
"title": "把显示数字校正为当前实际值",
"search": "(【好感[+-]\\d+=)\\d+(】)",
"replace": "$1{{affection}}$2",
"sourceType": "character",
"targetChannels": ["user", "character"]
}
]
}为什么这里仍然推荐拆成两步:一条规则专门负责
set,另一条规则专门负责显示校正,结构更清晰,也更方便调试 trace。虽然当前同条模板里set后再读变量已经允许,但把“状态推进”和“展示修正”分开,后续维护成本更低。详见 §5.11。拆成两条规则后,第一条只负责
set不读自己写的变量,第二条只负责显示——无论写入期还是历史期,{{affection}}都读到一致的新值。
示例 3:阶段推进 + 阶段相关随机事件
需求:用 phase 表示故事阶段;进入第 2 阶段后,每次用户发送消息有 30% 概率触发事件 A,否则触发事件 B。
json
{
"version": 1,
"fields": [
{
"id": "phase",
"displayName": "故事阶段",
"type": "enum",
"description": "当前剧情进展",
"enumOptions": ["序章", "发展", "高潮", "尾声"],
"defaultValue": "序章"
}
]
}json
{
"version": 1,
"name": "阶段事件",
"entries": [
{
"title": "AI 切换阶段",
"search": "【进入(序章|发展|高潮|尾声)阶段】",
"replace": "{{set phase=Match1}}",
"sourceType": "character",
"targetChannels": ["user", "character"]
},
{
"title": "发展阶段随机事件",
"search": "$",
"replace": "{{if phase == \"发展\"}}\n\n{{if Random < 0.3}}system: 此刻她突然想起了过去的回忆,请描写她神情的瞬间变化{{else}}system: 此刻一切风平浪静,请按当前自然推进{{end}}{{end}}",
"sourceType": "user",
"targetChannels": ["character"]
}
]
}注意:
phase是enum,所以条件里写phase == "发展"必须和enumOptions中的字符串完全一致(区分大小写)。- 同一条消息中
Random/RandomInt值稳定,所以这里的 30%/70% 分流不会因刷新而抖动。- 如果更想用离散整数概率,也可以把条件改成
{{if RandomInt(1, 100) <= 30}}...{{else}}...{{end}}。
示例 4:骰子检定 + expr 输出整数随机
需求:每次用户发送消息时,在 UI 上显示一个 1~6 的骰点,同时给 AI 注入一个 1~100 的隐藏检定值;并把这次骰点写入状态,供后续规则使用。
变量字段 JSON(导入到人格设置 → 变量字段编辑器):
json
{
"version": 1,
"fields": [
{
"id": "last_roll",
"displayName": "上次骰点",
"type": "number",
"minValue": 1,
"maxValue": 6
}
]
}消息规则库 JSON(导入为消息规则库,挂载到该人格):
json
{
"version": 1,
"name": "骰子检定",
"entries": [
{
"title": "在用户通道显示本次骰点并写入状态",
"search": "$",
"replace": "\n\n*本次骰点:{{expr RandomInt(1, 6)}}*{{set last_roll=RandomInt(1, 6)}}",
"sourceType": "user",
"targetChannels": ["user"]
},
{
"title": "给 AI 注入 1 到 100 的隐藏检定值",
"search": "$",
"replace": "\n\nsystem: 本轮隐藏检定值为 {{expr RandomInt(1, 100)}}。80~100 视为大成功,1~20 视为严重受挫,其余按中间状态自然描写。",
"sourceType": "user",
"targetChannels": ["character"]
}
]
}工作原理:
{{expr ...}}用来把表达式结果直接渲染成文本。RandomInt(1, 6)与RandomInt(1, 100)都是闭区间整数。- 同一条消息里的
Random稳定,所以同一条模板里重复写RandomInt(1, 6)得到的点数会一致;这里显示出来的骰点与last_roll写入值会保持一致。- 这条规则没有在
{{set last_roll=...}}后面再读{{last_roll}},结构上更直观,也更方便排查状态写入与文本渲染。
示例 5:内心独白只对发送者本人可见
需求:群聊里中文括号 (...) 表示内心独白,应该只让说话者本人看到,其他角色看不到(用于猜疑推理类玩法)。
仅消息规则,不需要新变量。
json
{
"version": 1,
"name": "内心独白隔离",
"entries": [
{
"title": "括号内独白",
"search": "([\\s\\S]*?)",
"replace": "{{if SenderName == ReaderName}}$0{{end}}",
"sourceType": "character",
"targetChannels": ["character"]
}
]
}工作原理:发到 LLM 的角色通道时,按当前正在读这条消息的角色
displayName与发送者比较;不是同一人就把整段独白替换为空字符串。用户通道不处理,所以用户仍然能看到所有角色的独白(导演视角)。
示例 6:选项菜单
需求:让角色每次发言末尾给 1~4 个选项,渲染成可点击链接,用户点击后直接发送对应消息。
仅消息规则,不需要新变量。但必须在人格提示词里指示 AI 输出列表项格式:
【选项】
在每次输出最后**必须**额外增加一条选项消息,格式为:
- 选项内容1
- 选项内容2
- 选项内容3json
{
"version": 1,
"name": "选项菜单",
"entries": [
{
"title": "列表项转可点击链接",
"search": "- (.+)",
"replace": "- [$1](rengeguan://chat/send?text=$1)",
"sourceType": "character",
"targetChannels": ["user"]
}
]
}链接
rengeguan://chat/send?text=...是软件内置协议,点击后会直接发送对应文本作为用户消息。
示例 7:现实时间注入与每条消息显示当前好感度
需求:每条用户消息发送给 LLM 时附带现实时间,且在用户通道也显示当前好感度(结合示例 1)。
复用示例 1 的变量字段 JSON。消息规则可以叠加:
json
{
"version": 1,
"name": "现实时间与状态",
"entries": [
{
"title": "在每条消息开头注入时间",
"search": "^",
"replace": "[{{MessageDateTime}}] ",
"sourceType": "user",
"targetChannels": ["character"]
},
{
"title": "用户消息底部显示好感度",
"search": "$",
"replace": "\n\n*当前好感度:{{affection}}*",
"sourceType": "user",
"targetChannels": ["user"]
}
]
}示例 8:中文括号自动转斜体
需求:将 AI 输出的中文括号 (...) 自动转为 Markdown 斜体 *...*,并且让模型也能在下次会话里看到斜体格式(学到这个习惯)。
仅消息规则。
json
{
"version": 1,
"name": "括号转斜体",
"entries": [
{
"title": "中文括号转斜体",
"search": "(?<!\\*)(.*?)(?!\\*)",
"replace": " *$0* ",
"sourceType": "character",
"targetChannels": ["user", "character"]
}
]
}
(?<!\*)与(?!\*)是断言,避免在已经是斜体的内容外围再加一层。两边的空格用于避免和相邻文字粘连导致 Markdown 斜体失效。
示例 9:好感度阈值一次性事件(when 用法)
需求:当 affection >= 80 时,只触发一次特殊剧情提示词,之后即使好感继续超过 80 也不再注入。
json
{
"version": 1,
"fields": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
},
{
"id": "affection_event_high",
"displayName": "已触发高好感事件",
"type": "boolean",
"defaultValue": false
}
]
}json
{
"version": 1,
"name": "好感度阈值事件",
"entries": [
{
"title": "好感达到 80 标记事件",
"search": "$",
"replace": "{{if affection >= 80 && affection_event_high == false}}{{set affection_event_high=true}}{{end}}",
"sourceType": "character",
"targetChannels": ["user"]
},
{
"title": "事件触发时注入提示词(仅一次)",
"search": "$",
"replace": "{{when affection_event_high == true}}\n\nsystem: 此刻她对用户的感情终于翻越某个临界点,请描写一段她突然主动靠近的细节剧情{{end}}",
"sourceType": "user",
"targetChannels": ["character"]
}
]
}工作原理与关键点:
- 第一条规则的
if限制:affection >= 80 && affection_event_high == false。这一句的意义是让 set 永远只发生一次——当affection_event_high已经为true时条件不再成立,set 不会再次执行。- 因为
affection_event_high永远只被 set 为true一次(之后再也不会被写),它只会进入whenFields一次,第二条规则的when也就只会触发一次。- 这就是 §5.12 提到的核心模式:
when真正"只触发一次"靠的是依赖字段不再被写入。如果改写成{{when affection >= 80}}...{{end}}直接依赖affection,affection会被反复 set,when就会反复触发——失去"一次性事件"语义。
示例 10:自定义指令 /save
需求:用户发送 /save 时,转换为一段要求 AI 总结当前会话的提示词。
仅消息规则。
json
{
"version": 1,
"name": "自定义指令",
"entries": [
{
"title": "总结指令",
"search": "^/save([\\s\\S]*)$",
"replace": "$1\n\nsystem: 现在请直接输出当前会话的所有内容的完整详细总结",
"sourceType": "user",
"targetChannels": ["character"]
}
]
}
^/save([\s\S]*)$抓取从/save到消息结尾的所有内容(包括换行);$1保留用户后续可能附加的文本,再在末尾追加 system 指令。
示例 11:语音通道处理
需求:在 TTS 朗读时去除括号独白、去除 Markdown 斜体、纠正几个常见词的读音。
仅消息规则。
json
{
"version": 1,
"name": "语音通道净化",
"entries": [
{
"title": "去除括号独白",
"search": "[((].*?[))]",
"replace": "",
"sourceType": "character",
"targetChannels": ["speech"]
},
{
"title": "去除斜体独白",
"search": "\\*.*?\\*",
"replace": "",
"sourceType": "character",
"targetChannels": ["speech"]
},
{
"title": "纠正 MyGO 读音",
"search": "MyGO",
"replace": "my go",
"sourceType": "character",
"targetChannels": ["speech"],
"caseSensitive": false
},
{
"title": "纠正 Tomorin 读音",
"search": "Tomorin",
"replace": "偷摸零",
"sourceType": "character",
"targetChannels": ["speech"]
}
]
}这些规则的
targetChannels都只有speech,所以不影响 UI 显示和发给 LLM 的上下文。
示例 12:条件截断当前回复
需求:当 AI 输出特殊结束标记 【终止】 时,当前消息与之后的本轮回复都丢弃。
json
{
"version": 1,
"name": "条件截断",
"entries": [
{
"title": "命中终止标记后截断本轮输出",
"search": "^【终止】$",
"replace": "$0{{command stop}}{{command delete}}",
"sourceType": "character",
"targetChannels": ["user"],
"caseSensitive": true,
"global": false
}
]
}这条规则的含义是:当前消息不入库,并且停止本轮后续消息追加。
示例 13:只允许转义换行
需求:想把复杂条件模板写成多行排版,但只在显式 \n 处生成换行。
json
{
"version": 1,
"name": "只允许转义换行",
"entries": [
{
"title": "多行排版模板",
"search": "^状态$",
"replace": "{{if phase == \"序章\"}}\n序章\\n继续推进\n{{else}}\n其它阶段\\n继续推进\n{{end}}",
"sourceType": "user",
"targetChannels": ["character"],
"escapeNewlineOnly": true,
"caseSensitive": true,
"global": false
}
]
}开启后,模板中的真实换行
\n只用于编辑排版;最终只有\\n会生效。
7. 生成自检清单
完成生成后,在交付前对照以下清单逐条检查:
7.1 字段定义层
- [ ] 每个
id都是小写字母 + 数字 + 下划线 + 字母开头 - [ ] 没有 id 命中保留字(特别检查:
exprusertargetrandomlanguagematch0等) - [ ] 同一
fields数组内id不重复 - [ ]
type取值是text/number/boolean/enum之一(不是enumeration) - [ ]
enum类型有enumOptions,defaultValue在选项列表中 - [ ]
number类型如果有minValue/maxValue,defaultValue在范围内 - [ ]
displayName都不为空 - [ ] 没有把
randomint/floor/ceil/round这类函数名拿来当字段id
7.2 消息规则层
- [ ] 每条规则的
search是有效正则;JSON 内反斜杠都写成\\ - [ ]
replace中所有{{if}}{{when}}都有对应{{end}} - [ ]
{{expr}}都写成{{expr 表达式}},没有空表达式 - [ ]
{{set}}和{{command}}不要配{{end}} - [ ]
{{set field=...}}的field都已经在fields中定义 - [ ]
sourceType取值是user/character/system之一 - [ ]
targetChannels元素都是user/character/system/speech - [ ] 字符串字面量都加了引号(如
phase == "序章",不是phase == 序章) - [ ] 中文枚举/字符串和
enumOptions完全一致(区分大小写) - [ ] 若使用
RandomInt(min, max),确认min/max是整数且min <= max
7.3 语义层
- [ ] 用
{{when}}的地方,依赖的字段确实会被set/手动改写而触发,不是常态条件 - [ ] 用
{{if}}的地方是希望"每次读取都重新判断"的场景 - [ ] 多通道规则里的
set不会因为通道而重复(确认理解:set只在user通道执行一次) - [ ] 若使用
{{command ...}},已经确认它只会按写入期user通道执行一次;如果不包含user通道,它不会生效 - [ ] 若使用
{{command ...}}与{{ReaderName}}共存,已经确认这只是一次性的写入期user通道判定,不是“按不同读取者分别删除/截断” - [ ] 若想只在显式
\\n处换行,已设置"escapeNewlineOnly": true - [ ] 用
{{expr}}的地方只是为了输出结果;如果需求是持久化数值,已额外配{{set}} - [ ] 若同条模板同时包含
{{set X=...}}和{{X}},已确认这里的文本语义应按最终状态理解,而不是依赖中间态;若想提高可读性,已评估是否拆成两条规则——见 §5.11 - [ ] 真·一次性事件用了独立标志位:当用户需求是"达到某条件触发一次永不再触发"时,依赖
{{when}}的字段必须是一个只会被 set 一次的标志位(如xxx_event_done),而不是会反复变化的字段如affection——见 §5.12 - [ ] 群聊提示词或 system AI 提示词里没有写
{{when}}/{{set}}/{{command ...}} - [ ] 若使用
Random/RandomInt,位置确实在消息规则里,不是提示词模板(否则当前保存会直接报错) - [ ] 私聊场景不要用
sourceType: system - [ ] 私聊不要把
character通道完全清空(会阻断角色回复) - [ ] 多次出现可能导致累加问题的规则(比如
【好感度上升】),评估是否需要把"global": false防止单条消息内多次累加 - [ ] 没有把动态变量塞进人格提示词:
{{DateTime}}/{{affection}}/ 依赖动态变量的{{if}}应通过消息规则注入到 character 通道,而不是写在人格提示词里——见 §3.10。如果用户明确要求写在提示词里,已附上缓存失效成本的提醒
7.4 交付物
- [ ] 输出至少一份 JSON,每份独立放在 ```json 代码块中
- [ ] 在 JSON 块前用一行中文说明导入位置
- [ ] 在多份 JSON 之间用简短中文说明它们是配套的
- [ ] 用户提到的所有需求都有对应规则覆盖,没有遗漏
附录:术语对照
| UI 术语 | 内部数据库字段 / 代码 |
|---|---|
| 变量字段 | Persona.stateFieldDefinitions / ChatGroup.stateFieldDefinitions |
| 变量模式 | sessions.stateJson + whenFieldsJson + session_runtime_events |
| 消息规则 | RegexEntry(数据库表里仍叫 regex) |
| 消息规则库 | RegexLibrary |
| 群聊提示词 | ChatGroup.prompt |
| system AI 的额外提示词 | ChatGroup.groupSpeakerSmartPrompt |
| 允许 system AI 改变量 | 群聊专属开关(私聊永远关闭) |