KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

04. 先不用模型,手动完成记忆生命周期 — keel 龙骨

到现在为止,你已经回答了两个问题:什么值得保存,一条记录需要哪些字段。接下来先把模型拿掉,只完成四件事:保存、读取、修改和删除。

到现在为止,你已经回答了两个问题:什么值得保存,一条记录需要哪些字段。接下来先把模型拿掉,只完成四件事:保存、读取、修改和删除。

这样做很重要。模型会带来不确定输出,向量会带来近似检索;如果同时引入它们,你很难判断错误来自数据层、模型层还是检索层。

1. 为什么先选 SQLite

SQLite 是一个嵌入式关系数据库:数据保存在普通文件中,Python 标准库可以直接操作,不需要先部署数据库服务。它适合这一阶段,因为你能清楚看到:

SQLite 不是“简化版记忆系统”。它承担的是结构化事实存储;未来即使把向量迁移到专用数据库,这部分生命周期数据通常仍需要可靠的数据存储。

2. 先读懂最小表结构

CREATE TABLE memories (
    memory_id       TEXT PRIMARY KEY,
    tenant_id       TEXT NOT NULL,
    owner_id        TEXT NOT NULL,
    kind            TEXT NOT NULL,
    content         TEXT NOT NULL,
    source_type     TEXT NOT NULL,
    source_ref      TEXT NOT NULL,
    status          TEXT NOT NULL,
    version         INTEGER NOT NULL,
    created_at      TEXT NOT NULL,
    updated_at      TEXT NOT NULL,
    expires_at      TEXT
);

不要急着背字段。把它们分成三组更容易理解:

谁的数据:tenant_id、owner_id
数据是什么:kind、content、source_type、source_ref
现在能否使用:status、version、updated_at、expires_at

3. 第一次完成“保存后重新打开”

进入项目目录运行:

cd courses/foundation/persistent-memory/course/project
python examples/01_store_and_recall.py

这个示例按下面的顺序工作:

flowchart LR
    A[创建 SQLite 文件] --> B[写入 Alice 的一条记忆]
    B --> C[关闭数据库连接]
    C --> D[重新打开同一文件]
    D --> E[按 Alice 的作用域查询]

你应该看到类似输出:

recalled after reopen: 1
读者希望排障结论使用中文。

这次实验只证明持久化成立。它还没有证明语义检索、安全写入或生产可用性。

4. 沿着代码看一次写入

打开 src/persistent_memory_course/repository.py,找到 add()。它完成两件必须一起成功的操作:

写入 memories 主记录
写入 memory_events 审计事件

它们放在同一个数据库事务中。如果主记录写入成功而审计失败,事务会整体回滚;这样不会留下“有数据但没有来源记录”的中间状态。

下面是这个过程的简化版本:

def add_memory(connection, record, actor):
    # with 会开启一个事务。代码正常结束才提交,出现异常就回滚。
    with connection:
        connection.execute(
            "INSERT INTO memories (...) VALUES (...)",
            record,
        )
        connection.execute(
            "INSERT INTO memory_events (...) VALUES (...)",
            (record.memory_id, "created", actor),
        )

这里的重点不是 SQL 语法,而是状态变化和审计事件具有同一成功边界。

5. 查询时先做作用域隔离

安全的查询应该先把候选集合限制在当前租户和用户:

SELECT * FROM memories
WHERE tenant_id = ?
  AND owner_id = ?
  AND status = 'active'
  AND (expires_at IS NULL OR expires_at > ?);

tenant_id 和 owner_id 的值来自登录身份或服务身份,不来自模型回答。即使 Alice 和 Bob 保存了完全相同的正文,Alice 的查询也只能返回 Alice 的记录。

在测试中,这条边界通过两个内容相同、owner 不同的样本验证。你可以运行:

python -m pytest tests/test_repository.py -q

6. 更新不是静默覆盖

假设旧记录是:

v1:用户偏好英文报告

用户后来改为中文。更容易解释的做法是:

  1. 创建一条新记录 v2;
  2. 让 v2.supersedes_id 指向 v1;
  3. 把 v1.status 改为 superseded;
  4. 查询只返回 active 的 v2;
  5. 审计中仍能解释偏好何时发生过变化。

这两个修改也必须在同一事务中完成。否则可能出现旧记录已经失效、新记录却没有成功写入的空窗。

7. 删除分成“停止使用”和“物理清理”

这里先使用逻辑删除:把状态改为 deleted,正常查询不再返回它。这样可以验证访问路径,同时保留必要的审计元数据。

生产环境还要继续处理向量索引、缓存、备份和日志中的副本。第八章会完整讨论遗忘;现在只需要确认一件事:删除不是把正文从 prompt 中排除,而是改变持久数据的生命周期。

8. 本章的检查点

继续之前,你应该能解释:

数据层现在已经可以独立工作。下一步才把它放回 Agent 运行,判断应该在什么时候读取和写入。

下一章:持久记忆怎样接回 Harness?

进入 keel 阅读