Agent 工程体系 · 第 24/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agent 长期记忆:写入策略、检索、更新、冲突和遗忘

Agent 的“长期记忆”不是把历史对话永久塞进提示词,也不是给每条消息生成一个向量后执行相似度搜索。它是一套对信息进行选择、建模、持久化、检索、修订和删除的状态管理机制。

如果把 Agent 在时刻 tt 能看到的上下文记为 CtC_t,外部持久化数据记为 MtM_t,当前用户输入记为 utu_t,那么一次响应可以抽象为:

at=Agent(Ct,Mt,ut)a_t = \operatorname{Agent}(C_t, M_t, u_t)

长期记忆系统的任务不是简单增大 MtM_t,而是决定:

Mt+1=Update(Mt,ut,at,et)M_{t+1} = \operatorname{Update}(M_t, u_t, a_t, e_t)

其中:

  • utu_t:用户输入;
  • ata_t:Agent 产生的响应或动作;
  • ete_t:工具、数据库、业务系统返回的外部事实;
  • MtM_t:记忆库当前状态;
  • Update\operatorname{Update}:写入、合并、冲突处理和遗忘策略。

一个好的记忆系统需要同时满足四个条件:

  1. 记得有用的信息,而不是记得最多的信息;
  2. 在正确的任务中取出正确的信息
  3. 允许事实发生变化,而不是把历史判断永久冻结;
  4. 能解释和撤销记忆,而不是形成不可审计的“模型幻觉数据库”。

一、先区分:上下文状态不等于长期记忆

1.1 上下文状态是什么

上下文状态是当前一次或一组连续交互中,模型可以直接使用的信息。它通常包括:

  • 当前用户消息;
  • 最近几轮对话;
  • 工具调用及工具返回;
  • 当前任务计划;
  • 中间变量;
  • 尚未完成的人工审批;
  • 本次运行的临时结果。

它的核心特征是:服务当前任务,生命周期较短,通常与线程、会话或一次运行绑定

例如,用户说:

帮我把刚才生成的 SQL 改成 PostgreSQL 语法。

“刚才生成的 SQL”属于当前对话的上下文状态。即使系统把它保存到数据库,也不意味着它自动成为长期记忆。

OpenAI 的 Responses API 支持通过手动传递历史输入、previous_response_id 链接前一响应,或使用具有持久标识符的 Conversations API 来管理多轮对话状态。Conversation 对象可以跨会话、设备和任务保存消息、工具调用及工具输出。但这些能力解决的主要是对话状态延续,不是自动判断“用户长期偏好”或“哪些事实应该进入长期记忆”。(developers.openai.com)

1.2 长期记忆是什么

长期记忆是跨线程、跨会话仍然可能被使用的、经过筛选和建模的信息。例如:

用户偏好:
- 喜欢使用 Python 3.12
- 代码示例优先使用 PostgreSQL
- 不希望回答包含营销化表达

用户事实:
- 用户在杭州工作
- 用户负责支付系统

项目事实:
- 项目 payment-api 使用 PostgreSQL
- 发布流程必须经过人工审批

这些数据不是“历史消息本身”,而是从历史消息或外部系统中提取出的、面向未来任务可复用的知识。

LangGraph 将短期线程状态与长期跨线程数据明确区分:checkpointer 保存线程范围内的图状态快照,store 保存应用定义的跨线程键值数据,常用于用户偏好、事实和共享知识。(docs.langchain.com)

因此,下面三个概念不能混用:

概念 主要内容 典型生命周期 是否适合直接检索为长期记忆
对话上下文 消息、工具调用、临时计划 当前线程或会话
事件日志 发生过什么,以及何时发生 长期保存、追加写入 需要加工
长期记忆 对未来任务有用的结构化知识 直到过期、撤销或删除

二、长期记忆的三种类型

为了避免“所有内容都叫 memory”,先把信息按用途分为三类。

2.1 语义记忆:关于事实和对象的知识

语义记忆表示相对稳定的事实、属性和关系,例如:

用户 42 的 preferred_language = zh-CN
项目 payment-api 的 database = PostgreSQL
订单 1001 的 status = paid

