提示词模板: 动态发送提示词#
理论篇#
在传统的角色卡编写中, 我们通常会录入角色的固定人设和世界的故事背景. 更用心的作者或许还会进一步设想角色在不同阶段下的反应: 在恋爱后与初识时截然不同的态度, 或是在某个重大事件发生前后世界的变迁.
然而在实际应用中, 要实现这种动态变化相当困难. 因为 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 选择性发送提示词的核心理论.
实操篇#
接下来, 让我们进入实操环节. 我们将继续以 "好感度动态人设" 为例, 亲手实现根据好感度的不同, 发送不同提示词的功能.
在开始之前, 请放轻松. 如果你没有任何编程基础, 可能会觉得下面的符号有些陌生. 这完全正常!
我们的目标是理解其原理. 在学会之后, 你就可以使用 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
可以理解为 "在其他所有情况下". 它总是放在逻辑链的最后, 当前面所有的
if
和else if
条件都不成立时, 它会提供一个最终的、默认的备用方案.
让我们用络络好感度来举一个更生动的例子:
<%_ if (getvar('stat_data.角色.络络.好感度') < 30) { _%>
【络络对你态度平淡,甚至有些冷漠。】
<%_ } else if (getvar('stat_data.角色.络络.好感度') < 60) { _%>
【络络对你抱有好感,但仍保持着一些距离。】
<%_ } else { _%>
【络络现在非常信任你,愿意和你分享她的小秘密。】
<%_ } _%>
这段代码的逻辑非常清晰, 而且是按顺序执行的:
- 首先检查
if
程序会先判断 "络络的好感度" 是否小于 30.
如果成立 (比如好感度是 20), 则发送第一段描述, 然后整个逻辑块结束, 后面的else if
和else
都不会被执行.- 然后检查
else if
如果第一个
if
条件不成立 (比如好感度是 45), 程序会接着判断else if
的条件, 即好感度是否小于 60.
如果成立, 则发送第二段描述, 然后逻辑块结束.- 最后执行
else
如果前面的
if
和else if
条件都不成立 (比如好感度是 80), 程序就会执行else
部分, 发送最后那段默认的描述.
你看, 通过 if
、else if
和 else
的组合, 我们可以像搭建阶梯一样, 构建出层次分明、逻辑严谨的互动反应.
再举一个判断文本是否相等的简单例子:
<%_ if (getvar('stat_data.事件.天气') === '晴天') { _%>
【今天阳光明媚,适合出门散步。】
<%_ } else if (getvar('stat_data.事件.天气') === '雨天') { _%>
【外面下着雨,记得带伞。】
<%_ } else { _%>
【今天天气一般。】
<%_ } _%>
我们用 ===
来判断变量是否 "等于" 某个值 (这里是 '晴天'
或 '雨天'
).
如果天气是 "晴天", AI 会看到第一句话.
否则,如果天气是 "雨天", AI 会看到第二句话.
在其他所有情况下 (比如天气是 "多云" 或 "阴天"), AI 看到的都会是最后那句 "天气一般".
恭喜你! 现在, 你已经掌握了利用 EJS 编写动态提示词的核心逻辑. 通过 if
、else if
和 else
的组合, 你已经可以构建出丰富多变的互动逻辑了.
变量不存在该怎么办#
你也许已经用上了 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();
_%>