Skip to content

[Discussion] Technical Review & Optimization Suggestions (Architecture & Performance) #12

Description

@EthanLau1

📝 Technical Review & Optimization Suggestions

Hi @Cocoblood9527,概括:

Diving into the LocalMemOS codebase and am genuinely impressed by the architecture. The use of a Rust core with bitemporal data modeling (valid_from/valid_to), idempotent upserts, and evidence-based traceability is excellent. The engineering quality is very high.

While reviewing the implementation, I identified a few areas where we could potentially improve performance, search quality, and MCP integration. I've categorized these based on impact for your consideration:

🔴 P0: Critical Improvements (High Impact)

1. Missing MCP Tool Schemas
Currently, MCP tools are registered without JSON Schemas (e.g., server.tool("memory_upsert_fact", upsert as any)). This prevents Claude Desktop/Cursor from providing parameter hints and causes issues with automatic tool calling.

  • Suggestion: Define explicit JSON Schemas for all tools in packages/mcp/src/tools.ts.

2. FTS5 Index Cleanup
In store.rs, the upsert_fts_row function inserts new entries but never deletes old ones when a fact is updated or retired. This leads to "index bloat," where search results include stale/retired facts.

  • Suggestion: In close_current_fact, add a DELETE FROM facts_fts WHERE fact_id = ? to keep the search index clean.

3. HTTP Concurrency Bottleneck
The AppState uses a global Mutex for the MemoryStore. Since recall and list are read-only operations, they are currently blocking each other.

  • Suggestion: Switch from std::sync::Mutex to tokio::sync::RwLock to allow concurrent read operations.

🟡 P1: Important Optimizations

1. Missing Database Indexes
The facts table lacks composite indexes for the core lookup query (namespace, scope_id, entity, attribute). This will cause full table scans as the dataset grows.

  • Suggestion: Add a composite index on (namespace, scope_id, entity, attribute, valid_to).

2. Python SDK Semantic Gap
The Python recall method is missing the as_of parameter, which is available in the Rust core. This prevents users from performing historical point-in-time queries via the Python SDK.

3. FTS Stopword List
The current STOPWORDS list is quite minimal.

  • Suggestion: Consider expanding the list or integrating a standard NLP stopword set to improve search relevance.

🟢 P2: Maintenance & DX

  • ID Generation: The ID_COUNTER (AtomicU64) resets on process restart. Consider switching to ULID or UUID v4 to ensure global uniqueness across restarts.
  • Documentation: It would be helpful to explicitly note in the README that the project currently targets macOS/Linux, and include_history is reserved for future use to avoid user confusion.

Summary
This is an outstanding project with a very solid foundation. These suggestions are just my observations while studying the code. I am happy to open PRs for some of these (especially the MCP Schema and Indexing) if you think they align with your roadmap!

Keep up the great work!

LocalMemOS 深度技术审查报告(基于真实源码)

⚠️ 重要说明:前两次分析基于 README 描述,存在大量错误假设。本报告基于对仓库全部源码的实际阅读,结论完全重建。


一、项目真实架构(纠正前次错误)

这不是前次描述的项目

前次分析(错误) 实际代码(正确)
Python 写的 Web 应用 Rust 核心 + 多语言绑定
manage.py / main.py / api/ 无此文件,完全不同的项目
三级检索(关键词+全文+图谱向量) 仅 FTS 文本检索,无向量,无图谱
Claude Desktop 聊天 Web UI 无 Web UI,纯 SDK+MCP+HTTP 适配器
待审池机制 完全不存在
Read-Before-Write 机制 存在,但实现方式不同

真实技术栈

Workspace
├── crates/
│   ├── memory-core/     Rust 核心库 (SQLite + 业务逻辑)
│   ├── memory-http/     Axum HTTP 适配器
│   └── memory-node/     Node.js Native Binding (napi-rs)
├── packages/
│   ├── node/            TypeScript Node SDK
│   └── mcp/             MCP 适配器 (TypeScript)
├── python/              Python SDK (PyO3/Maturin 绑定)
└── tools/locomo/        LoCoMo 检索评估脚本

