Appearance
角色扮演聊天设定生成指南(AI Skill)
这份文档面向 AI 使用。如果你是人类,可以把本文链接丢给 AI,让它根据你的需求直接生成一整份“聊天设定 JSON”。默认优先输出并导入完整聊天设定;只有你明确要求局部内容时,才分别输出变量字段 / 消息规则 / 提示词注入列表 JSON。
当用户提出"帮我做一套好感度系统 / 做选项菜单 / 加阶段推进 / 处理括号独白 / 做关键词触发知识注入 / 给群聊角色制造信息差"之类的角色扮演功能需求时,按本文档的格式与约束输出 JSON。
阅读顺序建议:先看 §1 交付物 → §2 顶层结构 → §3 原理速览 → §4 字段约束 → §5 雷区 → §6 主题示例。生成前对照 §7 自检清单。
1. 你要交付什么
默认交付 1 份完整聊天设定 JSON。只有用户明确要求局部内容时,才改为输出某一类列表 JSON。
1.1 先判定需要哪些机制
在写 JSON 之前,先根据需求回答下面 3 个问题:
| 问题 | 如果答案是“是” | 如果答案是“否” |
|---|---|---|
| 是否需要跨轮持久状态(如好感度、阶段、锁位、一次性事件标志) | 输出 stateFieldDefinitions | 省略 stateFieldDefinitions |
| 是否需要解析/改写消息、写变量、改变显示,或给某个通道附加动态文本 | 输出 regexEntries | 省略 regexEntries |
| 是否需要在请求前固定或按条件注入指令 / 知识 / 状态摘要 | 输出 promptInjectionEntries | 省略 promptInjectionEntries |
生成时默认遵循两条原则:
- 先找到满足需求的最小闭环,不要为了显得“完整”而额外补机制。
- 完整聊天设定 JSON 指的是“可直接导入的顶层对象”,不是
stateFieldDefinitions、regexEntries、promptInjectionEntries三者都必须出现。
| 交付物 | 用途 | 用户导入位置 |
|---|---|---|
| 完整聊天设定 JSON | 一次性定义本需求需要的变量字段 / 消息规则 / 提示词注入 | 聊天设定 → JSON 编辑器 |
| 变量字段列表 JSON | 仅定义自定义状态字段(如 affection、phase) | 聊天设定中的变量字段 JSON 编辑器 |
| 消息规则列表 JSON | 仅定义按正则匹配并改写消息内容、写入变量、处理显示的规则 | 聊天设定中的消息规则 JSON 编辑器 |
| 提示词注入列表 JSON | 仅定义请求前注入的指令 / 知识 / 状态摘要 | 聊天设定中的提示词注入 JSON 编辑器 |
输出格式约定:
- 默认只输出 1 个完整聊天设定 JSON,不要习惯性拆成“变量字段 JSON + 消息规则 JSON”两份。
- 完整聊天设定 JSON 只保留本需求需要的顶层字段;没有用到的
stateFieldDefinitions/regexEntries/promptInjectionEntries直接省略。 - 如果同一需求同时涉及“变量字段 + 消息规则 + 提示词注入”,必须合并到同一份聊天设定 JSON 中。
- 只有当用户明确说“只补规则 / 只补注入 / 只补变量字段”时,才输出对应的局部列表 JSON。
- 每份 JSON 单独放一个 ````json` 代码块,前面用一行中文说明导入位置;如果是局部列表 JSON,要额外说明“它需要放回哪一份聊天设定里”。
- 不要把 Dart 模型字段名写进 JSON。导入 JSON 字段名应使用
stateFieldDefinitions、regexEntries、promptInjectionEntries、condition、regexCaseSensitive等真实导入字段名。
2. 顶层结构
2.1 完整聊天设定 JSON
“完整”指可导入的顶层对象,不代表三个可选数组都必须出现,例如:
json
{
"version": 1,
"name": "括号转斜体",
"regexEntries": [
{
"title": "中文括号转斜体",
"search": "(?<!\\*)(.*?)(?!\\*)",
"replace": " *$0* ",
"sourceType": "character",
"targetChannels": ["user", "character"]
}
]
}json
{
"version": 1,
"name": "好感度与选项菜单",
"stateFieldDefinitions": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"visibility": "visible",
"description": "可选,字段说明",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
}
],
"regexEntries": [
{
"title": "解析好感度上升标记",
"search": "【好感度上升】",
"replace": "$0{{set affection=affection+5}}",
"sourceType": "character",
"targetChannels": ["user"]
}
]
}json
{
"version": 1,
"name": "选项",
"promptInjectionEntries": [
{
"title": "要求 AI 输出选项",
"depth": 0,
"conditionType": "none",
"template": "<instruction>在每次输出最后追加 1~4 个以 `- ` 开头的选项。</instruction>"
}
]
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
version | number | 是 | 固定为 1 |
name | string | 是 | 聊天设定名称 |
stateFieldDefinitions | array | 否 | 变量字段定义数组 |
regexEntries | array | 否 | 消息规则数组 |
promptInjectionEntries | array | 否 | 提示词注入数组 |
补充:
stateFieldDefinitions、regexEntries、promptInjectionEntries当前都可以省略;省略时等价于空数组。- 当需求同时涉及“要求 AI 按某格式输出”与“解析 AI 输出并更新状态”时,通常要在同一份聊天设定里同时生成
promptInjectionEntries + regexEntries。
2.2 变量字段列表 JSON
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
version | number | 是 | — | 固定为 1 |
stateFieldDefinitions | array | 是 | — | 字段定义数组 |
stateFieldDefinitions[].id | string | 是 | — | 见 §4.1,全小写、字母开头、不与保留字冲突 |
stateFieldDefinitions[].displayName | string | 是 | — | 用户可见名(可中文) |
stateFieldDefinitions[].type | string | 是 | — | text / number / boolean / enum |
stateFieldDefinitions[].visibility | string | 否 | visible | visible / hidden |
stateFieldDefinitions[].enumOptions | string[] | enum 时建议必填 | — | 枚举选项 |
stateFieldDefinitions[].minValue / maxValue | number | 否 | — | 仅 number 生效 |
stateFieldDefinitions[].defaultValue | any | 否 | — | 类型须与 type 匹配,enum 必须命中 enumOptions,number 在 min/max 之间 |
2.3 消息规则列表 JSON
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
version | number | 是 | — | 固定为 1 |
regexEntries | array | 是 | — | 规则条目数组 |
regexEntries[].title | string | 是 | — | 条目名称 |
regexEntries[].search | string | 是 | — | 正则表达式(Dart RegExp 兼容) |
regexEntries[].replace | string | 是 | — | 替换模板(支持 $N / {{...}} / {{set}} / {{command ...}}) |
regexEntries[].sourceType | string | 否 | character | user / character / system 三选一 |
regexEntries[].targetChannels | string[] | 否 | ["user","character"] | 元素从 user / character / system / speech 选;非法值会导致导入失败 |
regexEntries[].escapeNewlineOnly | bool | 否 | false | 开启后,replace 中的实际换行只用于编辑排版,不参与最终输出;只有显式 \\n 才会生成换行 |
regexEntries[].enabled | bool | 否 | true | 是否启用 |
regexEntries[].caseSensitive | bool | 否 | false | 正则是否区分大小写 |
regexEntries[].global | bool | 否 | true | true 处理所有命中,false 只处理第一个 |
2.4 提示词注入列表 JSON
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
version | number | 是 | — | 固定为 1 |
promptInjectionEntries | array | 是 | — | 提示词注入条目数组 |
promptInjectionEntries[].title | string | 是 | — | 条目名称 |
promptInjectionEntries[].enabled | bool | 否 | true | 是否启用 |
promptInjectionEntries[].depth | number | 否 | 0 | 注入深度 |
promptInjectionEntries[].conditionType | string | 否 | none | none / plainText / regex |
promptInjectionEntries[].sourceTypes | string[] | 条件 | ["user","character","system"] | 仅 conditionType != none 时有意义 |
promptInjectionEntries[].recentRoundCount | number | 条件 | 8 | 仅 conditionType != none 时有意义 |
promptInjectionEntries[].condition | string | 条件 | "" | 普通匹配或正则条件文本 |
promptInjectionEntries[].template | string | 是 | "" | 注入模板 |
promptInjectionEntries[].regexCaseSensitive | bool | 否 | false | 仅 regex 条件时有意义 |
2.5 先判定需要哪些机制,再组装顶层对象
先根据 §1.1 判断当前需求是否真的需要 stateFieldDefinitions、regexEntries、promptInjectionEntries,再把需要的部分组装进同一份顶层 JSON。
- 需要什么就放什么;不需要的部分直接省略。
- 优先满足需求的最小闭环,不要为了“完整”额外补状态、规则或注入。
- 长期稳定且始终存在的角色设定,优先直接写人格提示词,不要硬塞进聊天设定。
| 需求 | 优先机制 |
|---|---|
| 持久状态、阶段推进、锁位、一次性事件标志 | stateFieldDefinitions |
| 解析某条消息、改写显示、写变量、把动态信息附到某条消息上 | regexEntries |
| 每次请求固定深度插入指令 / 知识 / 状态摘要,或按最近几轮话题触发 | promptInjectionEntries |
| 长期稳定且始终存在的角色设定 | 直接写人格提示词 |
补充:
- 如果需求既要“要求 AI 按某格式输出”,又要“解析 AI 输出并更新状态”,通常要同时生成
promptInjectionEntries + regexEntries。 - 如果需求只是在最近几轮命中关键词时追加一段知识或指令,优先用提示词注入,不要硬塞进人格提示词。
3. 原理速览
3.1 三种角色 / 通道概念
每条消息都有三个独立维度:
来源(消息规则
sourceType):消息是谁发的user= 用户身份发的character= AI 角色发的system= 群聊导演 system AI 发的(含用户替 system 发言)
通道(消息规则
targetChannels):消息给谁看时走这条规则user= 显示给用户看的(界面气泡)character= 发送给 AI 角色的(影响 LLM 上下文)system= 发送给群聊 system AI 选人时的speech= TTS 语音生成时
一条规则只在 sourceType 与当前消息来源一致时被触发。触发后会在 targetChannels 列出的通道分别运行替换。
通道选择可直接按下面的规则决定:
- 只想影响用户看到的显示效果:用
user - 只想影响LLM 看到的上下文:用
character - 既想让用户看到,又想让 LLM 在后续上下文中看到:用
user + character - 想影响群聊导演 system AI:加上
system - 想影响语音朗读结果:加上
speech
例如:
- 状态快照、裁定说明、格式约束这类主要写给 LLM 的文本,默认不要放进
user - 纯显示层的格式清理、可点击菜单、用户面板,默认不要因为方便就额外放进
character
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中用;人格提示词、群聊提示词、system AI 的额外提示词、提示词注入模板里都不要写。
表达式支持范围(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变量,因此是否可用取决于当前上下文是否提供Random:- 消息规则:支持,且
Random基于消息 ID 稳定。 - 提示词注入:支持,且
Random是单次请求随机值。 - 人格提示词、群聊提示词、system AI 的额外提示词:不支持,当前保存时会直接报错。
- 消息规则:支持,且
{{expr ...}}能读取的变量,取决于它所在的位置:在提示词里读提示词可用变量,在提示词注入里读提示词注入可用变量,在消息规则里读消息内置变量 / 捕获组 / 自定义状态快照。
3.3 大小写规则(重要)
- 变量名 / 关键字 /
true/false/null大小写不敏感:{{User}}与{{user}}完全等价。 - 字符串字面量比较仍区分大小写:
phase == "warming"不等于phase == "WARMING"。 - 因此条件里写中文枚举(如
phase == "序章")时,必须和defaultValue/enumOptions里写的完全一致。
3.4 变量分类
写模板前,先确定当前写的是哪一种位置:普通提示词、提示词注入,还是消息规则 replace。然后只使用该位置允许的变量与模板能力,不要把一个位置的能力直接搬到另一个位置。
| 普通提示词中可用 | 提示词注入中可用 | 消息规则 replace 中可用 | |
|---|---|---|---|
提示词内置变量 {{User}} {{Target}} {{DateTime}} {{DayOfWeek}} {{Language}} {{HasKnowledge}} {{KnowledgeBaseCount}} {{KnowledgeEntryTitles}} {{HasLongTermMemory}} | 是 | 是 | 否 |
消息内置变量 {{MessageDateTime}} {{MessageDate}} {{MessageTime}} {{MessageDayOfWeek}} {{SenderName}} | 否 | 否 | 是 |
特殊变量 {{ReaderName}} {{Random}} | 仅部分链路支持,见下文 | 是 | 是 |
匹配变量 {{Match0}}、{{Match1}}... 与 $0 $1... | 否 | 仅 regex 条件注入可用 | 是 |
自定义状态字段 {{affection}} 等 | 是 | 是 | 是(取消息时刻快照) |
补充:
- 消息规则里的
Random是 0~1 的稳定伪随机值,基于消息 ID 生成,刷新/重启后值不变。 - 提示词注入里的
Random是单次请求随机值;同一请求内复用,下一次请求会重算。 - 需要整数随机时,可以写
{{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 让同一条消息对不同读者呈现不同结果。例如让"内心独白"只有发送者自己能看见。
补充:在提示词注入链路里,ReaderName 也可用,但此时语义会收窄为“本次要回复的角色 displayName”:
- 私聊 → 当前人格
displayName - 群聊 → 当前发言人格
displayName - 当前提示词注入不接入导演 system AI 选人 prompt
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.13 与示例 11。
when 适合写"刚发生了状态变化时给一段提示词"。if 适合写"展示当前值"。
when 仅在以下场景生效:
- 人格提示词
- 提示词注入模板
- 消息规则的
replace
不支持 when 的位置:
- 群聊提示词(
ChatGroup.prompt) - 群聊 system AI 的额外提示词(
ChatGroup.groupSpeakerSmartPrompt)
在这两处写 {{when}},当前保存时会直接报错;即使历史数据里残留了 {{when}},运行时也不会展开或消费 whenFields。提示词注入里的 when 则是“请求成功后才真正消费”,如果请求失败、取消或中断,不会消耗 whenFields。
3.8 私聊 vs 群聊
- 变量字段定义来源:当前统一来自“挂载聊天设定后的合并结果”。私聊取自人格挂载的
chatSettingIds,群聊取自群聊挂载的chatSettingIds。 - 消息规则挂载:私聊读取人格挂载聊天设定中的
regexEntries;群聊只读取群聊公共挂载聊天设定中的regexEntries,不叠加发言者人格自己挂载的。 - 提示词注入挂载:私聊读取人格挂载聊天设定中的
promptInjectionEntries;群聊只读取群聊公共挂载聊天设定中的promptInjectionEntries。 - 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}}、{{DayOfWeek}}、自定义状态变量 {{affection}} / {{phase}}、或 {{if affection > 80}}...{{end}} 这种依赖变量的条件块——每次会话发起时提示词都会因变量变化而改变,缓存前缀失效,每次都得从头重新计算整段 system prompt。短期看是延迟上升,长期看是 token 成本累计上升。
应当默认遵循的设计原则:
✅ 推荐 1:动态变化内容优先放在消息规则里,通过改写"用户消息" / "角色消息"的 character 通道把动态值注入 LLM 上下文。
✅ 推荐 2:如果需求是“按最近几轮话题命中关键词后,在固定深度补一段知识 / 指令”,应该用提示词注入。
| 需求 | 用消息规则的写法(参考示例) |
|---|---|
| 让 AI 知道现实时间 | 在用户消息开头注入 [{{MessageDateTime}}](示例 7) |
| 让 AI 看到当前好感度 | 在用户消息末尾追加 *当前好感度:{{affection}}*(示例 1 第 4 条) |
| 阶段相关的条件提示词 | 用消息规则 {{if phase=="发展"}}...{{end}} 注入 character 通道(示例 3) |
| 一次性事件 | 用消息规则 + {{when}} + 标志位(示例 11) |
| 需求 | 用提示词注入的写法(参考示例) |
|---|---|
| 要求 AI 在每次输出最后附加选项 | 在 depth: 0 注入 <instruction>...</instruction>(示例 6) |
| 最近几轮提到特定关键词时补充知识 | 用 conditionType: "plainText" 或 "regex" 的提示词注入(示例 7) |
| 群聊里针对不同角色注入不同信息差 | 用 {{if ReaderName == "..."}}...{{end}} 的提示词注入(示例 8) |
❌ 不推荐(除非用户明确要求):
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",这是无效的 type 值
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 visibility 的使用建议
visible:会显示在会话设置等用户界面中,适合用户需要观察或手动调整的状态,如affection、phase。hidden:不在用户界面显式展示,适合内部锁位、一次性事件标志、技术状态位,如affection_event_high、event_a1_done。- 做“一次性事件”时,独立标志字段通常应优先设为
hidden。
4.6 消息规则字段约束
search必须是有效正则(DartRegExp兼容)。空search或非法正则在 JSON 导入时会直接报错。replace编译失败(如{{if ... 没有 {{end}}}})在 JSON 导入时会直接报错。replace中{{set field=...}}:field必须已经在当前聊天设定的stateFieldDefinitions里定义,否则运行时会报错,本次set整体回滚(文本替换照常)。- 表达式求值失败(如类型不能比较、除以 0、
RandomInt(1.5, 6)、RandomInt(6, 1))也会回滚set,不写messageRuleStatePatch。
4.7 提示词注入字段约束
conditionType只允许none/plainText/regex。conditionType != none时:sourceTypes至少要有一个来源。recentRoundCount必须大于0。condition不能为空。
conditionType == plainText时,condition使用普通匹配语法,不是简单字面量匹配。conditionType == regex时,condition必须是合法正则;若模板用到{{Match1}}等捕获变量,条件类型也必须是regex。template必须是合法提示词模板,且不要写{{set}}/{{command}}/{{SenderName}}/{{MessageDateTime}}等消息规则专属能力。
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 的字段必须先在 stateFieldDefinitions 里定义
如果消息规则里写了 {{set affection=...}},那么变量字段 JSON 里必须有 id: "affection" 的字段。否则 set 会失败但文本替换照样进行,用户无法察觉。
生成时务必前后呼应:每个 set 引用的字段都列在 stateFieldDefinitions 里。
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 提示词注入不是消息规则
- 提示词注入的模板里可以写
{{if}}/{{when}}/{{ReaderName}}/{{Random}}/RandomInt(...)。 - 但提示词注入不能写
{{set}}/{{command}}/{{SenderName}}/{{MessageDateTime}}/{{MessageDate}}/{{MessageTime}}。 - 提示词注入里若需要根据正则捕获组拼装文本,必须把
conditionType设为regex,并使用condition,不要误写成消息规则的search/replace结构。
5.9 Random/RandomInt、取整函数与 expr 的作用域
- 要显示表达式计算的结果必须使用
{{expr ...}}表达式 {{expr ...}}可以在提示词和消息规则里使用,但它只负责输出文本,不会写状态。floor/ceil/round是通用表达式函数:凡是能写表达式的地方(if/when/expr/set)都能用。Random在消息规则与提示词注入中都可用,但语义不同:- 消息规则:基于消息 ID 稳定。
- 提示词注入:基于当前请求随机。
RandomInt(min, max)本质依赖Random,因此可放在消息规则和提示词注入里;若写进人格提示词、群聊提示词、system AI 的额外提示词,当前保存会直接报错。RandomInt的参数必须是整数,且min <= max。RandomInt(1, 6)是闭区间 1~6。放在消息规则里时结果稳定;放在提示词注入里时会随请求重新抽样。
典型写法:
text
{{expr RandomInt(1, 6)}}
{{expr floor(Random*10)}}
{{set dice=RandomInt(1, 6)}}
{{if RandomInt(1, 100) <= 30}}触发事件 A{{else}}触发事件 B{{end}}5.10 关键字与变量名的歧义
{{if affection > 80}} 中 affection 是变量名;{{if "warming"}} 中 "warming" 是字符串字面量。永远给字符串加引号,否则会被当成变量名(值为 null)。
5.11 targetChannels 的语义边界
- 只想影响显示(不让 LLM 看见):
["user"] - 只想影响LLM 看到的内容(不影响显示):
["character"] - 大多数"规则改写 + 变量更新"场景:
["character"](只给 LLM)或["user","character"](同时显示和发给 LLM) - TTS 处理(去除括号独白、纠正读音):
["speech"]
5.12 同条模板里 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.13 {{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 都会反复触发,不是只触发一次。
真·一次性事件必须用独立标志变量(参见示例 11):
加一个布尔字段,例如
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.14 set 写入"相同值"不会进入 whenFields
{{set affection=affection}} 或 {{set phase="序章"}}(当前 phase 已经是"序章")这类值未变化的赋值:
- 不写入
messageRuleStatePatchruntime event - 不把字段加入
whenFields - 等于无操作
利用这一点:
- 想避免触发某个
when,写一个"重复赋当前值"的set是没用的(不会消耗它) - 反过来,如果你确实想让 when 触发(哪怕值没变),也做不到——必须 set 一个真正不同的值
5.15 规则间的执行顺序与 isGlobal
多条规则按 regexEntries 数组顺序串行执行:第 N 条规则的输入文本 = 第 N-1 条规则改写后的输出文本。这是示例 2(好感度自动校正)能成立的前提。
写规则集时要按依赖关系排顺序:
- 先做正则捕获 +
set的规则 - 再做读取
{{当前变量值}}的规则 - 最后做"格式化输出"或"清理符号"的规则
isGlobal 字段对 set 累加效果的影响:
"global": true(默认):正则的所有命中都执行替换。如果模板里有set,每个命中都会触发一次 set。比如一条消息里出现 3 次【好感度上升】,affection会被 +5 三次。"global": false:只处理第一个命中,set最多触发一次。
如果不希望同一条消息内多次累加(比如 AI 偶尔在一条消息中重复输出标记),可以把 "global" 设为 false,或在正则上加更严格的边界。
6. 完整主题示例
下面每个示例默认都给出完整可导入的聊天设定 JSON。只有在确实适合“局部补丁”的场景,才单独给某一类列表 JSON。
示例 1:好感度系统(基础版)
需求:让 AI 用 【好感度上升】 / 【好感度下降】 标记好感变化,自动累加;每次用户发送消息后,发给 AI 的上下文里附加当前好感度。
json
{
"version": 1,
"name": "好感度系统",
"stateFieldDefinitions": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"visibility": "visible",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
}
],
"regexEntries": [
{
"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"]
}
],
"promptInjectionEntries": []
}工作原理:AI 在文本中输出
【好感度上升】之类标记 → 在用户通道保留显示,但set会更新数值。每次用户发送消息时,规则会在角色通道末尾追加当前好感度,让 LLM 始终知道情感状态。
示例 2:好感度系统(AI 自报增量 + 自动校正显示值)
需求:让 AI 用 【好感+5=55】("增量=结果"格式)的语法报告好感变化。增量由 AI 给出,结果数值由规则用真实当前值覆盖——这样即使 AI 算错,UI 上看到的也永远是真实状态。
json
{
"version": 1,
"name": "好感度系统(自报增量)",
"stateFieldDefinitions": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"visibility": "visible",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
}
],
"regexEntries": [
{
"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"]
}
],
"promptInjectionEntries": [
{
"title": "要求 AI 自报增量格式",
"depth": 0,
"conditionType": "none",
"template": "<instruction>当好感度发生变化时,必须在消息末尾输出 `【好感+N=变化后的值】` 或 `【好感-N=变化后的值】`,N 为变化量。</instruction>"
}
]
}为什么这里仍然推荐拆成两步:一条规则专门负责
set,另一条规则专门负责显示校正,结构更清晰,也更方便调试 trace。虽然当前同条模板里set后再读变量已经允许,但把“状态推进”和“展示修正”分开,后续维护成本更低。详见 §5.12。拆成两条规则后,第一条只负责
set不读自己写的变量,第二条只负责显示——无论写入期还是历史期,{{affection}}都读到一致的新值。
示例 3:阶段推进 + 阶段相关随机事件
需求:用 phase 表示故事阶段;进入第 2 阶段后,每次用户发送消息有 30% 概率触发事件 A,否则触发事件 B。
json
{
"version": 1,
"name": "阶段事件",
"stateFieldDefinitions": [
{
"id": "phase",
"displayName": "故事阶段",
"type": "enum",
"visibility": "visible",
"enumOptions": ["序章", "发展", "高潮", "尾声"],
"defaultValue": "序章"
}
],
"regexEntries": [
{
"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"]
}
],
"promptInjectionEntries": []
}注意:
phase是enum,所以条件里写phase == "发展"必须和enumOptions中的字符串完全一致(区分大小写)。- 同一条消息中
Random/RandomInt值稳定,所以这里的 30%/70% 分流不会因刷新而抖动。- 如果更想用离散整数概率,也可以把条件改成
{{if RandomInt(1, 100) <= 30}}...{{else}}...{{end}}。
示例 4:骰子检定 + expr 输出整数随机
需求:每次用户发送消息时,在 UI 上显示一个 1~6 的骰点,同时给 AI 注入一个 1~100 的隐藏检定值;并把这次骰点写入状态,供后续规则使用。
json
{
"version": 1,
"name": "骰子检定",
"stateFieldDefinitions": [
{
"id": "last_roll",
"displayName": "上次骰点",
"type": "number",
"visibility": "visible",
"minValue": 1,
"maxValue": 6
}
],
"regexEntries": [
{
"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"]
}
],
"promptInjectionEntries": []
}工作原理:
{{expr ...}}用来把表达式结果直接渲染成文本。RandomInt(1, 6)与RandomInt(1, 100)都是闭区间整数。- 同一条消息里的
Random稳定,所以同一条模板里重复写RandomInt(1, 6)得到的点数会一致;这里显示出来的骰点与last_roll写入值会保持一致。- 这条规则没有在
{{set last_roll=...}}后面再读{{last_roll}},结构上更直观,也更方便排查状态写入与文本渲染。
示例 5:内心独白只对发送者本人可见
需求:群聊里中文括号 (...) 表示内心独白,应该只让说话者本人看到,其他角色看不到(用于猜疑推理类玩法)。
仅消息规则,不需要新变量。
json
{
"version": 1,
"name": "内心独白隔离",
"regexEntries": [
{
"title": "括号内独白",
"search": "([\\s\\S]*?)",
"replace": "{{if SenderName == ReaderName}}$0{{end}}",
"sourceType": "character",
"targetChannels": ["character"]
}
]
}工作原理:发到 LLM 的角色通道时,按当前正在读这条消息的角色
displayName与发送者比较;不是同一人就把整段独白替换为空字符串。用户通道不处理,所以用户仍然能看到所有角色的独白(导演视角)。
示例 6:选项菜单
需求:让角色每次发言末尾给 1~4 个选项,渲染成可点击链接,用户点击后直接发送对应消息。
json
{
"version": 1,
"name": "选项菜单",
"regexEntries": [
{
"title": "列表项转可点击链接",
"search": "- (.+)",
"replace": "- [$1](rengeguan://chat/send?text=$1)",
"sourceType": "character",
"targetChannels": ["user"]
}
],
"promptInjectionEntries": [
{
"title": "要求 AI 输出选项列表",
"depth": 0,
"conditionType": "none",
"template": "<instruction>在每次输出最后必须额外增加 1~4 个选项,格式为:\n- 选项内容1\n- 选项内容2\n- 选项内容3\n选项部分中,用“我”代指 {{User}},用“你”代指当前角色。</instruction>"
}
]
}链接
rengeguan://chat/send?text=...是软件内置协议,点击后会直接发送对应文本作为用户消息。
示例 7:关键词触发知识注入
需求:最近几轮一旦提到“吃醋 / 嫉妒 / 占有欲”,就在请求尾部补一段行为指导,让角色优先表现细腻情绪,而不是立刻升级冲突。
json
{
"version": 1,
"name": "关系竞争知识注入",
"regexEntries": [],
"promptInjectionEntries": [
{
"title": "普通匹配:吃醋话题",
"enabled": true,
"depth": 0,
"conditionType": "plainText",
"sourceTypes": ["user", "character"],
"recentRoundCount": 6,
"condition": "吃醋 | 嫉妒 | 占有欲",
"template": "<world_context>当话题涉及关系竞争时,优先表现细腻的情绪波动,而不是立刻升级冲突。</world_context>"
}
]
}这类“看最近几轮话题再补一段知识 / 指令”的需求,优先用提示词注入,不要硬塞进人格提示词。
示例 8:群聊按 ReaderName 注入信息差
需求:群聊里针对不同角色注入不同世界信息,让每个角色知道的东西不一样。
json
{
"version": 1,
"name": "群聊信息差注入",
"regexEntries": [],
"promptInjectionEntries": [
{
"title": "按当前发言角色补充知识",
"depth": 0,
"conditionType": "none",
"template": "{{if ReaderName == \"千早爱音\"}}<world_context>注入内容A</world_context>{{elseif ReaderName == \"长崎素世\"}}<world_context>注入内容B</world_context>{{end}}"
}
]
}提示词注入里的
ReaderName表示“本次要回复的角色displayName”。私聊是当前人格,群聊是当前发言人格。
示例 9:现实时间注入与每条消息显示当前好感度
需求:每条用户消息发送给 LLM 时附带现实时间,且在用户通道也显示当前好感度(结合示例 1)。
json
{
"version": 1,
"name": "现实时间与状态",
"stateFieldDefinitions": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"visibility": "visible",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
}
],
"regexEntries": [
{
"title": "在每条消息开头注入时间",
"search": "^",
"replace": "[{{MessageDateTime}}] ",
"sourceType": "user",
"targetChannels": ["character"]
},
{
"title": "用户消息底部显示好感度",
"search": "$",
"replace": "\n\n*当前好感度:{{affection}}*",
"sourceType": "user",
"targetChannels": ["user"]
}
],
"promptInjectionEntries": []
}示例 10:中文括号自动转斜体
需求:将 AI 输出的中文括号 (...) 自动转为 Markdown 斜体 *...*,并且让模型也能在下次会话里看到斜体格式(学到这个习惯)。
仅消息规则。
json
{
"version": 1,
"name": "括号转斜体",
"regexEntries": [
{
"title": "中文括号转斜体",
"search": "(?<!\\*)(.*?)(?!\\*)",
"replace": " *$0* ",
"sourceType": "character",
"targetChannels": ["user", "character"]
}
]
}
(?<!\*)与(?!\*)是断言,避免在已经是斜体的内容外围再加一层。两边的空格用于避免和相邻文字粘连导致 Markdown 斜体失效。
示例 11:好感度阈值一次性事件(when 用法)
需求:当 affection >= 80 时,只触发一次特殊剧情提示词,之后即使好感继续超过 80 也不再注入。
json
{
"version": 1,
"name": "好感度阈值事件",
"stateFieldDefinitions": [
{
"id": "affection",
"displayName": "好感度",
"type": "number",
"visibility": "visible",
"minValue": 0,
"maxValue": 100,
"defaultValue": 50
},
{
"id": "affection_event_high",
"displayName": "已触发高好感事件",
"type": "boolean",
"visibility": "hidden",
"defaultValue": false
}
],
"regexEntries": [
{
"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"]
}
],
"promptInjectionEntries": []
}工作原理与关键点:
- 第一条规则的
if限制:affection >= 80 && affection_event_high == false。这一句的意义是让 set 永远只发生一次——当affection_event_high已经为true时条件不再成立,set 不会再次执行。- 因为
affection_event_high永远只被 set 为true一次(之后再也不会被写),它只会进入whenFields一次,第二条规则的when也就只会触发一次。- 这就是 §5.13 提到的核心模式:
when真正"只触发一次"靠的是依赖字段不再被写入。如果改写成{{when affection >= 80}}...{{end}}直接依赖affection,affection会被反复 set,when就会反复触发——失去"一次性事件"语义。
示例 12:自定义指令 /save
需求:用户发送 /save 时,转换为一段要求 AI 总结当前会话的提示词。
仅消息规则。
json
{
"version": 1,
"name": "自定义指令",
"regexEntries": [
{
"title": "总结指令",
"search": "^/save([\\s\\S]*)$",
"replace": "$1\n\nsystem: 现在请直接输出当前会话的所有内容的完整详细总结",
"sourceType": "user",
"targetChannels": ["character"]
}
]
}
^/save([\s\S]*)$抓取从/save到消息结尾的所有内容(包括换行);$1保留用户后续可能附加的文本,再在末尾追加 system 指令。
示例 13:语音通道处理
需求:在 TTS 朗读时去除括号独白、去除 Markdown 斜体、纠正几个常见词的读音。
仅消息规则。
json
{
"version": 1,
"name": "语音通道净化",
"regexEntries": [
{
"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 的上下文。
示例 14:条件截断当前回复
需求:当 AI 输出特殊结束标记 【终止】 时,当前消息与之后的本轮回复都丢弃。
json
{
"version": 1,
"name": "条件截断",
"regexEntries": [
{
"title": "命中终止标记后截断本轮输出",
"search": "^【终止】$",
"replace": "$0{{command stop}}{{command delete}}",
"sourceType": "character",
"targetChannels": ["user"],
"caseSensitive": true,
"global": false
}
]
}这条规则的含义是:当前消息不入库,并且停止本轮后续消息追加。
示例 15:只允许转义换行
需求:想把复杂条件模板写成多行排版,但只在显式 \n 处生成换行。
json
{
"version": 1,
"name": "只允许转义换行",
"stateFieldDefinitions": [
{
"id": "phase",
"displayName": "故事阶段",
"type": "enum",
"visibility": "visible",
"enumOptions": ["序章", "发展", "高潮", "尾声"],
"defaultValue": "序章"
}
],
"regexEntries": [
{
"title": "多行排版模板",
"search": "^状态$",
"replace": "{{if phase == \"序章\"}}\n序章\\n继续推进\n{{else}}\n其它阶段\\n继续推进\n{{end}}",
"sourceType": "user",
"targetChannels": ["character"],
"escapeNewlineOnly": true,
"caseSensitive": true,
"global": false
}
],
"promptInjectionEntries": []
}开启后,模板中的真实换行
\n只用于编辑排版;最终只有\\n会生效。
7. 生成自检清单
完成生成后,在交付前对照以下清单逐条检查:
7.1 字段定义层
- [ ] 每个
id都是小写字母 + 数字 + 下划线 + 字母开头 - [ ] 没有 id 命中保留字(特别检查:
exprusertargetrandomlanguagematch0等) - [ ] 同一
stateFieldDefinitions数组内id不重复 - [ ]
type取值是text/number/boolean/enum之一(不是enumeration) - [ ]
enum类型有enumOptions,defaultValue在选项列表中 - [ ]
number类型如果有minValue/maxValue,defaultValue在范围内 - [ ]
displayName都不为空 - [ ] 内部锁位、一次性事件标志等字段已评估是否更适合
"visibility": "hidden" - [ ] 没有把
randomint/floor/ceil/round这类函数名拿来当字段id
7.2 消息规则层
- [ ] 每条规则的
search是有效正则;JSON 内反斜杠都写成\\ - [ ]
replace中所有{{if}}{{when}}都有对应{{end}} - [ ]
{{expr}}都写成{{expr 表达式}},没有空表达式 - [ ]
{{set}}和{{command}}不要配{{end}} - [ ]
{{set field=...}}的field都已经在stateFieldDefinitions中定义 - [ ]
sourceType取值是user/character/system之一 - [ ]
targetChannels元素都是user/character/system/speech - [ ] 字符串字面量都加了引号(如
phase == "序章",不是phase == 序章) - [ ] 中文枚举/字符串和
enumOptions完全一致(区分大小写) - [ ] 若使用
RandomInt(min, max),确认min/max是整数且min <= max
7.3 提示词注入层
- [ ] 需要按最近几轮话题触发知识 / 指令时,优先用了
promptInjectionEntries,而不是硬塞进人格提示词 - [ ]
conditionType != none时,已同时补全sourceTypes、recentRoundCount、condition - [ ] 若使用
{{Match1}}等捕获变量,conditionType已设为regex - [ ] 提示词注入模板里没有写
{{set}}/{{command}}/{{SenderName}}/{{MessageDateTime}} - [ ] 若使用
Random/RandomInt,已确认自己写的是“请求级随机”而非“消息级稳定随机”
7.4 语义层
- [ ] 用
{{when}}的地方,依赖的字段确实会被set/手动改写而触发,不是常态条件 - [ ] 用
{{if}}的地方是希望"每次读取都重新判断"的场景 - [ ] 多通道规则里的
set不会因为通道而重复(确认理解:set只在user通道执行一次) - [ ] 若使用
{{command ...}},已经确认它只会按写入期user通道执行一次;如果不包含user通道,它不会生效 - [ ] 若使用
{{command ...}}与{{ReaderName}}共存,已经确认这只是一次性的写入期user通道判定,不是“按不同读取者分别删除/截断” - [ ] 若想只在显式
\\n处换行,已设置"escapeNewlineOnly": true - [ ] 用
{{expr}}的地方只是为了输出结果;如果需求是持久化数值,已额外配{{set}} - [ ] 若同条模板同时包含
{{set X=...}}和{{X}},已确认这里的文本语义应按最终状态理解,而不是依赖中间态;若想提高可读性,已评估是否拆成两条规则——见 §5.12 - [ ] 真·一次性事件用了独立标志位:当用户需求是"达到某条件触发一次永不再触发"时,依赖
{{when}}的字段必须是一个只会被 set 一次的标志位(如xxx_event_done),而不是会反复变化的字段如affection——见 §5.13 - [ ] 群聊提示词或 system AI 提示词里没有写
{{when}}/{{set}}/{{command ...}} - [ ] 若使用
Random/RandomInt,已确认自己写的是消息规则、提示词注入,还是普通提示词;不要混淆三者语义 - [ ] 私聊场景不要用
sourceType: system - [ ] 私聊不要把
character通道完全清空(会阻断角色回复) - [ ] 多次出现可能导致累加问题的规则(比如
【好感度上升】),评估是否需要把"global": false防止单条消息内多次累加 - [ ] 没有把动态变量塞进人格提示词:
{{DateTime}}/{{affection}}/ 依赖动态变量的{{if}}应优先通过消息规则或提示词注入处理,而不是写在人格提示词里——见 §3.10。如果用户明确要求写在提示词里,已附上缓存失效成本的提醒
7.5 交付物
- [ ] 输出至少一份 JSON,每份独立放在 ```json 代码块中
- [ ] 在 JSON 块前用一行中文说明导入位置
- [ ] 如果输出的是局部列表 JSON,已说明它需要放回哪一份聊天设定中
- [ ] 用户提到的所有需求都有对应规则覆盖,没有遗漏
附录:术语对照
| UI 术语 | 内部数据库字段 / 代码 |
|---|---|
| 聊天设定 | ChatSetting |
| 变量字段 | ChatSetting.stateFieldDefinitions |
| 变量模式 | sessions.stateJson + whenFieldsJson + session_runtime_events |
| 消息规则 | ChatSetting.regexEntries / RegexEntry(数据库表里仍叫 regex) |
| 提示词注入 | ChatSetting.promptInjectionEntries / PromptInjectionEntry |
| 私聊聊天设定来源 | 人格挂载的 chatSettingIds |
| 群聊聊天设定来源 | 群聊挂载的 chatSettingIds |
| 群聊提示词 | ChatGroup.prompt |
| system AI 的额外提示词 | ChatGroup.groupSpeakerSmartPrompt |
| 允许 system AI 改变量 | 群聊专属开关(私聊永远关闭) |