提示词模板: 动态发送提示词#

理论篇#

在传统的角色卡编写中, 我们通常会录入角色的固定人设和世界的故事背景. 更用心的作者或许还会进一步设想角色在不同阶段下的反应: 在恋爱后与初识时截然不同的态度, 或是在某个重大事件发生前后世界的变迁.

然而在实际应用中, 要实现这种动态变化相当困难. 因为 AI 在每次交互时都会读取你所提供的全部信息, 但它的注意力分配是随机的, 非常容易混淆不同情境下的设定.

举个例子: 我先是设定了 100 字的现代背景, 之后又根据主角穿越后的情况, 补充了 300 字的古代背景.
我的设想是, 只有在角色触发“穿越”事件后, AI 才应采用古代背景进行描述.
但结果往往是, AI 仅仅因为古代背景的篇幅更长, 就错误地将其作为当前的主要设定, 导致整个故事线陷入混乱, 无法按预期展开.

要解决这个问题, 思路其实非常直接: 让 AI 在特定条件下 "看" 不到那部分暂时无关的设定.

下面是一份典型的 "全蓝灯" 提示词:

【这是一个现代世界,故事发生在中国】   // 背景或世界观
【络络:女、17岁、高中生、喜欢吃炸鸡】 // 人物设定
【络络在与user熟悉之前,会非常拘谨】   // 低好感度时候的表现
【络络与user熟悉之后,会非常话痨】     // 高好感度时候的表现

对于如此简短的描述, AI 或许还能分辨出络络当前应处于何种状态. 但当设定变得复杂, 比如世界观冗长或角色众多时, AI 就往往难以准确处理了.

这时, 我们便可以利用提示词模板插件所提供的 EJS 语法, 将提示词的发送与否和当前变量情况进行关联.

好感度较低时,我们只发送:

【这是一个现代世界,故事发生在中国】
【络络:女、17岁、高中生、喜欢吃炸鸡】
【络络现在的好感度是{{get_message_variable::络络.好感度}},因此她应当表现得非常拘谨】 // 低好感度时候的表现

随着好感度的不断升高, 发送的提示词则变为:

【这是一个现代世界,故事发生在中国】
【络络:女、17岁、高中生、喜欢吃炸鸡】
【络络现在的好感度是{{get_message_variable::络络.好感度}},因此她应当表现得像个话痨】 // 高好感度时候的表现

这样一来, AI 就不会再混淆络络在两种状态下的表现了.

当然, 上面的例子只是为了说明原理. 在实际创作中, 为了避免让玩家察觉到角色性格的突兀转变, 我们可能需要追求更平滑的过渡.

因此, 一个更优的设计可以是这样:

【这是一个现代世界,故事发生在中国】
【络络:女、17岁、高中生、喜欢吃炸鸡】

当好感度为0~40时发送:
【络络现在的好感度是{{get_message_variable::络络.好感度}}。在此阶段,她应当表现得非常拘谨。随着好感度的提升,她可能会慢慢变得愿意与人交谈。】

当好感度为40~80时发送:
【络络现在的好感度是{{get_message_variable::络络.好感度}}。在此阶段,她表现得相对平和,已经能与{{user}}进行简单的交流。随着好感度的提升,她甚至可能会主动和user开玩笑。】

当好感度为80~100时发送:
【络络现在的好感度是{{get_message_variable::络络.好感度}}。在此阶段,她对于陌生人可能依旧拘谨,但对于熟人,尤其是user,一定会表现得非常话痨。】

像这样分阶段、渐进式的提示词, 其效果显然比“一刀切”的状态切换要自然得多.

不过一切提示词都取决于你的实际需求. 如果你想要实现的是世界的突变而非角色的动态成长, 例如一觉醒来世界从现代变为古代, 那么你的提示词自然也无需进行这样平滑的过渡.

理解了以上内容, 你便掌握了利用 EJS 选择性发送提示词的核心理论.

实操篇#

提示

建议使用成熟的代码软件并用 AI 辅助编写 (我在类脑的直播教程/青空莉的文档), 你也可以下载 webstorm, 它对 .ejs 结尾的提示词模板语法文件有直接的报错检查.

接下来, 让我们进入实操环节. 我们将继续以 "好感度动态人设" 为例, 亲手实现根据好感度的不同, 发送不同提示词的功能.