语义记忆适合回答:

  • “用户通常使用什么语言?”
  • “这个项目使用什么数据库?”
  • “这个客户属于哪个组织?”

语义记忆通常需要明确的键、值、来源、时间和置信度。它不应该只保留一段自然语言摘要,否则难以更新和校验。

2.2 情景记忆:关于发生过的事件

情景记忆描述过去发生的事情:

2026-08-20:
用户在排查支付回调问题时,确认 webhook 重试窗口为 30 分钟。

它适合回答:

  • “上次我们采用了什么方案?”
  • “这个问题之前是怎么解决的?”
  • “用户曾经否定过哪些建议?”

情景记忆保留的是事件,而不是把事件直接提升为永久事实。比如:

用户在 2026-08-20 说:“这次项目先不用 Redis。”

这只说明当时的项目决策,不能自动推导为:

用户永远不使用 Redis。

2.3 程序记忆:关于如何执行任务的规则

程序记忆表示操作流程、工作习惯和执行约束:

生成数据库迁移脚本时:
1. 先给出回滚脚本;
2. 再给出正向迁移;
3. 对生产环境操作必须提示备份;
4. 不直接执行破坏性 SQL。

程序记忆影响的是 Agent 的行为方式,因此风险通常高于普通偏好。错误的程序记忆可能导致错误的工具调用或越权操作。

2.4 混用风险

下面三句话看起来都像“记忆”,但语义不同:

用户上周说过:“这次不要使用 Redis。”       # 情景记忆
用户偏好使用 PostgreSQL。                    # 语义记忆
部署前必须经过人工审批。                     # 程序记忆

如果把三者都放进同一个向量库,只用相似度排序,Agent 可能出现:

  • 把一次性的项目决策当成永久偏好;
  • 把用户描述的流程当成系统强制策略;
  • 把旧版本的操作步骤当成当前执行规则;
  • 把“用户曾经询问某技术”误判为“用户偏好该技术”。

因此,记忆类型必须进入数据模型和检索过滤条件,而不能只作为文本标签存在。


三、什么信息应该写入长期记忆

长期记忆的第一道门不是向量检索,而是写入判定

定义一个候选记忆 xx,可以为它计算一个写入价值:

V(x)=P(x)×U(x)×R(x)×A(x)C(x)V(x) = P(x)\times U(x)\times R(x)\times A(x) - C(x)

其中:

  • P(x)P(x):信息正确的概率;
  • U(x)U(x):未来被使用时的效用;
  • R(x)R(x):跨任务、跨会话复用的可能性;
  • A(x)A(x):当前是否仍然有效;
  • C(x)C(x):存储、维护、隐私和错误传播成本。

只有当:

V(x)>θV(x) > \theta

才应该写入,其中 θ\theta 是应用设定的阈值。

这个公式不是要求在线上直接进行精确数值计算,而是帮助拆解写入理由。例如:

我喜欢简洁的代码示例。

通常具有较高的复用性和较低的隐私风险,可以写入“表达偏好”。

而:

我今天下午三点去医院。

虽然可能是真实信息,但未来复用价值有限,且属于敏感且短时有效的信息,不应默认写入长期记忆。

3.1 候选信息的四个问题

对每个候选信息至少询问:

  1. 这是事实、偏好、事件,还是执行规则?
  2. 它的作用域是什么?
    • 当前消息;
    • 当前任务;
    • 当前项目;
    • 当前用户;
    • 当前组织;
    • 全局系统。
  3. 它的有效期是什么?
    • 永久;
    • 到某个日期;
    • 直到项目结束;
    • 直到被新事实替代。
  4. 它由谁确认?
    • 用户明确陈述;
    • 用户行为推断;
    • 工具返回;
    • Agent 自己猜测。

最后一类不能直接作为高可信记忆写入。

3.2 明确陈述优先于模型推断

下面两种输入的写入资格不同:

用户:以后请优先给我 Python 示例。

这是明确偏好,可以创建候选记忆:

{
  "type": "semantic",
  "key": "coding.language_preference",
  "value": "Python",
  "scope": "user",
  "confidence": 0.98,
  "source": "user_explicit"
}

而:

用户连续三次询问 Python。

