跳转至

世界书:设定按关键词进场

网页版 ChatGPT 的设定只能写进 system prompt:几万字的世界观塞不下,塞进去了也稀释注意力,聊天里提没提到都占着位置。酒馆的世界书(World Info,也叫 Lorebook)把设定拆成条目,每个条目带一组关键词;每轮生成前扫一遍最近的聊天,命中了才把这一条注入。下面的「效果」都是直接调 world-info.js 的 checkWorldInfo 跑出来的,判定过程取自酒馆自己打的 [WI] 调试日志。

所用的书是随包的 Eldoria 译成中文的「埃尔多利亚」,四个条目:埃尔多利亚、影牙、林间空地、力量,绑在塞拉菲娜的卡上。出厂设置:扫描深度 2、预算 25%、递归开、整词匹配开。

只扫最近几条,关键词命中才进场

问题:设定全塞进 system prompt,上下文贵,注意力被稀释,大世界根本塞不下。

功能:每轮把最近 N 条消息(出厂 2 条)连成一段扫描文本,每条消息前加一个 \x01 分隔符,方便写正则锚定说话人:

模版 · public/scripts/world-info.js:295-297
        const MATCHER = '\x01';
        const JOINER = '\n' + MATCHER;
        let result = MATCHER + this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);

条目的任一主关键词出现在扫描文本里,就进入候选。关键词先展开宏,所以可以写 {{char}}:

模版 · public/scripts/world-info.js:4910-4935
            // 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;
            }

常驻条目(社区叫「蓝灯」)不看关键词,直接进候选,但之后的概率、分组、预算照样要过:

模版 · public/scripts/world-info.js:4893-4897
            if (entry.constant) {
                log('activated because of constant');
                activatedNow.add(entry);
                continue;
            }

效果:扮演主线第一轮,扫描文本是开场白加用户这句(消息前带名字,world_info_include_names 出厂开着):

效果 · 第一轮的扫描文本(最近 2 条,新的在前)
[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) ---

第一轮循环里,开场白的「森林」「野兽」「魔法」各命中一条。这三条的内容进了这一轮;第四条「林间空地」是第二轮循环找到的,见下面递归。

次关键词消歧

问题:同一个词在不同话题里指不同的东西,只按一个词触发,就会在错的话题里塞进错的设定。

功能:条目可以再挂一组次关键词,和主关键词按四种逻辑组合:次关键词任一命中、全都命中、全都没命中、不全命中:

模版 · public/scripts/world-info.js:4955-4975
                    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(次关键词全都没命中)

「新款苹果手机什么时候发布?」→ 激活:苹果公司
「院子里的苹果熟了,摘几个吧。」→ 激活:苹果树

递归:条目引出条目

问题:设定之间互相引用:聊天里提到了埃尔多利亚,它的条目里又说到林间空地,可聊天里没出现「林间空地」,模型就不知道那是什么。

功能:一轮循环激活的条目,内容拼起来加进扫描文本,再扫一轮,直到没有新条目:

模版 · public/scripts/world-info.js:5096-5100
        // 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');
        }
模版 · public/scripts/world-info.js:5135-5144
        // 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(只能被递归触发,第一轮扫描一律压住):

模版 · public/scripts/world-info.js:4860-4868
            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%),可以再设一个绝对上限:

模版 · public/scripts/world-info.js:4736-4741
    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 的除外),递归也停:

模版 · public/scripts/world-info.js:5057-5076
            // 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% / 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 最高的,否则按权重抽签。条目还可以带触发概率:

模版 · public/scripts/world-info.js:5451-5465
        // 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;
            }
        }
模版 · public/scripts/world-info.js:5041-5048
                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 里,起点、终点都是当时的聊天长度:

模版 · public/scripts/world-info.js:604-611
    #getEntryTimedEffect(type, entry, isProtected) {
        return {
            hash: this.#getEntryHash(entry),
            start: this.#chat.length,
            end: this.#chat.length + Number(entry[type]),
            protected: !!isProtected,
        };
    }

每轮检查时,聊天没有比起点更长,就说明这是重来(swipe、regenerate),计时作废:

模版 · public/scripts/world-info.js:626-630
            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 种:

模版 · public/scripts/world-info.js:855-864
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::名}} 取用:

模版 · public/scripts/world-info.js:5203-5247
    [...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;
            }

效果:构造六条蓝灯条目各放一个位置,打开作者注释,拿第二轮的真实聊天整轮拼一次:

效果 · 各位置的条目落在哪(构造的输入,dry run)
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),命中的条目通过事件送进世界书,只替代关键词判定,之后的概率、分组、预算照常:

模版 · public/scripts/world-info.js:1020-1029
    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"
   ]
  }
 }
}