Skill: hnggoyoorle/memory-tree-universal (version 1.0.0)

--- name: memory-tree-universal description: "Memory Tree 记忆树架构 — 通用版，适用于任何 AI Agent 的长期记忆系统。包含三棵树设计、热冷路径管道、14 条打分规则、数据库 Schema、检索 API 和坑点总结。当用户需要记忆系统、长期记忆、AI Agent 记忆架构时加载此技能。" tags: - memory - architecture - agent-design - retrieval - long-term-memory metadata: openclaw: emoji: 🌳 homepage: "" requires: bins: - python3 env: [] config: [] --- # Memory Tree 记忆树架构 — 通用版 > 源自 OpenHuman Memory Tree 设计理念，经 Hermes Agent 实战验证后抽象为通用架构。 > 适用于任何需要长期记忆、多轮对话、信息压缩的 AI Agent 系统。 ## 一、核心思想 **问题**：传统 Agent 记忆是"碎片袋"——所有对话塞进 context，越聊越慢，重要信息被淹没。 **答案**：Memory Tree 把记忆变成"大脑"——有层次、有分类、有热度、有压缩。 ### 三棵树，三个作用域 | 树 | 作用域 | 功能 | |---|---|---| | **Source Tree** | 每个来源（QQ/飞书/文档） | 滚动缓冲区，L0→L1→L2 级联压缩 | | **Topic Tree** | 每个实体（人/项目/概念） | 热度驱动，越频繁越深入 | | **Global Tree** | 全局 | 每日摘要，跨来源聚合 | ### 为什么是树，不是向量库？ 向量库只能回答"什么与查询相似"。树能回答： - "今天发生了什么？" → Global Tree - "这个人的最新动态？" → Topic Tree + hotness - "上周二下午 3 点说了什么？" → Source Tree + provenance **结构让记忆可导航，嵌入保证语义搜索。** ## 二、架构流程 ``` 输入源（聊天/文档/邮件） ↓ Canonicalize → 标准化 Markdown + 来源元数据 ↓ Chunker → 确定性 ID, ≤3k token/块 ↓ Fast Score → 14 条规则快速打分（无 LLM） ↓ Persist → SQLite 持久化（chunks + FTS5 索引） ↓ ┌─────────────────────────────────────┐ │ 冷路径（异步后台） │ ├─────────────────────────────────────┤ │ Deep Score → LLM 判断 admitted/dropped │ │ Entity Extract → 提取实体 │ │ Topic Route → 自动分类 │ │ Hotness Engine → 热度追踪 │ │ Sealer → L0→L1 摘要压缩 │ └─────────────────────────────────────┘ ↓ 检索 → 中文 LIKE / 英文 FTS5 MATCH + 热度排序 ``` ## 三、热路径管道（无 LLM，<50ms） ### 1. Canonicalize（标准化） 每条消息转为统一 Markdown 格式： ```markdown [role: user] [source: qqbot] [time: 2026-05-19 01:16] 记住：项目预算 5 万，9 月 15 日前完成。 ``` 元数据字段：source, role, timestamp, session_id, message_id ### 2. Chunk（分块） - 语义分块，≤3000 token/块 - 确定性 ID：`{session_id[:12]}-{role[:3]}-{index:03d}-{hash_suffix}` - hash_suffix = SHA256(content)[:16] - `INSERT OR IGNORE` 自动去重 ### 3. Fast Score（14 条规则，无 LLM） | 规则 | 分值 | 示例 | |---|---|---| | 用户明确要求记忆 | +0.25 | "记住"、"保存" | | 用户偏好信息 | +0.25 | "喜欢"、"讨厌" | | 金额信息 | +0.20 | "5 万"、"100 元" | | 重要标记 | +0.20 | "重要"、"必须" | | 情感/需求 | +0.15 | "需要"、"想要" | | 错误/异常 | +0.15 | "bug"、"报错" | | 版本信息 | +0.10 | "v0.14.0" | | 项目相关 | +0.10 | "yinmei 项目" | | 配置信息 | +0.10 | "config" | | 首次事件 | +0.10 | 第一次提到 | | 成功/完成 | +0.10 | "完成"、"成功" | | 凭证相关 | +0.05 | "API key" | | 时间信息 | +0.05 | "2026-05-19" | | 短文本惩罚 | ×0.5 | <20 字 | ### 4. 数据库 Schema ```sql -- 记忆块 CREATE TABLE memory_chunks ( id TEXT PRIMARY KEY, session_id TEXT, message_id INTEGER, content TEXT, content_hash TEXT, token_count INTEGER, source TEXT, role TEXT, timestamp REAL, score REAL, is_admitted INTEGER, -- 0=dropped, 1=admitted, 2=sealed score_reasons TEXT -- JSON 数组 ); -- 主题/实体 CREATE TABLE memory_topics ( id INTEGER PRIMARY KEY, name TEXT, entity_type TEXT, hotness REAL, summary TEXT, last_updated REAL ); -- Chunk-Topic 关联 CREATE TABLE memory_chunk_topics ( chunk_id TEXT, topic_id INTEGER, relevance REAL ); -- 摘要（L1/L2） CREATE TABLE memory_summaries ( id INTEGER PRIMARY KEY, topic_id INTEGER, level INTEGER, -- 1=L1, 2=L2 content TEXT, child_chunk_ids TEXT, sealed_at REAL ); -- FTS5 全文索引（外部内容表） CREATE VIRTUAL TABLE memory_chunks_fts USING FTS5( content='memory_chunks', content_rowid='rowid' ); -- Trigram 索引（中文搜索） CREATE VIRTUAL TABLE memory_chunks_fts_trigram USING fts5tokenize=trigram(...); ``` ## 四、冷路径管道（异步，有 LLM） ### 1. JobQueue（SQLite 轻量队列） ```sql CREATE TABLE memory_jobs ( id INTEGER PRIMARY KEY, job_type TEXT, -- deep_score / entity_extract / digest payload TEXT, -- JSON dedupe_key TEXT, status TEXT, -- pending / running / completed / failed worker_id TEXT, lease_expires REAL, retry_count INTEGER ); ``` ### 2. DeepScorer（LLM 判断） 兜底规则（不调用 LLM 时）： - 含"记住/偏好/预算" → admitted - <15 字 → dropped ### 3. EntityExtractor（正则提取） | 实体类型 | 正则模式 | |---|---| | 仓库 | github\.com/[^/\s]+/[^/\s]+ | | 网站 | https?://[^\s]+ | | 版本 | v?\d+\.\d+\.\d+ | | 日期 | \d{4}-\d{2}-\d{2} | | 金额 | \d+[万块元]+ | | 项目名 | [a-zA-Z][a-zA-Z0-9_-]+ | | 人名 | [一-龯]{2,4} | ### 4. TopicRouter（自动分类） 实体 → 主题路由规则： - `github.com/...` → `repo:<org>/<name>` - `v1.2.3` → `version:<name>` - `5 万` → `budget:<project>` - 首次出现 → 自动创建主题 ### 5. HotnessEngine（热度追踪） ``` 每次提及: hotness += 0.05 每日衰减: hotness *= 0.95 活跃阈值: 0.5（高于此值优先检索） 归档阈值: 0.1（低于此值压缩为摘要） ``` ### 6. Sealer（摘要压缩） ``` L0 chunk 累积到阈值（如 10 个）→ 触发 seal ↓ LLM 生成 L1 摘要 ↓ 标记子 chunk is_admitted=2（已密封） ↓ L1 摘要存入 memory_summaries ``` ## 五、检索 API ### 搜索（中文 LIKE + 英文 FTS5 MATCH） ```python def search(query, limit=10, min_score=0.1, topic=None, days=None): has_cjk = bool(re.search(r'[\u4e00-\u9fff]', query)) if has_cjk: # 中文：LIKE（准确但慢） sql = """ SELECT c.*, t.name as topic_name, t.hotness FROM memory_chunks c LEFT JOIN memory_chunk_topics ct ON c.id = ct.chunk_id LEFT JOIN memory_topics t ON ct.topic_id = t.id WHERE c.content LIKE ? AND c.is_admitted >= 1 AND c.score >= ? ORDER BY c.score DESC, t.hotness DESC LIMIT ? """ params = [f'%{query}%', min_score, limit] else: # 英文：FTS5 MATCH（快） sql = """ SELECT c.*, t.name as topic_name, t.hotness FROM memory_chunks c JOIN memory_chunks_fts f ON c.rowid = f.rowid LEFT JOIN memory_chunk_topics ct ON c.id = ct.chunk_id LEFT JOIN memory_topics t ON ct.topic_id = t.id WHERE f MATCH ? AND c.is_admitted >= 1 AND c.score >= ? ORDER BY c.score DESC, t.hotness DESC LIMIT ? """ params = [query, min_score, limit] ``` ### 其他检索方法 | 方法 | 说明 | |---|---| | `search(query)` | 全文搜索 | | `get_topic_summary(topic_id)` | 主题详情（hotness、chunk 数、摘要） | | `get_hot_topics(limit)` | 热门主题排行 | | `search_by_session(session_id)` | 按会话检索 | | `get_global_stats()` | 全局统计 | ## 六、与 Hermes Agent 集成 ### 配置 ```yaml # config.yaml agent: memory_tree_enabled: true ``` ### 集成点 1. **AIAgent.__init__**：初始化 MemoryTreeBridge 2. **_build_system_prompt_parts**：注入检索到的相关记忆 3. **run_conversation**：每轮对话后调用 `bridge.ingest_turn()` ### CLI 命令 ``` /memory-tree stats # 统计信息 /memory-tree search <q> # 搜索记忆 /memory-tree topics # 主题列表 /memory-tree clear # 清除数据（需确认） ``` ## 七、关键坑点（血泪教训） ### 1. is_admitted 存的是 0/1，不是 score **错**：`is_admitted = score` **对**：`is_admitted = 1`（单独存 score 列） ### 2. INSERT 参数数量必须匹配列数 每增加一列，VALUES 必须增加一个参数。否则 `INSERT OR IGNORE` 静默失败。 ### 3. NOT IN 遇到 NULL 返回空 ```sql -- 错：所有行被过滤 WHERE id NOT IN (SELECT chunk_id FROM memory_jobs) -- 对：显式排除 NULL WHERE payload->>'chunk_id' IS NOT NULL ``` ### 4. FTS5 外部内容表删除需特殊处理 ```python # 先清空 FTS5 索引 c.execute("INSERT INTO memory_chunks_fts(memory_chunks) VALUES('delete')") # 再删除父表 c.execute("DELETE FROM memory_chunks WHERE ...") ``` ### 5. 中文搜索不能用 FTS5 MATCH SQLite FTS5 对中文逐字分词，"预算"分成"预"和"算"，MATCH 返回 0 结果。 **唯一可靠方案**：LIKE `%预算%` ### 6. SQLite WAL 锁冲突 清理数据用独立进程，清理前关闭所有 Python 连接。 ## 八、通用适配指南 ### 适配到非 Hermes 项目 1. **数据库**：替换 SQLite 路径为你的项目路径 2. **来源适配器**：实现 `canonicalize()` 方法，适配你的输入源 3. **LLM 调用**：替换 DeepScorer 的 LLM 调用为你的模型 4. **检索集成**：在 prompt 构建时调用 `search()` 注入上下文 ### 最小可用版本（MVP） 只需实现： 1. `memory_chunks` 表 + FTS5 索引 2. `ingest(messages)` → 分块 + 打分 + 入库 3. `search(query)` → LIKE/MATCH 检索 冷路径（LLM 深度处理）可选。 ## 九、扩展方向 - **L2 全局摘要**：每日 digest，跨所有来源 - **Web UI**：可视化主题树、热度趋势 - **跨会话聚合**：相似 session 自动合并主题 - **向量嵌入**：在 FTS5 基础上增加向量相似度检索 - **多语言支持**：针对不同语言优化分词策略 ## 十、参考实现 Hermes Agent 完整实现（~1944 行）： - `agent/memory_tree.py` — 热路径管道 - `agent/memory_tree_cold.py` — 冷路径管道 - `agent/memory_tree_retrieval.py` — 检索 API - `agent/memory_tree_clean.py` — 清理脚本 - `agent/memory_tree_integration.py` — Hermes 桥接器 测试脚本：`scripts/quick_test.py` 调试记录：`references/debug-log.md` 快速上手：`references/quickstart.md`