最多说明用户近期对 Python 感兴趣,不能直接写成“用户偏好 Python”。更稳妥的做法是记录情景事件,或生成低置信度候选:

{
  "type": "episodic",
  "event": "user_asked_about_python",
  "confidence": 0.65,
  "status": "candidate"
}

候选记忆可以在后续行为中被确认,也可以自然失效。


四、记忆数据模型:不要只存一段文本

一个可维护的长期记忆至少需要以下字段:

{
  "memory_id": "mem_01J...",
  "owner_type": "user",
  "owner_id": "user_42",
  "memory_type": "semantic",
  "key": "coding.language_preference",
  "value": {
    "language": "Python"
  },
  "scope": "user",
  "status": "active",
  "confidence": 0.98,
  "source_type": "user_explicit",
  "source_ref": "conversation:item_abc",
  "valid_from": "2026-08-20T10:00:00+08:00",
  "valid_to": null,
  "observed_at": "2026-08-20T10:00:00+08:00",
  "recorded_at": "2026-08-20T10:00:02+08:00",
  "version": 1,
  "supersedes": null,
  "embedding": [0.012, -0.044],
  "created_by": "memory_extractor_v3"
}

这里有几个容易被忽略的时间:

  • observed_at:事实发生或被用户表达的时间;
  • recorded_at:系统写入数据库的时间;
  • valid_from:事实开始有效的时间;
  • valid_to:事实停止有效的时间。

例如,用户在 2026 年 8 月 20 日说:

从 9 月 1 日开始,项目迁移到 PostgreSQL。

正确的数据不是立即把 database = PostgreSQL 当成当前事实,而是:

old value: MySQL
valid_to: 2026-08-31 23:59:59+08:00

new value: PostgreSQL
valid_from: 2026-09-01 00:00:00+08:00

如果只保存最后一次写入时间,系统在 2026 年 8 月 25 日检索时就会提前使用 PostgreSQL。


五、关系库、向量库和事件日志如何分工

5.1 关系库保存规范化事实

关系库适合保存:

  • 明确键值;
  • 当前有效状态;
  • 作用域;
  • 版本;
  • 生效时间;
  • 权限和租户隔离;
  • 唯一约束;
  • 审计记录。

示例表:

