Commit 2e6f62a519aca4f3e0d99110a2455adfd3c1739e
1 parent
ef48bf0b
feat: implement Memory Phase 2 — hippocampus retrieval + cerebellum plans
- Hippocampus keyword matching engine (extractKeywords + score) - Cerebellum plan storage (saveCerebellumPlan) - Auto-sync hippocampus-index.json + hippocampus.md - POST /:agentId/memory/cerebellum API endpoint - buildSystemPrompt injects matched plans from hippocampus - Use count & lastUsed tracking on plan access - Chinese n-gram + English word tokenizer with stop words - Fix: score formula uses entry keyword count as denominator - Fix: useCount updates original index entry not spread copy - Verified: OKR plan matched + injected into CEO prompt
Showing
9 changed files
with
573 additions
and
7 deletions
data/agents/ceo/memory/cerebellum/blm战略规划.md
0 → 100644
| 1 | +# 方案: BLM战略规划 | |
| 2 | + | |
| 3 | +> 创建时间: 2026-03-25 | |
| 4 | + | |
| 5 | +## 适用条件 | |
| 6 | +- 需要做年度或长期战略规划 | |
| 7 | +- 需要分析业绩/机会差距 | |
| 8 | + | |
| 9 | +## 执行步骤 | |
| 10 | +1. 双差分析:业绩差距+机会差距 | |
| 11 | +2. 战略意图四维度(愿景→目标→近期→指标) | |
| 12 | +3. 市场洞察(PEST+5Forces) | |
| 13 | +4. 创新焦点识别 | |
| 14 | +5. 业务设计五要素(客户×价值×盈利×范围×控制) | |
| 15 | +6. 执行体系(关键任务+组织+人才+氛围) | |
| 16 | + | |
| 17 | +## 注意事项 | |
| 18 | +- 差距是战略的起点不是终点 | |
| 19 | +- 业务设计必须有战略控制点 | ... | ... |
data/agents/ceo/memory/cerebellum/okr目标制定.md
0 → 100644
| 1 | +# 方案: OKR目标制定 | |
| 2 | + | |
| 3 | +> 创建时间: 2026-03-25 | |
| 4 | + | |
| 5 | +## 适用条件 | |
| 6 | +- 需要制定季度/年度目标 | |
| 7 | +- 需要将战略分解为可衡量指标 | |
| 8 | + | |
| 9 | +## 执行步骤 | |
| 10 | +1. 从战略目标出发识别3-5个Objective | |
| 11 | +2. 每个Objective制定2-4个Key Result | |
| 12 | +3. 用SMART原则检查每个KR | |
| 13 | +4. 层层对齐:公司OKR→部门OKR→个人OKR | |
| 14 | +5. 设定每周跟踪和月度复盘节奏 | |
| 15 | + | |
| 16 | +## 注意事项 | |
| 17 | +- O要有鼓舞性,KR要可量化 | |
| 18 | +- 目标完成率70%为最佳(太高说明目标太保守) | |
| 19 | +- 避免把KR当成任务清单 | ... | ... |
data/agents/ceo/memory/hippocampus-index.json
0 → 100644
| 1 | +[ | |
| 2 | + { | |
| 3 | + "scenario": "OKR目标制定", | |
| 4 | + "keywords": [ | |
| 5 | + "OKR", | |
| 6 | + "目标", | |
| 7 | + "制定", | |
| 8 | + "关键结果", | |
| 9 | + "objective", | |
| 10 | + "key result" | |
| 11 | + ], | |
| 12 | + "planFile": "okr目标制定.md", | |
| 13 | + "confidence": 0.85, | |
| 14 | + "useCount": 1, | |
| 15 | + "lastUsed": "2026-03-25", | |
| 16 | + "createdAt": "2026-03-25" | |
| 17 | + }, | |
| 18 | + { | |
| 19 | + "scenario": "BLM战略规划", | |
| 20 | + "keywords": [ | |
| 21 | + "BLM", | |
| 22 | + "战略", | |
| 23 | + "规划", | |
| 24 | + "差距分析", | |
| 25 | + "业务设计", | |
| 26 | + "战略制定" | |
| 27 | + ], | |
| 28 | + "planFile": "blm战略规划.md", | |
| 29 | + "confidence": 0.9, | |
| 30 | + "useCount": 0, | |
| 31 | + "lastUsed": null, | |
| 32 | + "createdAt": "2026-03-25" | |
| 33 | + } | |
| 34 | +] | |
| 0 | 35 | \ No newline at end of file | ... | ... |
data/agents/ceo/memory/hippocampus.md
0 → 100644
| 1 | +# 海马体 — 经验记忆索引 | |
| 2 | + | |
| 3 | +> 记录过往处理过的情景模式。遇到相似任务时,优先调用小脑中的固化方案。 | |
| 4 | +> ⚠️ 此文件由系统自动生成,勿手动编辑。真实数据在 hippocampus-index.json。 | |
| 5 | + | |
| 6 | +| # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 | | |
| 7 | +|---|---------|-----------|---------|--------|---------|----------| | |
| 8 | +| 1 | OKR目标制定 | OKR, 目标, 制定, 关键结果, objective, key result | okr目标制定.md | 0.85 | 0 | - | | |
| 9 | +| 2 | BLM战略规划 | BLM, 战略, 规划, 差距分析, 业务设计, 战略制定 | blm战略规划.md | 0.9 | 0 | - | | ... | ... |
data/agents/ceo/memory/memory.md
| 1 | 1 | # 我的记忆 |
| 2 | 2 | |
| 3 | 3 | ## 关键事实 |
| 4 | -- [2026-03-25] [TASK from master] → ## 战略规划的 3 个核心要点 | |
| 4 | +- [2026-03-25] [TASK from master] → **BLM 模型的核心思想**:以差距分析为起点,通过"战略制定"(市场洞察、战略意图、创新焦点、业务设计五要素)与"战略执行"(关键任务、组织、人才、文化)的双轮驱动,实现业绩差距和机会差距的闭合, | |
| 5 | +- [2026-03-25] [TASK from master] → 是的,我记得刚才我们讨论的是 **BLM 模型**(Business Leadership Model,业务领先模型)。 | |
| 6 | +- [2026-03-25] [TASK from master] → ...馈收集机制 | 编辑器产品化 | | |
| 5 | 7 | |
| 6 | 8 | ## 已验证的模式 |
| 7 | 9 | (尚无记录) | ... | ... |
data/agents/ceo/todo.md
| ... | ... | @@ -10,3 +10,6 @@ |
| 10 | 10 | | T-mn4aq3tt | 【完成首个项目剧本大纲】首个互动剧项目《深渊回响》已立项,现在需要完成详细剧本大纲。具体要求:1) 参考 workspace/project_001_proposal.md 中的项目设定;2) 设计完整的故事结构:开端(研究站失联)、发展(救援队员逐步发现真相)、高潮(核心秘密揭露)、结局(多重分支结局);3) 设计核心角色及其对话风格;4) 设计关键互动节点和分支逻辑;5) 将剧本大纲输出到 workspace/project_001_script_outline.md;6) 完成后向我汇报 | ✅ 完成 | master-001 | P1 | 2026-03-24 07:31 | - | |
| 11 | 11 | | T-mn4b2s1a | 【完成技术架构搭建】首个互动剧项目《深渊回响》剧本大纲已完成,现在需要完成技术架构搭建。具体要求:1) 参考 workspace/project_001_proposal.md 和 workspace/project_001_script_outline.md;2) 设计技术架构方案,包括:AI对话引擎选型、分支逻辑系统、用户状态管理、内容存储方案;3) 明确技术选型理由和依赖库;4) 输出技术架构文档到 workspace/project_001_tech_architecture.md;5) 完成后向我汇报,标志着 NarrativeAI Inc. 公司设立任务全部完成 | ✅ 完成 | master-001 | P0 | 2026-03-24 07:41 | - | |
| 12 | 12 | | T-mn5g4hlb | 列出你知道的关于战略规划的3个核心要点,简要回答即可,不需要使用工具。 | ✅ 完成 | master | P1 | 2026-03-25 02:50 | - | |
| 13 | +| T-mn5g8r4s | 请用一句话总结BLM模型的核心思想。不需要使用任何工具,直接回答即可。 | ✅ 完成 | master | P2 | 2026-03-25 02:53 | - | | |
| 14 | +| T-mn5g9cri | 你还记得刚才我们讨论了什么模型吗?如果你的记忆中有相关信息,请引用它。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 02:54 | - | | |
| 15 | +| T-mn5glp6q | 帮我制定下季度的OKR目标,公司方向是AI互动剧。简要列出结构即可,不需要工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:03 | - | | ... | ... |
server/routes/agent-chat.js
| ... | ... | @@ -20,7 +20,7 @@ const { ClaudeCliProtocol } = require('@mas/acp-core'); |
| 20 | 20 | const { Parser } = require('../src/core/parser'); |
| 21 | 21 | const AgentManager = require('../src/core/agent-manager'); |
| 22 | 22 | const { GoalsService } = require('../services/goals'); |
| 23 | -const { saveSessionSummary, loadRecentMemory } = require('../services/memory-service'); | |
| 23 | +const { saveSessionSummary, loadRecentMemory, retrieveRelevantPlans } = require('../services/memory-service'); | |
| 24 | 24 | const systemConfig = require('../config'); |
| 25 | 25 | const router = express.Router(); |
| 26 | 26 | |
| ... | ... | @@ -293,9 +293,16 @@ function buildSystemPrompt(agentDir) { |
| 293 | 293 | if (memory.trim()) parts.push('---\n\n# 我的记忆\n\n' + memory.trim()); |
| 294 | 294 | |
| 295 | 295 | // short-term — 近期对话摘要 (Phase 1 MVP) |
| 296 | - const recentMemory = loadRecentMemory(config?.id || path.basename(agentDir)); | |
| 296 | + const agentIdForMemory = config?.id || path.basename(agentDir); | |
| 297 | + const recentMemory = loadRecentMemory(agentIdForMemory); | |
| 297 | 298 | if (recentMemory) parts.push('---\n\n' + recentMemory); |
| 298 | 299 | |
| 300 | + // hippocampus — 海马体检索相关经验 (Phase 2) | |
| 301 | + // 使用当前任务文本作为检索 key(从 todo.md 或最后一条消息提取) | |
| 302 | + const todoContent = loadFile(agentDir, 'todo.md'); | |
| 303 | + const hippocampusPlans = retrieveRelevantPlans(agentIdForMemory, todoContent); | |
| 304 | + if (hippocampusPlans) parts.push('---\n\n' + hippocampusPlans); | |
| 305 | + | |
| 299 | 306 | // skills |
| 300 | 307 | let mainSkill = loadFile(agentDir, '.claude/skills/main_skill/SKILL.md'); |
| 301 | 308 | if (mainSkill.trim()) { |
| ... | ... | @@ -623,6 +630,23 @@ router.get('/list', (req, res) => { |
| 623 | 630 | } catch (err) { res.status(500).json({ error: err.message }); } |
| 624 | 631 | }); |
| 625 | 632 | |
| 633 | +// ── Phase 2: 小脑方案固化 API ── | |
| 634 | +router.post('/:agentId/memory/cerebellum', (req, res) => { | |
| 635 | + const { saveCerebellumPlan } = require('../services/memory-service'); | |
| 636 | + const { scenario, keywords, steps, conditions, notes, rawContent, confidence } = req.body; | |
| 637 | + if (!scenario) return res.status(400).json({ error: 'scenario is required' }); | |
| 638 | + | |
| 639 | + const result = saveCerebellumPlan(req.params.agentId, { | |
| 640 | + scenario, keywords, steps, conditions, notes, rawContent, | |
| 641 | + confidence: confidence || 0.7, | |
| 642 | + }); | |
| 643 | + if (result) { | |
| 644 | + res.json({ success: true, path: result }); | |
| 645 | + } else { | |
| 646 | + res.status(500).json({ error: 'Failed to save cerebellum plan' }); | |
| 647 | + } | |
| 648 | +}); | |
| 649 | + | |
| 626 | 650 | router.get('/:agentId/info', (req, res) => { |
| 627 | 651 | const agentDir = path.join(DATA_DIR, req.params.agentId); |
| 628 | 652 | if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); | ... | ... |
server/scripts/test-memory.js
0 → 100644
| 1 | +#!/usr/bin/env node | |
| 2 | +/** | |
| 3 | + * Memory System Phase 1 MVP — 集成测试 | |
| 4 | + * | |
| 5 | + * 测试流程: | |
| 6 | + * 1. 清理 CEO 的 short-term 和 memory.md | |
| 7 | + * 2. 发送第一次对话 → 验证 session 摘要和 memory.md 写入 | |
| 8 | + * 3. 发送第二次对话 → 验证 prompt 中包含近期记忆 | |
| 9 | + */ | |
| 10 | + | |
| 11 | +const http = require('http'); | |
| 12 | +const fs = require('fs'); | |
| 13 | +const path = require('path'); | |
| 14 | + | |
| 15 | +const BASE = 'http://localhost:3000'; | |
| 16 | +const AGENT = 'ceo'; | |
| 17 | +const AGENT_DIR = path.resolve(__dirname, '../../data/agents', AGENT); | |
| 18 | +const SHORT_TERM_DIR = path.join(AGENT_DIR, 'memory', 'short-term'); | |
| 19 | +const MEMORY_MD = path.join(AGENT_DIR, 'memory', 'memory.md'); | |
| 20 | + | |
| 21 | +let passed = 0, failed = 0; | |
| 22 | + | |
| 23 | +function assert(condition, msg) { | |
| 24 | + if (condition) { passed++; console.log(` ✅ ${msg}`); } | |
| 25 | + else { failed++; console.log(` ❌ ${msg}`); } | |
| 26 | +} | |
| 27 | + | |
| 28 | +function post(url, body) { | |
| 29 | + return new Promise((resolve, reject) => { | |
| 30 | + const data = JSON.stringify(body); | |
| 31 | + const u = new URL(url); | |
| 32 | + const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST', | |
| 33 | + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } | |
| 34 | + }, res => { | |
| 35 | + let buf = ''; | |
| 36 | + res.on('data', c => buf += c); | |
| 37 | + res.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve(buf); } }); | |
| 38 | + }); | |
| 39 | + req.on('error', reject); | |
| 40 | + req.write(data); | |
| 41 | + req.end(); | |
| 42 | + }); | |
| 43 | +} | |
| 44 | + | |
| 45 | +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } | |
| 46 | + | |
| 47 | +function waitForCompletion(agentId, convId, timeoutMs = 120000) { | |
| 48 | + const metaPath = path.join(AGENT_DIR, '..', agentId, 'conversations', convId, 'meta.json'); | |
| 49 | + const start = Date.now(); | |
| 50 | + return new Promise((resolve, reject) => { | |
| 51 | + const check = () => { | |
| 52 | + if (Date.now() - start > timeoutMs) return reject(new Error('Timeout')); | |
| 53 | + try { | |
| 54 | + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); | |
| 55 | + if (meta.status === 'completed' || meta.status === 'failed') return resolve(meta); | |
| 56 | + } catch {} | |
| 57 | + setTimeout(check, 2000); | |
| 58 | + }; | |
| 59 | + check(); | |
| 60 | + }); | |
| 61 | +} | |
| 62 | + | |
| 63 | +async function main() { | |
| 64 | + console.log('\n🧪 Memory System Phase 1 — 集成测试\n'); | |
| 65 | + console.log('═'.repeat(50)); | |
| 66 | + | |
| 67 | + // ── Step 0: 清理 ── | |
| 68 | + console.log('\n📋 Step 0: 清理历史数据'); | |
| 69 | + // 清理 short-term | |
| 70 | + if (fs.existsSync(SHORT_TERM_DIR)) { | |
| 71 | + const files = fs.readdirSync(SHORT_TERM_DIR).filter(f => f.startsWith('session-')); | |
| 72 | + files.forEach(f => fs.unlinkSync(path.join(SHORT_TERM_DIR, f))); | |
| 73 | + console.log(` 🗑️ 清理了 ${files.length} 个 session 文件`); | |
| 74 | + } | |
| 75 | + // 重置 memory.md | |
| 76 | + fs.writeFileSync(MEMORY_MD, '# 我的记忆\n\n## 关键事实\n(尚无记录)\n\n## 已验证的模式\n(尚无记录)\n\n## 失败教训\n(尚无记录)\n', 'utf-8'); | |
| 77 | + console.log(' 🗑️ memory.md 已重置'); | |
| 78 | + | |
| 79 | + // ── Step 1: 第一次对话 ── | |
| 80 | + console.log('\n📋 Step 1: 发送第一次对话'); | |
| 81 | + const resp1 = await post(`${BASE}/api/agent/task/dispatch`, { | |
| 82 | + from: 'master', to: AGENT, | |
| 83 | + task: '请用一句话总结BLM模型的核心思想。不需要使用任何工具,直接回答即可。', | |
| 84 | + priority: 'P2', autoExecute: true, | |
| 85 | + }); | |
| 86 | + assert(resp1.success, `dispatch 成功 (taskId: ${resp1.taskId})`); | |
| 87 | + const convId1 = resp1.executionConvId; | |
| 88 | + | |
| 89 | + console.log(' ⏳ 等待对话完成...'); | |
| 90 | + const meta1 = await waitForCompletion(AGENT, convId1); | |
| 91 | + assert(meta1.status === 'completed', `对话完成 (status: ${meta1.status})`); | |
| 92 | + | |
| 93 | + // 等待 memory 写入(异步) | |
| 94 | + await sleep(2000); | |
| 95 | + | |
| 96 | + // 验证 short-term | |
| 97 | + const sessions1 = fs.existsSync(SHORT_TERM_DIR) | |
| 98 | + ? fs.readdirSync(SHORT_TERM_DIR).filter(f => f.startsWith('session-')) | |
| 99 | + : []; | |
| 100 | + assert(sessions1.length === 1, `short-term 有 ${sessions1.length} 个 session 文件`); | |
| 101 | + | |
| 102 | + if (sessions1.length > 0) { | |
| 103 | + const sessionContent = fs.readFileSync(path.join(SHORT_TERM_DIR, sessions1[0]), 'utf-8'); | |
| 104 | + assert(sessionContent.includes('# Session 摘要'), 'session 包含标题'); | |
| 105 | + assert(sessionContent.includes('BLM') || sessionContent.includes('战略'), 'session 包含任务关键词'); | |
| 106 | + assert(sessionContent.includes('工具调用'), 'session 包含 toolCall 统计'); | |
| 107 | + console.log(` 📄 Session 文件: ${sessions1[0]} (${sessionContent.length} 字)`); | |
| 108 | + } | |
| 109 | + | |
| 110 | + // 验证 memory.md | |
| 111 | + const memContent1 = fs.readFileSync(MEMORY_MD, 'utf-8'); | |
| 112 | + const hasNewFact = !memContent1.includes('(尚无记录)') || memContent1.match(/- \[202/); | |
| 113 | + assert(hasNewFact, `memory.md 有新事实记录`); | |
| 114 | + console.log(` 📝 memory.md: ${memContent1.length} 字`); | |
| 115 | + | |
| 116 | + // ── Step 2: 第二次对话 ── | |
| 117 | + console.log('\n📋 Step 2: 发送第二次对话 (验证近期记忆注入)'); | |
| 118 | + const resp2 = await post(`${BASE}/api/agent/task/dispatch`, { | |
| 119 | + from: 'master', to: AGENT, | |
| 120 | + task: '你还记得刚才我们讨论了什么模型吗?如果你的记忆中有相关信息,请引用它。不需要使用工具。', | |
| 121 | + priority: 'P2', autoExecute: true, | |
| 122 | + }); | |
| 123 | + assert(resp2.success, `第二次 dispatch 成功 (taskId: ${resp2.taskId})`); | |
| 124 | + const convId2 = resp2.executionConvId; | |
| 125 | + | |
| 126 | + console.log(' ⏳ 等待第二次对话完成...'); | |
| 127 | + const meta2 = await waitForCompletion(AGENT, convId2); | |
| 128 | + assert(meta2.status === 'completed', `第二次对话完成 (status: ${meta2.status})`); | |
| 129 | + | |
| 130 | + await sleep(2000); | |
| 131 | + | |
| 132 | + // 验证 short-term 增长 | |
| 133 | + const sessions2 = fs.readdirSync(SHORT_TERM_DIR).filter(f => f.startsWith('session-')); | |
| 134 | + assert(sessions2.length === 2, `short-term 有 ${sessions2.length} 个 session 文件 (期望 2)`); | |
| 135 | + | |
| 136 | + // 验证 memory.md 条目增长 | |
| 137 | + const memContent2 = fs.readFileSync(MEMORY_MD, 'utf-8'); | |
| 138 | + const factCount = (memContent2.match(/- \[202/g) || []).length; | |
| 139 | + assert(factCount >= 2, `memory.md 有 ${factCount} 条事实 (期望 ≥2)`); | |
| 140 | + | |
| 141 | + // 检查最后的 messages.log 是否表明 Agent "记住了" | |
| 142 | + try { | |
| 143 | + const logPath = path.join(AGENT_DIR, 'conversations', convId2, 'messages.log'); | |
| 144 | + const log2 = fs.readFileSync(logPath, 'utf-8'); | |
| 145 | + const mentionsBLM = log2.includes('BLM') || log2.includes('记忆') || log2.includes('之前'); | |
| 146 | + assert(mentionsBLM, `Agent 在第二次对话中引用了之前的信息`); | |
| 147 | + } catch { console.log(' ⚠️ 无法检查 messages.log'); } | |
| 148 | + | |
| 149 | + // ── 结果 ── | |
| 150 | + console.log('\n' + '═'.repeat(50)); | |
| 151 | + console.log(`\n🏁 测试结果: ${passed} 通过, ${failed} 失败\n`); | |
| 152 | + | |
| 153 | + | |
| 154 | + if (failed > 0) process.exit(1); | |
| 155 | +} | |
| 156 | + | |
| 157 | +main().catch(err => { console.error('❌ 测试崩溃:', err); process.exit(1); }); | ... | ... |
server/services/memory-service.js
| ... | ... | @@ -6,9 +6,10 @@ |
| 6 | 6 | * - 对话结束 → 关键事实追加 → memory.md |
| 7 | 7 | * - 新对话开始 → 加载最近 N 个 session 摘要注入 prompt |
| 8 | 8 | * |
| 9 | - * Phase 2 (TODO): | |
| 10 | - * - 海马体关键词匹配 | |
| 11 | - * - 小脑方案固化 | |
| 9 | + * Phase 2: | |
| 10 | + * - 海马体关键词匹配 → 检索相关小脑方案 | |
| 11 | + * - 小脑方案固化 → cerebellum/ 存储成功方案 | |
| 12 | + * - 海马体索引自动更新 | |
| 12 | 13 | */ |
| 13 | 14 | |
| 14 | 15 | const fs = require('fs'); |
| ... | ... | @@ -18,6 +19,8 @@ const DATA_DIR = path.resolve(__dirname, '../../data/agents'); |
| 18 | 19 | const MAX_SHORT_TERM = 10; // 最多保留最近 N 个 session 摘要 |
| 19 | 20 | const MAX_MEMORY_MD_SIZE = 3000; // memory.md 最大字符数 |
| 20 | 21 | const INJECT_SESSION_COUNT = 5; // prompt 注入最近 N 个 session |
| 22 | +const HIPPOCAMPUS_MIN_SCORE = 0.3; // 海马体匹配最低分 | |
| 23 | +const MAX_CEREBELLUM_INJECT = 2; // 最多注入 N 个小脑方案 | |
| 21 | 24 | |
| 22 | 25 | // ════════════════════════════════════════ |
| 23 | 26 | // 1. 对话结束 → 自动保存 Session 摘要 |
| ... | ... | @@ -190,7 +193,295 @@ function appendToMemoryMd(agentId, task, result, meta) { |
| 190 | 193 | } |
| 191 | 194 | |
| 192 | 195 | // ════════════════════════════════════════ |
| 193 | -// 内部工具函数 | |
| 196 | +// 4. Phase 2: 海马体检索 (Hippocampus) | |
| 197 | +// ════════════════════════════════════════ | |
| 198 | + | |
| 199 | +/** | |
| 200 | + * 根据任务文本检索海马体,返回匹配的小脑方案 | |
| 201 | + * | |
| 202 | + * @param {string} agentId | |
| 203 | + * @param {string} taskText - 用户的任务文本 | |
| 204 | + * @returns {string} 格式化的小脑方案文本(用于注入 prompt) | |
| 205 | + */ | |
| 206 | +function retrieveRelevantPlans(agentId, taskText) { | |
| 207 | + try { | |
| 208 | + const agentDir = path.join(DATA_DIR, agentId); | |
| 209 | + const index = loadHippocampusIndex(agentDir); | |
| 210 | + if (index.length === 0) return ''; | |
| 211 | + | |
| 212 | + // 从任务文本提取关键词 | |
| 213 | + const taskKeywords = extractKeywords(taskText); | |
| 214 | + if (taskKeywords.length === 0) return ''; | |
| 215 | + | |
| 216 | + // 计算每个索引条目的匹配分数 | |
| 217 | + const scored = index.map(entry => { | |
| 218 | + const entryKeywords = (entry.keywords || []).map(k => k.toLowerCase()); | |
| 219 | + const hits = taskKeywords.filter(kw => | |
| 220 | + entryKeywords.some(ek => ek.includes(kw) || kw.includes(ek)) | |
| 221 | + ); | |
| 222 | + const score = entryKeywords.length > 0 | |
| 223 | + ? hits.length / entryKeywords.length // 命中了 entry 的多少比例关键词 | |
| 224 | + : 0; | |
| 225 | + return { ...entry, score, hits }; | |
| 226 | + }); | |
| 227 | + | |
| 228 | + // 过滤 + 排序 | |
| 229 | + const matches = scored | |
| 230 | + .filter(e => e.score >= HIPPOCAMPUS_MIN_SCORE) | |
| 231 | + .sort((a, b) => { | |
| 232 | + // 先按分数,再按置信度,最后按使用次数 | |
| 233 | + if (b.score !== a.score) return b.score - a.score; | |
| 234 | + if ((b.confidence || 0) !== (a.confidence || 0)) return (b.confidence || 0) - (a.confidence || 0); | |
| 235 | + return (b.useCount || 0) - (a.useCount || 0); | |
| 236 | + }) | |
| 237 | + .slice(0, MAX_CEREBELLUM_INJECT); | |
| 238 | + | |
| 239 | + if (matches.length === 0) return ''; | |
| 240 | + | |
| 241 | + // 加载对应的小脑方案文件 | |
| 242 | + const cerebellumDir = path.join(agentDir, 'memory', 'cerebellum'); | |
| 243 | + const plans = []; | |
| 244 | + | |
| 245 | + for (const match of matches) { | |
| 246 | + const planPath = path.join(cerebellumDir, match.planFile); | |
| 247 | + if (fs.existsSync(planPath)) { | |
| 248 | + const planContent = fs.readFileSync(planPath, 'utf-8').trim(); | |
| 249 | + plans.push(`### 经验方案: ${match.scenario} (置信度 ${Math.round((match.confidence || 0) * 100)}%, 使用 ${match.useCount || 0} 次)\n\n${planContent}`); | |
| 250 | + | |
| 251 | + // 更新原始索引中的使用次数 + 最后使用时间 | |
| 252 | + const origEntry = index.find(e => e.planFile === match.planFile); | |
| 253 | + if (origEntry) { | |
| 254 | + origEntry.useCount = (origEntry.useCount || 0) + 1; | |
| 255 | + origEntry.lastUsed = new Date().toISOString().substring(0, 10); | |
| 256 | + } | |
| 257 | + } | |
| 258 | + } | |
| 259 | + | |
| 260 | + if (plans.length > 0) { | |
| 261 | + // 保存更新后的使用统计 | |
| 262 | + saveHippocampusIndex(agentDir, index); | |
| 263 | + console.log(`[Memory] 🐴 海马体命中 ${plans.length} 个方案 (agent: ${agentId})`); | |
| 264 | + } | |
| 265 | + | |
| 266 | + if (plans.length === 0) return ''; | |
| 267 | + | |
| 268 | + return [ | |
| 269 | + `# 经验参考(海马体检索)`, | |
| 270 | + '', | |
| 271 | + '> 以下是你过去处理类似任务的成功经验。可以参考,但不必完全照搬。', | |
| 272 | + '', | |
| 273 | + ...plans, | |
| 274 | + ].join('\n'); | |
| 275 | + } catch (err) { | |
| 276 | + console.error(`[Memory] ⚠️ 海马体检索失败:`, err.message); | |
| 277 | + return ''; | |
| 278 | + } | |
| 279 | +} | |
| 280 | + | |
| 281 | +// ════════════════════════════════════════ | |
| 282 | +// 5. Phase 2: 小脑固化 (Cerebellum) | |
| 283 | +// ════════════════════════════════════════ | |
| 284 | + | |
| 285 | +/** | |
| 286 | + * 保存一个成功方案到小脑,并更新海马体索引 | |
| 287 | + * | |
| 288 | + * @param {string} agentId | |
| 289 | + * @param {object} plan - { scenario, keywords, steps, notes } | |
| 290 | + * @returns {string|null} 保存成功返回文件路径 | |
| 291 | + */ | |
| 292 | +function saveCerebellumPlan(agentId, plan) { | |
| 293 | + try { | |
| 294 | + const agentDir = path.join(DATA_DIR, agentId); | |
| 295 | + const cerebellumDir = path.join(agentDir, 'memory', 'cerebellum'); | |
| 296 | + | |
| 297 | + if (!fs.existsSync(cerebellumDir)) { | |
| 298 | + fs.mkdirSync(cerebellumDir, { recursive: true }); | |
| 299 | + } | |
| 300 | + | |
| 301 | + // 生成文件名: 基于 scenario 的 slug | |
| 302 | + const slug = (plan.scenario || 'plan') | |
| 303 | + .toLowerCase() | |
| 304 | + .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-') | |
| 305 | + .replace(/^-|-$/g, '') | |
| 306 | + .substring(0, 40); | |
| 307 | + const filename = `${slug}.md`; | |
| 308 | + const filepath = path.join(cerebellumDir, filename); | |
| 309 | + | |
| 310 | + // 构造方案内容 | |
| 311 | + const content = [ | |
| 312 | + `# 方案: ${plan.scenario}`, | |
| 313 | + '', | |
| 314 | + `> 创建时间: ${new Date().toISOString().substring(0, 10)}`, | |
| 315 | + '', | |
| 316 | + ]; | |
| 317 | + | |
| 318 | + if (plan.conditions) { | |
| 319 | + content.push('## 适用条件', ...plan.conditions.map(c => `- ${c}`), ''); | |
| 320 | + } | |
| 321 | + | |
| 322 | + if (plan.steps) { | |
| 323 | + content.push('## 执行步骤', ...plan.steps.map((s, i) => `${i + 1}. ${s}`), ''); | |
| 324 | + } | |
| 325 | + | |
| 326 | + if (plan.notes) { | |
| 327 | + content.push('## 注意事项', ...plan.notes.map(n => `- ${n}`), ''); | |
| 328 | + } | |
| 329 | + | |
| 330 | + if (plan.rawContent) { | |
| 331 | + content.push('## 详细内容', plan.rawContent, ''); | |
| 332 | + } | |
| 333 | + | |
| 334 | + fs.writeFileSync(filepath, content.join('\n'), 'utf-8'); | |
| 335 | + | |
| 336 | + // 更新海马体索引 | |
| 337 | + const index = loadHippocampusIndex(agentDir); | |
| 338 | + | |
| 339 | + // 检查是否已有同名条目 | |
| 340 | + const existing = index.findIndex(e => e.planFile === filename); | |
| 341 | + const entry = { | |
| 342 | + scenario: plan.scenario, | |
| 343 | + keywords: plan.keywords || [], | |
| 344 | + planFile: filename, | |
| 345 | + confidence: plan.confidence || 0.7, | |
| 346 | + useCount: 0, | |
| 347 | + lastUsed: null, | |
| 348 | + createdAt: new Date().toISOString().substring(0, 10), | |
| 349 | + }; | |
| 350 | + | |
| 351 | + if (existing >= 0) { | |
| 352 | + // 更新已有条目(保留使用次数) | |
| 353 | + entry.useCount = index[existing].useCount || 0; | |
| 354 | + entry.lastUsed = index[existing].lastUsed; | |
| 355 | + index[existing] = entry; | |
| 356 | + } else { | |
| 357 | + index.push(entry); | |
| 358 | + } | |
| 359 | + | |
| 360 | + saveHippocampusIndex(agentDir, index); | |
| 361 | + | |
| 362 | + // 同步更新 hippocampus.md (人类可读版) | |
| 363 | + syncHippocampusMd(agentDir, index); | |
| 364 | + | |
| 365 | + console.log(`[Memory] 🧩 小脑方案已固化: ${agentId}/cerebellum/${filename}`); | |
| 366 | + return filepath; | |
| 367 | + } catch (err) { | |
| 368 | + console.error(`[Memory] ⚠️ 小脑方案保存失败:`, err.message); | |
| 369 | + return null; | |
| 370 | + } | |
| 371 | +} | |
| 372 | + | |
| 373 | +// ════════════════════════════════════════ | |
| 374 | +// 海马体索引 JSON 读写 | |
| 375 | +// ════════════════════════════════════════ | |
| 376 | + | |
| 377 | +/** | |
| 378 | + * 加载海马体索引 (JSON 格式,比 md 表格更可靠) | |
| 379 | + * 文件: memory/hippocampus-index.json | |
| 380 | + */ | |
| 381 | +function loadHippocampusIndex(agentDir) { | |
| 382 | + const indexPath = path.join(agentDir, 'memory', 'hippocampus-index.json'); | |
| 383 | + try { | |
| 384 | + return JSON.parse(fs.readFileSync(indexPath, 'utf-8')); | |
| 385 | + } catch { | |
| 386 | + return []; | |
| 387 | + } | |
| 388 | +} | |
| 389 | + | |
| 390 | +/** 保存海马体索引 */ | |
| 391 | +function saveHippocampusIndex(agentDir, index) { | |
| 392 | + const indexPath = path.join(agentDir, 'memory', 'hippocampus-index.json'); | |
| 393 | + const dir = path.dirname(indexPath); | |
| 394 | + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | |
| 395 | + fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8'); | |
| 396 | +} | |
| 397 | + | |
| 398 | +/** | |
| 399 | + * 同步 hippocampus.md — 人类可读的 markdown 表格 | |
| 400 | + * (JSON 是程序真正使用的,md 只是给人看) | |
| 401 | + */ | |
| 402 | +function syncHippocampusMd(agentDir, index) { | |
| 403 | + const mdPath = path.join(agentDir, 'memory', 'hippocampus.md'); | |
| 404 | + const rows = index.map((e, i) => | |
| 405 | + `| ${i + 1} | ${e.scenario} | ${(e.keywords || []).join(', ')} | ${e.planFile} | ${e.confidence} | ${e.useCount || 0} | ${e.lastUsed || '-'} |` | |
| 406 | + ); | |
| 407 | + | |
| 408 | + const content = [ | |
| 409 | + '# 海马体 — 经验记忆索引', | |
| 410 | + '', | |
| 411 | + '> 记录过往处理过的情景模式。遇到相似任务时,优先调用小脑中的固化方案。', | |
| 412 | + '> ⚠️ 此文件由系统自动生成,勿手动编辑。真实数据在 hippocampus-index.json。', | |
| 413 | + '', | |
| 414 | + '| # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 |', | |
| 415 | + '|---|---------|-----------|---------|--------|---------|----------|', | |
| 416 | + ...(rows.length > 0 ? rows : ['| | (暂无经验记录) | | | | | |']), | |
| 417 | + '', | |
| 418 | + ].join('\n'); | |
| 419 | + | |
| 420 | + fs.writeFileSync(mdPath, content, 'utf-8'); | |
| 421 | +} | |
| 422 | + | |
| 423 | +// ════════════════════════════════════════ | |
| 424 | +// 关键词提取 | |
| 425 | +// ════════════════════════════════════════ | |
| 426 | + | |
| 427 | +/** 中文停用词表(常见无意义词) */ | |
| 428 | +const STOP_WORDS = new Set([ | |
| 429 | + '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', | |
| 430 | + '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', | |
| 431 | + '自己', '这', '他', '她', '它', '那', '些', '所以', '因为', '但是', '而且', '或者', | |
| 432 | + '如果', '虽然', '所有', '可以', '应该', '需要', '能够', '已经', '正在', '请', | |
| 433 | + '把', '被', '让', '给', '从', '对', '将', '于', '之', '等', '以', '及', '与', | |
| 434 | + 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', | |
| 435 | + 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', | |
| 436 | + 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for', | |
| 437 | + 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'about', 'and', 'or', | |
| 438 | + 'but', 'not', 'this', 'that', 'it', 'its', 'my', 'your', 'his', 'her', | |
| 439 | +]); | |
| 440 | + | |
| 441 | +/** | |
| 442 | + * 从文本中提取关键词 | |
| 443 | + * 策略: 中文按 2-4 字切片 + 英文按空格分词 + 去停用词 | |
| 444 | + */ | |
| 445 | +function extractKeywords(text) { | |
| 446 | + if (!text || text.length < 2) return []; | |
| 447 | + | |
| 448 | + const keywords = new Set(); | |
| 449 | + | |
| 450 | + // 1. 英文词 (≥2字母) | |
| 451 | + const englishWords = text.match(/[a-zA-Z]{2,}/g) || []; | |
| 452 | + englishWords.forEach(w => { | |
| 453 | + const lower = w.toLowerCase(); | |
| 454 | + if (!STOP_WORDS.has(lower) && lower.length >= 3) { | |
| 455 | + keywords.add(lower); | |
| 456 | + } | |
| 457 | + }); | |
| 458 | + | |
| 459 | + // 2. 中文:提取连续中文字符序列,再分割成 2-4 字的 ngram | |
| 460 | + const chineseSegments = text.match(/[\u4e00-\u9fff]{2,}/g) || []; | |
| 461 | + for (const seg of chineseSegments) { | |
| 462 | + // 完整段 (如果 ≤6 字) | |
| 463 | + if (seg.length <= 6 && !STOP_WORDS.has(seg)) { | |
| 464 | + keywords.add(seg); | |
| 465 | + } | |
| 466 | + // 2-gram | |
| 467 | + for (let i = 0; i <= seg.length - 2; i++) { | |
| 468 | + const bi = seg.substring(i, i + 2); | |
| 469 | + if (!STOP_WORDS.has(bi)) keywords.add(bi); | |
| 470 | + } | |
| 471 | + // 3-gram (如果段够长) | |
| 472 | + if (seg.length >= 3) { | |
| 473 | + for (let i = 0; i <= seg.length - 3; i++) { | |
| 474 | + const tri = seg.substring(i, i + 3); | |
| 475 | + if (!STOP_WORDS.has(tri)) keywords.add(tri); | |
| 476 | + } | |
| 477 | + } | |
| 478 | + } | |
| 479 | + | |
| 480 | + return [...keywords].slice(0, 30); // 最多 30 个关键词 | |
| 481 | +} | |
| 482 | + | |
| 483 | +// ════════════════════════════════════════ | |
| 484 | +// Phase 1 内部工具函数 | |
| 194 | 485 | // ════════════════════════════════════════ |
| 195 | 486 | |
| 196 | 487 | /** 从 title 提取任务描述 */ |
| ... | ... | @@ -314,7 +605,15 @@ function truncateMemory(content) { |
| 314 | 605 | } |
| 315 | 606 | |
| 316 | 607 | module.exports = { |
| 608 | + // Phase 1 | |
| 317 | 609 | saveSessionSummary, |
| 318 | 610 | loadRecentMemory, |
| 319 | 611 | appendToMemoryMd, |
| 612 | + // Phase 2 | |
| 613 | + retrieveRelevantPlans, | |
| 614 | + saveCerebellumPlan, | |
| 615 | + extractKeywords, | |
| 616 | + // 内部 (测试用) | |
| 617 | + loadHippocampusIndex, | |
| 618 | + saveHippocampusIndex, | |
| 320 | 619 | }; | ... | ... |