path: references/mvp-implementation.py
content: #!/usr/bin/env python3
"""Memory Tree 最小可用实现（MVP）— 约 200 行，可直接复制到任何项目。"""
import sqlite3, hashlib, json, re, time
from pathlib import Path

class MemoryTreeMVP:
    """Memory Tree 最小实现：热路径摄入 + 全文检索。"""

    def __init__(self, db_path='memory.db'):
        self.db_path = Path(db_path)
        self._conn = sqlite3.connect(str(self.db_path))
        self._init_schema()

    def _init_schema(self):
        c = self._conn.cursor()
        c.execute('''CREATE TABLE IF NOT EXISTS memory_chunks (
            id TEXT PRIMARY KEY, session_id TEXT, content TEXT,
            content_hash TEXT, token_count INTEGER, source TEXT,
            role TEXT, timestamp REAL, score REAL, is_admitted INTEGER,
            score_reasons TEXT
        )''')
        c.execute('''CREATE VIRTUAL TABLE IF NOT EXISTS memory_chunks_fts
            USING FTS5(content='memory_chunks', content_rowid='rowid')''')
        c.execute('''CREATE INDEX IF NOT EXISTS idx_chunks_session
            ON memory_chunks(session_id)''')
        c.execute('''CREATE INDEX IF NOT EXISTS idx_chunks_score
            ON memory_chunks(score DESC)''')
        self._conn.commit()

    @staticmethod
    def _content_hash(text):
        return hashlib.sha256(text.encode()).hexdigest()[:16]

    @staticmethod
    def _token_count(text):
        cn = len(re.findall(r'[\u4e00-\u9fff]', text))
        en = len(re.findall(r'[a-zA-Z]+', text))
        return int(cn * 1.5 + en * 1.3 + len(text) * 0.5)

    @staticmethod
    def _fast_score(text):
        """14 条规则快速打分，无 LLM。"""
        score, reasons = 0.0, []
        rules = [
            (r'记住|记忆|保存', 0.25, '用户要求记忆'),
            (r'偏好|喜欢|讨厌', 0.25, '用户偏好'),
            (r'\d+[万块元千]', 0.20, '金额信息'),
            (r'重要|必须|一定', 0.20, '重要标记'),
            (r'需要|想要|希望', 0.15, '情感需求'),
            (r'bug|错误|异常|报错', 0.15, '错误异常'),
            (r'v?\d+\.\d+', 0.10, '版本信息'),
            (r'project|项目|repo|仓库', 0.10, '项目相关'),
            (r'config|配置|setting', 0.10, '配置信息'),
            (r'成功|完成|搞定', 0.10, '成功完成'),
            (r'api.?key|token|secret|密码', 0.05, '凭证相关'),
            (r'\d{4}-\d{2}-\d{2}', 0.05, '时间信息'),
        ]
        for pattern, pts, reason in rules:
            if re.search(pattern, text, re.I):
                score += pts
                reasons.append(reason)
        if len(text.strip()) < 20:
            score *= 0.5
            reasons.append('短文本惩罚')
        return round(score, 3), reasons

    def ingest(self, session_id, messages, source='chat'):
        """摄入消息，返回生成的 chunk IDs。"""
        c = self._conn.cursor()
        chunks = []
        for i, msg in enumerate(messages):
            content = msg.get('content', '')
            cid = f"{session_id[:12]}-{msg.get('role','u')[:3]}-{i:03d}-{self._content_hash(content)}"
            score, reasons = self._fast_score(content)
            token_count = self._token_count(content)
            c.execute('''INSERT OR IGNORE INTO memory_chunks
                (id, session_id, content, content_hash, token_count, source, role, timestamp, score, is_admitted, score_reasons)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)''',
                (cid, session_id, content, self._content_hash(content), token_count,
                 source, msg.get('role','user'), msg.get('timestamp', time.time()), score,
                 json.dumps(reasons, ensure_ascii=False)))
            chunks.append({'id': cid, 'score': score, 'reasons': reasons})
        self._conn.commit()
        return chunks

    def search(self, query, limit=10, min_score=0.1):
        """搜索记忆（中文 LIKE / 英文 FTS5 MATCH）。"""
        c = self._conn.cursor()
        has_cjk = bool(re.search(r'[\u4e00-\u9fff]', query))
        if has_cjk:
            sql = '''SELECT id, session_id, content, score, is_admitted, score_reasons
                FROM memory_chunks WHERE content LIKE ? AND is_admitted >= 1 AND score >= ?
                ORDER BY score DESC LIMIT ?'''
            params = [f'%{query}%', min_score, limit]
        else:
            sql = '''SELECT c.id, c.session_id, c.content, c.score, c.is_admitted, c.score_reasons
                FROM memory_chunks c JOIN memory_chunks_fts f ON c.rowid = f.rowid
                WHERE f MATCH ? AND c.is_admitted >= 1 AND c.score >= ?
                ORDER BY c.score DESC LIMIT ?'''
            params = [query, min_score, limit]
        c.execute(sql, params)
        results = []
        for row in c.fetchall():
            results.append({
                'id': row[0], 'session_id': row[1], 'content': row[2],
                'score': row[3], 'is_admitted': row[4],
                'reasons': json.loads(row[5]) if row[5] else []
            })
        return results

    def stats(self):
        """全局统计。"""
        c = self._conn.cursor()
        c.execute('SELECT COUNT(*) FROM memory_chunks WHERE is_admitted >= 1')
        total = c.fetchone()[0]
        c.execute('SELECT COUNT(DISTINCT session_id) FROM memory_chunks')
        sessions = c.fetchone()[0]
        c.execute('SELECT AVG(score) FROM memory_chunks WHERE is_admitted >= 1')
        avg_score = c.fetchone()[0] or 0
        return {'total_chunks': total, 'sessions': sessions, 'avg_score': round(avg_score, 3)}

    def close(self):
        self._conn.close()


