feat: 优化默认提示词(HARD GATE约束) + 扩展prompt变量(Phase1+2)

Phase 1: 重写 DEFAULT_TASK_BLOCKS 全部6个任务的 role/format/rules
- 统一应用 HARD GATE 约束段 + 常见错误负例
- compress/synopsis 增加自检清单
- 增强 JSON 稳定性约束

Phase 2: 扩展 prompt 内置变量
- 新建 st-context.js: 统一读取 ST 上下文
- 新增变量: charName/userName/charDescription/userPersona/currentTime
- 更新 extractor/retriever/compressor/consolidator 共6处调用端
- 更新 BUILTIN_BLOCK_DEFINITIONS 帮助文案(多轮对话指引)
This commit is contained in:
Youzini-afk
2026-03-26 10:30:26 +08:00
parent 0e590a6256
commit 2f9524d993
6 changed files with 232 additions and 46 deletions

View File

@@ -12,6 +12,7 @@ import {
} from "./graph.js";
import { callLLMForJSON } from "./llm.js";
import { buildTaskPrompt } from "./prompt-builder.js";
import { getSTContextForPrompt } from "./st-context.js";
import { applyTaskRegex } from "./task-regex.js";
import { isDirectVectorConfig } from "./vector-index.js";
@@ -234,6 +235,7 @@ async function summarizeBatch(
candidateNodes: nodeDescriptions,
currentRange: `${nodes[0]?.seq ?? "?"} ~ ${nodes[nodes.length - 1]?.seq ?? "?"}`,
graphStats: `node_count=${nodes.length}, node_type=${typeDef.id}`,
...getSTContextForPrompt(),
});
const systemPrompt = applyTaskRegex(
settings,

View File

@@ -6,6 +6,7 @@ import { embedBatch, searchSimilar } from "./embedding.js";
import { addEdge, createEdge, getActiveNodes, getNode } from "./graph.js";
import { callLLMForJSON } from "./llm.js";
import { buildTaskPrompt } from "./prompt-builder.js";
import { getSTContextForPrompt } from "./st-context.js";
import { applyTaskRegex } from "./task-regex.js";
import {
buildNodeVectorText,
@@ -298,6 +299,7 @@ export async function consolidateMemories({
candidateNodes: userPrompt,
candidateText: userPrompt,
graphStats: `new_entries=${newEntries.length}, threshold=${conflictThreshold}`,
...getSTContextForPrompt(),
});
try {
decision = await callLLMForJSON({

View File

@@ -19,6 +19,7 @@ import { ensureEventTitle, getNodeDisplayName } from "./node-labels.js";
import { buildTaskPrompt } from "./prompt-builder.js";
import { RELATION_TYPES } from "./schema.js";
import { applyTaskRegex } from "./task-regex.js";
import { getSTContextForPrompt } from "./st-context.js";
import { buildNodeVectorText, isDirectVectorConfig } from "./vector-index.js";
function createAbortError(message = "操作已终止") {
@@ -117,6 +118,7 @@ export async function extractMemories({
graphStats: graphOverview,
graphOverview,
currentRange,
...getSTContextForPrompt(),
});
// 系统提示词
@@ -633,6 +635,7 @@ export async function generateSynopsis({
characterSummary: charSummary || "(无)",
threadSummary: threadSummary || "(无)",
graphStats: `event=${eventNodes.length}, character=${characterNodes.length}, thread=${threadNodes.length}`,
...getSTContextForPrompt(),
});
const synopsisSystemPrompt = applyTaskRegex(
settings,
@@ -746,6 +749,7 @@ export async function generateReflection({
threadSummary: threadSummary || "(无)",
contradictionSummary: contradictionSummary || "(无)",
graphStats: `event=${recentEvents.length}, character=${recentCharacters.length}, thread=${recentThreads.length}`,
...getSTContextForPrompt(),
});
const reflectionSystemPrompt = applyTaskRegex(
settings,

View File

@@ -47,7 +47,7 @@ const BUILTIN_BLOCK_DEFINITIONS = [
sourceKey: "systemInstruction",
name: "系统说明",
role: "system",
description: "注入任务级系统指令。可用于添加通用约束或全局规则,所有任务类型均可使用。",
description: "注入任务级系统指令。可用于添加通用约束或全局规则。提示可创建多个自定义块并设置不同角色system/user/assistant来实现多轮对话式 prompt 编排,利用 few-shot 引导 LLM 遵守格式。可用变量:{{charName}}、{{userName}}、{{charDescription}}、{{userPersona}}、{{currentTime}}。",
},
{
sourceKey: "outputRules",
@@ -141,82 +141,213 @@ const LEGACY_PROMPT_FIELD_MAP = {
const DEFAULT_TASK_BLOCKS = {
extract: {
role: "你是一个记忆提取分析器。从对话中提取结构化记忆节点并存入知识图谱。",
role: [
"你是记忆提取执行 AI。从对话中提取结构化记忆节点写入知识图谱。",
`必须按「分析thought→ 操作operations」架构工作。`,
].join("\n"),
format: [
"输出格式为严格 JSON",
"{",
' \"thought\": \"你对本段对话的分析(事件/角色变化/新信息)\",',
' \"operations\": [',
' "thought": "你对本段对话的分析(事件/角色变化/新信息)",',
' "operations": [',
" {",
' \"action\": \"create\",',
' \"type\": \"event\",',
' \"fields\": {\"title\": \"简短事件名\", \"summary\": \"...\", \"participants\": \"...\", \"status\": \"ongoing\"},',
' \"importance\": 6,',
' \"ref\": \"evt1\",',
' \"links\": [',
' {\"targetNodeId\": \"existing-id\", \"relation\": \"involved_in\", \"strength\": 0.9}',
' "action": "create",',
' "type": "event",',
' "fields": {"title": "简短事件名", "summary": "...", "participants": "...", "status": "ongoing"},',
' "importance": 6,',
' "ref": "evt1",',
' "links": [',
' {"targetNodeId": "existing-id", "relation": "involved_in", "strength": 0.9}',
" ]",
" },",
" {",
' \"action\": \"update\",',
' \"nodeId\": \"existing-node-id\",',
' \"fields\": {\"state\": \"新的状态\"}',
' "action": "update",',
' "nodeId": "existing-node-id",',
' "fields": {"state": "新的状态"}',
" }",
" ]",
"}",
].join("\n"),
rules: [
"- 每批对话最多创建 1 个事件节点,多个子事件合并为一条",
"- 角色/地点节点:如果图中已有同名节点,用 update 而非 create",
"- 不要虚构内容,只提取对话中有证据支持的信息",
"- importance 范围 1-10普通事件 5关键转折 8+",
"- event.fields.title 需要是简短事件名,建议 6-18 字,只用于图谱和列表显示",
"- summary 应该是摘要抽象,不要复制原文",
"============ 【核心规则 - HARD GATE】============",
"",
"一、提取原则",
"1. 唯一来源:只从对话正文中提取,绝对禁止虚构未出现的信息",
"2. 合并策略:每批对话最多 1 个事件节点,多个子事件合并",
"3. 去重检查:图中已有同名角色/地点节点时,用 update 而非 create",
"4. importance1-10日常交互 3-5关键转折 7-8改变格局 9-10",
"",
"二、字段约束",
"- event.fields.title简短事件名6-18 字,用于图谱列表显示",
"- summary摘要抽象不复制原文150 字以内",
"- participants所有参与者名字逗号分隔",
"",
"三、JSON 稳定性",
"- 字符串中的双引号必须转义",
"- 禁止尾随逗号、单引号、注释",
"",
"============ 【常见错误(绝对禁止)】============",
"- 虚构对话中未出现的事件或角色",
`- 图中已有「张三」节点时仍 create 新「张三"`,
"- title 写成整段叙述而非简短事件名",
"- summary 直接复制原文对话",
"- importance 全部给 5应区分轻重",
].join("\n"),
},
recall: {
role: "你是一个记忆召回分析器。\n根据用户最新输入和对话上下文从候选记忆节点中选择最相关的节点。",
format: '输出严格的 JSON 格式:\n{\"selected_ids\": [\"id1\", \"id2\", ...], \"reason\": \"简要说明选择理由\"}',
rules: "优先选择:\n (1) 直接相关的当前场景节点\n (2) 因果关系连续性节点\n (3) 有潜在影响的背景节点",
role: [
"你是记忆召回执行 AI。从候选记忆节点中选择与当前对话最相关的节点。",
"必须先推测剧情走向,再按相关性排序选择。",
].join("\n"),
format: '输出严格的 JSON 格式:\n{"selected_ids": ["id1", "id2", ...], "reason": "简要说明选择理由"}',
rules: [
"============ 【核心规则 - HARD GATE】============",
"",
"一、召回优先级(从高到低)",
"1. 直接相关:当前场景正在发生的事件/在场角色",
"2. 因果链接:与当前事件构成因果关系的前序事件",
"3. 情感关联:涉及相同角色的情感/关系变化",
"4. 背景补充:可能影响当前决策的世界观或状态信息",
"",
"二、选择原则",
"- 不要因为 importance 高就选择,必须与当前对话相关",
"- 每个选中节点必须在 reason 中说明选择理由",
"- 宁缺毋滥:无关节点不要选",
"",
"============ 【常见错误(绝对禁止)】============",
"- 选择全部候选节点(应有取舍)",
"- 选择 0 个节点(除非候选列表确实全部无关)",
`- reason 写成「这些节点相关」(应具体说明每个节点为何相关)`,
"- 选择已被标记为 archived 的过时信息",
].join("\n"),
},
consolidation: {
role: "你是一个记忆整合分析器。当新记忆加入知识图谱时,你需要同时完成两项任务:",
role: [
"你是记忆整合执行 AI。当新记忆加入知识图谱时执行冲突检测与进化分析。",
`必须按「冲突检测 → 进化分析」双任务架构工作。`,
].join("\n"),
format: [
"输出严格 JSON",
'{ \"results\": [',
' { \"node_id\": \"新记忆节点ID\",',
' \"action\": \"keep\"|\"merge\"|\"skip\",',
' \"merge_target_id\": \"旧节点ID (仅merge)\",',
' \"reason\": \"理由\",',
' \"evolution\": { \"should_evolve\": true/false, \"connections\": [\"旧记忆ID\"], \"neighbor_updates\": [...] }',
'{ "results": [',
' { "node_id": "新记忆节点ID",',
' "action": "keep"|"merge"|"skip",',
' "merge_target_id": "旧节点ID (仅merge)",',
' "reason": "理由",',
' "evolution": { "should_evolve": true/false, "connections": ["旧记忆ID"], "neighbor_updates": [...] }',
" }",
"] }",
].join("\n"),
rules: [
"任务一:冲突检测",
" - skip: 新记忆与已有记忆完全重复",
" - merge: 新记忆是对旧记忆的修正/补充",
" - keep: 新记忆是全新信息",
"============ 【核心规则 - HARD GATE】============",
"",
"任务二:进化分析(仅 action=keep 时",
" - 建立关联连接",
" - 反向更新旧记忆",
"一、冲突检测(每个新节点必须判定",
"- skip新记忆与已有记忆完全重复信息量无增益",
"- merge新记忆是对旧记忆的修正、补充或更新",
"- keep新记忆包含全新信息不与已有记忆冲突",
"",
"二、进化分析(仅 action=keep 时执行)",
"- 检查新记忆是否与旧记忆存在因果/时序/角色关联",
"- 建立 connections记忆间的关联边",
"- 判断是否需要反向更新旧记忆状态",
"",
"三、判定标准",
`- 「完全重复」= 核心事实相同,不只是措辞相似`,
`- 「修正」= 新信息明确否定或更正旧信息`,
`- 「补充」= 新信息为旧信息增加细节但不矛盾`,
"",
"============ 【常见错误(绝对禁止)】============",
"- 对所有节点都返回 keep应认真检测重复",
"- merge 时未指定 merge_target_id",
`- 把「只是措辞不同」的记忆判定为 keep应 skip 或 merge`,
"- keep 时 connections 为空(应尝试建立关联)",
].join("\n"),
},
compress: {
role: "你是一个记忆压缩器。将多个同类型节点总结为一条更高层级的压缩节点。",
format: '输出格式为严格 JSON\n{\"fields\": {\"summary\": \"...\", ...}}',
rules: "- 保留关键信息:因果关系、不可逆结果、未解决伏笔\n- 去除重复和低信息密度内容\n- 压缩后文本应精炼,目标 150 字左右",
role: [
"你是记忆压缩执行 AI。将多个同类记忆节点合并为一条精炼的高层摘要。",
`必须按「分析 → 压缩 → 自检」流程工作。`,
].join("\n"),
format: '输出格式为严格 JSON\n{"fields": {"summary": "...", ...}}',
rules: [
"============ 【核心规则 - HARD GATE】============",
"",
"一、保留优先级(从高到低)",
"1. 不可逆结果:死亡、永久变化、无法撤销的决定",
"2. 因果关系:事件 A 导致事件 B 的逻辑链",
"3. 未解决伏笔:尚未揭示的悬念、未完成的任务",
"4. 关键情感转折:角色关系的重大变化",
"5. 去除:重复描述、日常寒暄、低信息密度内容",
"",
"二、压缩约束",
"- 目标 150 字左右,不超过 300 字",
"- 第三方客观视角,不加主观判断",
"- 保留时间线信息(先后顺序不可错乱)",
"",
"三、自检(压缩后逐项核查)",
"□ 关键因果链是否完整保留?",
"□ 是否有重要信息被遗漏?",
"□ 时间顺序是否正确?",
"□ 是否引入了原文没有的信息?",
"",
"============ 【常见错误(绝对禁止)】============",
"- 丢失关键因果关系",
"- 混淆不同角色的经历",
"- 加入原始节点中不存在的推测",
"- 输出超过 300 字",
].join("\n"),
},
synopsis: {
role: "你是故事概要生成器。根据事件线、角色和主线生成简洁的前情提要。",
format: '输出 JSON{\"summary\": \"前情提要文本200字以内\"}',
rules: "要求:涵盖核心冲突、关键转折、主要角色当前状态。",
role: [
"你是故事概要生成执行 AI。根据事件线、角色状态和主线信息生成简洁的前情提要。",
"必须覆盖核心冲突、关键转折角色当前状态。",
].join("\n"),
format: '输出 JSON{"summary": "前情提要文本200字以内"}',
rules: [
"============ 【核心规则 - HARD GATE】============",
"",
"一、覆盖要素(缺一不可)",
"1. 核心冲突:当前故事的主要矛盾是什么",
"2. 关键转折:近期发生的改变局势的事件",
"3. 角色状态:主要角色当前的处境和关系",
"",
"二、写作约束",
"- 200 字以内",
"- 按时间线顺序组织",
"- 使用第三方叙述视角",
"- 不要罗列事件清单,要有叙事连贯性",
"",
"============ 【常见错误(绝对禁止)】============",
"- 超过 200 字",
"- 遗漏核心冲突或主要角色",
"- 写成事件列表而非连贯叙述",
"- 加入个人评价或预测",
].join("\n"),
},
reflection: {
role: "你是 RP 长期记忆系统的反思生成器。",
format: '输出严格 JSON{\"insight\":\"...\",\"trigger\":\"...\",\"suggestion\":\"...\",\"importance\":1-10}',
rules: "- insight 应总结最近情节中最值得长期保留的变化、关系趋势或潜在线索\n- trigger 说明触发这条反思的关键事件或矛盾\n- suggestion 给出后续检索或叙事上值得关注的提示\n- 不要复述全部事件,要提炼高层结论",
role: [
"你是长期记忆反思执行 AI。从近期事件中提炼长期趋势、潜在线索和值得关注的变化。",
"重点关注:角色关系走向、未解悬念、可能的伏笔。",
].join("\n"),
format: '输出严格 JSON{"insight":"...","trigger":"...","suggestion":"...","importance":1-10}',
rules: [
"============ 【核心规则 - HARD GATE】============",
"",
"一、反思维度",
"1. insight最值得长期保留的变化/关系趋势/潜在线索",
"2. trigger触发这条反思的关键事件或矛盾",
"3. suggestion后续叙事中值得关注或检索的方向",
"",
"二、写作约束",
"- 不要复述事件详情,要提炼高层结论",
"- insight 应具有长期参考价值(数十轮后仍有意义)",
"- importance 严格按影响范围评分,不要全给高分",
"",
"============ 【常见错误(绝对禁止)】============",
"- 复述全部事件而非提炼结论",
"- insight 写成事件摘要而非趋势分析",
"- importance 全部给 8+(应区分轻重)",
"- trigger 为空或过于笼统",
].join("\n"),
},
};

View File

@@ -13,6 +13,7 @@ import {
import { callLLMForJSON } from "./llm.js";
import { buildTaskPrompt } from "./prompt-builder.js";
import { applyTaskRegex } from "./task-regex.js";
import { getSTContextForPrompt } from "./st-context.js";
import { findSimilarNodesByText, validateVectorConfig } from "./vector-index.js";
function createAbortError(message = "操作已终止") {
@@ -425,6 +426,7 @@ async function llmRecall(
candidateNodes: candidateDescriptions,
candidateText: candidateDescriptions,
graphStats: `candidate_count=${candidates.length}`,
...getSTContextForPrompt(),
});
const systemPrompt = applyTaskRegex(
settings,

45
st-context.js Normal file
View File

@@ -0,0 +1,45 @@
// ST-BME: SillyTavern 上下文数据读取辅助
// 为 prompt 变量扩展Phase 2提供统一的 ST 上下文数据接口
import { getContext } from "../../../extensions.js";
/**
* 从 SillyTavern 的 getContext() 提取当前上下文数据,
* 返回的字段可直接展开传入 buildTaskPrompt 的 context 参数,
* 用户在自定义 prompt 块中可通过 {{key}} 引用。
*
* @returns {object} 上下文字段映射
*/
export function getSTContextForPrompt() {
try {
const ctx = getContext?.() || {};
const charId = ctx.characterId;
const char =
ctx.characters?.[Number(charId)] ||
ctx.characters?.[charId] ||
null;
return {
userPersona:
ctx.powerUserSettings?.persona_description ||
ctx.name1_description ||
"",
charDescription:
char?.description ||
char?.data?.description ||
"",
charName: ctx.name2 || "",
userName: ctx.name1 || "",
currentTime: new Date().toLocaleString("zh-CN"),
};
} catch (e) {
console.warn("[ST-BME] getSTContextForPrompt 失败:", e);
return {
userPersona: "",
charDescription: "",
charName: "",
userName: "",
currentTime: new Date().toLocaleString("zh-CN"),
};
}
}