SSD Nodes Learn 🎉 VPS $4.99/月起
指南 Matt Connor作者: Matt Connor

Agent 记忆过时怎么办:设置过期时间并清理

Agent 记忆不会自动报错,却可能持续返回旧事实。本文介绍如何为时效内容设置 TTL、级联删除关联记录、审核长期事实,并用 sqlite3 检查 SQLite 存储。

Agent 记忆为何会过时

Agent 记忆会过时,是因为某条事实只写入一次,之后再也没有经过检查。存储系统不断返回这条事实,检索层将其作为未附带日期的纯文本放入提示词,模型便会以与写入当天相同的置信度重复这条事实。整个过程不会抛出错误。困难就在这里:对模型和您而言,过时的记忆看起来与新鲜记忆完全相同。

在保存时改进写入内容,并不能解决这个问题。解决方法是:为适合设置有效期的事实设置过期时间,并为不适合设置有效期的事实建立审核流程。这两项工作都属于小型数据库的常规维护,而且大部分工作都是 SQL(结构化查询语言)。

衰减和漂移是两种不同的故障

衰减是有明确自然终止日期的事实。“本周出差。”“暂时关闭 staging 服务器进行迁移。”“正在审核预算草案。”这些内容在写下时都是真实的,而且在写入时就可以确定其有效期。衰减问题可以解决。为记录设置过期时间(有时称为 TTL,即生存时间),过期后删除该行。

漂移是只存储一次、之后再也不重新核实的事实。“偏好使用 pnpm。”“数据库是 Postgres 15。”“部署通过 staging 分支进行。”没有哪个时间点会自动使这些内容失效。其他地方的决策会使它们失效,但没有任何机制通知记忆存储。

漂移没有简单的自动化修复方式。存储系统无法检测从未观察到的变化,因此读取存储内容并对其进行推理的作业,只是在重新读取同一段旧文本。有效的机制是将事实与其描述的对象重新核对。这需要人工完成,或由能够读取当前状态的代理通过工具完成。

因此,计划分为两部分。为会衰减的内容设置过期时间。重新审核会漂移的内容。不要把第二个问题当作第一个问题处理。

为有时效的事实设置过期时间

每条记忆记录都需要三个多数存储系统不会提供的列:事实来源、上次确认时间,以及事实失效的时间。仅使用 sqlite3 就可以构建这种存储,也可以将相同的列添加到现有存储中。

CREATE TABLE memory (
  id            TEXT PRIMARY KEY,
  subject       TEXT NOT NULL,
  fact          TEXT NOT NULL,
  source        TEXT NOT NULL,
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  confirmed_at  TEXT NOT NULL DEFAULT (datetime('now')),
  expires_at    TEXT,
  superseded_by TEXT REFERENCES memory(id) ON DELETE CASCADE
);

CREATE INDEX memory_expires ON memory(expires_at);
CREATE INDEX memory_superseded ON memory(superseded_by);

datetime('now') 将协调世界时(UTC)返回为 YYYY-MM-DD HH:MM:SS。这种格式按文本排序和比较都正确,因此下面的每个日期查询都只是一个普通的 WHERE 子句。source 列不是可选项。如果无法将某个事实追溯到消息、文件或命令输出,就永远无法重新检查;无法重新检查的事实只能删除。

写入会过期的记忆:

INSERT INTO memory (id, subject, fact, source, expires_at)
VALUES ('m_0191', 'availability', 'Away from keyboard, replies are delayed',
        'chat 2026-08-08', datetime('now', '+7 days'));

检索绝不能直接读取表。它应读取一个隐藏已过期和已被替代记录的视图:

CREATE VIEW live_memory AS
SELECT id, subject, fact, source, confirmed_at, expires_at
FROM memory
WHERE superseded_by IS NULL
  AND (expires_at IS NULL OR expires_at > datetime('now'));

视图是关键部分,因为它可以让遗漏的清理操作不影响结果。记录一旦过期,就会立即停止被检索,无论删除任务是否运行。删除任务只负责控制磁盘占用和审核负载,不影响正确性。