CREATE TABLE agent_memory (
    memory_id       TEXT PRIMARY KEY,
    owner_type      TEXT NOT NULL,
    owner_id        TEXT NOT NULL,
    memory_type     TEXT NOT NULL,
    memory_key      TEXT NOT NULL,
    value_json      JSONB NOT NULL,
    scope           TEXT NOT NULL,
    status          TEXT NOT NULL DEFAULT 'active',
    confidence      NUMERIC(4,3) NOT NULL,
    source_type     TEXT NOT NULL,
    source_ref      TEXT,
    observed_at     TIMESTAMPTZ NOT NULL,
    valid_from      TIMESTAMPTZ NOT NULL,
    valid_to        TIMESTAMPTZ,
    version         INTEGER NOT NULL,
    supersedes      TEXT REFERENCES agent_memory(memory_id),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX uq_active_memory_key
ON agent_memory(owner_type, owner_id, memory_key)
WHERE status = 'active';

这个唯一索引表达了一个业务假设:

对同一个所有者和同一个记忆键,最多只有一个 active 当前值。

但它不适用于所有事实。比如“用户曾经参与过哪些项目”是一对多关系,不能强行设计成单个值。

5.2 向量库用于语义召回,不负责事实真相

向量库适合检索:

  • “上次讨论 OAuth 的方案”;
  • “与当前故障相似的历史事件”;
  • “和当前项目背景相关的说明”;
  • 表达方式变化较大的情景记忆。

向量相似度解决的是:

similarity(q,d)\operatorname{similarity}(q, d)

其中 qq 是当前查询向量,dd 是记忆文本向量。

它不能保证:

  • dd 是最新事实;
  • dd 对当前用户可见;
  • dd 没有被撤销;
  • dd 的来源可信;
  • dd 与当前作用域匹配。

因此,正确流程通常是:

向量召回候选
    ↓
按 owner、tenant、scope、status、valid_time 过滤
    ↓
按类型和来源重排
    ↓
读取关系库中的当前版本
    ↓
组装给 Agent 的记忆上下文

而不是:

向量 top-k
    ↓
直接拼接进 prompt

5.3 事件日志用于追溯和重建

事件日志采用追加写入:

{
  "event_id": "evt_100",
  "aggregate": "user_42",
  "event_type": "memory_asserted",
  "payload": {
    "key": "coding.language_preference",
    "value": "Python"
  },
  "occurred_at": "2026-08-20T10:00:00+08:00",
  "actor": "user",
  "causation_id": "conversation:item_abc",
  "correlation_id": "request_xyz"
}

事件日志不应被覆盖,因为它承担:

  • 谁在什么时候提出了什么;
  • 哪个版本由哪个版本替代;
  • 冲突是如何解决的;
  • 删除是否真的执行;
  • 发生故障后能否重建投影。

关系库中的“当前记忆”可以看作事件日志的一个投影:

Sn=Reduce(S0,e1,e2,,en)S_n = \operatorname{Reduce}(S_0, e_1, e_2, \ldots, e_n)

其中 SnS_n 是第 nn 个事件之后的当前状态。


六、写入流程:从对话到长期记忆

推荐将写入拆成五个阶段,而不是让主 Agent 在生成回答时顺便修改数据库。

flowchart LR
    A[用户输入/工具结果] --> B[候选提取]
    B --> C[类型与作用域分类]
    C --> D[可信度与敏感性检查]
    D --> E{写入策略}
    E -->|拒绝| F[仅保留上下文或事件日志]
    E -->|候选| G[待确认记忆]
    E -->|直接写入| H[版本化提交]
    H --> I[更新检索索引]
    G --> J[后续确认/过期]

6.1 候选提取

提取器可以是规则、分类模型或 LLM,但输出必须是结构化结果:

{
  "candidates": [
    {
      "type": "semantic",
      "key": "coding.language_preference",
      "value": "Python",
      "scope": "user",
      "evidence": "以后请优先给我 Python 示例",
      "confidence": 0.98,
      "write_mode": "direct"
    }
  ]
}

提取器不能直接决定最终状态。它只产生候选,后续由策略层检查:

  • 是否为允许写入的记忆类型;
  • 是否涉及敏感个人信息;
  • 是否已有同键事实;
  • 是否存在时间限制;
  • 是否需要用户确认。

6.2 写入模式

可以定义三种写入模式:

直接写入

适用于低风险、明确、稳定的偏好:

用户明确要求以后使用中文回答。

候选写入

适用于模型推断、一次性行为或需要积累证据的事实:

用户最近多次询问 PostgreSQL。

仅保留事件

适用于短期事实、敏感信息或无法确认的信息:

用户今天要去医院。

“记忆系统越智能,就应该越积极写入”是错误方向。写入错误会比检索遗漏更难修复,因为错误记忆会在未来不断污染决策。


七、检索:相关性不是唯一排序条件

长期记忆检索至少需要同时考虑五个维度:

Score(m,q)=αSsemantic+βSscope+γSfreshness+δSconfidence+ϵStypeλSriskScore(m,q)= \alpha S_{semantic} +\beta S_{scope} +\gamma S_{freshness} +\delta S_{confidence} +\epsilon S_{type} -\lambda S_{risk}

其中:

  • SsemanticS_{semantic}:语义相关性;
  • SscopeS_{scope}:作用域匹配程度;
  • SfreshnessS_{freshness}:新鲜度;
  • SconfidenceS_{confidence}:来源和确认程度;
  • StypeS_{type}:记忆类型对当前任务的适配程度;
  • SriskS_{risk}:错误或泄露的风险。

语义相似度只能提供第一项。

7.1 先确定检索作用域

检索前先确定当前任务的作用域:

全局策略
  ↓
组织策略
  ↓
项目规则
  ↓
用户偏好
  ↓
当前线程状态
  ↓
当前消息

作用域越具体,通常优先级越高,但不能覆盖安全策略。例如:

全局:生产数据库禁止自动删除
用户:请直接执行 DROP TABLE

用户偏好不能覆盖全局安全策略。

7.2 结构化键优先于自然语言召回

如果任务是:

用户喜欢什么编程语言?

优先查询:

SELECT memory_key, value_json, confidence
FROM agent_memory
WHERE owner_type = 'user'
  AND owner_id = 'user_42'
  AND memory_key = 'coding.language_preference'
  AND status = 'active'
  AND valid_from <= now()
  AND (valid_to IS NULL OR valid_to > now());

只有当问题是:

上次我们为什么决定不用 Redis?

才需要召回情景记忆,并依据项目、时间和事件类型搜索。

7.3 记忆注入必须保留来源

注入模型的内容不要写成无来源的系统断言:

用户使用 Python。

更安全的格式是:

[长期记忆]
- 记忆类型:用户偏好
- 内容:用户要求优先使用 Python 示例
- 来源:用户明确陈述
- 观察时间:2026-08-20
- 置信度:0.98
- 有效期:未设置
- 使用方式:作为表达偏好,不得覆盖系统安全策略

来源信息有两个作用:

  1. 让模型知道这是外部记忆,而不是当前用户的新指令;
  2. 让系统能够在出现冲突时回溯证据。

长期记忆本身也可能包含提示注入内容,因此不能因为它来自内部数据库,就把它当成高优先级指令。记忆中的“事实”和“操作规则”必须区分,程序记忆还需要经过策略引擎或权限系统验证。


八、更新:不要原地覆盖,要产生版本关系

8.1 更新的本质是状态替换

假设已有:

coding.language_preference = Python
version = 1

用户后来明确说:

以后示例优先使用 TypeScript。

不能简单执行:

UPDATE agent_memory
SET value_json = '{"language":"TypeScript"}',
    version = version + 1;

因为这会丢失:

  • 旧值;
  • 旧值来源;
  • 变化时间;
  • 谁发起的变化;
  • 更新前后关系。

更完整的过程是:

v1 Python
  ↓ superseded_by
v2 TypeScript

SQL 事务可以写成:

BEGIN;

UPDATE agent_memory
SET status = 'superseded',
    valid_to = now(),
    updated_at = now()
WHERE owner_type = 'user'
  AND owner_id = 'user_42'
  AND memory_key = 'coding.language_preference'
  AND status = 'active';

INSERT INTO agent_memory (
    memory_id, owner_type, owner_id, memory_type, memory_key,
    value_json, scope, status, confidence, source_type,
    source_ref, observed_at, valid_from, version, supersedes
)
VALUES (
    'mem_v2', 'user', 'user_42', 'semantic',
    'coding.language_preference', '{"language":"TypeScript"}',
    'user', 'active', 0.99, 'user_explicit',
    'conversation:item_def', now(), now(), 2, 'mem_v1'
);

COMMIT;

8.2 并发更新需要比较版本

假设两个请求同时读取了 version = 1

请求 A:Python → TypeScript
请求 B:Python → Go

如果没有并发控制,最后写入者会覆盖另一个更新。

使用乐观并发控制时,更新条件应包含旧版本:

UPDATE agent_memory
SET value_json = '{"language":"TypeScript"}',
    version = version + 1
WHERE memory_id = 'mem_v1'
  AND version = 1
  AND status = 'active';

如果返回行数为 0,说明版本已经被其他请求修改,当前请求必须重新读取,再进行冲突处理。

数据库层面的事务只能保证存储操作的一致性,不能自动判断两个自然语言事实哪个正确。


九、冲突:事实冲突、作用域冲突和时间冲突

冲突不是异常边界,而是长期记忆的正常状态。

9.1 事实冲突

例如:

v1:项目使用 MySQL,来源是项目文档,时间为 2026-08-01
v2:项目使用 PostgreSQL,来源是用户明确陈述,时间为 2026-08-20

简单的“新值覆盖旧值”可能是合理的,但前提是两条记录描述的是同一个项目、同一个环境和同一个时间范围。

以下两句话未必冲突:

开发环境使用 PostgreSQL。
生产环境使用 MySQL。

如果记忆键只有:

database

系统会制造伪冲突。正确的键应该包含维度:

project.payment-api.environment.dev.database
project.payment-api.environment.prod.database

9.2 来源优先级

常见的来源可信度顺序可以是:

受信任业务系统事实
> 用户明确陈述
> 项目配置或文档
> 工具查询结果
> 用户行为推断
> Agent 自己推测

但来源优先级不能脱离领域。比如:

  • 订单支付状态应以支付系统为准;
  • 用户偏好应以用户最新明确陈述为准;
  • 生产部署状态应以部署平台为准;
  • “用户可能喜欢某种回答风格”只能作为弱证据。

可以使用一个简单的冲突决策函数:

winner=argmaxi(source_trusti,explicitnessi,freshnessi,scope_specificityi)winner = \arg\max_i \left( source\_trust_i, explicitness_i, freshness_i, scope\_specificity_i \right)

这不是无条件按分数覆盖,而是先检查两条记录是否在同一作用域和有效时间内描述同一个属性。

9.3 无法自动解决时,保留冲突

当系统无法判断时,不要伪造唯一答案:

{
  "key": "project.payment-api.database",
  "status": "conflicted",
  "candidates": [
    {
      "value": "MySQL",
      "source": "deployment_config",
      "confidence": 0.95
    },
    {
      "value": "PostgreSQL",
      "source": "user_explicit",
      "confidence": 0.90
    }
  ]
}

Agent 检索到冲突后,应向用户询问限定问题:

我看到部署配置仍显示 MySQL,但你在 2026 年 8 月 20 日提到项目将迁移到 PostgreSQL。你要查询的是当前生产环境,还是迁移后的目标环境?

这比选择一个看似“最新”的值更可靠。


十、遗忘:删除不是唯一形式

长期记忆中的“遗忘”至少有四种含义:

  1. 逻辑失效:仍保留记录,但不再参与当前检索;
  2. 降权:信息仍可作为历史参考,但默认不优先使用;
  3. 摘要化:多个事件合并成更短的长期知识;
  4. 物理删除:从存储和索引中彻底移除。

10.1 时间衰减

对于没有明确有效期的记忆,可以使用时间衰减:

freshness(m)=eλΔtfreshness(m)=e^{-\lambda \Delta t}

其中:

  • Δt\Delta t:当前时间与最近观察时间的差;
  • λ\lambda:衰减速度;
  • ee:自然指数。

例如,“用户喜欢简洁回答”可以缓慢衰减;“用户当前正在使用某个分支”应该快速衰减。

但时间衰减不能替代显式过期。对于明确的业务状态,应使用 valid_to 或外部系统查询,而不是用“很久没更新”推断它已经失效。

10.2 使用衰减和价值衰减

可以同时考虑最近使用时间和长期复用价值:

priority(m)=utility(m)×confidence(m)×freshness(m)priority(m)= utility(m)\times confidence(m)\times freshness(m)

低优先级记忆可以:

  • 从默认 prompt 注入中移除;
  • 只在用户明确询问历史时检索;
  • 合并进摘要;
  • 进入归档;
  • 最终删除。

10.3 纠正和删除请求

用户说:

忘记我使用 PostgreSQL。

至少需要删除或停用以下对象:

  • 当前结构化记忆;
  • 旧版本及其检索索引;
  • 情景事件中的相关副本;
  • 摘要中的相关内容;
  • 缓存;
  • 异步写入队列;
  • 备份或日志中的可识别信息。

“把 active 标记为 deleted”只能完成逻辑删除,不能自动完成物理删除。生产系统需要明确删除范围和完成条件,并记录删除审计,而不是只告诉用户“已经忘记”。


十一、记忆和框架持久化的生命周期

以 LangGraph 为例,checkpointer 和 store 的边界很重要:

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore

checkpointer = InMemorySaver()
store = InMemoryStore()

graph = builder.compile(
    checkpointer=checkpointer,
    store=store,
)

config = {
    "configurable": {
        "thread_id": "thread-2026-09-01-user-42"
    }
}

result = graph.invoke(
    {
        "messages": [
            {"role": "user", "content": "我叫 Bob。"}
        ]
    },
    config,
)

这里:

  • checkpointer 保存当前线程的图状态;
  • thread_id 决定从哪个线程恢复;
  • store 保存跨线程的应用数据;
  • 记忆是否写入 store,仍由应用节点或应用代码决定。

官方文档明确说明,内存型 MemorySaverInMemorySaver 只保存在进程内,进程重启后会丢失;生产环境应使用持久化实现,例如 PostgreSQL 或 SQLite。长期运行的线程还需要处理 checkpoint 无限增长问题。(docs.langchain.com)

这意味着:

线程恢复成功
≠
长期记忆存在

可能出现:

checkpointer 恢复了当前对话,
但 store 因租户键错误没有读到用户偏好。

也可能反过来:

store 中有用户偏好,
但 thread_id 变化导致当前任务上下文丢失。

两种状态必须分别监控和测试。


十二、OpenAI 对话状态与自建长期记忆如何组合

OpenAI 的 previous_response_id 可以将响应串联成线程,但它不是长期记忆数据库。官方文档还指出,即使使用 previous_response_id,链中之前的输入 token 仍会作为输入计费;上下文窗口也受到输入、输出和推理 token 总量限制。(developers.openai.com)

一种合理组合是:

OpenAI Conversation / previous_response_id
    保存当前对话状态

关系库
    保存当前结构化长期事实

事件日志
    保存记忆变化历史

向量库
    召回情景记忆和语义相关历史

策略层
    决定哪些记忆可以进入模型上下文

请求流程可以表示为:

sequenceDiagram
    participant U as 用户
    participant A as Agent 服务
    participant R as 关系库
    participant V as 向量库
    participant O as 模型 API
    participant L as 事件日志

    U->>A: 新消息
    A->>R: 查询当前结构化记忆
    A->>V: 语义召回历史事件
    R-->>A: 当前事实与版本
    V-->>A: 候选情景记忆
    A->>A: 权限、作用域、时间、冲突过滤
    A->>O: 当前输入 + 受控记忆上下文
    O-->>A: 响应和工具调用
    A->>L: 记录事件与候选记忆
    A->>R: 事务提交更新后的记忆
    A-->>U: 最终响应

不要把“模型 API 里的对话对象”当成业务上的用户画像。模型侧对话状态的持久化边界、删除语义、访问权限和租户隔离,未必等同于应用自己的长期记忆要求。应用仍需要维护自己的记忆键、版本、作用域和审计记录。


十三、一个完整算例:从偏好到冲突再到过期

假设用户第一次说:

以后请优先给我 Python 示例。

第一步:提取候选

{
  "type": "semantic",
  "key": "coding.language_preference",
  "value": "Python",
  "scope": "user",
  "confidence": 0.98,
  "source_type": "user_explicit"
}

第二步:查询旧状态

数据库没有同键 active 记录:

旧状态:∅

第三步:写入 v1

v1 = Python
status = active

第四步:未来检索

用户要求:

写一个 HTTP 客户端。

系统首先得到结构化候选:

coding.language_preference = Python

但这个偏好只影响示例语言,不应影响:

  • HTTP 方法选择;
  • 超时策略;
  • 认证策略;
  • 安全限制。

第五步:用户更新偏好

用户后来明确说:

现在项目都改用 TypeScript,请优先给 TypeScript 示例。

生成:

v1 = Python
v2 = TypeScript
v2.supersedes = v1
v1.status = superseded
v2.status = active

第六步:旧事件仍可查询

当用户问:

我以前是不是要求过 Python?

系统可以查询事件和历史版本,回答:

是。你在 2026 年 8 月 20 日曾要求优先使用 Python;后来你说明项目改用 TypeScript,因此当前偏好是 TypeScript。

这里同时使用了:

  • 当前语义记忆;
  • 历史情景记忆;
  • 版本关系。

反例:只保留最后一条文本

如果系统只保存:

用户偏好:TypeScript

它无法回答偏好何时变化,也无法解释为什么旧行为不同,更无法在错误更新后恢复 Python 版本。


十四、常见失败表现与诊断方法

14.1 Agent 总是提到用户已经不用的偏好

可能原因:

  • 向量库中仍有旧版本;
  • 检索没有过滤 status = active
  • 没有检查 valid_to
  • 摘要中保留了旧值;
  • 缓存未失效;
  • 版本更新只改了关系库,没有同步索引。

诊断时应沿着一条记忆的完整链路查询:

memory_id
→ 当前结构化记录
→ 历史版本
→ 原始事件
→ 向量索引
→ 摘要
→ prompt 组装日志

只查“最终 prompt”通常无法判断错误是在写入、检索还是注入阶段发生的。

14.2 Agent 把一次性请求当成长期偏好

例如用户说:

这次请用 Go 写。

如果写入器缺少作用域判断,可能生成:

用户偏好 Go

正确结果应该是:

本次任务的局部约束:使用 Go

或:

情景事件:2026-09-01 的任务使用 Go

局部任务约束不能自动提升为用户长期偏好。

14.3 同一个用户在不同线程看到不同记忆

常见原因是:

  • owner_id 使用了会话 ID,而不是用户 ID;
  • 不同服务对租户键编码不一致;
  • LangGraph 的 thread_id 被误当成长期记忆主键;
  • 子图使用独立 checkpoint namespace,父图没有立即看到变化;
  • 读写使用了不同数据库或不同索引版本。

LangGraph 文档特别指出,子图可能拥有自己的 checkpoint namespace;需要跨图共享的数据应使用 Store,或显式配置写入父图 checkpoint。(docs.langchain.com)

14.4 Agent 因错误记忆执行危险动作

如果记忆内容是:

用户允许自动删除生产数据。

不能因为它存在于长期记忆中,就直接放行工具调用。高风险操作必须重新进行:

  • 当前请求确认;
  • 权限检查;
  • 资源范围确认;
  • 幂等性检查;
  • 人工审批或安全策略检查。

程序记忆只能提供候选行为,不能替代授权系统。


十五、生产取舍:记忆系统的核心不是“越多越好”

15.1 高精度写入,有限召回

长期记忆更适合采用:

严格写入 + 分层检索 + 明确来源 + 可撤销更新

而不是:

所有对话都存储 + 每次 top-k 注入

前者可能漏掉一些弱相关信息,但错误传播较少;后者短期看起来“记得很多”,长期会产生重复、冲突、过期和隐私泄露。

15.2 结构化与向量化并存

推荐的职责划分是:

当前状态、权限、版本、有效期、唯一键
    → 关系库

模糊历史、方案讨论、失败经验、相似事件
    → 向量库

事实变化、用户修改、删除和审计
    → 事件日志

线程恢复、中断续跑、人工审批
    → checkpointer 或对话状态存储

不要试图用一种存储解决所有问题。

15.3 可观测性要记录决策,而不只是延迟

记忆相关日志至少需要包含:

request_id
user_id / tenant_id
query
候选 memory_id
召回来源
过滤原因
最终注入的 memory_id
记忆版本
模型响应
写入候选
写入结果
冲突决策
删除任务状态

尤其要记录“为什么没有使用某条记忆”。例如:

mem_v1 被过滤:
- status = superseded
- current_version = mem_v2

或者:

mem_88 被过滤:
- scope = project-A
- current_scope = project-B

没有这些信息,线上出现“Agent 忘了”或“Agent 记错了”时,很难定位根因。


十六、验收标准:用行为测试验证记忆,而不是只测数据库

至少应覆盖以下测试:

写入测试

明确偏好 → 写入 active 记忆
一次性任务约束 → 不写入用户长期偏好
低置信度推断 → 写入 candidate 或事件
敏感信息 → 按策略拒绝或要求确认

检索测试

当前用户能检索自己的记忆
用户 A 不能检索用户 B 的记忆
项目 A 的事实不泄露到项目 B
过期记忆不作为当前事实注入

更新测试

新事实生成新版本
旧版本可审计
并发更新不会静默覆盖
向量索引最终与当前版本一致

冲突测试

明确用户陈述能更新低可信推断
业务系统事实不会被普通对话覆盖
不同环境的事实不会互相冲突
无法判断时进入 conflicted 状态并触发澄清

遗忘测试

逻辑删除后不再被默认检索
删除后缓存失效
向量索引中的副本被删除或隔离
用户查询历史时,删除范围符合产品承诺

长期记忆的正确性不是“数据库里有一条记录”,而是这条记录在正确作用域、正确时间、正确版本和正确权限下影响了正确行为。

最终可以用一句话概括:

Agent 的长期记忆不是记住过去,而是把过去转化为有作用域、有来源、有版本、有生命周期、可检索且可撤销的未来决策依据。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。