3.7 KiB
ENA Planner
ENA Planner 是一个可选的、发送前剧情规划子系统。它独立于核心记忆系统,默认关闭。
实现:ena-planner/(独立模块)、ui/panel-ena-sections.js、runtime/planner-recall-controller.js。
做什么
在用户消息真正发给模型之前,先用一个单独的"规划师"LLM 生成幕后指引,附加到用户输入里。规划师不直接扮演角色、不写正文,只规划下一轮 AI 回复的走向。
产出带标签的规划文本:
<plot>— 剧情走向指引(导演式的下一步规划)<note>— 写作笔记(风格/连贯性指示)
过滤后的规划输出会附加到真实用户输入后面再发送:原始输入 + \n\n + 规划输出。
管线
拦截发送(点击发送/回车)
→ 构建规划消息(buildPlannerMessages)
→ 收集上下文:角色卡 + BME 记忆召回 + 近期 AI 对话 + 历史 <plot> + 世界书 + 用户输入
→ 渲染模板/宏(EJS、ST 宏)
→ 组装提示词块(优先用 planner 任务预设,回退遗留块)
→ 调用规划师 LLM(callPlanner,可流式)
→ 过滤响应(去 think,保留配置的标签)
→ 注入 textarea 并发送
启动入口 initEnaPlanner(bmeRuntime):迁移旧存储、加载配置/日志、安装发送拦截器、暴露 window.stBmeEnaPlanner。
发送拦截条件:planner 启用、未在规划中、textarea 非空、输入非 trivial、未 bypass、(若 skipIfPlotPresent)输入未含 <plot>。
与 ST-BME 的集成
ENA Planner 集成的是召回,不是提取:
- 它调用 BME 召回获取记忆块作为规划上下文(
runPlannerRecallForEna)。 - 规划输出注入用户文本后,主生成会把规划标签当作用户消息的一部分看到。
- 它不直接运行提取,也不把规划结果写进记忆图谱。后续提取走正常聊天/提取路径。
召回交接(handoff)
规划输出附加到 textarea 后,正常生成本会对"增强后的输入"(原文 + <plot>/<note>)做召回——但那会用改变后的文本检索,结果偏差。
为避免这点,ENA 注册一个一次性召回交接:含原始用户输入、增强消息、召回结果、注入文本,source 标记 planner-handoff。正常生成召回会读取这个交接,把召回输入覆盖回原始用户文本,并复用已算好的召回结果(cachedRecallPayload)。
这套机制的实现见 runtime/planner-recall-controller.js、runtime/reroll-recall-input.js、runtime/generation-recall-transactions.js。
规划召回 vs 正常召回
| 维度 | 规划召回 | 正常召回 |
|---|---|---|
| 时机 | 规划师 LLM 调用前 | 主生成前 |
| 查询源 | 原始用户输入 | 用户输入(或交接复用) |
| 上下文 | recallLlmContextMessages(夹 0..20) |
标准召回上下文 |
| 门禁 | 跳过 trivial、需图谱可读、历史恢复、向量就绪 | 同左 |
配置(独立存储)
ENA 配置存在 STBME_EnaPlanner.json,不在主设置里。关键默认:
| 设置 | 默认 |
|---|---|
enabled |
false |
skipIfPlotPresent |
true |
plotCount |
2 |
responseKeepTags |
plot, note, plot-log, state |
includeGlobalWorldbooks |
false |
| API stream | true |
logsPersist / logsMax |
true / 20 |
面板的"ENA 规划器"区(data-config-section="planner")提供启用、跳过已有 plot、测试输入、API 预设、世界书选项、保留/排除标签、plot 数量、日志等控件。
关于命名:代码里 "ENA" 作为模块/产品名使用(Ena Planner / ENA 规划器),未找到明确的缩写展开;实际提示词角色是"剧情规划师 / Story Planner"。