从 Letta 看有状态 Agent:记住用户之后,还要管理记忆
本文是 Agent 工程里的经典架构 系列的一篇。文中的业务场景和代码为讲解设计,产品能力以所链接的官方资料为准。
一个助手能够记住用户的语言偏好,看起来只需要在提示词里加一段资料。但部署成服务后,问题很快变多:几台 Worker 读到的是同一份资料吗?用户改了偏好,正在执行的任务用哪个版本?错误记忆如何修正?状态一旦跨越请求,就需要明确的管理方式。
Letta 文档介绍了有状态 Agent 的开发与运行能力。其状态与记忆概念描述了记忆块、消息和工具等对象的关系;该具体页面属于 V1 文档,当前接入应以官网对应版本的 SDK 文档为准。这里借这些概念讨论状态设计,不提供可能随版本变化的 API 调用教程。
我会先给 Agent 一个稳定身份,再把用户记忆、单次任务进度和原始对话分开保存。记忆用于影响后续交互,任务进度用于恢复当前工作,原始记录用于追溯来源。它们可能引用同一事件,但不能简单混成一段不断追加的字符串。
下面的自建例子用 SQLite 保存一条偏好。键同时包含租户和 Agent 标识,更新使用版本条件,并记录来源。它不是 Letta SDK,也没有实现完整记忆系统;它只展示一个在选择任何有状态框架之前,都需要确定的存储契约。
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE memory (tenant TEXT, agent TEXT, value TEXT, "
"source TEXT, version INT, PRIMARY KEY(tenant, agent))")
conn.execute("INSERT INTO memory VALUES ('t1','a1','简短回复','用户明确设置',1)")
conn.commit()
def update(tenant, agent, expected, value, source):
with conn:
changed = conn.execute(
"UPDATE memory SET value=?,source=?,version=version+1 "
"WHERE tenant=? AND agent=? AND version=?",
(value, source, tenant, agent, expected),
).rowcount
return changed == 1
assert update("t1", "a1", 1, "中文简短回复", "用户消息 m17")
assert not update("t1", "a1", 1, "英文回复", "过期推断")
assert not update("t2", "a1", 2, "覆盖内容", "另一租户")
row = conn.execute("SELECT value,source,version FROM memory").fetchone()
assert row == ("中文简短回复", "用户消息 m17", 2)
conn.close()
print("记忆保留来源,过期更新与跨租户键更新均未生效")
完整示例可下载,使用 Python 3.9 或以上版本运行:python3 example.py。只依赖标准库,不调用真实业务服务。
稳定身份帮助状态跨越进程,但身份本身不能提供权限隔离。调用者是否有权访问某个租户和 Agent,应由认证与授权层判断;不能把模型传来的租户编号直接当作可信上下文。共享记忆尤其需要说明共享给谁、哪些字段可写、谁能撤销共享。
示例选择“冲突就拒绝”,而没有自动合并。两段摘要同时修改同一记忆块时,简单取最后一次写入可能丢掉前一次纠正。自动拼接又可能把互相矛盾的事实都留下。对低风险偏好可以重新读取后合并,对审批与业务承诺则应采用更严格的数据模型。
来源字段也有实际用途。用户主动说“以后用中文回复”,与模型从几次对话中推测用户偏好,是不同可信等级。记忆应能区分明确陈述和推断,并允许用户纠正。把推断写成确定事实,会让错误在后续对话中被反复强化。
另一个重要边界是,记忆持久化不等于工具效果可恢复。系统记得“准备退款”,不能说明钱已经退了;保存过一条工具调用消息,也不能自动避免外部服务重复执行。仍然需要操作编号、业务流水和结果确认。记忆框架与工作流运行时承担的责任,需要分别检查。
选型时,我会拿几种故障做验证:进程退出后能否恢复会话,旧消息被移出上下文后能否检索,记忆冲突如何处理,工具超时后能否辨别实际结果。还要检查导出、删除、访问审计和升级兼容性。产品演示里连续聊十分钟,覆盖不了这些长期使用的问题。
成本也要拆开看。常驻记忆会随每轮调用进入上下文,归档检索增加查询与选择成本,持续摘要需要额外模型调用。不是保存得越多就越聪明,而是要观察每类记忆是否改善任务完成率,并控制无关信息进入上下文的比例。
对我来说,有状态 Agent 的价值在于形成持续服务能力,而工程难点在于让这种持续性可控。选择 Letta 或其他方案之前,先定义状态归属、更新规则和证据来源,能够让后续接入更顺畅,也能防止把一个产品的“支持记忆”误解成它已经解决了所有持久性和一致性问题。