在开始之前, 请放轻松. 如果你没有任何编程基础, 可能会觉得下面的符号有些陌生. 这完全正常!

我们的目标是理解其原理. 在学会之后, 你就可以使用 Nova Creator 等写卡预设或自己更准确地指挥 AI 进行编写代码, 并且能够自己看看他出错在什么地方.

区分代码与文本的核心语法: <%_ _%>#

首先我们要明白, 提示词模板扩展的 EJS 语法是一种能将 "代码指令" 嵌入到 "普通文本" 中的技术. 为了让系统知道哪部分是给它下达指令的代码, 哪部分是需要发送给 AI 的故事情节, 我们需要一个特殊的标记. 这个标记就是 <%_ _%>.

你可以把它想象成一对 "特殊的括号". 所有被这对括号包裹起来的内容, 都会被系统理解为一条需要执行的代码指令; 而括号外面的所有内容, 则被视为普通的提示词文本, 和正常世界书编写内容无异.

if 设置条件#

在 EJS 中, 我们最常使用的指令就是 if.

其基本结构是:

if (设定的条件) {
  这里是条件成立时才会被发送的提示词
}

这行代码的意思是: 如果 (if) 括号里的 "条件" 成立了, 那么花括号 {} 里的提示词就会被发送.

现在, 我们将这个结构用 EJS 的 "特殊括号" 包裹起来. 请注意, if (...) { 是指令的开始部分, 而 } 是指令的结束部分, 它们需要被分别包裹:

<%_ if (设定的条件) { _%>
这里是条件成立时才会被发送的提示词
<%_ } _%>

你看, 通过换行, 整个结构变得清晰易读. 现在, 你再回头看之前 MVU 卡中的代码, 是不是感觉变得能看懂些了?

getvar() 获取变量#

但在实际代码中, if 后的括号里并不是 "某个条件" 这几个字, 而是一长串代码. 我们就是通过编写这长串代码, 将提示词与变量情况相关联, 从而判断提示词是否该被发送: 比如角色好感度大于 30.

getvar() 函数就是我们获取变量数据的信使. 它能准确地从我们之前设置的变量文件夹 (stat_data) 中, 取出我们需要的那个变量值. 令人惊讶地是 (并非), 用它获取变量值的方法和 {{get_message_variable::变量}} 差不多!

还记得我们之前是怎么获取络络好感度的吗? {{get_message_variable::stat_data.角色.络络.好感度}}. 与之相应地, 我们用 getvar() 获取络络好感度的方法是 getvar('stat_data.角色.络络.好感度').

组装一个完整的EJS代码块#

现在, 我们将所有部件组装起来, 看看一个完整、正确的 EJS 代码块是什么样的:

<%_ if (getvar('stat_data.角色.络络.好感度') < 30) { _%>
这里是当络络的好感度小于30时我们希望AI看到的专属描述
<%_ } _%>

matchChatMessages() 模拟绿灯#

除了 getvar(), 你也可以在 if 中使用 matchChatMessages() 来像绿灯那样, 只在正文最后一次用户输入和最后一次 AI 回复中提到了某个关键字时发送一段提示词. 例如:

<%_ if (matchChatMessages(['络络', '笨蛋'])) { _%>
这里是当正文最后一次用户输入和最后一次 AI 回复中提到了 "络络"  "笨蛋" 我们希望AI看到的专属描述
<%_ } _%>

else 表示条件不成立时发送提示词#

你已经学会了 if, 它可以处理 "条件成立时发送提示词". 但如果我们的逻辑不止 "条件成立时发送提示词", 还有 "条件不成立时发送提示词" 的情况呢? 让我们用 else (否则) 来处理:

if (设定的条件) {
  这里是条件成立时才会被发送的提示词
} else {
  这里是条件不成立时才会被发送的提示词
}

这样一来, 我们可以对络络低好感度和高好感度的情况分别发送提示词:

<%_ if (getvar('stat_data.角色.络络.好感度') < 30) { _%>
这里是当络络的好感度小于30时我们希望AI看到的专属描述
<%_ } else { _%>
这里是当络络的好感度大于等于30时我们希望AI看到的专属描述
<%_ } _%>

else if 构建多层逻辑#

为了让好感度变化更加平滑, 我们可以增加更多条件判断, 让不同区间下的好感度 (低好感、中好感、高好感) 对应有完全不同的提示词.