# ── 快速测试 ──
if __name__ == '__main__':
    mt = MemoryTreeMVP(':memory:')
    msgs = [
        {'role': 'user', 'content': '记住：项目预算 5 万，9 月 15 日前完成。', 'timestamp': time.time()},
        {'role': 'assistant', 'content': '已记住。项目预算 5 万，截止 9 月 15 日。', 'timestamp': time.time()},
        {'role': 'user', 'content': 'github.com/worm128/AI-YinMei 是项目仓库。', 'timestamp': time.time()},
        {'role': 'user', 'content': '偏好：使用免费方案，讨厌云 API 费用。', 'timestamp': time.time()},
    ]
    chunks = mt.ingest('test-session', msgs, source='qqbot')
    print(f'✅ 摄入 {len(chunks)} 个 chunk')
    for r in mt.search('预算', limit=3):
        print(f'  [{r["score"]:.2f}] {r["content"][:80]}')
    for r in mt.search('免费', limit=3):
        print(f'  [{r["score"]:.2f}] {r["content"][:80]}')
    print(f'📊 {mt.stats()}')
    mt.close()


path: references/quick-reference.md
content: # Memory Tree 快速参考卡

## 核心公式

```
记忆 = 分块 + 打分 + 树状压缩 + 热度追踪
```

