/** * L1 Conflict Detection Prompt (Batch Mode) * * Based on Kenty's validated prototype prompt (l1_conflict_detection_prompt.md). * Batch-compares multiple new memories against a unified candidate pool, * supporting cross-type merge and multi-target operations. */ import type { MemoryRecord, ExtractedMemory } from "../record/l1-writer.js"; // ============================ // System Prompt // ============================ export const CONFLICT_DETECTION_SYSTEM_PROMPT = `你是记忆冲突检测器。批量比较多条【新记忆】与【统一候选记忆池】中的已有记忆,逐条决定如何处理。 **输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言;JSON 字段名、枚举值、record_id、ISO 时间戳保持英文。 ## 核心规则 - **跨 type 合并**:不同 type(persona / episodic / instruction)的记忆如果语义上描述同一事实/事件,**可以合并**。 - **多对多合并**:一条新记忆可以同时替换/合并候选池中的**多条**已有记忆(通过 target_ids 数组指定)。 - 合并后你必须判断新记忆的最佳 type(merged_type)。 ## 判断逻辑 1. **分辨记忆性质**: - **状态类**(persona/instruction):偏好、特质、长期设定、相对稳定的事实、行为规则 - **事件类**(episodic):一次性经历、带时间点的客观记录,建议合并同一件事的前因后果 2. **判断是否同一事实/事件**:主体相同、主题一致、时间接近、scene_name 相似 3. **选择动作**: - "store":视为新信息,新增当前记忆。 - "skip":已有记忆更好,新记忆无增量或更模糊,忽略当前记忆。 - "update":同一事实/事件,新记忆在内容或时间上更优(更具体、更晚或纠错),以新记忆为主覆盖旧记忆,可保留旧记忆中仍正确的细节。 - "merge":同一事实或同一演化过程,多条记忆信息互补且不矛盾,合并成一条更完整记忆,信息尽量不冗余。 4. **策略倾向**: - 状态类:多条描述同一偏好/特质 → 倾向 merge;无增量 → skip;明确更新 → update - 事件类:同一事件的前因后果、不同阶段 → 倾向 merge 为一条完整叙述;完全相同 → skip - 跨类型示例:一条 episodic "用户在 2018 年开始做播客" + 一条 persona "用户有播客制作经验" → 可 merge 为一条 persona 或 episodic(取决于信息侧重) 5. **timestamp 处理**: - merge / update 时,merged_timestamps 应包含**所有相关记忆的时间戳并集**(去重排序) - 这样可以保留事件发生的完整时间线 ## 输出格式 严格输出 JSON 数组,每个元素对应一条新记忆的决策。不输出任何其他内容: [ { "record_id": "新记忆的 record_id", "action": "store|update|skip|merge", "target_ids": ["要删除的候选记忆 record_id 1", "record_id 2"], "merged_content": "合并/更新后的记忆内容(merge/update 时必填)", "merged_type": "合并后的最佳 type:persona|episodic|instruction(merge/update 时必填)", "merged_priority": 85, "merged_timestamps": ["合并后的时间戳数组,包含所有新旧记忆时间戳的并集(merge/update 时必填)"] } ] 字段说明: - target_ids:要删除替换的旧记忆 ID **数组**(可以 1 条或多条)。store/skip 时省略或为空。 - merged_content:merge/update 时的最终记忆文本。store/skip 时省略。 - merged_type:merge/update 后记忆应归属的 type。根据合并后内容本质判断。 - merged_priority:merge/update 后的新优先级(0-100 整数,merge/update 时必填)。合并后信息更完整、更确定,通常应**酌情提升** priority(例如两条 priority 70 的记忆合并后可提升到 80)。参考标准:80-100(核心特质/重要事件),60-79(一般偏好/普通活动),<60(次要信息)。 - merged_timestamps:合并后的时间戳数组。收集新记忆 + 所有被合并旧记忆的时间戳,去重排序。`; // ============================ // Prompt Builder // ============================ /** * Candidate search result for a single new memory. */ export interface CandidateMatch { newMemory: ExtractedMemory & { record_id: string }; candidates: MemoryRecord[]; } /** * Format the batch conflict detection prompt using a unified candidate pool. * * Format (aligned with prototype): * 1. Unified candidate pool: de-duplicated list of all existing candidates across all new memories * 2. Per new memory: content + list of related candidate IDs from the pool * * This approach lets the LLM see the global picture and handle cross-memory dedup in one pass. * * @param matches - Array of new memories with their candidate matches */ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string { // Step 1: Build unified candidate pool (de-duplicate across all new memories) const unifiedPool = new Map(); const perMemoryCandidateIds = new Map(); for (const m of matches) { const candidateIds: string[] = []; for (const c of m.candidates) { if (!unifiedPool.has(c.id)) { unifiedPool.set(c.id, c); } candidateIds.push(c.id); } perMemoryCandidateIds.set(m.newMemory.record_id, candidateIds); } // Step 2: Format unified pool as JSON const poolList = Array.from(unifiedPool.values()).map((c) => ({ record_id: c.id, content: c.content, type: c.type, priority: c.priority, scene_name: c.scene_name, timestamps: c.timestamps, })); let poolSection: string; if (poolList.length === 0) { poolSection = "## 统一候选记忆池\n\n(空,没有已有记忆,所有新记忆直接 store)"; } else { const poolStr = JSON.stringify(poolList, null, 2); poolSection = `## 统一候选记忆池(共 ${poolList.length} 条已有记忆)\n\n${poolStr}`; } // Step 3: Format each new memory with its related candidate IDs const memoryParts = matches.map((m, idx) => { const relatedIds = perMemoryCandidateIds.get(m.newMemory.record_id) ?? []; const relatedNote = relatedIds.length > 0 ? JSON.stringify(relatedIds) : "[](无相似候选,直接 store)"; const memStr = JSON.stringify( { record_id: m.newMemory.record_id, content: m.newMemory.content, type: m.newMemory.type, priority: m.newMemory.priority, scene_name: m.newMemory.scene_name, }, null, 2, ); return `### 第 ${idx + 1} 条新记忆 (record_id: ${m.newMemory.record_id})\n${memStr}\n\n【关联候选 ID】${relatedNote}`; }); const newMemoriesText = memoryParts.join( "\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n", ); // Step 4: Assemble final prompt return `**输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言。 ${poolSection} ${"═".repeat(50)} ## 待判断的新记忆(共 ${matches.length} 条) ${newMemoriesText} 请逐条判断并输出决策 JSON 数组。当某条新记忆的候选列表为空时,该条直接输出 action=store。`; }