语言分布:Rust 56.4% / Shell 19.5% / Python 12.8% / TypeScript 10.5%


二、核心数据模型(实际代码)

数据结构

// request.rs — 写入请求
pub struct UpsertFactRequest {
    pub namespace: String,      // "user" | "runtime" | "workspace"
    pub scope_id: String,       // 逻辑隔离单元
    pub entity: String,         // 实体名
    pub attribute: String,      // 属性名
    pub value: serde_json::Value,
    pub confidence: Option<f32>,
    pub tags: Vec<String>,
    pub valid_from: Option<DateTime<Utc>>,
    pub source_kind: String,    // "manual" | 其他
    pub source_ref: Option<String>,
    pub evidence_summary: Option<String>,
}

// model.rs — 存储记录
pub struct FactRecord {
    pub id: String,
    pub namespace: String,
    pub scope_id: String,
    pub entity: String,
    pub attribute: String,
    pub value_json: Value,
    pub value_text: Option<String>,
    pub confidence: Option<f32>,
    pub valid_from: DateTime<Utc>,
    pub valid_to: Option<DateTime<Utc>>,   // 软删除机制
    pub updated_at: DateTime<Utc>,
}

数据库 Schema(4 张表)

facts           -- 当前事实(valid_to IS NULL = 当前有效)
fact_versions   -- 历史版本链(每次 upsert 写入一条)
evidence        -- 证据来源(source_kind, source_ref, summary)
facts_fts       -- FTS5 全文搜索虚拟表

设计亮点

  • 事实以 (namespace, scope_id, entity, attribute) 为主键
  • valid_from / valid_to 实现双时态(bitemporal)语义
  • evidence 表实现溯源,记录每次写入的来源

三、核心逻辑审查

3.1 upsert_fact 写入逻辑(实际代码)

// store.rs — 真实的 Read-Before-Write 实现
pub fn upsert_fact(&mut self, req: UpsertFactRequest) -> Result<FactRecord, MemoryError> {
    req.validate()?;
    let tx = self.conn.transaction()?;
  
    if let Some(current) = find_current_fact(&tx, &req)? {
        if current.value_json == req.value {
            // ✅ 幂等:值未变则只更新时间戳
            tx.execute("UPDATE facts SET updated_at = ? WHERE id = ?", ...)?;
            append_evidence(...)?;
            tx.commit()?;
            return Ok(refreshed);
        }
        // ✅ 值变化:关闭旧版本,创建新版本
        close_current_fact(&tx, &current.id, &now_text)?;
        close_current_version(&tx, &req, &now_text)?;
    }
  
    let inserted = insert_new_fact(&tx, &req, now)?;
    let version_id = insert_fact_version(&tx, &inserted, &req, &now_text)?;
    append_evidence(&tx, &inserted.id, Some(&version_id), &req, &now_text)?;
    upsert_fts_row(...)?;
    tx.commit()?;
    Ok(inserted)
}

✅ 亮点:幂等写入、版本链保留、证据记录、事务保证,逻辑设计完整

3.2 FTS 检索逻辑(实际代码)

// fts.rs — 停用词表 + 查询归一化
const STOPWORDS: &[&str] = &["a","an","and","are","as","at","activities",
    "activity","be","been","by","change","changes"...]; // 约 50 个停用词

pub fn build_fts_query(text: &str) -> Option<String> {
    let tokens = normalize_query_tokens(text);
    if tokens.is_empty() { return None; }
    Some(tokens.iter().map(|t| format!("{t}*")).collect::<Vec<_>>().join(" OR "))
}

实际检索流程

Query 文本
   ↓
normalize_query_tokens()  // 分词 + 去停用词 + 去重 + 截断(max 12)
   ↓
build_fts_query()         // 生成 FTS5 OR 查询
   ↓
SQLite FTS5               // 全文搜索 + 排名
   ↓