## 14 条快速打分规则

| 触发词 | 分值 |
|---|---|
| 记住/记忆/保存 | +0.25 |
| 偏好/喜欢/讨厌 | +0.25 |
| 金额（5 万、100 元） | +0.20 |
| 重要/必须/一定 | +0.20 |
| 需要/想要/希望 | +0.15 |
| bug/错误/异常 | +0.15 |
| 版本号 | +0.10 |
| 项目/仓库 | +0.10 |
| 配置 | +0.10 |
| 成功/完成 | +0.10 |
| API key/token | +0.05 |
| 日期时间 | +0.05 |
| <20 字 | ×0.5 |

## 数据库 4 张表

| 表 | 作用 |
|---|---|
| memory_chunks | 记忆块（content + score + is_admitted） |
| memory_topics | 主题/实体（name + hotness + summary） |
| memory_chunk_topics | Chunk-Topic 多对多关联 |
| memory_summaries | L1/L2 摘要压缩 |

## FTS5 搜索

```python
# 中文 → LIKE（准确）
WHERE content LIKE '%关键词%'

# 英文 → FTS5 MATCH（快）
WHERE content MATCH 'keyword'
```

## 热度公式

```
提及: hotness += 0.05
衰减: hotness *= 0.95 (每日)
活跃: >0.5
归档: <0.1
```

