架构
系统概览
┌─────────────────────────────────────────────────────────────┐
│ MCP 客户端 │
│ (Claude Code / 机器人控制器) │
└──────────────────────────┬──────────────────────────────────┘
│ MCP Protocol (stdio)
┌──────────────────────────▼──────────────────────────────────┐
│ MCP 服务器层 │
│ │
│ learn recall save_perception forget update session │
│ │ │ │ │ │ │ │
│ └───────┴─────────┴─────────────┴───────┴───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ 验证器 │ Pydantic L1 │
│ └──────┬───────┘ │
│ │ │
│ ┌──────────────────────┼──────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ auto_classify dedup search.py │
│ (类别/标签/ (精确 → (BM25 + Vec │
│ 置信度/范围) jaccard → → RRF 融合) │
│ cosine) │
│ └──────────────────────┼──────────────────────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ 操作层 │ │
│ │ memories.py │ 插入/更新/touch │
│ │ sessions.py │ 创建/结束/汇总 │
│ │ search.py │ fts_search/vec_search │
│ │ tags.py │ 添加/移除/规范化 │
│ └──────┬───────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌───────────┐ ┌──────────┐ │
│ │ FTS5 │ │ memories │ │ vec0 │ │
│ │ (BM25) │ │ (SQLite) │ │ (向量) │ │
│ └─────────┘ └───────────┘ └──────────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ memory.db │ ~/.robotmem/ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
FastEmbed Ollama OpenAI 兼容
(ONNX) (HTTP) (HTTP)
搜索管道
recall 搜索管道通过 Reciprocal Rank Fusion 组合两个排序信号:
查询: "how to grasp a cup"
│
├──→ BM25 (FTS5) ──→ 排序列表 A
│ 分词 → jieba (CJK) → FTS5 MATCH
│ ORDER BY bm25() 分数
│
└──→ 向量 (vec0) ──→ 排序列表 B
embed_one(query) → float[384]
WHERE embedding MATCH blob AND k=N
ORDER BY 余弦距离
├──→ RRF 融合 (k=60)
│ score(d) = Σ 1/(k + rank_i + 1) 对每个列表
│
├──→ 来源加权
│ 真实世界数据 × 1.5 加成
│
├──→ 置信度过滤
│ confidence >= min_confidence(默认 0.3)
│
├──→ context_filter(结构化)
│ 点路径匹配已解析的 context JSON
│ 运算符: $lt, $lte, $gt, $gte, $ne, 等值
│
├──→ spatial_sort(最近邻)
│ 坐标数组的欧几里得距离
│ 可选 max_distance 截断
│
├──→ Top-K 截断
│
└──→ MaxScore 归一化(最佳 = 1.0)
RRF 公式
score(document) = Σ 1 / (k + rank + 1)
其中 k=60(可通过 rrf_k 配置)。较高的 k 值会给同时出现在多个列表中的文档更高的权重,而非看重单个列表中的排名。
数据库 Schema
memories(核心表)
| 列名 | 类型 | 说明 |
|---|---|---|
id |
INTEGER PK | 自增 ID |
session_id |
TEXT | 关联的会话(external_id) |
collection |
TEXT | 逻辑命名空间 |
type |
TEXT | "fact" 或 "perception" |
content |
TEXT | 记忆文本(最多 300 字符) |
human_summary |
TEXT | 简短摘要(最多 200 字符) |
context |
TEXT | JSON 上下文(params/spatial/robot/task) |
perception_type |
TEXT | visual/tactile/auditory/proprioceptive/procedural |
perception_data |
BLOB | 原始传感器数据 |
perception_metadata |
TEXT | 格式/单位元数据 |
category |
TEXT | 自动分类的类别 |
confidence |
REAL | 0.0-1.0(默认 0.9) |
decay_rate |
REAL | 每日衰减率(默认 0.01) |
status |
TEXT | active / superseded / invalidated |
superseded_by |
INTEGER | 替代记忆的 ID |
content_hash |
TEXT | 用于去重的 SHA-256 前缀 |
embedding |
BLOB | 浮点向量(384d 或 768d) |
access_count |
INTEGER | 召回命中计数器 |
return_count |
INTEGER | 返回给用户的次数 |
last_accessed |
TEXT | 最后一次召回命中的 ISO 时间戳 |
created_at |
TEXT | ISO 时间戳 |
updated_at |
TEXT | ISO 时间戳 |
sessions
| 列名 | 类型 | 说明 |
|---|---|---|
id |
INTEGER PK | 自增 |
external_id |
TEXT UNIQUE | UUID 会话标识符 |
collection |
TEXT | 关联的集合 |
context |
TEXT | 会话上下文 JSON(最大 64KB) |
session_count |
INTEGER | 复用计数器 |
status |
TEXT | active / ended |
client_type |
TEXT | "mcp_direct" |
session_outcomes
| 列名 | 类型 | 说明 |
|---|---|---|
id |
INTEGER PK | 自增 |
session_id |
TEXT | 会话 external_id |
score |
REAL | Episode 成功评分(0.0-1.0) |
memory_tags
| 列名 | 类型 | 说明 |
|---|---|---|
memory_id |
INTEGER | 外键,指向 memories.id |
tag |
TEXT | 来自受控词汇表的标签 |
source |
TEXT | "auto" 或 "user" |
主键:(memory_id, tag)
tag_meta
| 列名 | 类型 | 说明 |
|---|---|---|
tag |
TEXT PK | 标签标识符 |
parent |
TEXT | 父标签(NULL = 根维度) |
display_name |
TEXT | 人类可读名称 |
虚拟表
| 表名 | 引擎 | 用途 |
|---|---|---|
memories_fts |
FTS5 | 全文搜索(content、human_summary、scope_files、scope_entities) |
memories_vec |
vec0 | 向量相似度搜索(float[384]) |
索引
idx_mem_collection ON memories(collection)
idx_mem_status ON memories(status)
idx_mem_session ON memories(session_id)
idx_mem_type ON memories(type)
idx_mem_hash ON memories(content_hash) WHERE content_hash IS NOT NULL
idx_mem_no_embed ON memories(collection) WHERE embedding IS NULL AND status='active'
idx_memory_tags_tag ON memory_tags(tag)
标签分类体系
标签系统使用 9 维树结构,包含 50+ 个标签:
metacognition ← reasoning, cognitive_bias, decision_framework, ...
capability ← build, debug, design, review, architecture, ...
domain ← cs_fundamentals, ai_ml, finance, ...
technique ← patterns, anti_patterns, recipes, ...
timing ← when_to_start, when_to_stop, when_to_switch
boundary ← tradeoff, constraint, not_applicable, ...
experience ← war_story, postmortem, gotcha, root_cause, ...
self_defect ← hallucination, sycophancy, overengineering, ...
reflection ← accuracy_calibration, behavior_rule, preference, ...
标签由正则规则(auto_classify.py)自动推断,并以 source="auto" 存储在 memory_tags 中。
自动分类管道
learn(insight="grip_force=12.5N works best because sensor was calibrated")
│
├── classify_category() → "root_cause"(匹配到 "because")
├── estimate_confidence() → 0.90(文件路径 +0.05,因果关系 +0.05)
├── extract_scope() → {scope_files: [], scope_entities: ["grip_force"]}
├── classify_tags() → ["root_cause", "observation"]
└── build_context_json() → 合并用户上下文 + 来源标记
所有分类器都是纯正则——不依赖 LLM,亚毫秒级执行。
合并算法
end_session 的合并操作会合并冗余记忆:
1. 查询可合并的记忆:
- 同一会话 + 集合
- status = active
- category 不在 (constraint, postmortem, gotcha) 中 ← 受保护
- confidence < 0.95 ← 高置信度保留
- perception_type IS NULL ← 感知记忆不合并
2. 不足 3 条则跳过
3. 按 category 分组
4. 每组内:两两 Jaccard 相似度
- > 0.50 阈值 → 贪心聚类
- 聚类约束:簇内所有对必须超过阈值
5. 每个簇选择代表:
- 优先级:confidence 降序 → access_count 降序 → created_at 降序
6. 非代表记忆 → status = 'superseded',superseded_by = 代表.id
容错模式
三层防御
每个 MCP 工具都遵循一致的防御模式:
| 层级 | 阶段 | 机制 |
|---|---|---|
| L1 | 执行前 | Pydantic 验证、类型检查、范围约束 |
| L2 | 执行中 | 每个操作 try-except、优雅降级、safe_db_transaction |
| L3 | 执行后 | 结构化响应、日志记录、访问计数器更新 |
安全数据库原语
| 原语 | 用途 | 失败时行为 |
|---|---|---|
safe_db_write |
单条 SQL 写入 | 返回 None(锁超时/磁盘满/数据损坏) |
safe_db_transaction |
多条 SQL 原子批量操作 | 返回 (False, None) 并回滚 |
mcp_error_boundary |
MCP 工具装饰器 | 捕获所有异常,返回 {"error": "..."} |
服务冷却
当 Ollama/外部嵌入服务失败时:
- 指数退避:60s → 120s → 240s → 300s(最大值)
- 冷却期间:embedder.available = False,搜索降级为仅 BM25
- 成功后重置冷却计数器
嵌入管道
┌─────────────────────┐
config.embed_backend│ │
┌──────────┤ create_embedder() │
│ │ │
│ └─────────────────────┘
│
┌────▼──────┐ ┌───────────────┐
│ "onnx" │ │ "ollama" │
│ │ │ │
│ FastEmbed │ │ OllamaEmbed │
│ ONNX CPU │ │ HTTP API │
│ ~5ms/查询 │ │ ~20-50ms/查询 │
│ 384d │ │ 768d │
└───────────┘ └───────────────┘
│ │
│ Embedder Protocol │
│ ├── embed_one() │
│ ├── embed_batch() │
│ ├── available │
│ └── close() │
└───────────┬───────────┘
│
┌──────▼───────┐
│ float[dim] │
│ → vec0 BLOB │
└──────────────┘
Web UI 架构
python -m robotmem web
│
┌────▼────────────────────────┐
│ Flask App Factory │
│ create_app() │
│ │
│ ┌────────────────────────┐ │
│ │ api_bp (Blueprint) │ │
│ │ /api/doctor │ │
│ │ /api/stats │ │
│ │ /api/memories │ │
│ │ /api/search │ │
│ │ /api/memory/<id> │ │
│ │ /api/sessions │ │
│ │ /api/collections │ │
│ │ /api/categories │ │
│ │ /api/recent-failures │ │
│ │ /api/sessions/<id>/ │ │
│ │ memories │ │
│ └────────────────────────┘ │
│ │
│ GET / → index.html │
│ CogDatabase(共享连接) │
└─────────────────────────────┘
REST API 端点
| 方法 | 端点 | 说明 |
|---|---|---|
| GET | /api/doctor |
健康检查:FTS5/vec0 同步、零命中率、数据库大小 |
| GET | /api/stats |
总计数、类型/类别分布、集合列表 |
| GET | /api/memories |
分页列表,支持过滤(collection、type、category、confidence、days) |
| GET | /api/search?q= |
FTS5 全文搜索,跨集合 |
| GET | /api/memory/<id> |
单条记忆详情 |
| DELETE | /api/memory/<id> |
软删除(需提供原因) |
| PUT | /api/memory/<id> |
更新记忆字段 |
| GET | /api/sessions |
分页会话列表,包含记忆计数 |
| GET | /api/collections |
集合列表,包含计数 |
| GET | /api/categories |
类别列表,包含计数 |
| GET | /api/recent-failures |
最近的 postmortem/gotcha 记忆 |
| GET | /api/sessions/<id>/memories |
会话内的记忆(时间线) |