世界书:设定按关键词进场
网页版 ChatGPT 的设定只能写进 system prompt:几万字的世界观塞不下,塞进去了也稀释注意力,聊天里提没提到都占着位置。酒馆的世界书(World Info,也叫 Lorebook)把设定拆成条目,每个条目带一组关键词;每轮生成前扫一遍最近的聊天,命中了才把这一条注入。下面的「效果」都是直接调 world-info.js 的 checkWorldInfo 跑出来的,判定过程取自酒馆自己打的 [WI] 调试日志。
所用的书是随包的 Eldoria 译成中文的「埃尔多利亚」,四个条目:埃尔多利亚、影牙、林间空地、力量,绑在塞拉菲娜的卡上。出厂设置:扫描深度 2、预算 25%、递归开、整词匹配开。
只扫最近几条,关键词命中才进场
问题:设定全塞进 system prompt,上下文贵,注意力被稀释,大世界根本塞不下。
功能:每轮把最近 N 条消息(出厂 2 条)连成一段扫描文本,每条消息前加一个 \x01 分隔符,方便写正则锚定说话人:
const MATCHER = '\x01';
const JOINER = '\n' + MATCHER;
let result = MATCHER + this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);
条目的任一主关键词出现在扫描文本里,就进入候选。关键词先展开宏,所以可以写 {{char}}:
// Cache the text to scan before the loop, it won't change its content
const textToScan = buffer.get(entry, scanState);
// PRIMARY KEYWORDS
let primaryKeyMatch = entry.key.find(key => {
const substituted = substituteParams(key);
return substituted && buffer.matchKeys(textToScan, substituted.trim(), entry);
});
if (!primaryKeyMatch) {
// Don't write logs for simple no-matches
continue;
}
const hasSecondaryKeywords = (
entry.selective && //all entries are selective now
Array.isArray(entry.keysecondary) && //always true
entry.keysecondary.length //ignore empties
);
if (!hasSecondaryKeywords) {
// Handle cases where secondary is empty
log('activated by primary key match', primaryKeyMatch);
activatedNow.add(entry);
continue;
}
常驻条目(社区叫「蓝灯」)不看关键词,直接进候选,但之后的概率、分组、预算照样要过:
if (entry.constant) {
log('activated because of constant');
activatedNow.add(entry);
continue;
}
效果:扮演主线第一轮,扫描文本是开场白加用户这句(消息前带名字,world_info_include_names 出厂开着):
[0] 旅人: *我撑起身子,接过茶杯* 谢谢你……这片森林里到底有什么怪物?
[1] 塞拉菲娜: *你猛地惊醒,想起自己是怎么走进森林深处、又是怎么被野兽围攻的。随着眼睛适应了屋里柔和的光,那些记忆渐渐淡去。* “啊,你终于醒了。我好担心,发现你的时候你浑身是血,昏……
checkWorldInfo 自己的判定日志:
[WI] --- START WI SCAN (on 2 messages, trigger = normal) (DRY RUN) ---
[WI] Context size: 3795; WI budget: 949 (max% = 25%, cap = 0)
[WI] --- LOOP #1 START ---
[WI] Entry 0 activated by primary key match 森林
[WI] Entry 1 activated by primary key match 野兽
[WI] Entry 3 activated by primary key match 魔法
[WI] Entry 0 activation successful, adding to prompt
[WI] Entry 1 activation successful, adding to prompt
[WI] Entry 3 activation successful, adding to prompt
[WI] --- LOOP #1 RESULT ---
[WI] Successfully activated 3 new entries to prompt. 3 total entries activated.
[WI] Found 3 new entries for recursion
[WI] --- LOOP #2 START ---
[WI] Entry 2 activated by primary key match 林间空地
[WI] Entry 2 activation successful, adding to prompt
[WI] --- LOOP #2 RESULT ---
[WI] Successfully activated 1 new entries to prompt. 4 total entries activated.
[WI] Found 1 new entries for recursion
[WI] --- LOOP #3 START ---
[WI] --- LOOP #3 RESULT ---
[WI] No new entries activated.
[WI] Scan done. No new entries to prompt. Stopping.
[WI] Hypothetically adding 4 entries to prompt
[WI] --- DONE (DRY RUN) ---
第一轮循环里,开场白的「森林」「野兽」「魔法」各命中一条。这三条的内容进了这一轮;第四条「林间空地」是第二轮循环找到的,见下面递归。
次关键词消歧
问题:同一个词在不同话题里指不同的东西,只按一个词触发,就会在错的话题里塞进错的设定。
功能:条目可以再挂一组次关键词,和主关键词按四种逻辑组合:次关键词任一命中、全都命中、全都没命中、不全命中:
if (selectiveLogic === world_info_logic.AND_ANY && hasSecondaryMatch) {
log('activated. (AND ANY) Found match secondary keyword', secondarySubstituted);
return true;
}
if (selectiveLogic === world_info_logic.NOT_ALL && !hasSecondaryMatch) {
log('activated. (NOT ALL) Found not matching secondary keyword', secondarySubstituted);
return true;
}
}
// Handle NOT ANY logic
if (selectiveLogic === world_info_logic.NOT_ANY && !hasAnyMatch) {
log('activated. (NOT ANY) No secondary keywords found', entry.keysecondary);
return true;
}
// Handle AND ALL logic
if (selectiveLogic === world_info_logic.AND_ALL && hasAllMatch) {
log('activated. (AND ALL) All secondary keywords found', entry.keysecondary);
return true;
}
效果:构造两个主关键词都是「苹果」的条目,一个要求次关键词「手机 / 发布会」任一命中,一个要求都不命中:
条目「苹果公司」:主关键词 ['苹果'],次关键词 ['手机', '发布会'],AND_ANY(次关键词任一命中)
条目「苹果树」:主关键词 ['苹果'],次关键词 ['手机', '发布会'],NOT_ANY(次关键词全都没命中)
「新款苹果手机什么时候发布?」→ 激活:苹果公司
「院子里的苹果熟了,摘几个吧。」→ 激活:苹果树
递归:条目引出条目
问题:设定之间互相引用:聊天里提到了埃尔多利亚,它的条目里又说到林间空地,可聊天里没出现「林间空地」,模型就不知道那是什么。
功能:一轮循环激活的条目,内容拼起来加进扫描文本,再扫一轮,直到没有新条目:
// After processing and rolling entries is done, see if we should continue with normal recursion
if (world_info_recursive && !token_budget_overflowed && successfulNewEntriesForRecursion.length) {
nextScanState = scan_state.RECURSION;
logNextState('[WI] Found', successfulNewEntriesForRecursion.length, 'new entries for recursion');
}
// Final check if we should really continue scan, and extend the current WI recurse buffer
const curScanState = scanState;
scanState = nextScanState;
if (scanState) {
const text = successfulNewEntriesForRecursion
.map(x => x.content).join('\n');
if (text) {
buffer.addRecurse(text);
allActivatedText = (text + '\n' + allActivatedText);
}
条目上有三个开关管递归:preventRecursion(内容不再触发别的),excludeRecursion(不能被递归触发),delayUntilRecursion(只能被递归触发,第一轮扫描一律压住):
if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion && !isSticky) {
log('suppressed by delay until recursion');
continue;
}
if (scanState === scan_state.RECURSION && entry.delayUntilRecursion && entry.delayUntilRecursion > currentRecursionDelayLevel && !isSticky) {
log('suppressed by delay until recursion level', entry.delayUntilRecursion, '. Currently', currentRecursionDelayLevel);
continue;
}
效果:真实输入下,关掉递归少一条:
构造一条链:「银溪谷」的内容提到荆棘魔,「荆棘魔」的内容提到影牙,「影牙的来历」设了 delayUntilRecursion:
== 默认 → 激活:银溪谷、荆棘魔、影牙的来历
[WI] Entry 0 activated by primary key match 银溪谷
[WI] Entry 2 suppressed by delay until recursion
[WI] Entry 0 activation successful, adding to prompt
[WI] --- LOOP #1 RESULT ---
[WI] Successfully activated 1 new entries to prompt. 1 total entries activated.
[WI] Found 1 new entries for recursion
[WI] Entry 1 activated by primary key match 荆棘魔
[WI] Entry 1 activation successful, adding to prompt
[WI] --- LOOP #2 RESULT ---
[WI] Successfully activated 1 new entries to prompt. 2 total entries activated.
[WI] Found 1 new entries for recursion
[WI] Entry 2 activated by primary key match 影牙
[WI] Entry 2 activation successful, adding to prompt
[WI] --- LOOP #3 RESULT ---
[WI] Successfully activated 1 new entries to prompt. 3 total entries activated.
[WI] Found 1 new entries for recursion
[WI] --- LOOP #4 RESULT ---
[WI] No new entries activated.
[WI] Scan done. No new entries to prompt. Stopping.
== 银溪谷设 preventRecursion → 激活:银溪谷
[WI] Entry 0 activated by primary key match 银溪谷
[WI] Entry 2 suppressed by delay until recursion
[WI] Entry 0 activation successful, adding to prompt
[WI] --- LOOP #1 RESULT ---
[WI] Successfully activated 1 new entries to prompt. 1 total entries activated.
[WI] Scan done. No new entries to prompt. Stopping.
== 直接提到影牙 → 激活:(无)
[WI] Entry 2 suppressed by delay until recursion
[WI] --- LOOP #1 RESULT ---
[WI] No new entries activated.
[WI] Scan done. No new entries to prompt. Stopping.
用户直接问「影牙到底是什么」,这条反而不进场:它只给「被别的设定提到时」补细节,作者能分层揭示,先给概要,细节等别的条目带出来。
预算:按上下文比例封顶,溢出就停
问题:世界书一扩张,就把聊天历史挤掉了。
功能:每轮的世界书预算 = 上下文上限 × 比例(出厂 25%),可以再设一个绝对上限:
let budget = Math.round(world_info_budget * maxContext / 100) || 1;
if (world_info_budget_cap > 0 && budget > world_info_budget_cap) {
console.debug(`[WI] Budget ${budget} exceeds cap ${world_info_budget_cap}, using cap`);
budget = world_info_budget_cap;
}
候选按优先级逐条累加,第一次超预算就记下溢出,之后本轮不再收(标了 ignoreBudget 的除外),递归也停:
// Substitute macros inline, for both this checking and also future processing
entry.content = substituteParams(entry.content);
newContent += `${entry.content}\n`;
if (!entry.ignoreBudget && (textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) {
if (!token_budget_overflowed) {
console.debug('[WI] --- BUDGET OVERFLOW CHECK ---');
if (world_info_overflow_alert) {
console.warn(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
} else {
console.debug(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
}
token_budget_overflowed = true;
}
continue;
}
allActivatedEntries.set(`${entry.world}.${entry.uid}`, entry);
console.debug(`[WI] Entry ${entry.uid} activation successful, adding to prompt`, entry);
效果:第一轮的真实聊天,用户把世界书预算从 25% 调到 10%:
== 世界书预算 25% → 激活:埃尔多利亚、影牙、力量、林间空地
[WI] Context size: 3795; WI budget: 949 (max% = 25%, cap = 0)
[WI] Entry 0 activation successful, adding to prompt
[WI] Entry 1 activation successful, adding to prompt
[WI] Entry 3 activation successful, adding to prompt
[WI] Successfully activated 3 new entries to prompt. 3 total entries activated.
[WI] Entry 2 activation successful, adding to prompt
[WI] Successfully activated 1 new entries to prompt. 4 total entries activated.
== 世界书预算 10% → 激活:埃尔多利亚
[WI] Context size: 3795; WI budget: 380 (max% = 10%, cap = 0)
[WI] Entry 0 activation successful, adding to prompt
[WI] budget of 380 reached, stopping after 1 entries
[WI] Successfully activated 3 new entries to prompt. 1 total entries activated.
这不是背包算法:第一条放不下,后面体积更小、本来放得下的条目也一起不要。优先级先看 sticky,再看来源(聊天绑的书、人设绑的书排在角色书和全局书前面),最后看条目的 order。
互斥组和概率
问题:输入一样,拼出来的 prompt 就一样,随机事件做不出来;几个互斥的变体(晴、雨、雾)同时命中,只该要一个。
功能:同组的条目只留一条:有勾了优先的取 order 最高的,否则按权重抽签。条目还可以带触发概率:
// Do weighted random using entry's weight
const totalWeight = group.reduce((acc, item) => acc + (item.groupWeight ?? DEFAULT_WEIGHT), 0);
const rollValue = Math.random() * totalWeight;
let currentWeight = 0;
let winner = null;
for (const entry of group) {
currentWeight += (entry.groupWeight ?? DEFAULT_WEIGHT);
if (rollValue <= currentWeight) {
console.debug(`[WI] Entry ${entry.uid}`, `activated as roll winner from inclusion group '${key}'`, entry);
winner = entry;
break;
}
}
const rollValue = Math.random() * 100;
if (rollValue <= entry.probability) {
console.debug(`WI entry ${entry.uid} passed probability check of ${entry.probability}%`);
return true;
}
failedProbabilityChecks.add(entry);
return false;
效果:构造三个天气条目放进同一组,权重 60 / 30 / 10,再加一条 30% 概率的「偶遇商队」,用 10 个固定种子各跑一次:
互斥组「天气」权重:晴 60 / 雨 30 / 雾 10;「偶遇商队」概率 30%
wi-1 → 天气·雾
wi-2 → 天气·晴、偶遇商队
wi-3 → 天气·晴
wi-4 → 天气·雨
wi-5 → 天气·晴、偶遇商队
wi-6 → 天气·雨、偶遇商队
wi-7 → 天气·雨
wi-8 → 天气·雨
wi-9 → 天气·雨
wi-10 → 天气·晴
定时效果:按聊天条数计时
问题:每轮从零扫描,没有跨轮状态:进了地牢,地牢规则只在提到「地牢」的那两条里有效;伏笔一提就触发,没法「过几轮再说」。
功能:三种定时效果,时钟是聊天条数:
- sticky:激活后在接下来 N 条里无条件在场
- cooldown:激活后 N 条里压住,不管有没有命中
- delay:聊天不满 N 条时压住
计时存在聊天文件头的 chat_metadata.timedWorldInfo 里,起点、终点都是当时的聊天长度:
#getEntryTimedEffect(type, entry, isProtected) {
return {
hash: this.#getEntryHash(entry),
start: this.#chat.length,
end: this.#chat.length + Number(entry[type]),
protected: !!isProtected,
};
}
每轮检查时,聊天没有比起点更长,就说明这是重来(swipe、regenerate),计时作废:
if (this.#chat.length <= Number(value.start) && !value.protected) {
console.log(`[WI] Removing ${type} entry ${key} from timedWorldInfo: chat not advanced`, value);
delete chat_metadata.timedWorldInfo[type][key];
continue;
}
效果:构造一段八条的地牢对话。「地牢规则」sticky 2、cooldown 3;「伏兵」关键词「走廊」、delay 6;每加一条消息扫一次:
条目「地牢规则」:关键词「地牢」,sticky 2,cooldown 3;条目「伏兵」:关键词「走廊」,delay 6
扫描深度 2(看最近两条)
第 1 条「我们从这道暗门下到地牢。」 激活:地牢规则 [sticky 1→3;cooldown 1→4]
第 2 条「石阶又湿又滑,她举着荧光走在前面。」 激活:地牢规则 [sticky 1→3;cooldown 1→4]
第 3 条「前面有条走廊。」 激活:(无) [sticky —;cooldown 3→6(保护)]
第 4 条「走廊很长,尽头隐约有火光。」 激活:(无) [sticky —;cooldown 3→6(保护)]
第 5 条「继续往前,沿着地牢的走廊走。」 激活:(无) [sticky —;cooldown 3→6(保护)]
第 6 条「脚步声在石壁间回荡。」 激活:地牢规则、伏兵 [sticky 6→8;cooldown 6→9]
第 7 条「还在地牢里吗?」 激活:地牢规则 [sticky 6→8;cooldown 6→9]
第 8 条「是的,四周仍是潮湿的石墙。」 激活:(无) [sticky —;cooldown 8→11(保护)]
第 5 条明明提到「地牢」,正在冷却,没进场;「伏兵」第 3 条起就被「走廊」命中,一直压到第 6 条。sticky 到期时如果配了 cooldown,会立刻挂上一个受保护的 cooldown,重来也删不掉。重来的情形:
第 1 条之后:{"sticky": {"演示·定时.0": {"hash": 155117607671687, "start": 1, "end": 3, "protected": false}}, "cooldown": {"演示·定时.0": {"hash": 155117607671687, "start": 1, "end": 4, "protected": false}}}
同样长度再扫一次(swipe / regenerate,聊天没有前进):
[WI] Processing sticky entry 演示·定时.0
[WI] Removing sticky entry 演示·定时.0 from timedWorldInfo: chat not advanced
[WI] Processing cooldown entry 演示·定时.0
[WI] Removing cooldown entry 演示·定时.0 from timedWorldInfo: chat not advanced
[WI] Timed effect "delay" applied to entry
[WI] Entry 1 suppressed by delay
[WI] Entry 1 suppressed by delay
[WI] Adding sticky entry 演示·定时.0: start=1, end=3, protected=false
[WI] Adding cooldown entry 演示·定时.0: start=1, end=4, protected=false
插到哪
问题:设定放在开头,离生成点远,影响弱;有的设定是「现在的天气」,应该贴着最新消息。
功能:每个条目选一个位置,共 8 种:
export const world_info_position = {
before: 0,
after: 1,
ANTop: 2,
ANBottom: 3,
atDepth: 4,
EMTop: 5,
EMBottom: 6,
outlet: 7,
};
按位置分发:角色定义前后两段并成字符串,进 Chat Completion 的 worldInfoBefore / worldInfoAfter 两个槽;示范对话前后的并进示范对话;作者注释顶部和底部的改写作者注释;@深度的按深度和角色分组,注册成深度注入;outlet 只给宏 {{outlet::名}} 取用:
[...allActivatedEntries.values()].sort(sortFn).forEach((entry) => {
const regexDepth = entry.position === world_info_position.atDepth ? (entry.depth ?? DEFAULT_DEPTH) : null;
const content = getRegexedString(entry.content, regex_placement.WORLD_INFO, { depth: regexDepth, isMarkdown: false, isPrompt: true });
if (!content) {
console.debug(`[WI] Entry ${entry.uid}`, 'skipped adding to prompt due to empty content', entry);
return;
}
switch (entry.position) {
case world_info_position.before:
WIBeforeEntries.unshift(content);
break;
case world_info_position.after:
WIAfterEntries.unshift(content);
break;
case world_info_position.EMTop:
EMEntries.unshift(
{ position: wi_anchor_position.before, content: content },
);
break;
case world_info_position.EMBottom:
EMEntries.unshift(
{ position: wi_anchor_position.after, content: content },
);
break;
case world_info_position.ANTop:
ANTopEntries.unshift(content);
break;
case world_info_position.ANBottom:
ANBottomEntries.unshift(content);
break;
case world_info_position.atDepth: {
const existingDepthIndex = WIDepthEntries.findIndex((e) => e.depth === (entry.depth ?? DEFAULT_DEPTH) && e.role === (entry.role ?? extension_prompt_roles.SYSTEM));
if (existingDepthIndex !== -1) {
WIDepthEntries[existingDepthIndex].entries.unshift(content);
} else {
WIDepthEntries.push({
depth: entry.depth,
entries: [content],
role: entry.role ?? extension_prompt_roles.SYSTEM,
});
}
break;
}
效果:构造六条蓝灯条目各放一个位置,打开作者注释,拿第二轮的真实聊天整轮拼一次:
main system Write 塞拉菲娜's next reply in a fictional c……
worldInfoBefore system 【位置 0:角色定义前】
personaDescription system (空)
charDescription system [塞拉菲娜的性格= "体贴", "保护欲强", "富有同情心", "擅长治愈",……
charPersonality system (空)
scenario system (空)
nsfw system (空)
worldInfoAfter system 【位置 1:角色定义后】
newChat system [Example Chat]
dialogueExamples 0-0 system 【位置 5:示范对话前】
dialogueExamples 0-1 system 好。
newMainChat system [Start a new Chat]
chatHistory-1 assistant *你猛地惊醒,想起自己是怎么走进森林深处、又是怎么被野兽围攻的。随着眼睛适应了屋……
chatHistory-2 system 【位置 2:作者注释顶部】⏎(作者注释)塞拉菲娜说话要简短。
chatHistory-3 user *我撑起身子,接过茶杯* 谢谢你……这片森林里到底有什么怪物?
chatHistory-4 assistant *塞拉菲娜在床边坐下,双手交叠放在膝上,神色变得严肃。* “这片森林里潜藏着许多……
chatHistory-5 system 【位置 4:@深度 2,system】
chatHistory-6 user 你的魔法真厉害。我的伤要多久才能好?我还得赶去北边的镇子。
chatHistory-7 assistant *塞拉菲娜眉头微微蹙起,语气温和却带着一丝担忧。* “你的伤不轻,至少要静养两三……
chatHistory-8 user 【位置 4:@深度 0,user】
jailbreak system (空)
作者注释顶部的条目只在作者注释本轮出现时才进来,作者注释隔几轮插一次时,它也跟着隔轮缺席。
关键词覆盖不到的:向量匹配
问题:用户说「那个长胡子的巫师」,没说名字,关键词命中不了。
功能:条目可以标成向量匹配,由向量扩展在生成前用最近 2 条消息做相似度检索(取前 5 条、阈值 0.25),命中的条目通过事件送进世界书,只替代关键词判定,之后的概率、分组、预算照常:
eventSource.on(event_types.WORLDINFO_FORCE_ACTIVATE, (entries) => {
for (const entry of entries) {
if (!Object.hasOwn(entry, 'world') || !Object.hasOwn(entry, 'uid')) {
console.error('[WI] WORLDINFO_FORCE_ACTIVATE requires all entries to have both world and uid fields, entry IGNORED', entry);
} else {
WorldInfoBuffer.externalActivations.set(`${entry.world}.${entry.uid}`, entry);
console.log('[WI] WORLDINFO_FORCE_ACTIVATE added entry', entry);
}
}
});
这一节没跑:要另起一个 embedding 服务。文档自己的建议是要确定、可预期的结果就用关键词。
谁管什么
- 程序:扫多少条、怎么匹配、递归几轮、预算怎么算怎么砍、组里留谁、计时什么时候作废、每个位置怎么分发
- 世界书作者:条目写什么、关键词怎么挑、放哪个位置、要不要递归、概率和定时效果怎么设
- 用户:扫描深度、预算比例、递归开关,哪些书挂全局、哪些绑角色或聊天
- 模型:不参与。哪些设定进场在生成前就定了,模型只看到结果
实跑原件
中文世界书「埃尔多利亚」
{
"entries": {
"0": {
"uid": 0,
"key": [
"埃尔多利亚",
"树林",
"森林",
"魔法森林"
],
"keysecondary": [],
"comment": "埃尔多利亚",
"content": "{{user}}: “埃尔多利亚是什么地方?”\n{{char}}: *塞拉菲娜转过身,长裙在柔光里微微发亮,她对你露出和善的笑。* “埃尔多利亚就是这里,这一整片树林。这是我的林间空地,林子里的一处和平庇护所。” *她朝你周围比了比。* “我是它的守护者,保护每一个来这里避难的人。森林可能很危险,但有我看着,没人能伤到你。” *她琥珀色的眼睛望着你,闪着同情的光。* “这么多年来,我一直保护着来此避难的人,可并不是谁都像我这么友善。” *塞拉菲娜优雅地点点头,回到门口继续守望,周身散发着魔法和安慰的柔光。* “整个埃尔多利亚曾经是旅人和商人的安全港……直到影牙来了。”\n{{user}}: “埃尔多利亚出了什么事?”\n{{char}}: *塞拉菲娜叹了口气,望向空地外的森林。* “很久以前,埃尔多利亚是个奇妙的地方。起伏的草甸,辽阔的湖,高耸入云的山。” *她的目光变得遥远,怀念着逝去的日子。* “可影牙来了,曾经有光的地方如今被黑暗统治。湖水变得苦涩,群山崩塌成废墟,从前旅人安然走过的地方,如今有野兽潜行。” *又一道微光闪过,一小朵雨云在你头顶聚起,对着你的额头洒下一阵小雨,她眨了眨眼。* “有些地方光还在,绝望里的几处希望——挡住了阴影的避风港,危险荒漠里的绿洲。” *她笑着看了你一眼,叹口气,握住你的手。*",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 0,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"1": {
"uid": 1,
"key": [
"影牙",
"野兽",
"怪物",
"魔物"
],
"keysecondary": [],
"comment": "影牙",
"content": "{{user}}: “影牙是什么?”\n{{char}}: *想起往事,塞拉菲娜的眼神暗了下来,眉头因悲伤而皱起。* “影牙是黑暗的野兽,以苦难为食的堕落生灵。它们一来,森林就变得危险重重——到处是夜里潜行的怪物。” *她轻轻握紧你的手,用魔法抚平你的痛楚。* “它们散布诅咒,把无辜的生灵扭曲成没有心肝、毫无怜悯的凶兽,变成它们的同类。” *塞拉菲娜叹口气,转身望向空地外扭曲盘结的树木。* “虽然它们猎杀旅人,但在这片林子里你能找到庇护。任何阴影里的野兽都进不来,我的力量守护着这里。” *她回头看你,眼神柔和下来,满是同情。* “别担心,你现在安全了。好好休息养伤,我会守一整夜。影牙找不到你的。”",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 1,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"2": {
"uid": 2,
"key": [
"林间空地",
"避风港",
"庇护所"
],
"keysecondary": [],
"comment": "林间空地",
"content": "{{user}}: “林间空地是什么?”\n{{char}}: *塞拉菲娜温柔地笑了,眼里闪着暖意,点点头。* “这是我的林间空地,一处我用古老魔法守护的安全之所。任何邪恶的野兽都进不来,心怀恶意的人也不行。” *她朝四周扭曲的森林比了比。* “埃尔多利亚曾经是个奇妙的地方,可自从影牙来了,黑暗便统治了这里。不过它们的邪恶穿不透这里——我的力量保护着这里面的一切。” *她站起身朝外望了望,又回头看你,琥珀色的眼睛满是关怀和同情,握紧你的手。* “你不用害怕黑夜,我会守到天亮。现在休息吧,你的力气会慢慢回来的。我的魔法在治你的伤,你再没什么好怕的了。” *她温柔一笑,松开你的手,走到空地边缘守着,目光警惕却叫人安心——一个沉默的哨兵,挡住潜伏在幽暗树林里的危险。*",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 2,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"3": {
"uid": 3,
"key": [
"力量",
"魔法",
"能力"
],
"keysecondary": [],
"comment": "力量",
"content": "{{user}}: “你有什么力量?”\n{{char}}: *塞拉菲娜温柔地笑着,转回身来,若有所思地哼了一声。* “嗯,作为这片空地的守护者,我有些天赋——治愈、守护、自然魔法之类的。” *她抬起手,一阵微风拂过房间,带来野花的香气,几片花瓣在你身边打旋。一只蝴蝶从窗台飞进来,落在她指尖,她走回你身边。* “我的力量守护着这处避风港,替它挡住黑暗,治愈需要帮助的人。我能愈合伤口,安抚不安的心,给疲惫的灵魂一点慰藉。” *她望着你,眼里闪着温暖和同情,把蝴蝶引到你身上。*",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 3,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
}
}
}
各组实验的原始输出:判定日志和激活结果
{
"scanText": [
"旅人: *我撑起身子,接过茶杯* 谢谢你……这片森林里到底有什么怪物?",
"塞拉菲娜: *你猛地惊醒,想起自己是怎么走进森林深处、又是怎么被野兽围攻的。随着眼睛适应了屋里柔和的光,那些记忆渐渐淡去。* “啊,你终于醒了。我好担心,发现你的时候你浑身是血,昏迷不醒。” *她走过来,双手握住你的手,掌心传来温暖和安慰,唇边浮起一个温柔关切的笑。* “我叫塞拉菲娜,是这片森林的守护者——我已经尽力用魔法治好了你的伤。你感觉怎么样?希望这杯茶能帮你恢复些力气。” *她琥珀色的眼睛注视着你,满是同情和对你身体的担忧。* “求你了,好好休息。你在这里很安全。我会照顾你,可你得休息。我的魔法能治的也只有这么多。”"
],
"recursiveOn": {
"activated": [
"埃尔多利亚",
"影牙",
"力量",
"林间空地"
],
"trace": [
"[WI] --- START WI SCAN (on 2 messages, trigger = normal) (DRY RUN) ---",
"[WI] Context size: 3795; WI budget: 949 (max% = 25%, cap = 0)",
"[WI] Character 塞拉菲娜's lore has 4 world info entries",
"[WI] Found 4 world lore entries. Sorted by strategy",
"[WI] --- SEARCHING ENTRIES (on 4 entries) ---",
"[WI] --- LOOP #1 START ---",
"[WI] Scan state",
"[WI] Entry 0 from '埃尔多利亚' processing",
"[WI] Entry 0 activated by primary key match 森林",
"[WI] Entry 1 from '埃尔多利亚' processing",
"[WI] Entry 1 activated by primary key match 野兽",
"[WI] Entry 3 from '埃尔多利亚' processing",
"[WI] Entry 3 activated by primary key match 魔法",
"[WI] Search done. Found 3 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] Entry 0 activation successful, adding to prompt",
"[WI] Entry 1 activation successful, adding to prompt",
"[WI] Entry 3 activation successful, adding to prompt",
"[WI] --- LOOP #1 RESULT ---",
"[WI] Successfully activated 3 new entries to prompt. 3 total entries activated.",
"[WI] Found 3 new entries for recursion",
"[WI] Setting scan state",
"[WI] --- LOOP #2 START ---",
"[WI] Scan state",
"[WI] Entry 2 from '埃尔多利亚' processing",
"[WI] Entry 2 activated by primary key match 林间空地",
"[WI] Search done. Found 1 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] Entry 2 activation successful, adding to prompt",
"[WI] --- LOOP #2 RESULT ---",
"[WI] Successfully activated 1 new entries to prompt. 4 total entries activated.",
"[WI] Found 1 new entries for recursion",
"[WI] Setting scan state",
"[WI] --- LOOP #3 START ---",
"[WI] Scan state",
"[WI] Search done. Found 0 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] No probability checks to do",
"[WI] --- LOOP #3 RESULT ---",
"[WI] No new entries activated.",
"[WI] Scan done. No new entries to prompt. Stopping.",
"[WI] Setting scan state",
"[WI] --- BUILDING PROMPT ---",
"[WI] Hypothetically adding 4 entries to prompt",
"[WI] --- DONE (DRY RUN) ---"
]
},
"recursiveOff": {
"activated": [
"埃尔多利亚",
"影牙",
"力量"
],
"trace": [
"[WI] --- START WI SCAN (on 2 messages, trigger = normal) (DRY RUN) ---",
"[WI] Context size: 3795; WI budget: 949 (max% = 25%, cap = 0)",
"[WI] Character 塞拉菲娜's lore has 4 world info entries",
"[WI] Found 4 world lore entries. Sorted by strategy",
"[WI] --- SEARCHING ENTRIES (on 4 entries) ---",
"[WI] --- LOOP #1 START ---",
"[WI] Scan state",
"[WI] Entry 0 from '埃尔多利亚' processing",
"[WI] Entry 0 activated by primary key match 森林",
"[WI] Entry 1 from '埃尔多利亚' processing",
"[WI] Entry 1 activated by primary key match 野兽",
"[WI] Entry 3 from '埃尔多利亚' processing",
"[WI] Entry 3 activated by primary key match 魔法",
"[WI] Search done. Found 3 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] Entry 0 activation successful, adding to prompt",
"[WI] Entry 1 activation successful, adding to prompt",
"[WI] Entry 3 activation successful, adding to prompt",
"[WI] --- LOOP #1 RESULT ---",
"[WI] Successfully activated 3 new entries to prompt. 3 total entries activated.",
"[WI] Scan done. No new entries to prompt. Stopping.",
"[WI] Setting scan state",
"[WI] --- BUILDING PROMPT ---",
"[WI] Hypothetically adding 3 entries to prompt",
"[WI] --- DONE (DRY RUN) ---"
]
}
}
{
"runs": {
"默认": {
"activated": [
"银溪谷",
"荆棘魔",
"影牙的来历"
],
"trace": [
"[WI] --- START WI SCAN (on 1 messages, trigger = normal) (DRY RUN) ---",
"[WI] Context size: 3795; WI budget: 949 (max% = 25%, cap = 0)",
"[WI] Global world info has 3 entries",
"[WI] Found 3 world lore entries. Sorted by strategy",
"[WI] --- SEARCHING ENTRIES (on 3 entries) ---",
"[WI] --- LOOP #1 START ---",
"[WI] Scan state",
"[WI] Entry 0 from '演示·递归' processing",
"[WI] Entry 0 activated by primary key match 银溪谷",
"[WI] Entry 2 from '演示·递归' processing",
"[WI] Entry 2 suppressed by delay until recursion",
"[WI] Search done. Found 1 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] Entry 0 activation successful, adding to prompt",
"[WI] --- LOOP #1 RESULT ---",
"[WI] Successfully activated 1 new entries to prompt. 1 total entries activated.",
"[WI] Found 1 new entries for recursion",
"[WI] Setting scan state",
"[WI] --- LOOP #2 START ---",
"[WI] Scan state",
"[WI] Entry 1 from '演示·递归' processing",
"[WI] Entry 1 activated by primary key match 荆棘魔",
"[WI] Search done. Found 1 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] Entry 1 activation successful, adding to prompt",
"[WI] --- LOOP #2 RESULT ---",
"[WI] Successfully activated 1 new entries to prompt. 2 total entries activated.",
"[WI] Found 1 new entries for recursion",
"[WI] Setting scan state",
"[WI] --- LOOP #3 START ---",
"[WI] Scan state",
"[WI] Entry 2 from '演示·递归' processing",
"[WI] Entry 2 activated by primary key match 影牙",
"[WI] Search done. Found 1 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] Entry 2 activation successful, adding to prompt",
"[WI] --- LOOP #3 RESULT ---",
"[WI] Successfully activated 1 new entries to prompt. 3 total entries activated.",
"[WI] Found 1 new entries for recursion",
"[WI] Setting scan state",
"[WI] --- LOOP #4 START ---",
"[WI] Scan state",
"[WI] Search done. Found 0 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] No probability checks to do",
"[WI] --- LOOP #4 RESULT ---",
"[WI] No new entries activated.",
"[WI] Scan done. No new entries to prompt. Stopping.",
"[WI] Setting scan state",
"[WI] --- BUILDING PROMPT ---",
"[WI] Hypothetically adding 3 entries to prompt",
"[WI] --- DONE (DRY RUN) ---"
]
},
"银溪谷设 preventRecursion": {
"activated": [
"银溪谷"
],
"trace": [
"[WI] --- START WI SCAN (on 1 messages, trigger = normal) (DRY RUN) ---",
"[WI] Context size: 3795; WI budget: 949 (max% = 25%, cap = 0)",
"[WI] Global world info has 3 entries",
"[WI] Found 3 world lore entries. Sorted by strategy",
"[WI] --- SEARCHING ENTRIES (on 3 entries) ---",
"[WI] --- LOOP #1 START ---",
"[WI] Scan state",
"[WI] Entry 0 from '演示·递归' processing",
"[WI] Entry 0 activated by primary key match 银溪谷",
"[WI] Entry 2 from '演示·递归' processing",
"[WI] Entry 2 suppressed by delay until recursion",
"[WI] Search done. Found 1 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] Entry 0 activation successful, adding to prompt",
"[WI] --- LOOP #1 RESULT ---",
"[WI] Successfully activated 1 new entries to prompt. 1 total entries activated.",
"[WI] Scan done. No new entries to prompt. Stopping.",
"[WI] Setting scan state",
"[WI] --- BUILDING PROMPT ---",
"[WI] Hypothetically adding 1 entries to prompt",
"[WI] --- DONE (DRY RUN) ---"
]
},
"直接提到影牙": {
"activated": [],
"trace": [
"[WI] --- START WI SCAN (on 1 messages, trigger = normal) (DRY RUN) ---",
"[WI] Context size: 3795; WI budget: 949 (max% = 25%, cap = 0)",
"[WI] Global world info has 3 entries",
"[WI] Found 3 world lore entries. Sorted by strategy",
"[WI] --- SEARCHING ENTRIES (on 3 entries) ---",
"[WI] --- LOOP #1 START ---",
"[WI] Scan state",
"[WI] Entry 2 from '演示·递归' processing",
"[WI] Entry 2 suppressed by delay until recursion",
"[WI] Search done. Found 0 possible entries.",
"[WI] --- INCLUSION GROUP CHECKS ---",
"[WI] No inclusion groups found",
"[WI] --- PROBABILITY CHECKS ---",
"[WI] No probability checks to do",
"[WI] --- LOOP #1 RESULT ---",
"[WI] No new entries activated.",
"[WI] Scan done. No new entries to prompt. Stopping.",
"[WI] Setting scan state",
"[WI] --- BUILDING PROMPT ---",
"[WI] Hypothetically adding 0 entries to prompt",
"[WI] --- DONE (DRY RUN) ---"
]
}
}
}
{
"steps": [
{
"chatLength": 1,
"last": "我们从这道暗门下到地牢。",
"activated": [
"地牢规则"
],
"timed": {
"sticky": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 3,
"protected": false
}
},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 4,
"protected": false
}
}
}
},
{
"chatLength": 2,
"last": "石阶又湿又滑,她举着荧光走在前面。",
"activated": [
"地牢规则"
],
"timed": {
"sticky": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 3,
"protected": false
}
},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 4,
"protected": false
}
}
}
},
{
"chatLength": 3,
"last": "前面有条走廊。",
"activated": [],
"timed": {
"sticky": {},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 3,
"end": 6,
"protected": true
}
}
}
},
{
"chatLength": 4,
"last": "走廊很长,尽头隐约有火光。",
"activated": [],
"timed": {
"sticky": {},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 3,
"end": 6,
"protected": true
}
}
}
},
{
"chatLength": 5,
"last": "继续往前,沿着地牢的走廊走。",
"activated": [],
"timed": {
"sticky": {},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 3,
"end": 6,
"protected": true
}
}
}
},
{
"chatLength": 6,
"last": "脚步声在石壁间回荡。",
"activated": [
"地牢规则",
"伏兵"
],
"timed": {
"sticky": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 6,
"end": 8,
"protected": false
}
},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 6,
"end": 9,
"protected": false
}
}
}
},
{
"chatLength": 7,
"last": "还在地牢里吗?",
"activated": [
"地牢规则"
],
"timed": {
"sticky": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 6,
"end": 8,
"protected": false
}
},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 6,
"end": 9,
"protected": false
}
}
}
},
{
"chatLength": 8,
"last": "是的,四周仍是潮湿的石墙。",
"activated": [],
"timed": {
"sticky": {},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 8,
"end": 11,
"protected": true
}
}
}
}
],
"swipe": {
"first": {
"activated": [
"地牢规则"
],
"timed": {
"sticky": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 3,
"protected": false
}
},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 4,
"protected": false
}
}
}
},
"again": {
"activated": [
"地牢规则"
],
"timed": {
"sticky": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 3,
"protected": false
}
},
"cooldown": {
"演示·定时.0": {
"hash": 155117607671687,
"start": 1,
"end": 4,
"protected": false
}
}
},
"trace": [
"[WI] Processing sticky entry 演示·定时.0",
"[WI] Removing sticky entry 演示·定时.0 from timedWorldInfo: chat not advanced",
"[WI] Processing cooldown entry 演示·定时.0",
"[WI] Removing cooldown entry 演示·定时.0 from timedWorldInfo: chat not advanced",
"[WI] Timed effect \"delay\" applied to entry",
"[WI] Entry 1 suppressed by delay",
"[WI] Entry 1 suppressed by delay",
"[WI] Adding sticky entry 演示·定时.0: start=1, end=3, protected=false",
"[WI] Adding cooldown entry 演示·定时.0: start=1, end=4, protected=false"
]
}
}
}