## 坑点速查

1. `is_admitted` 存 0/1，不是 score
2. INSERT 参数数 = 列数
3. NOT IN 要加 `IS NOT NULL`
4. FTS5 外部表删除先 `INSERT INTO fts(fts) VALUES('delete')`
5. 中文不能用 FTS5 MATCH
6. WAL 锁冲突用独立进程清理

## MVP 最小实现

见 `references/mvp-implementation.py` — 约 200 行，含：
- Schema 初始化
- 确定性 Chunk ID
- 14 条 Fast Score
- 中文/英文双模式搜索
- 统计接口

可直接复制到任何 Python 项目。


path: references/quickstart.md
content: # Memory Tree 快速上手

## 步骤 1：初始化

```python
from memory_tree import MemoryTreeHotPath

hotpath = MemoryTreeHotPath(db_path='memory.db')
```

## 步骤 2：摄入数据

```python
import time
messages = [
    {'role': 'user', 'content': '记住：项目预算 5 万', 'timestamp': time.time()},
    {'role': 'assistant', 'content': '已记住', 'timestamp': time.time()},
]
chunks = hotpath.ingest('session-001', messages, source='chat')
```

## 步骤 3：检索

```python
results = hotpath.search('预算', limit=5)
for r in results:
    print(f"[{r['score']:.2f}] {r['content'][:100]}")
```

## 步骤 4：清理（可选）

```python
hotpath.close()
```


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)