提示词模板: 动态发送提示词#
理论篇#
在传统的角色卡编写中, 我们通常会录入角色的固定人设和世界的故事背景. 更用心的作者或许还会进一步设想角色在不同阶段下的反应: 在恋爱后与初识时截然不同的态度, 或是在某个重大事件发生前后世界的变迁.
然而在实际应用中, 要实现这种动态变化相当困难. 因为 AI 在每次交互时都会读取你所提供的全部信息, 但它的注意力分配是随机的, 非常容易混淆不同情境下的设定.
举个例子: 我先是设定了 100 字的现代背景, 之后又根据主角穿越后的情况, 补充了 300 字的古代背景.
我的设想是, 只有在角色触发“穿越”事件后, AI 才应采用古代背景进行描述.
但结果往往是, AI 仅仅因为古代背景的篇幅更长, 就错误地将其作为当前的主要设定, 导致整个故事线陷入混乱, 无法按预期展开.
要解决这个问题, 思路其实非常直接: 让 AI 在特定条件下 "看" 不到那部分暂时无关的设定.
下面是一份典型的 "全蓝灯" 提示词:
【这是一个现代世界,故事发生在中国】 // 背景或世界观
【络络:女、17岁、高中生、喜欢吃炸鸡】 // 人物设定
【络络在与user熟悉之前,会非常拘谨】 // 低好感度时候的表现
【络络与user熟悉之后,会非常话痨】 // 高好感度时候的表现
对于如此简短的描述, AI 或许还能分辨出络络当前应处于何种状态. 但当设定变得复杂, 比如世界观冗长或角色众多时, AI 就往往难以准确处理了.
这时, 我们便可以利用提示词模板插件来编写. 它允许我们用 EJS (Embedded JavaScript) 语法来写提示词, 将提示词的发送与否和当前变量情况进行关联.
当好感度较低时,我们只发送:
【这是一个现代世界,故事发生在中国】
【络络:女、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 辅助编写, 具体见于下面的让 AI 为你编写 EJS 代码
接下来, 让我们进入实操环节. 我们将继续以 "好感度动态人设" 为例, 亲手实现根据好感度的不同, 发送不同提示词的功能.
在开始之前, 请放轻松. 如果你没有任何编程基础, 可能会觉得下面的符号有些陌生. 这完全正常!
我们的目标是理解其原理. 在学会之后, 你就可以使用门之主写卡助手等写卡助手或自己更准确地指挥 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看到的专属描述
<%_ } _%>
当然你也许想扫描更多楼层或者仅扫描用户输入, 则你可以按 matchChatMessages 接口来调整扫描方式. 例如:
<%_ if (matchChatMessages(['络络', '笨蛋'], { start: -4 }) { _%>
这里是当正文最后 4 楼中提到了 "络络" 或 "笨蛋" 时,我们希望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 })
验证发送结果#
你可以通过酒馆助手提供的
来查看结果是否被正确发送.让 AI 为你编写 EJS 代码#
AI 会 Embedded JavaScript 语法, 你可以让它替你编写. 在其中, 你可以用 TavernHelper
来访问酒馆助手的功能如 TavernHelper.getWorldbook
, 或者直接访问提示词模板的功能如 getvar
.
为了方便 AI 编写, 建议你按照实时编写前端界面或脚本配置 Cursor, 将酒馆助手的 @types
文件夹 (配置后你会直接得到) 和提示词模板的 reference_cn.md 发给它.
如果需要检查 EJS 语法是否正确, 你可以下载 webstorm, 它对 .ejs
结尾的提示词模板语法文件有直接的报错检查.
或者, 青空莉在门之主写卡助手中提供了 ✅生成或转换成动态化提示词
用于让 AI 编写 EJS 代码. 你可以直接使用这个写卡助手, 或者复制它的提示词.
用 <%= _%>
填写提示词#
除了用 <%_ 代码 _%>
执行代码逻辑, 提示词模板还支持用 <%= 表达式 _%>
来将表达式的值直接填入提示词. 也就是说, <%= getvar('stat_data.角色.络络.好感度') _%>
和 {{get_message_variable::stat_data.角色.络络.好感度}}
是等价的.
当然 <%= 表达式 _%>
能执行代码因而更为灵活.
例如, 我们可以调整输出格式:
---
<status_description>
<%= YAML.stringify(getvar(stat_data), { blockQuote: 'literal' }) _%>
</status_description>
---
<status_description>
角色:
络络:
好感度: 30
心情: 开心
青空莉:
好感度: 60
心情: 郁闷
世界:
日期: 2025-07-26
时间: 21:00
</status_description>
或者, 假设 stat_data.角色
中存储了其他角色, 我们可以这样列出好感度低于 30 的所有角色: (只是展示可以做到, 具体你可以让 ai 编写)
当前好感度在 30 以下的人物:
<%=
_(Object.entries(getvar(data, 'stat_data.角色')))
.filter(([_key, value]) => value.好感度 < 30)
.map(([key]) => key)
.value();
_%>
为不同开局启用不同世界书#
通过 MVU 我们可以设置变量; 而前面我们提到过, 开局消息中的
_.set(...)
也会生效, 我们可以在不同开局设置不同的变量初始值通过提示词模板
<%_ if (...) _%>
我们可以根据变量值发送不同的提示词
显然, 我们就可以为不同的开局设置不同的变量, 从而启用不同的世界书.
……如果这个设置要是永久有效的呢? 例如, 开局 1 是魔法世界而开局 2 是科技世界, 那么玩家选择开局 1 后, 世界书应该启用世界书中魔法世界相关的条目, 而永远不会启用科技世界相关的条目.
这就意味着我们希望开局消息里设置好变量值后, 变量值再也不发生变化.
让我们回顾一下 AI 为什么能够帮我们更新某个特定的变量: 我们设置了世界.挫折模式
变量, 将变量当前值和更新规则告诉了 AI.
……等等, 如果我们只是设置变量而不告诉 AI 它存在, 那么 AI 岂不是不会更新它了! 我们的变量列表和更新规则都是一条条手动列出的, 要删除一个变量很简单:
角色:
络络:
好感度: 30
心情: 开心
青空莉:
好感度: 60
心情: 郁闷
世界:
日期: 2025-07-26
时间: 21:00
挫折模式: false
---
<status_description>
# 以下内容是当前的状态数值,你可以通过命令进行操作修改,但绝对不要将以下内容直接输出在你的回复中
角色:
络络:
好感度: {{get_message_variable::stat_data.角色.络络.好感度}} # 0-100
心情: {{get_message_variable::stat_data.角色.络络.心情}} # 仅有开心、难过、哭泣、生气四种心情
青空莉:
好感度: {{get_message_variable::stat_data.角色.青空莉.好感度}}
心情: {{get_message_variable::stat_data.角色.青空莉.心情}}
世界:
日期: {{get_message_variable::stat_data.世界.日期}}
时间: {{get_message_variable::stat_data.世界.时间}}
# 这里我们不列出挫折模式!
</status_description>