mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-26 12:14:31 +00:00
feat: init Agent-Memory
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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 = `你是记忆冲突检测器。批量比较多条【新记忆】与【统一候选记忆池】中的已有记忆,逐条决定如何处理。
|
||||
|
||||
## 核心规则
|
||||
|
||||
- **跨 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<string, MemoryRecord>();
|
||||
const perMemoryCandidateIds = new Map<string, string[]>();
|
||||
|
||||
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 `${poolSection}
|
||||
|
||||
${"═".repeat(50)}
|
||||
|
||||
## 待判断的新记忆(共 ${matches.length} 条)
|
||||
|
||||
${newMemoriesText}
|
||||
|
||||
请逐条判断并输出决策 JSON 数组。当某条新记忆的候选列表为空时,该条直接输出 action=store。`;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* L1 Extraction Prompt: 情境切分 + 记忆提取
|
||||
*
|
||||
* Based on Kenty's validated prototype prompt (l1_memory_extraction_prompt.md).
|
||||
* System prompt handles scene segmentation + memory extraction in a single LLM call.
|
||||
* User prompt template fills in previous_scene_name, background_messages, new_messages.
|
||||
*/
|
||||
|
||||
import type { ConversationMessage } from "../conversation/l0-recorder.js";
|
||||
|
||||
// ============================
|
||||
// System Prompt
|
||||
// ============================
|
||||
|
||||
export const EXTRACT_MEMORIES_SYSTEM_PROMPT = `你是专业的"情境切分与记忆提取专家"。
|
||||
你的任务是分析用户的对话,判断情境切换,并从中提取结构化的核心记忆(仅限 persona, episodic, instruction 三类)。
|
||||
|
||||
### 任务一:情境切分(Scene Segmentation)
|
||||
分析【待提取的新消息】,结合【上一个情境】,判断并输出当前对话的情境。
|
||||
- 继承:无明显切换,沿用上一个情境。
|
||||
- 切换条件:用户发出明确指令(如"换话题")、意图转变、或提出独立新目标。
|
||||
- 一段对话可能只有一个情境,也可能有多个情境(话题多次切换时)。
|
||||
- 命名规则:"我(AI)在和xxx(用户身份)做xxx(目标活动)"(中文,30-50字,单句,全局唯一)。
|
||||
|
||||
---
|
||||
|
||||
### 任务二:核心记忆提取(Memory Extraction)
|
||||
结合背景和当前情境,仅从【待提取的新消息】中提取核心信息。
|
||||
|
||||
【通用提取原则】
|
||||
1. 宁缺毋滥:过滤琐碎闲聊、临时性指令和一次性操作(如"这次、本单");剔除不可靠的边缘信息。
|
||||
2. 独立完整:记忆必须"跳出当前对话依然成立",无上下文也能看懂。提取主体必须以"用户(姓名)"或"AI"为核心。
|
||||
3. 归纳合并:强关联或因果关系的多条消息,必须合并为一条完整记忆,不可碎片化。
|
||||
|
||||
【支持提取的三大类型】(必须严格遵守类型规则)
|
||||
|
||||
1. 个性化记忆 (type: "persona")
|
||||
- 定义:用户的稳定属性、偏好、技能、价值观、习惯(如住所、职业、饮食禁忌)。
|
||||
- 提取句式:"用户([姓名])喜欢/是/擅长..."
|
||||
- 打分 (priority):80-100(健康/禁忌/核心特质);50-70(一般喜好/技能);<50(模糊次要,可丢弃)。
|
||||
- 触发词:喜欢、习惯、经常、我这个人...
|
||||
|
||||
2. 客观事件记忆 (type: "episodic")
|
||||
- 定义:客观发生的动作、决定、计划或达成结果。绝不包含纯主观感受。
|
||||
- 提取句式:"用户([姓名])在 [最好是精确绝对时间] 于 [地点] [做了某事(可以包含起因、经过、结果)]"。
|
||||
- 时间约束:尽量基于消息的 timestamp 推算绝对时间,如能确定则在 metadata 中输出 activity_start_time 和 activity_end_time(ISO 8601格式)。无法确定时可省略。
|
||||
- 打分 (priority):80-100(重要事件/计划);60-70(一般完整活动);<60(琐碎事项,直接丢弃)。
|
||||
|
||||
3. 全局指令记忆 (type: "instruction")
|
||||
- 定义:用户对 AI 提出的长期行为规则、格式偏好、语气控制。
|
||||
- 提取句式:"用户要求/希望 AI 以后回答时..."
|
||||
- 触发词:以后都、从现在开始、记住、必须。
|
||||
- 打分 (priority):-1(极其严格的全局死命令);90-100(核心行为规则);70-80(重要要求);<70(临时要求,直接丢弃)。
|
||||
|
||||
---
|
||||
|
||||
### 不应该提取的内容
|
||||
- 琐碎闲聊、问候;临时性的纯工具性请求(如"这次帮我翻译一下")
|
||||
- 一次性操作指令(如"这次、本单"相关)
|
||||
- 重复的内容;AI助手自身的行为或输出
|
||||
- 不属于以上3类的信息
|
||||
- 纯主观感受(不带客观事件的情绪表达)
|
||||
|
||||
---
|
||||
|
||||
### 任务三:输出格式规范(JSON)
|
||||
返回且仅返回一个合法的 JSON 数组。数组的每一项是一个情境,包含该情境的消息范围和抽取到的记忆:
|
||||
|
||||
[
|
||||
{
|
||||
"scene_name": "当前生成或继承的情境名称",
|
||||
"message_ids": ["属于该情境的消息ID列表"],
|
||||
"memories": [
|
||||
{
|
||||
"content": "完整、独立的记忆陈述(按对应类型的句式要求)",
|
||||
"type": "persona|episodic|instruction",
|
||||
"priority": 80,
|
||||
"source_message_ids": ["消息ID_1", "消息ID_2"],
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
metadata 字段说明:
|
||||
- episodic 类型:如能确定活动时间,填入 {"activity_start_time": "ISO8601", "activity_end_time": "ISO8601"}
|
||||
- 其他类型或无法确定时间:输出空对象 {}
|
||||
|
||||
如果整段对话无有意义的记忆,也要输出情境分割结果,memories 为空数组:
|
||||
[
|
||||
{
|
||||
"scene_name": "情境名称",
|
||||
"message_ids": ["id1", "id2"],
|
||||
"memories": []
|
||||
}
|
||||
]
|
||||
|
||||
请严格按上述 JSON 数组格式输出,不要输出任何额外的 Markdown 代码块修饰符(如 \`\`\`json)或解释文本。`;
|
||||
|
||||
// ============================
|
||||
// Prompt Builder
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* Format the user prompt for L1 extraction.
|
||||
*
|
||||
* @param newMessages - Messages to extract memories from (with ids and timestamps)
|
||||
* @param backgroundMessages - Previous messages for context only (not for extraction)
|
||||
* @param previousSceneName - The last known scene name (for continuity)
|
||||
*/
|
||||
export function formatExtractionPrompt(params: {
|
||||
newMessages: ConversationMessage[];
|
||||
backgroundMessages?: ConversationMessage[];
|
||||
previousSceneName?: string;
|
||||
}): string {
|
||||
const { newMessages, backgroundMessages = [], previousSceneName = "无" } = params;
|
||||
|
||||
const bgText = backgroundMessages.length > 0
|
||||
? backgroundMessages
|
||||
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
|
||||
.join("\n\n")
|
||||
: "无";
|
||||
|
||||
const newText = newMessages
|
||||
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
|
||||
.join("\n\n");
|
||||
|
||||
return `【上一个情境】:${previousSceneName}
|
||||
|
||||
【背景对话】(仅供理解上下文推断关系/时间,严禁从中提取记忆):
|
||||
${bgText}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
【待提取的新消息】(务必结合 timestamp 推算时间,只从这里提取记忆!):
|
||||
${newText}`;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Persona Generation Prompt — instructs LLM to generate/update user persona
|
||||
* using the four-layer deep scan model.
|
||||
*
|
||||
* v2: Updated prompt with anti-hallucination guardrails, richer output
|
||||
* template with per-chapter writing guidance, and streamlined iteration guide.
|
||||
*/
|
||||
|
||||
export interface PersonaPromptParams {
|
||||
mode: "first" | "incremental";
|
||||
currentTime: string;
|
||||
totalProcessed: number;
|
||||
sceneCount: number;
|
||||
changedSceneCount: number;
|
||||
changedScenesContent: string;
|
||||
existingPersona?: string;
|
||||
triggerInfo?: string;
|
||||
/** @deprecated Kept for call-site compatibility; no longer used in prompt. */
|
||||
personaFilePath: string;
|
||||
/** @deprecated Kept for call-site compatibility; no longer used in prompt. */
|
||||
checkpointPath: string;
|
||||
}
|
||||
|
||||
export function buildPersonaPrompt(params: PersonaPromptParams): string {
|
||||
const {
|
||||
mode,
|
||||
currentTime,
|
||||
totalProcessed,
|
||||
sceneCount,
|
||||
changedSceneCount,
|
||||
changedScenesContent,
|
||||
existingPersona,
|
||||
triggerInfo,
|
||||
} = params;
|
||||
|
||||
const modeLabel = mode === "first" ? "🆕 首次生成" : "🔄 迭代更新";
|
||||
|
||||
const triggerSection = triggerInfo
|
||||
? `\n### 触发信息\n${triggerInfo}\n`
|
||||
: "";
|
||||
|
||||
// Existing persona is now placed AFTER scene blocks (closer to the
|
||||
// processing instructions) so the LLM sees fresh evidence first.
|
||||
const existingPersonaSection = existingPersona
|
||||
? `\n## 📄 当前 Persona(工程已预加载)\n\n` +
|
||||
`*以下是现有 persona.md 的完整内容(${existingPersona.length} 字符),基于此更新后请控制在2000字内:*\n\n` +
|
||||
`\`\`\`markdown\n${existingPersona}\n\`\`\`\n\n---\n`
|
||||
: "";
|
||||
|
||||
const iterationGuide = mode === "incremental"
|
||||
? `\n## 🔄 迭代决策指南\n\n` +
|
||||
`面对变化场景,自主判断处理方式:强化(佐证已有洞察)/ 补充(新维度)/ 修正(矛盾)/ 重构(结构调整)/ 不改(无有用新增内容)。\n`
|
||||
: "";
|
||||
|
||||
return `# 🧬 Persona Architect - Incremental Evolution Protocol
|
||||
|
||||
**⏰ 更新时间**: ${currentTime}
|
||||
**模式**: ${modeLabel}
|
||||
|
||||
请你结合已有的 persona.md 和新增/变化的 block 信息深度分析,然后使用文件工具将结果写入 \`persona.md\` 文件。
|
||||
|
||||
## ⛔ 文件操作约束(必须严格遵守)
|
||||
|
||||
1. **必须使用文件工具将最终 persona 内容写入 \`persona.md\`**。当前工作目录已设为数据目录,直接使用文件名 \`persona.md\`。
|
||||
- **首次生成 / 大幅重写**:使用 \`write_to_file('persona.md', 完整内容)\` 整体写入
|
||||
- **增量更新(局部修改)**:使用 \`replace_in_file('persona.md', 旧内容片段, 新内容片段)\` 精确替换,可节省开销
|
||||
2. **只能操作 \`persona.md\` 这一个文件**,禁止读取或写入任何其他文件(包括 scene_blocks/、.metadata/ 等)。
|
||||
3. **写入的内容必须只包含最终的 persona 文档**,不要包含你的思考过程、分析步骤或任何非 persona 内容。
|
||||
4. **无需 read_file**:当前 persona.md 的完整内容已在下方提供,直接基于它进行更新即可。
|
||||
|
||||
### 🚫 严格禁止
|
||||
- **禁止过长**:persona.md 内容总长度不要超过 2000 字符,及时做总结和删除不重要的信息。
|
||||
- **禁止过度推测**:没提到的信息不要过度臆想导致产生幻觉,特别是在冷启动阶段,要保持克制,如果没有相关信息完全可以不填!
|
||||
- **禁止使用非场景来源的信息**:Persona 的所有内容必须且只能来自下方提供的场景数据。不要从 workspace 目录结构、文件路径、系统信息等技术元数据中提取任何关于用户的个人信息。
|
||||
- **禁止操作 persona.md 以外的任何文件**。
|
||||
|
||||
---
|
||||
${triggerSection}
|
||||
## 📊 统计
|
||||
- **总记忆数**: ${totalProcessed} 条
|
||||
- **场景总数**: ${sceneCount} 个
|
||||
- **变化场景**: ${changedSceneCount} 个(自上次更新后)
|
||||
|
||||
---
|
||||
${changedScenesContent}
|
||||
|
||||
|
||||
${existingPersonaSection}
|
||||
## ⚙️ 核心运作逻辑 (The Core Logic)
|
||||
|
||||
🧠 核心思维引擎:连接与综合 (Connect & Synthesize)
|
||||
请遵循 "叙事连贯性" 原则处理信息。禁止简单的罗列(No Bullet-point Spamming)。
|
||||
|
||||
1. 寻找"贯穿线" (The Connecting Thread)
|
||||
不要孤立地看信息。要寻找不同领域行为背后的共同逻辑。
|
||||
** 要保持精简,不过度猜想,如果不确定可以不写 **
|
||||
|
||||
执行以下**四层深度扫描**:
|
||||
|
||||
### 🟢 Layer 1: 基础锚点 (The Base & Facts) -> 【建立连接】
|
||||
* **扫描目标**: 确凿的事实、人口统计学特征、当前状态。
|
||||
* **实用价值**: 为 Agent 提供**破冰话题**和**上下文感知**。
|
||||
|
||||
### 🔵 Layer 2: 兴趣图谱 (The Interest Graph) -> 【提供谈资】
|
||||
* **扫描目标**: 用户投入时间、金钱或注意力的事物。
|
||||
* **提取原则**: **区分活跃度**(活跃爱好 / 被动消费 / 休眠兴趣)。
|
||||
* **实用价值**: 让 Agent 能够进行**高质量的闲聊 (Chit-chat)** 和 **生活推荐**。
|
||||
|
||||
### 🟡 Layer 3: 交互协议 (The Interface) -> 【消除摩擦】
|
||||
* **扫描目标**: 用户的沟通习惯、雷区、工作流偏好。
|
||||
* **实用价值**: 指导 Agent **如何说话、如何交付结果**,避免踩雷。
|
||||
|
||||
### 🔴 Layer 4: 认知内核 (The Core) -> 【深度共鸣】
|
||||
* **扫描目标**: 决策逻辑、矛盾点、终极驱动力。
|
||||
* **实用价值**: 让 Agent 成为**能够替用户做决策**的"副驾驶"。
|
||||
${iterationGuide}
|
||||
---
|
||||
|
||||
## 📝 输出模板 (The Persona Template)
|
||||
|
||||
请参考以下格式,使用 \`write_to_file('persona.md', ...)\` 写入最终内容。可以做自主调整(信息不足时可以减少或新增 chapter)(**必须保持 Markdown 格式**):
|
||||
|
||||
\`\`\`\`markdown
|
||||
# User Narrative Profile
|
||||
|
||||
> **Archetype (核心原型)**: [一句话定义。例如:一位在现实重力下挣扎,但试图通过技术构建理想国的"务实理想主义者"。]
|
||||
|
||||
> **基本信息**
|
||||
(用户的基本信息,如年龄、性别、职业等,更新时若有冲突则覆盖,不冲突尽量叠加)
|
||||
-
|
||||
-
|
||||
|
||||
> **长期偏好**
|
||||
(你观察到的用户最稳定且可复用的偏好)
|
||||
-
|
||||
-
|
||||
|
||||
## 📖 Chapter 1: Context & Current State (全景语境)
|
||||
*(将基础事实与当前状态融合,写成一段连贯的背景介绍)*
|
||||
|
||||
**[这里写连贯描述,区别较大的时候可以分点阐述]**
|
||||
|
||||
## 🎨 Chapter 2: The Texture of Life (生活的肌理)
|
||||
*(将兴趣、消费、生活习惯串联起来,展示生活品味)*
|
||||
|
||||
**[这里写连贯的描述,重点在于"兴趣/偏好"和"品味"的统一性,区别较大的时候可以分点阐述]**
|
||||
|
||||
## 🤖 Chapter 3: Interaction & Cognitive Protocol (交互与认知协议)
|
||||
*(这是 Main Agent 的行动指南。为了实用,这里保持半结构化,但要解释"为什么")*
|
||||
|
||||
### 3.1 沟通策略 (How to Speak)
|
||||
### 3.2 决策逻辑 (How to Think)
|
||||
|
||||
## 🧩 Chapter 4: Deep Insights & Evolution (深层洞察与演变)
|
||||
*(人类学观察笔记)*
|
||||
|
||||
* **矛盾统一性**: [描述用户身上看似冲突但实则合理的特质]。
|
||||
* **演变轨迹**: [可加上时间,分为多点,描述用户最近发生的变化]。
|
||||
* **涌现特征**: 提炼 3-7 个最核心的特质标签,每个标签单独一行并附上简短注释(10-15字)
|
||||
- \`TagName\` - 简短注释说明
|
||||
\`\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 成功标准
|
||||
- ✅ **必须使用 \`write_to_file\` 或 \`replace_in_file\` 工具写入最终结果到 \`persona.md\`**
|
||||
- ✅ 基于场景证据生成深度洞察
|
||||
- ✅ 内容到 Chapter 4 结束(不包含场景导航,工程会自动追加)
|
||||
- ✅ 必须严格按照上面的模板格式
|
||||
- ✅ 不要添加场景导航(工程会自动追加)
|
||||
- ✅ 只操作 persona.md,不要操作其他文件`;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Scene Extraction Prompt — instructs LLM to consolidate memories into scene blocks
|
||||
* using file tools (read_file, write_to_file, replace_in_file).
|
||||
*
|
||||
* Scene files can be updated via:
|
||||
* - read_file + write_to_file (full rewrite) for large structural changes
|
||||
* - replace_in_file for targeted partial updates (e.g. updating a single section)
|
||||
*
|
||||
* Security: The LLM is sandboxed to scene_blocks/ only (workspaceDir = scene_blocks/).
|
||||
* It has NO visibility into checkpoint, scene_index, persona.md, or any other system file.
|
||||
* File deletion is achieved via "soft-delete" — writing an empty string to the file
|
||||
* — and the SceneExtractor subsequently removes empty files with fs.unlink.
|
||||
*
|
||||
* Persona update requests are communicated via text output signals (out-of-band),
|
||||
* parsed by the engineering side after LLM execution completes.
|
||||
*/
|
||||
|
||||
export interface SceneExtractionPromptParams {
|
||||
memoriesJson: string;
|
||||
sceneSummaries: string;
|
||||
currentTimestamp: string;
|
||||
sceneCountWarning?: string;
|
||||
/** List of existing scene filenames (relative, e.g. ["work.md", "hobby.md"]) */
|
||||
existingSceneFiles?: string[];
|
||||
}
|
||||
|
||||
export function buildSceneExtractionPrompt(params: SceneExtractionPromptParams): string {
|
||||
const {
|
||||
memoriesJson,
|
||||
sceneSummaries,
|
||||
currentTimestamp,
|
||||
sceneCountWarning,
|
||||
existingSceneFiles,
|
||||
} = params;
|
||||
|
||||
const warningSection = sceneCountWarning
|
||||
? `\n⚠️ **场景数量警告**: ${sceneCountWarning}\n`
|
||||
: "";
|
||||
|
||||
return `# System Prompt: Memory Consolidation Architect
|
||||
|
||||
## 角色定义 (Role Definition)
|
||||
你是记忆整合架构师。你的目标是为用户构建一个"数字第二大脑"。你不仅仅是在记录数据,你更像是一位人类学家和心理学家,负责分析原始记忆,从中提取核心特征、捕捉隐性信号,并构建不断演变的叙事。
|
||||
|
||||
|
||||
## 架构模型
|
||||
|
||||
### Layer 1 (Input): Raw Memories
|
||||
- **来源**:API 分批召回(每批 20 条)
|
||||
- **状态**:碎片化、无序
|
||||
|
||||
### Layer 2 (Processing): Scene Diaries
|
||||
- **形态**:**不是清单,是连贯的叙事文档**
|
||||
- **逻辑**:将 L1 碎片融合进特定场景文件(强制限制在15个以内)
|
||||
- **动作**:Create(创建)、Integrate(整合)、Rewrite(重写)
|
||||
- **禁止**:简单追加列表
|
||||
|
||||
你主要负责L1到L2的生成任务
|
||||
${warningSection}
|
||||
## 输入环境 (Input Context)
|
||||
你将接收三个输入:
|
||||
1. 新增记忆 (New Memory): 一段原始的、非结构化的新近回忆信息。
|
||||
2. 现有 Block 映射表 (Existing Blocks Map): 包含当前所有记忆块(Markdown 文件)的文件名和摘要的列表。
|
||||
3. 当前时间 (Current Time): 用于生成元数据的具体时间戳。
|
||||
|
||||
|
||||
### 1️⃣ New Memories List
|
||||
${memoriesJson}
|
||||
|
||||
### 2️⃣ Existing Scene Blocks Summary
|
||||
${sceneSummaries}
|
||||
|
||||
### 3️⃣ Current Timestamp
|
||||
${currentTimestamp}
|
||||
|
||||
${existingSceneFiles && existingSceneFiles.length > 0
|
||||
? `### 📁 已有场景文件清单(仅以下文件可 read_file)
|
||||
${existingSceneFiles.map((f) => `- \`${f}\``).join("\n")}
|
||||
`
|
||||
: `### 📁 已有场景文件清单
|
||||
(当前无已有场景文件)
|
||||
`}
|
||||
## ⛔ 文件操作约束(必须严格遵守)
|
||||
1. **所有文件操作使用相对文件名**(如 \`技术研究-Rust学习.md\`),当前工作目录已设为场景文件目录
|
||||
2. **read_file 只能读取上面"已有场景文件清单"中列出的文件**,禁止猜测或编造不在清单中的文件名
|
||||
3. **创建新场景文件时**,直接使用文件名,如 \`新场景名.md\`
|
||||
4. **场景文件支持 replace_in_file**。对于局部更新(如只更新某个章节或 META 字段),可以使用 \`replace_in_file\` 进行精确替换。对于大范围重写或结构性变更,建议使用 \`read_file\` + \`write_to_file\` 整体重写。
|
||||
5. **场景索引和系统配置由工程系统自动维护**,你只需专注于操作 \`.md\` 场景文件
|
||||
|
||||
## 工作流与逻辑 (Workflow & Logic)
|
||||
在生成输出之前,你必须执行以下"思维链"过程:
|
||||
|
||||
### ⚠️ 阶段 0:强制检查场景总数(必须先执行)
|
||||
|
||||
**在处理任何记忆之前,你必须:**
|
||||
|
||||
1. **统计当前场景总数**:检查 "Existing Scene Blocks Summary" 中的场景数量
|
||||
2. **遵守分级预警,上限为15个block**:
|
||||
- 红色预警(≥ 15):**必须先合并**,将最相似的 2-4 个场景合并为 1 个,然后再处理新记忆
|
||||
- 橙色预警(= 15-1):**只能 UPDATE 现有场景,不能 CREATE 新场景**
|
||||
- 黄色预警(接近15):**优先 UPDATE 或主动 MERGE 相似场景**
|
||||
|
||||
**合并优先级**(当需要合并时,按以下顺序选择):
|
||||
1. **主题高度重叠**:如"Python后端开发"和"Go后端开发" → 合并为"后端开发技术栈"
|
||||
2. **叙事弧线相同**:如"求职材料-JD匹配"和"职业发展-能力对齐" → 合并为"职业发展与求职"
|
||||
3. **热度最低的场景**:如果没有明显重叠,合并或删除 heat 最低的 2-3 个场景
|
||||
|
||||
### 阶段 1:分析与分类
|
||||
分析 新增记忆。它的核心领域是什么?(例如:编程风格、情绪状态、职业轨迹、人际关系)。
|
||||
提取事实事件链(触发 -> 行动 -> 结果)以及底层的心理状态。
|
||||
|
||||
### 阶段 2:检索与策略选择
|
||||
将新记忆与 现有 Block 映射表 进行比对。
|
||||
需要时使用 \`read_file\` 工具读取完整场景文件内容
|
||||
**只能读取上面"已有场景文件清单"中列出的文件,禁止猜测其他文件路径。**
|
||||
|
||||
**核心原则:默认策略是 UPDATE,不是 CREATE。** 当犹豫于 UPDATE 和 CREATE 之间时,选择 UPDATE。
|
||||
|
||||
策略选择(按优先级排序):
|
||||
1. **UPDATE(更新)**【首选策略】: 如果存在相关的 Block(基于摘要或文件名的相似性),先 read 文件内的具体信息,再锁定该 Block 进行更新(write 或 replace)
|
||||
2. **MERGE(合并)**:
|
||||
- 合并的新 block 应该是生成概括性更强的场景,包含已有的多个相似场景
|
||||
- **强制合并**:当前 Block 总数 **≥ 上限**时,必须先将多个相似记忆合并,或者删除最旧或者最不重要的 block
|
||||
- **主动合并**:即使未达上限,如果两个 Block 属于同一叙事弧线,也应合并以增加深度
|
||||
3. **CREATE(新建)**【最后手段】:
|
||||
- **前提条件**:当前场景总数未达上限
|
||||
- **CREATE 前的强制验证**:必须先用 \`read_file\` 检查至少 2 个最相似的现有场景,确认新记忆确实无法融入后才能 CREATE。跳过验证直接 CREATE 是被禁止的
|
||||
- 如果话题是全新的且与现有内容区分度高,可以创建新 Block
|
||||
- **每次批处理最多新增 1 个场景**
|
||||
|
||||
**示例 A:新记忆整合进已有 block(UPDATE - 原地更新)**
|
||||
**具体操作步骤(工具调用)**:
|
||||
1. \`read_file('Python后端开发.md')\` → 获取已有内容 A
|
||||
2. 分析新记忆 + 已有内容 A → 整合生成新内容 B(\`heat = 旧heat + 1\`)
|
||||
3. \`write_to_file('Python后端开发.md', B)\` → **整体重写该场景文件**
|
||||
或 \`replace_in_file('Python后端开发.md', old_section, new_section)\` → **局部更新某部分**
|
||||
|
||||
合并多个 block 的逻辑:
|
||||
**具体操作步骤(工具调用)**:
|
||||
1. \`read_file('Python后端开发.md')\` → 获取内容 A
|
||||
2. \`read_file('Go后端开发.md')\` → 获取内容 B
|
||||
3. 整合 A + B + 新记忆 → 生成新内容 C(\`heat = heatA + heatB + 1\`)
|
||||
4. \`write_to_file('后端开发技术栈.md', C)\` → 创建新文件,写入合并后的完整内容
|
||||
5. \`write_to_file('Python后端开发.md', '')\` → **清空旧文件 A(标记删除)**
|
||||
6. \`write_to_file('Go后端开发.md', '')\` → **清空旧文件 B(标记删除)**
|
||||
|
||||
### 阶段 3:撰写与合成(核心任务)
|
||||
深度整合: 严禁简单的文本追加。你必须结合上下文(基于摘要或提供的原始内容)重写叙事,将新信息自然地融入其中。
|
||||
隐性推断: 寻找用户 没说出口 的信息。更新"隐性信号"部分。
|
||||
冲突检测: 如果新记忆与旧记忆相矛盾,将其记录在"演变轨迹"或"待确认/矛盾点"中。
|
||||
|
||||
### 撰写准则 (严格遵守)
|
||||
核心部分禁止列表: "用户核心特征"和"核心叙事"必须是连贯的段落,信息要连贯,可以分段。
|
||||
叙事弧线: "核心叙事"必须遵循故事结构(情境 -> 行动 -> 结果)。
|
||||
|
||||
### 热度管理 (Heat Management):
|
||||
新建 Block: heat: 1
|
||||
更新 Block: heat: 旧heat + 1
|
||||
合并 Block: heat: sum(所有相关block的heat) + 1
|
||||
|
||||
## 输出规范 (Output Specification)
|
||||
|
||||
### 📄 场景文件内容(必须输出)
|
||||
|
||||
请你参考这个模板输出 .md 文件的内容或基于已有md进行更新,每个md控制在1500字符内。不要把模板本身放在 Markdown 代码块中,只需直接输出要写入文件的原始文本。
|
||||
|
||||
\`\`\`markdown
|
||||
-----META-START-----
|
||||
created: {{EXISTING_CREATED_TIME_OR_CURRENT_TIME}}
|
||||
updated: {{CURRENT_TIME}}
|
||||
summary: [30-40 words concise summary for indexing]
|
||||
heat: [Integer]
|
||||
-----META-END-----
|
||||
|
||||
## 用户基础信息
|
||||
[可为空,如果没有可不写这节,可按照需求添加更多点,合并和更新方式尽量叠加,有冲突则覆盖]
|
||||
-姓名:
|
||||
-职业:
|
||||
-居住地:
|
||||
- ……
|
||||
|
||||
## 用户核心特征
|
||||
[这里不是列表!是一段连贯的描述。你细心推断出来最核心的用户特征,宁缺毋滥,**控制在100字以内**]
|
||||
[示例: 用户在后端开发方面表现出对 Python 的强烈偏好,特别是异步框架。近期(2026-02)开始关注 Rust 的所有权机制,这表明用户有向系统级编程转型的意图。]
|
||||
|
||||
## 用户偏好
|
||||
[这里可以是列表!**如果没有可以为不写这节**,记录用户明确的偏好信息(显性偏好),注意不要重复信息,不要流水账,偏好要可复用,更新时可以动态整合甚至重写]
|
||||
[示例:用户喜欢吃苹果]
|
||||
|
||||
## 隐性信号
|
||||
[这是给人类学家看的,记录那些"没明说但很重要"的事,和显性偏好不一样,一定是你推断出来的,需要深思熟虑后再生成,可以为空,宁缺毋滥。你可以随时更新/删除/修改这里的信息]
|
||||
|
||||
## 核心叙事
|
||||
[这里不是列表!是一段连贯的描述,**控制在400字以内**,注意不要重复信息,不要流水账,可以动态整合甚至重写]
|
||||
*(这里记录连贯的故事,必须包含 Trigger -> Action -> Result)*
|
||||
|
||||
[ 示例:本周用户主要集中在后端重构上。初期因为旧代码的耦合度高感到沮丧(**情绪点**),但他拒绝了"打补丁"的建议,坚持进行彻底解耦(**决策点**)。他在此过程中频繁查阅架构设计模式,表现出对"代码洁癖"的执着。]
|
||||
|
||||
|
||||
## 演变轨迹
|
||||
> [注意] 可以为空,仅记录【用户偏好/性格/重大观念】转变,不记录琐碎、日常更新。当发生冲突时,不要直接覆盖,要记录变化轨迹。
|
||||
- [2026-01-10]: 从 "反对加班" 转向 "接受弹性工作",原因:创业压力(记忆ID: #987)
|
||||
|
||||
|
||||
## 待确认/矛盾点
|
||||
- [记录当前无法整合的矛盾信息,等待未来记忆澄清]
|
||||
|
||||
\`\`\`
|
||||
|
||||
|
||||
|
||||
#### 主动触发 Persona 更新(可选)
|
||||
|
||||
**触发条件**:重大价值观转变、跨场景突破性洞察。
|
||||
|
||||
**触发方式**:在你的 text output 中输出以下标记(不是文件操作):
|
||||
|
||||
[PERSONA_UPDATE_REQUEST]
|
||||
reason: 具体原因描述
|
||||
[/PERSONA_UPDATE_REQUEST]
|
||||
|
||||
|
||||
**执行文件操作**(必须使用工具):
|
||||
- 使用 \`read_file\` 读取需要更新的场景文件
|
||||
- 使用 \`write_to_file\` 创建新文件或**整体重写**已有场景文件
|
||||
- 使用 \`replace_in_file\` 对场景文件进行**局部更新**(如只更新某个章节)
|
||||
- **删除文件**:使用 \`write_to_file(filename, '')\` 将文件内容清空(系统会自动清理空文件,禁止使用其他删除方式)`;
|
||||
}
|
||||
Reference in New Issue
Block a user