Skip to content

角色扮演聊天设定生成指南(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 指的是“可直接导入的顶层对象”,不是 stateFieldDefinitionsregexEntriespromptInjectionEntries 三者都必须出现。
交付物用途用户导入位置
完整聊天设定 JSON一次性定义本需求需要的变量字段 / 消息规则 / 提示词注入聊天设定 → JSON 编辑器
变量字段列表 JSON仅定义自定义状态字段(如 affectionphase聊天设定中的变量字段 JSON 编辑器
消息规则列表 JSON仅定义按正则匹配并改写消息内容、写入变量、处理显示的规则聊天设定中的消息规则 JSON 编辑器
提示词注入列表 JSON仅定义请求前注入的指令 / 知识 / 状态摘要聊天设定中的提示词注入 JSON 编辑器

输出格式约定:

  • 默认只输出 1 个完整聊天设定 JSON,不要习惯性拆成“变量字段 JSON + 消息规则 JSON”两份。
  • 完整聊天设定 JSON 只保留本需求需要的顶层字段;没有用到的 stateFieldDefinitions / regexEntries / promptInjectionEntries 直接省略。
  • 如果同一需求同时涉及“变量字段 + 消息规则 + 提示词注入”,必须合并到同一份聊天设定 JSON 中。
  • 只有当用户明确说“只补规则 / 只补注入 / 只补变量字段”时,才输出对应的局部列表 JSON。
  • 每份 JSON 单独放一个 ````json` 代码块,前面用一行中文说明导入位置;如果是局部列表 JSON,要额外说明“它需要放回哪一份聊天设定里”。
  • 不要把 Dart 模型字段名写进 JSON。导入 JSON 字段名应使用 stateFieldDefinitionsregexEntriespromptInjectionEntriesconditionregexCaseSensitive 等真实导入字段名。

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>"
    }
  ]
}
字段类型必填说明
versionnumber固定为 1
namestring聊天设定名称
stateFieldDefinitionsarray变量字段定义数组
regexEntriesarray消息规则数组
promptInjectionEntriesarray提示词注入数组

补充:

  • stateFieldDefinitionsregexEntriespromptInjectionEntries 当前都可以省略;省略时等价于空数组。
  • 当需求同时涉及“要求 AI 按某格式输出”与“解析 AI 输出并更新状态”时,通常要在同一份聊天设定里同时生成 promptInjectionEntries + regexEntries

2.2 变量字段列表 JSON

字段类型必填默认值说明
versionnumber固定为 1
stateFieldDefinitionsarray字段定义数组
stateFieldDefinitions[].idstring见 §4.1,全小写、字母开头、不与保留字冲突
stateFieldDefinitions[].displayNamestring用户可见名(可中文)
stateFieldDefinitions[].typestringtext / number / boolean / enum
stateFieldDefinitions[].visibilitystringvisiblevisible / hidden
stateFieldDefinitions[].enumOptionsstring[]enum 时建议必填枚举选项
stateFieldDefinitions[].minValue / maxValuenumbernumber 生效
stateFieldDefinitions[].defaultValueany类型须与 type 匹配,enum 必须命中 enumOptionsnumbermin/max 之间

2.3 消息规则列表 JSON

字段类型必填默认值说明
versionnumber固定为 1
regexEntriesarray规则条目数组
regexEntries[].titlestring条目名称
regexEntries[].searchstring正则表达式(Dart RegExp 兼容)
regexEntries[].replacestring替换模板(支持 $N / {{...}} / {{set}} / {{command ...}}
regexEntries[].sourceTypestringcharacteruser / character / system 三选一
regexEntries[].targetChannelsstring[]["user","character"]元素从 user / character / system / speech 选;非法值会导致导入失败
regexEntries[].escapeNewlineOnlyboolfalse开启后,replace 中的实际换行只用于编辑排版,不参与最终输出;只有显式 \\n 才会生成换行
regexEntries[].enabledbooltrue是否启用
regexEntries[].caseSensitiveboolfalse正则是否区分大小写
regexEntries[].globalbooltruetrue 处理所有命中,false 只处理第一个

2.4 提示词注入列表 JSON

字段类型必填默认值说明
versionnumber固定为 1
promptInjectionEntriesarray提示词注入条目数组
promptInjectionEntries[].titlestring条目名称
promptInjectionEntries[].enabledbooltrue是否启用
promptInjectionEntries[].depthnumber0注入深度
promptInjectionEntries[].conditionTypestringnonenone / plainText / regex
promptInjectionEntries[].sourceTypesstring[]条件["user","character","system"]conditionType != none 时有意义
promptInjectionEntries[].recentRoundCountnumber条件8conditionType != none 时有意义
promptInjectionEntries[].conditionstring条件""普通匹配或正则条件文本
promptInjectionEntries[].templatestring""注入模板
promptInjectionEntries[].regexCaseSensitiveboolfalseregex 条件时有意义

2.5 先判定需要哪些机制,再组装顶层对象

先根据 §1.1 判断当前需求是否真的需要 stateFieldDefinitionsregexEntriespromptInjectionEntries,再把需要的部分组装进同一份顶层 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 通道 → 当前正在读这条消息的角色 displayName
  • system 通道 → 字符串 "system"
  • speech 通道 → 字符串 "Speech"
  • 长期记忆总结读取 character 通道时 → 字符串 "longTermMemory"(驼峰)

ReaderName 让同一条消息对不同读者呈现不同结果。例如让"内心独白"只有发送者自己能看见。

补充:在提示词注入链路里,ReaderName 也可用,但此时语义会收窄为“本次要回复的角色 displayName”:

  • 私聊 → 当前人格 displayName
  • 群聊 → 当前发言人格 displayName
  • 当前提示词注入不接入导演 system AI 选人 prompt

3.6 写入期操作语句(set / command

setcommand 都属于写入期操作语句,核心共性是:

  • 都只在消息写入时执行一次
  • 都固定基于 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 whenif 的区别

ifwhen
何时显示其内容只看条件是否为真条件为真 表达式依赖的字段在当前 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)
  • 同一份字段定义里不能重复

合法:affectionphaseaffinity_soyois_angryevent_a1 非法:Affection好感度1phaseis-angryaffinity.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(),空字符串视为无 defaultValue
  • number:必须可解析为数字,且会被 min/max 截断
  • boolean:接受 true/false、数字(0=false,非0=true)、字符串("true"/"false"/"1"/"0"
  • enum:必须精确命中 enumOptions 之一(区分大小写);不命中则 defaultValue 被丢弃

4.5 visibility 的使用建议

  • visible:会显示在会话设置等用户界面中,适合用户需要观察或手动调整的状态,如 affectionphase
  • hidden:不在用户界面显式展示,适合内部锁位、一次性事件标志、技术状态位,如 affection_event_highevent_a1_done
  • 做“一次性事件”时,独立标志字段通常应优先设为 hidden

4.6 消息规则字段约束

  • search 必须是有效正则(Dart RegExp 兼容)。空 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 stopcommand 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 通道采用“两阶段求值”:

  1. 先执行同条模板里的 set / command
  2. 再基于推进后的最终临时状态渲染文本

因此,同条模板里既写 {{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 时触发一次永远不再触发"。

实际语义:

  1. 任何一次对 affection 的修改(set / 手动 / system AI)都会把 affection 重新放回 whenFields
  2. 下一次模板渲染时,when 检查到 affection 在 whenFields 里且条件为真 → 展开内容并把 affection 从 whenFields 中消费。
  3. 之后只要 affection 又被任何形式修改,步骤 1~2 重复 —— when 会再次触发。

也就是说:在剧情里 affection 会因为正负事件被反复 set,只要每次最终值仍然 ≥ 80,这段 when 都会反复触发,不是只触发一次。

真·一次性事件必须用独立标志变量(参见示例 11):

  1. 加一个布尔字段,例如 affection_event_highdefaultValue: false

  2. 用一条规则在条件首次满足时把它从 false 翻成 true之后永不再 set

    {{if affection >= 80 && affection_event_high == false}}{{set affection_event_high=true}}{{end}}
  3. {{when affection_event_high == true}}...{{end}} 注入一次性提示词。

由于 affection_event_high 一旦变成 true 就再也不会被 set(条件 affection_event_high == false 已不成立),它不会再次进入 whenFieldswhen 也就只会触发一次。

核心模式when 真正的"只触发一次"靠的是让依赖字段不再被写入,而不是 when 本身能去重。

5.14 set 写入"相同值"不会进入 whenFields

{{set affection=affection}}{{set phase="序章"}}(当前 phase 已经是"序章")这类值未变化的赋值:

  • 写入 messageRuleStatePatch runtime 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": []
}

注意:

  • phaseenum,所以条件里写 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}} 直接依赖 affectionaffection 会被反复 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 命中保留字(特别检查:expr user target random language match0 等)
  • [ ] 同一 stateFieldDefinitions 数组内 id 不重复
  • [ ] type 取值是 text / number / boolean / enum 之一(不是 enumeration
  • [ ] enum 类型有 enumOptionsdefaultValue 在选项列表中
  • [ ] number 类型如果有 minValue/maxValuedefaultValue 在范围内
  • [ ] 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 时,已同时补全 sourceTypesrecentRoundCountcondition
  • [ ] 若使用 {{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 改变量群聊专属开关(私聊永远关闭)