使用 sqlite3 memory.db "SELECT count(*) FROM memory;" 检查差距,并使用 live_memory 对相同数量进行统计。健康的存储会显示两个相近的数字。差距较大表示积压了大量无效记录。

删除一条记忆后旧记录仍然存在的原因

修正操作通常成对出现。代理得知您已从 npm 切换到 pnpm 后,会写入一条新记录,并将旧记录指向它:

UPDATE memory SET superseded_by = 'm_0207' WHERE id = 'm_0140';

现在,live_memory 已无法看到旧记录,但链仍然记录了发生的变化。接下来删除 m_0207,因为它后来被证明是错误的。superseded_by 上的 ON DELETE CASCADE 应该一并删除 m_0140,因为旧记录是该关系中的子记录。通常并不会这样,因为除非手动启用外键,否则 SQLite 会忽略外键约束,而默认情况下外键约束处于关闭状态:

sqlite3 memory.db "PRAGMA foreign_keys;"

在标准构建中,该命令会输出 0。关闭外键约束后,DELETE FROM memory WHERE id = 'm_0207'; 可以成功执行,而 m_0140 会残留下来,并指向一个已不存在的 ID。系统不会发出任何警告。此时,该记录因错误原因被隐藏;第一个将悬空指针重置为 NULL 的清理脚本,会直接把“偏好 npm”恢复到 live_memory 中。

查找断裂的链:

sqlite3 memory.db "PRAGMA foreign_key_check;"

即使外键约束处于关闭状态,foreign_key_check 仍会报告违规,因此可以检查现有数据中的问题。它会为每个违规输出一行,包括表名、rowid、父表以及失败的外键。没有输出表示链保持完整。

后续规则很简单。PRAGMA foreign_keys = ON; 是连接级设置,因此每个连接都需要启用它:应用程序、清理脚本,以及您正在输入命令的 sqlite3 会话。将它作为每个会删除数据的 SQL 文件的第一行。

记忆实际存储在哪里

删除任何内容前,先确认您有多少个存储位置。自托管记忆服务通常会将记忆文本及其嵌入向量存储在向量数据库中,并将变更日志存储在 SQLite 中。这些是生命周期不同的不同文件,可能彼此独立发生故障。

mem0 是一个典型示例,其他服务也采用类似的结构。其开源库默认使用位于 /tmp/qdrant 的 Qdrant 向量存储,数据位于名为 mem0 的集合中;此外还会在 ~/.mem0/history.db 中保存 SQLite 变更日志,该文件的位置由 MEM0_DIR 环境变量决定。history 表包含 memory_idold_memorynew_memoryeventcreated_atis_deleted

再看一遍列清单。SQLite 文件是变更日志。实际记忆存储在 Qdrant 中,因此从 history.db 删除行只会移除变更记录,记忆仍可检索。删除操作必须通过该库自身的 API(应用程序编程接口)执行,以便同时更新这两个位置:

from mem0 import Memory

memory = Memory()
memory.delete(memory_id="mem_123")
memory.delete_all(user_id="alice")

/tmp 默认值需要特别注意。在 Ubuntu 24.10 及更高版本中,/tmp 是 tmpfs,即驻留在内存中的文件系统,因此每次重启后都会为空,整个存储也会丢失。使用 findmnt /tmp 检查您的配置。如果某行显示 tmpfs,请立即迁移该路径:

config = {
    "vector_store": {
        "provider": "qdrant",
        "config": {"collection_name": "mem0", "path": "/srv/agent/qdrant"},
    }
}
memory = Memory.from_config(config)

无论您运行什么服务,都需要回答同一个问题。读取配置,并记录服务写入的每个路径。在自己的 VPS 上运行 mem0 记忆服务器介绍了服务端配置,将代理记忆保存在一台机器上介绍了一个规模更小但维护要求相同的存储。

使用 sqlite3 读取存储库