if (设定的条件1) {
  这里是条件1成立时才会被发送的提示词
} else if (设定的条件2) {
  这里是上面的条件1不成立而条件2成立时才会被发送的提示词
} else {
  这里是所有条件都不成立时才会被发送的提示词
}
else if

可以理解为 "否则,如果……". 它在前一个 if 条件不成立时, 提供一个新的判断条件. 你可以添加任意多个 else if 来构建更复杂的逻辑链.

else

可以理解为 "在其他所有情况下". 它总是放在逻辑链的最后, 当前面所有的 ifelse if 条件都不成立时, 它会提供一个最终的、默认的备用方案.

让我们用络络好感度来举一个更生动的例子:

<%_ if (getvar('stat_data.角色.络络.好感度') < 30) { _%>
络络对你态度平淡甚至有些冷漠。】
<%_ } else if (getvar('stat_data.角色.络络.好感度') < 60) { _%>
络络对你抱有好感但仍保持着一些距离。】
<%_ } else { _%>
络络现在非常信任你愿意和你分享她的小秘密。】
<%_ } _%>

这段代码的逻辑非常清晰, 而且是按顺序执行的:

首先检查 if

程序会先判断 "络络的好感度" 是否小于 30.
如果成立 (比如好感度是 20), 则发送第一段描述, 然后整个逻辑块结束, 后面的 else ifelse 都不会被执行.

然后检查 else if

如果第一个 if 条件不成立 (比如好感度是 45), 程序会接着判断 else if 的条件, 即好感度是否小于 60.
如果成立, 则发送第二段描述, 然后逻辑块结束.

最后执行 else

如果前面的 ifelse if 条件都不成立 (比如好感度是 80), 程序就会执行 else 部分, 发送最后那段默认的描述.

你看, 通过 ifelse ifelse 的组合, 我们可以像搭建阶梯一样, 构建出层次分明、逻辑严谨的互动反应.

再举一个判断文本是否相等的简单例子:

<%_ if (getvar('stat_data.事件.天气') === '晴天') { _%>
今天阳光明媚适合出门散步。】
<%_ } else if (getvar('stat_data.事件.天气') === '雨天') { _%>
外面下着雨记得带伞。】
<%_ } else { _%>
今天天气一般。】
<%_ } _%>

我们用 === 来判断变量是否 "等于" 某个值 (这里是 '晴天''雨天').

  • 如果天气是 "晴天", AI 会看到第一句话.

  • 否则,如果天气是 "雨天", AI 会看到第二句话.

  • 在其他所有情况下 (比如天气是 "多云" 或 "阴天"), AI 看到的都会是最后那句 "天气一般".

恭喜你! 现在, 你已经掌握了利用 EJS 编写动态提示词的核心逻辑. 通过 ifelse ifelse 的组合, 你已经可以构建出丰富多变的互动逻辑了.

变量不存在该怎么办#

你也许已经用上了 MVU beta, 可以在游玩中途新插入变量. 那么你会遇到一个问题: 该如何处理变量不存在的情况? 你可以:

判断变量是否存在

getvar('stat_data.角色.络络.好感度') !== undefined

变量存在则使用值,不存在则使用默认值

getvar('stat_data.角色.络络.好感度', { defaults: 0 })

验证发送结果#

你可以通过酒馆助手提供的输入框左下角的魔棒 ‣ 提示词查看器来查看结果是否被正确发送.

<%= _%> 填写提示词#

除了用 <%_ 代码 _%> 执行代码逻辑, 提示词模板还支持用 <%= 表达式 _%> 来将表达式的值直接填入提示词. 也就是说, <%= getvar('stat_data.角色.络络.好感度') _%>{{get_message_variable::stat_data.角色.络络.好感度}} 是等价的.

当然 <%= 表达式 _%> 能执行代码因而更为灵活. 假设 stat_data.角色 中存储了其他角色, 我们可以这样列出好感度低于 30 的所有角色: (只是展示可以做到, 具体你可以让 ai 编写)

当前好感度在 30 以下的人物:
<%=
  _(Object.entries(getvar(data, 'stat_data.角色')))
    .filter(([_key, value]) => value.好感度 < 30)
    .map(([key]) => key)
    .value();
_%>