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:用户偏好英文报告
用户后来改为中文。更容易解释的做法是:
- 创建一条新记录
v2; - 让
v2.supersedes_id指向v1; - 把
v1.status改为superseded; - 查询只返回 active 的
v2; - 审计中仍能解释偏好何时发生过变化。
这两个修改也必须在同一事务中完成。否则可能出现旧记录已经失效、新记录却没有成功写入的空窗。
7. 删除分成“停止使用”和“物理清理”
这里先使用逻辑删除:把状态改为 deleted,正常查询不再返回它。这样可以验证访问路径,同时保留必要的审计元数据。
生产环境还要继续处理向量索引、缓存、备份和日志中的副本。第八章会完整讨论遗忘;现在只需要确认一件事:删除不是把正文从 prompt 中排除,而是改变持久数据的生命周期。
8. 本章的检查点
继续之前,你应该能解释:
- 为什么先学 SQLite,而不是直接上向量数据库;
- 为什么作用域条件必须进入数据库查询;
- 为什么更新适合产生新版本;
- 为什么主记录和审计事件应放在同一事务中;
- 为什么逻辑删除只是遗忘流程的一部分。
数据层现在已经可以独立工作。下一步才把它放回 Agent 运行,判断应该在什么时候读取和写入。