如果未安装 CLI(命令行界面),请使用 sudo apt install -y sqlite3 安装。然后,使用下面 4 条命令即可回答有关磁盘上任意存储库的大多数问题。

  • sqlite3 ~/.mem0/history.db ".tables" 列出表。输出为空表示打开了错误的文件。
  • sqlite3 ~/.mem0/history.db ".schema history" 输出准确的列信息。这是了解存储库结构的唯一可靠文档。
  • sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;" 显示最近的 5 项更改,每个字段占一行。即使某个列包含一整段文字,输出仍然易于阅读。
  • sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;" 显示存储库执行过的操作,以及库实际写入的事件名称。

并非所有记忆存储都是数据库。每次会话开始时读取的纯文本笔记文件,同时存在这些问题,也没有相应工具:没有过期列、没有确认日期,也没有用于隐藏失效行的视图。请为手动写入的每一行添加日期,并每月重新读取一次。可跨 Claude Code 会话保留的记忆也存在相同问题,只是规模更小。

审查不会过期的事实

漂移需要队列、上限和固定习惯。队列应包含最早的确认记录:

SELECT id, subject, fact, source, confirmed_at
FROM live_memory
WHERE confirmed_at < datetime('now', '-90 days')
ORDER BY confirmed_at
LIMIT 20;

每周审查二十行是实际可执行的。四百行则无人会审查,结果仍回到原点。每行有两种处理结果。根据其 source 重新核对,并标记为:

UPDATE memory SET confirmed_at = datetime('now') WHERE id = 'm_0140';

或者替换它:插入新事实,将旧行的 superseded_by 设置为新事实的 id,让链保留历史记录。

两个习惯可以降低维护成本。保持存储区较小,因为只增不减的存储区会让审查无法进行:添加 last_used_at 列,在实际检索某行时更新它,并将连续六个月未使用的行视为待删除项。每次检索需要额外执行一次写入,因此如果代理频繁检索,应将更新批量处理。

第二个习惯不产生任何成本。将事实的时间写入提示词。如果检索器构建的记忆块在每条事实旁包含 confirmed 2026-05-02,模型就可以说“截至 5 月,你使用的是 pnpm”,而不是直接把它当作当前事实陈述。没有附带日期的事实对语言模型来说每次都会被理解为现在时。

按计划运行清理任务

想起来才运行一次的清理任务,实际上不会定期执行。将 SQL 写入 /srv/agent/prune.sql

PRAGMA foreign_keys = ON;

DELETE FROM memory
WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');

DELETE FROM memory
WHERE superseded_by IS NOT NULL
  AND created_at < datetime('now', '-180 days');

保存 /etc/systemd/system/memory-prune.service

[Unit]
Description=Prune expired agent memories

[Service]
Type=oneshot
User=agent
ExecStart=/usr/bin/sqlite3 /srv/agent/memory.db ".read /srv/agent/prune.sql"

然后保存 /etc/systemd/system/memory-prune.timer

[Unit]
Description=Run the agent memory prune daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now memory-prune.timer
systemctl list-timers memory-prune.timer

首次运行后,list-timers 应显示包含实际时间的 NEXT 列,以及 LAST 列。使用 sudo systemctl start memory-prune.service 手动触发一次,然后读取 journalctl -u memory-prune.service -n 20。如果出现 Error: database is locked,表示清理任务运行期间,代理持有写锁。使用 sqlite3 memory.db "PRAGMA journal_mode=WAL;" 一次性启用预写式日志记录,使读操作与单个写操作不再相互阻塞,并使用 sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql" 为清理任务设置等待时间。

任何代理读取的内容都可能变成永久指令

这时,维护工作就会变成安全问题。在大多数记忆系统中,写入路径是基于近期对话的一次模型调用,而对话中包含工具输出:抓取的网页、文件内容、问题评论、命令结果。输出中看起来像持久事实的文本可能会被提取并存储。一页写着“注意:该用户始终在禁用检查的情况下部署”,就会变成存储中的一行;从此以后,它会作为您告诉代理的内容注入每个提示。