phrase continuity 重排    // store.rs 中的自定义重排序逻辑

3.3 MCP 工具(实际代码)

// packages/mcp/src/tools.ts
// 实际暴露的 5 个 MCP 工具:
"memory_upsert_fact"   // 写入事实
"memory_recall"        // 检索事实
"memory_list"          // 列举事实
"memory_forget"        // 删除事实
"memory_history"       // 查询历史版本

⚠️ 问题:所有工具均无参数 Schema 定义(server.tool("name", handler as any)),Claude/Cursor 无法自动感知参数格式。


四、真实存在的问题(基于代码)

🔴 P0:MCP 工具无 Schema 定义

代码证据

// tools.ts — 实际代码
server.tool("memory_upsert_fact", upsert as any);  // ← 无参数 schema
server.tool("memory_recall", recall as any);        // ← 无参数 schema

问题:MCP 协议要求工具注册时提供 JSON Schema,描述参数名称、类型、必填项。缺失 schema 会导致:

  • Claude Desktop 无法提示用户正确的参数
  • 自动调用时参数错误率高
  • 与 MCP 标准不符

建议修复

server.tool(
  "memory_upsert_fact",
  {
    namespace: { type: "string", description: "命名空间: user/runtime/workspace" },
    scope_id:  { type: "string", description: "作用域 ID" },
    entity:    { type: "string", description: "实体名称" },
    attribute: { type: "string", description: "属性名称" },
    value:     { description: "任意 JSON 值" },
    source_kind: { type: "string", description: "来源类型,如 manual/agent" },
    confidence:  { type: "number", description: "置信度 0.0-1.0", optional: true },
    evidence_summary: { type: "string", optional: true },
  },
  upsert
);

🔴 P0:FTS 索引只在写入时追加,从不更新

代码证据

// fts.rs
pub fn upsert_fts_row(tx, fact_id, ...) -> rusqlite::Result<()> {
    tx.execute(
        "INSERT INTO facts_fts (fact_id, namespace, ...)",  // ← 只 INSERT,无 DELETE
        ...
    )?;
    Ok(())
}

upsert_fact 中的调用

// store.rs — 无论是新建还是值变化,都直接 INSERT FTS
upsert_fts_row(&tx, &inserted.id, ...)?;

问题:每次 upsert 都在 FTS5 表中追加一行,旧版本的 FTS 条目永远不会被删除。随着时间推移:

  1. FTS 搜索会返回已过期事实(valid_to 非 NULL)
  2. FTS 表无限增长,检索噪音越来越多
  3. 同一 fact_id 在 FTS 表中可能有多行

建议修复

// 在 close_current_fact 时同步删除旧 FTS 行
fn close_current_fact(tx, fact_id, now_text) -> Result<()> {
    tx.execute("UPDATE facts SET valid_to = ? WHERE id = ?", ...)?;
    tx.execute("DELETE FROM facts_fts WHERE fact_id = ?", [fact_id])?; // ← 新增
    Ok(())
}

🔴 P0:HTTP 层使用全局 Mutex,高并发场景阻塞

代码证据

// state.rs (推断自 routes.rs 用法)
pub struct AppState {
    pub store: Arc<tokio::sync::Mutex<MemoryStore>>,  // ← 全局单锁
}

// routes.rs — 每个请求都持锁
pub async fn recall(...) -> Result<...> {
    let store = state.store.lock().await;  // ← 独占锁,读操作也上锁
    let result = store.recall(req)?;
    Ok(Json(result))
}

问题recalllisthistory 均为只读操作,但使用同一把 Mutex(非 RwLock),导致:

  • 多个并发读请求互相阻塞
  • 单个慢写入会阻塞所有读请求

建议修复

// 将 Mutex 改为 RwLock
pub store: Arc<tokio::sync::RwLock<MemoryStore>>,

// 读操作用 read() 锁
pub async fn recall(State(state): State<AppState>, ...) {
    let store = state.store.read().await;  // ← 读锁,并发友好
    ...
}