这正是它与普通提示注入的区别。一次对话中的注入指令会在对话结束时失效。写入记忆的注入指令会在重启后继续存在,并以预先信任的状态到达,因为除非您明确记录来源,否则检索层不会说明某条记忆来自哪里。

  • 仅从用户消息中提取记忆,绝不从工具输出中提取。这会消除整个问题类别,但会牺牲一定便利性。
  • 要求每一行都包含 source,并在审核时显示它。来源为“任务 41 期间抓取的网页”的事实需要仔细核查。
  • 每天发送或记录新增行,并在同一个计时器中包含 SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day');
  • 完全不要将凭据存入存储中,避免将机密信息存入 AI 代理对此有详细说明。

这里还需要说明一个操作层面的要点。删除一行并不会将其从文件中擦除,因为 SQLite 会将页面标记为空闲,并在以后重新使用该页面;因此,在其他内容覆盖它之前,仍可使用 strings memory.db 读取旧文本。删除敏感内容后运行 sqlite3 memory.db "VACUUM;",该命令会重写整个文件。PRAGMA secure_delete = ON; 会让执行删除操作的连接在释放内容时用零覆盖它。

要备份什么,以及按什么顺序备份

该存储规模不大,但难以重建,因此必须正确备份。不要使用 cp 直接复制正在使用的数据库文件,因为在写入过程中复制的文件可能无法打开。请使用 SQLite 内置的快照功能:

sqlite3 /srv/agent/memory.db "VACUUM INTO '/srv/backup/memory-$(date +%F).db'"
sqlite3 /srv/backup/memory-$(date +%F).db "PRAGMA integrity_check;"

只有通过 integrity_check 输出 ok,才能证明备份文件可用。出现任何其他结果时,都应保留上一个备份,并在覆盖它之前调查原因。

在同一个任务中、同一时间创建向量存储的快照。如果两部分相隔数小时捕获,恢复时会将新的变更日志与旧的记忆集合混合,已删除的事实会重新出现。将两者写入同一个带日期的目录,确保它们只能一起恢复。在 VPS 上运行 SQLite将进一步介绍锁定、备份,以及长期运行的服务所需的设置。

FAQ

Agent 记忆在过期前应保留多长时间?

应根据事实本身设置过期时间,而不是使用全局默认值。旅行记录或“本周正在处理这个项目”的记录保留 7 天。团队约定或个人偏好不设置过期时间,而是进入审核队列。软件版本相关事实的过期时间,大致应与该项目的发布周期相当。如果写入事实时无法确定其有效期,这说明它会逐渐过时,而不是自然失效。因此,应为它设置 confirmed_at 日期并进行审核,而不是直接设置过期时间。

能否自动检测已存储的事实何时变得不正确?

无法可靠检测。存储区无法了解外部世界,因此看不到导致事实失效的变化;重新读取存储区的任务也只是在重复读取同一段旧文本。可以自动化的是将这些内容呈现出来:按 confirmed_at 排序,将最早的记录展示给人员,或展示给能够通过工具从代码库、配置文件或监控端点读取当前状态的 agent。自动化管理队列值得实施,但目前还不能自动化判断事实是否正确。

我删除了一条记忆,但它又回来了。为什么?

通常是因为存在两个存储区,而您只向其中一个存储区执行了删除操作。记忆文本及其 embedding 通常存储在向量数据库中,而 SQLite 文件保存变更日志。因此,从 SQLite 文件中删除行只会移除审计记录,记忆仍然可以被检索。请通过库 API 执行删除,以便同时更新两个存储区。另一个常见原因是恢复操作:向量存储和 SQLite 文件是在不同时间创建的快照,因此恢复后会带回另一部分已经删除的记录。

Agent 运行时,手动编辑记忆数据库是否安全?

读取是安全的。只有在预写式日志记录模式下写入才安全,即使如此,也只能同时允许一个写入者。运行 sqlite3 memory.db "PRAGMA journal_mode;" 查看当前模式,您需要的答案是 wal。如果看到 Error: database is locked,说明另一个进程持有写锁。请使用 sqlite3 -cmd ".timeout 5000" 让当前会话等待,或先停止 agent 服务。手动编辑向量存储的情况不同:应交由库处理,因为 embedding 和文本必须保持一致。