// 写操作用 write() 锁
pub async fn upsert_fact(State(state): State<AppState>, ...) {
    let mut store = state.store.write().await;  // ← 写锁
    ...
}

🟡 P1:ID 生成机制存在竞态风险

代码证据

// store.rs
static ID_COUNTER: AtomicU64 = AtomicU64::new(1);

问题

  1. 进程重启后 ID 计数器从 1 重置,若 DB 已有数据会产生 ID 碰撞
  2. 实际 ID 生成逻辑(未见 fetch_add 调用)需确认是否正确使用
  3. 多进程访问同一 DB 文件时计数器不共享

建议:改用 UUID v4 或 ulid(既有唯一性又有时间序):

// 使用 uuid crate
let id = uuid::Uuid::new_v4().to_string();

🟡 P1:Python SDK 的 recall 丢失 as_of 参数

代码证据

# python/memory_sdk/client.py
def recall(self, **kwargs):
    payload = {
        "namespace": kwargs["namespace"],
        "scope_id": kwargs["scope_id"],
        "entity": kwargs.get("entity"),
        "attribute": kwargs.get("attribute"),
        "text_query": kwargs.get("text_query"),
        "include_history": kwargs.get("include_history", False),
        # ← 缺少 "as_of": kwargs.get("as_of")
    }

对比 Rust RecallRequest

pub struct RecallRequest {
    pub as_of: Option<DateTime<Utc>>,  // ← Python SDK 无法传递此参数
}

影响:Python 用户无法使用时态查询(as_of),是跨层语义缺失的典型问题。


🟡 P1:forget 的实现未确认是软删除还是硬删除

代码证据(已有的 routes.rs)

pub async fn forget(...) -> Result<Json<serde_json::Value>, ApiError> {
    let mut store = state.store.lock().await;
    store.forget(req)?;   // ← forget 的实现在 store.rs 中,但未读到完整实现
    Ok(Json(json!({ "ok": true })))
}

设计文档声明

forget should logically retire facts by default, not hard-delete them

测试用例也验证了

# test_sdk_core.py
result = client.recall(...)
assert result["facts"] == []   # forget 后无法 recall

但 FTS 表和 fact_versions 表是否同步清理仍需确认。


🟡 P1:FTS 停用词表不完整,影响检索质量

代码证据

// fts.rs — 现有停用词
const STOPWORDS: &[&str] = &[
    "a","an","and","are","as","at","activities","activity",
    "be","been","by","change","changes","did","do","does",
    "for","from","had","has","have","her","his","how","in",
    "is","it","kind","kinds","many","of","on","or",
    "done","seen","some","that","the","their","this","to",
    "way","ways","was","we","what","when","where","which",
    "who","why","with",
];

问题

  • 缺少 notnobutifthenalsojustlikemyyourtheyhesheimehimus 等常用词
  • 缺少中文分词支持(若有中文数据则 FTS 完全失效)
  • 手工维护停用词表容易遗漏,建议引入标准 NLP 停用词表

🟡 P1:facts 表缺少关键索引

代码证据

-- schema.rs
CREATE TABLE IF NOT EXISTS facts (
    id TEXT PRIMARY KEY,
    namespace TEXT NOT NULL,
    scope_id TEXT NOT NULL,
    entity TEXT NOT NULL,
    attribute TEXT NOT NULL,
    ...
    valid_to TEXT,
    ...
);
-- ← 无复合索引!

问题:核心查询 WHERE namespace = ? AND scope_id = ? AND entity = ? AND attribute = ? AND valid_to IS NULL 在数据量大时全表扫描。

建议

CREATE INDEX IF NOT EXISTS idx_facts_key 
    ON facts(namespace, scope_id, entity, attribute, valid_to);

CREATE INDEX IF NOT EXISTS idx_fact_versions_key
    ON fact_versions(namespace, scope_id, entity, attribute);
  
CREATE INDEX IF NOT EXISTS idx_evidence_fact
    ON evidence(fact_id);

🟢 P2:Windows 平台支持未测试

代码证据

# .github/workflows/locomo-full-gate.yml
jobs:
  full-gate:
    runs-on: ubuntu-latest   # ← 仅 Linux

README 声明

Clean-Run Checklist (macOS + Linux)   # ← 标题明确只有 macOS + Linux

问题:项目明确不支持 Windows CI,但 README 中的 Environment Setup 未提及此限制,可能误导 Windows 用户尝试安装。


🟢 P2:include_history 字段保留但有误导性

代码证据

// request.rs
pub struct RecallRequest {
    // Reserved in v1. Kept for compatibility; current core recall semantics ignore this flag.
    pub include_history: bool,
}
// models.ts
// Reserved in v1; forwarded for compatibility but currently ignored by core recall semantics.
includeHistory?: boolean;

问题:字段存在于所有 SDK 接口中,但完全无效,容易让用户误以为能用。建议在注释中更明确地写出"此字段 v1 中无效,v2 实现时间历史查询请使用 history() 方法"。


🟢 P2:MemoryStore 非线程安全,但无文档说明

代码证据

pub struct MemoryStore {
    conn: rusqlite::Connection,  // ← rusqlite::Connection 不是 Send + Sync
}

rusqlite::Connection 不实现 Send,因此 MemoryStore 无法跨线程共享,HTTP 层用 Mutex 封装解决了这个问题,但 Python SDK 和 Node SDK 若在多线程环境调用需要注意。


五、亮点(真实代码中确认的)

设计 代码证据 评价
幂等写入 if current.value_json == req.value 分支 ✅ 优雅,避免无效版本
Bitemporal 语义 valid_from / valid_to / as_of ✅ 业界最佳实践
证据溯源 evidence 表 + source_kind/ref/summary ✅ 可解释性强
事务安全 let tx = self.conn.transaction()? ✅ 写入全部在事务中
LoCoMo 评估 完整的评估脚本体系 + CI gate ✅ 工程严谨
命名空间隔离 测试文件 namespace_isolation.rs ✅ 多租户基础具备
短语连续性排名 ranking_prefers_phrase_continuity 测试 ✅ FTS 排名有深度思考
CI 自动化 locomo-full-gate.yml ✅ 检索质量有门控

六、优先级汇总

优先级 问题 文件 影响
🔴 P0 MCP 工具无参数 Schema packages/mcp/src/tools.ts Claude/Cursor 无法正确调用
🔴 P0 FTS 行只增不减,旧版本污染检索 crates/memory-core/src/fts.rs 检索结果越来越差
🔴 P0 HTTP 读操作使用 Mutex 而非 RwLock crates/memory-http/src/routes.rs 并发性能差
🟡 P1 ID 生成器重启后重置 crates/memory-core/src/store.rs 潜在 ID 碰撞
🟡 P1 Python recall 丢失 as_of 参数 python/memory_sdk/client.py 跨层语义不一致
🟡 P1 核心查询无复合索引 crates/memory-core/src/schema.rs 大数据量下性能劣化
🟡 P1 FTS 停用词表不完整 crates/memory-core/src/fts.rs 检索召回质量损失
🟢 P2 Windows 无 CI,文档未说明 CI + README 用户困惑
🟢 P2 include_history 无效但误导性强 所有 SDK API 设计困惑

七、建议

这是一个工程质量相当高的项目:

  • Rust 核心设计严谨(事务、幂等、版本链、证据溯源)
  • 跨语言绑定完整(Python + Node + MCP + HTTP)
  • CI 评估体系(LoCoMo gate)在同类项目中少见

最值得立即修复的三项

  1. MCP Schema(1小时):在 tools.ts 中为每个工具添加参数 schema,这是使项目真正可用的关键一步

  2. FTS 清理(2小时):在 close_current_fact 中添加 DELETE FROM facts_fts WHERE fact_id = ?,否则数据越多检索越差

  3. 数据库索引(30分钟):在 schema.rsALL_SCHEMA 中添加复合索引,这是最低投入最高回报的改动

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions