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

Agent 语义、情景与程序记忆:数据模型、用途和混用风险

Agent 的“记忆”不是一个统一的数据结构。用户偏好、某次故障的经过、如何执行一次部署,虽然都可以被称为“记忆”,但它们的生命周期、可信度、检索方式和失败代价完全不同。

把这些内容全部写入一个向量库,再在提示词中统一检索,短期内可以工作;一旦系统进入多用户、多线程、长任务或高风险操作场景,问题通常会表现为:

  • Agent 把一次性的事件误当成用户长期偏好;
  • Agent 把“曾经这样做过”误当成“以后都应该这样做”;
  • Agent 把一段操作步骤当成事实陈述,或者把事实陈述当成操作指令;
  • 新旧记忆冲突时,没有依据、时间和适用范围可供判断;
  • 当前对话状态、长期记忆和程序规则相互污染,导致错误持续扩大。

因此,首先要区分三种长期记忆:

  1. 语义记忆:关于人、对象、规则和稳定事实的知识;
  2. 情景记忆:某一次具体经历、任务或事件的记录;
  3. 程序记忆:完成某类任务时可执行的步骤、策略和约束。

它们都可能由自然语言表示,也都可以存入关系数据库、文档库或向量库,但“存储形式相同”不意味着“数据模型相同”。


一、先区分上下文、状态和长期记忆

1. 上下文不是长期记忆

**上下文(context)**是当前一次模型调用可见的信息集合。它可能包含:

  • 系统指令;
  • 当前用户输入;
  • 最近几轮消息;
  • 工具调用及其结果;
  • 当前任务的中间变量;
  • 被检索出来的文档和记忆。

可以形式化为:

Ct=I+Ht+St+Rt+XtC_t = I + H_t + S_t + R_t + X_t

其中:

  • II:系统级指令;
  • HtH_t:当前线程的历史消息;
  • StS_t:当前任务状态;
  • RtR_t:本轮检索结果;
  • XtX_t:本轮用户输入和外部输入;
  • CtC_t:第 tt 次模型调用实际看到的上下文。

上下文的关键特征是:它服务于当前推理,不天然代表未来仍然成立的知识

例如:

用户说:“这次发布先不要执行数据库迁移,我要先做备份。”

这句话应该进入当前任务状态,可能成为本次发布的约束;但不能直接写成长期语义记忆:

用户不允许数据库迁移。

因为原话只表达了“这次发布”的暂时要求,而不是永久偏好。

2. 对话状态是可恢复的工作状态

**对话状态(conversation state)**是为了让一次连续交互能够继续进行而保存的状态。它通常包括消息、工具调用、工具输出以及任务中的中间结果。

OpenAI 的 Responses API 可以通过手动传入历史消息、previous_response_id 链接前一响应,或使用具有持久标识的 Conversations API 来维护多轮状态。Conversations 可以跨会话、设备和任务使用,并保存消息、工具调用和工具输出等项目。(developers.openai.com)

但这仍然是线程或会话连续性,不等于用户画像或跨任务长期记忆。

可以用下列关系表示:

当前请求
   │
   ├── 当前输入
   ├── 当前线程历史
   ├── 当前任务状态
   └── 检索到的长期记忆

其中只有最后一项通常来自语义、情景或程序记忆存储。

3. 检查点也不等于长期记忆

在图式 Agent 中,常见的持久化对象是检查点(checkpoint)。检查点保存某个线程在某个执行时刻的图状态,用于:

  • 对话连续性;
  • 人工介入后继续执行;
  • 故障恢复;
  • 时间旅行或从历史状态分叉。

LangGraph 将 checkpointerstore 明确区分:前者持久化单个 thread 的图状态快照,后者保存跨 thread 的应用级键值数据。前者适合短期、线程范围的记忆,后者适合用户偏好、事实和共享知识等长期记忆。(docs.langchain.com)

因此,一个更准确的分层是:

请求级上下文
    ↓
线程级状态 / 检查点
    ↓
长期记忆
    ├── 语义记忆
    ├── 情景记忆
    └── 程序记忆

二、三种记忆的核心区别

1. 语义记忆:回答“是什么”

**语义记忆(semantic memory)**保存脱离单次事件后仍具有复用价值的事实、概念、属性和关系。

典型内容包括:

用户的稳定称呼是“林工”。
用户所在时区是 Asia/Shanghai。
项目 payment-service 使用 PostgreSQL。
生产环境发布必须经过人工审批。
团队的默认日志级别是 INFO。

语义记忆的核心不是“内容看起来像事实”,而是:

该内容能否脱离产生它的具体事件,在未来多个任务中作为事实使用?

可以将一条语义记忆建模为:

ms=(subject,predicate,object,scope,source,confidence,validity)m_s = (subject, predicate, object, scope, source, confidence, validity)

例如:

{
  "memory_type": "semantic",
  "subject": "user:42",
  "predicate": "preferred_address",
  "object": "杭州",
  "scope": "user",
  "source": {
    "kind": "explicit_user_statement",
    "event_id": "evt-1001"
  },
  "confidence": 0.98,
  "valid_from": "2026-08-01T00:00:00+08:00",
  "valid_to": null,
  "updated_at": "2026-08-01T10:00:00+08:00"
}

这里的 source 很重要。仅保存:

{"preferred_address": "杭州"}

无法回答以下问题:

  • 是用户明确说的,还是 Agent 推断的?
  • 这个偏好属于用户本人,还是某个项目?
  • 它从什么时候生效?
  • 用户后来是否撤销?
  • 它能否用于高风险决策?

语义记忆适合:

  • 个性化称呼;
  • 稳定用户偏好;
  • 组织、项目和系统的长期属性;
  • 已确认的实体关系;
  • 经过审批的业务规则。

语义记忆不适合直接保存:

  • 某次任务的临时指令;
  • 未验证的猜测;
  • 一次性异常;
  • 带有强烈上下文依赖的操作结果。

2. 情景记忆:回答“发生过什么”

**情景记忆(episodic memory)**保存某次具体经历。它回答的不是抽象事实,而是:

在什么时间、什么场景、由谁、对什么对象、做了什么、结果如何?

典型内容包括:

2026-08-31,payment-service 发布到 staging 时,迁移脚本 V42 因索引已存在而失败。
2026-08-20,用户要求将周报发送到企业微信,而不是邮件。
2026-08-15,Agent 执行退款前发现订单状态为已关闭,因此请求人工确认。

可以将情景记忆建模为:

me=(event,participants,context,actions,outcome,evidence,time)m_e = (event, participants, context, actions, outcome, evidence, time)

示例:

{
  "memory_type": "episodic",
  "event_id": "deploy-2026-08-31-001",
  "subject": "payment-service",
  "context": {
    "environment": "staging",
    "commit": "a13f9d2",
    "operator": "agent:release"
  },
  "actions": [
    "运行数据库迁移 V42",
    "收到 duplicate index 错误",
    "回滚迁移事务"
  ],
  "outcome": "failed",
  "evidence": [
    {
      "kind": "tool_output",
      "tool_call_id": "tool-889",
      "digest": "sha256:..."
    }
  ],
  "occurred_at": "2026-08-31T21:16:00+08:00"
}

情景记忆适合:

  • 诊断相似故障;
  • 解释过去决策;
  • 恢复长任务的执行背景;
  • 查找某次交互中的用户意图;
  • 从历史案例中提取经验。

情景记忆的关键属性是时间性和场景性。下面两句话不能等价:

事件记忆:2026-08-31 在 staging 中 V42 迁移失败。
语义记忆:V42 迁移永远不能执行。

前者只说明一件事发生过;后者是一个跨时间、跨环境的普遍断言。要从前者推导后者,必须有额外验证。

3. 程序记忆:回答“怎么做”

**程序记忆(procedural memory)**保存完成任务的步骤、策略、工具选择和约束。它回答:

面对某种目标和前置条件,应该按什么顺序行动?

例如:

发布 payment-service 前:
1. 检查工作区是否干净;
2. 运行单元测试;
3. 备份生产数据库;
4. 在 staging 执行迁移;
5. 验证健康检查;
6. 等待人工审批;
7. 执行生产发布;
8. 观察错误率和延迟。

程序记忆不能只写成一段“经验文字”,否则模型很难知道:

  • 何时适用;
  • 哪些步骤必须执行;
  • 哪些步骤可以跳过;
  • 哪些步骤具有破坏性;
  • 失败时如何恢复;
  • 哪些动作需要审批。

更适合使用结构化模型:

mp=(goal,preconditions,steps,guards,effects,recovery,authority)m_p = (goal, preconditions, steps, guards, effects, recovery, authority)

示例:

{
  "memory_type": "procedural",
  "procedure_id": "release.payment-service.v3",
  "goal": "将 payment-service 发布到生产环境",
  "preconditions": [
    "测试通过",
    "当前提交已获得发布审批",
    "数据库备份成功"
  ],
  "steps": [
    {
      "id": "backup",
      "action": "backup_database",
      "required": true,
      "side_effect": "creates_backup"
    },
    {
      "id": "migrate_staging",
      "action": "run_migration",
      "environment": "staging",
      "required": true
    },
    {
      "id": "approve",
      "action": "request_human_approval",
      "required": true
    },
    {
      "id": "deploy_prod",
      "action": "deploy",
      "environment": "production",
      "required": true
    }
  ],
  "guards": [
    "禁止在没有备份标识时执行生产迁移"
  ],
  "recovery": [
    "生产迁移失败时停止后续发布并进入人工处理"
  ],
  "authority": "release-manager"
}

程序记忆适合:

  • 工具调用编排;
  • 标准作业流程;
  • 故障处理 runbook;
  • 代码生成约束;
  • 多步骤业务操作;
  • 需要审批、回滚或幂等控制的动作。

程序记忆不是事实库,也不是聊天历史。它必须受到权限、工具实际能力和当前环境状态的约束。


三、三种记忆的判别条件

遇到一条候选记忆时,可以依次回答三个问题。

问题一:它是否脱离原事件仍然成立?

如果答案是“是”,它可能是语义记忆。

用户明确说:“以后请叫我林工。”

这里的“以后”明确表达了跨会话范围,因此可以候选写入用户语义记忆。

如果原话是:

今天的会议请叫我林工,正式合同里仍使用实名。

它不是一个简单的全局称呼偏好,而是带有场景条件的语义记忆:

{
  "predicate": "preferred_name",
  "value": "林工",
  "scope": "meeting",
  "condition": "informal_meeting"
}

问题二:它是否必须依赖时间和事件才能理解?

如果答案是“是”,它应保留为情景记忆。

用户上周拒绝了短信通知。

这句话不能直接变成:

用户永远拒绝短信通知。

正确做法是记录事件:

{
  "memory_type": "episodic",
  "event": "notification_preference_declined",
  "channel": "sms",
  "occurred_at": "2026-08-25T..."
}

然后再根据多个事件或用户明确表述,决定是否形成语义记忆:

用户明确说:“今后不要通过短信通知我。”

问题三:它是否描述了可执行动作?

如果答案是“是”,它应进入程序记忆或任务状态,而不是普通事实。

先查询订单状态,再执行退款。

这不是“订单状态是什么”,而是工具调用顺序约束:

{
  "memory_type": "procedural",
  "goal": "refund_order",
  "steps": [
    "get_order",
    "verify_refundable",
    "request_approval_if_needed",
    "issue_refund"
  ]
}

如果把它写入自然语言向量库,检索时可能只得到一段相似文本,却无法保证 Agent 真的遵循顺序。程序性约束需要在编排器、状态机、工具权限或验证器中再次落地。


四、一个统一但不混淆的数据模型

三种记忆可以使用统一外壳,但不能共用完全相同的载荷。

MemoryRecord
├── identity
│   ├── memory_id
│   ├── memory_type
│   └── namespace
├── content
│   └── type-specific payload
├── provenance
│   ├── source
│   ├── evidence
│   └── author
├── scope
│   ├── tenant_id
│   ├── user_id
│   ├── project_id
│   └── thread_id
├── validity
│   ├── valid_from
│   ├── valid_to
│   └── status
├── confidence
├── access_policy
└── lifecycle
    ├── created_at
    ├── updated_at
    ├── supersedes
    └── retention_class

其中:

  • memory_type 决定载荷如何解释;
  • namespace 防止不同租户、用户和项目之间串数据;
  • provenance 表示来源和证据;
  • scope 表示适用边界;
  • validity 表示时间有效性;
  • confidence 表示系统对内容的信任程度;
  • access_policy 决定谁可以检索或修改;
  • supersedes 表示新记录替代旧记录,而不是静默覆盖。

一种关系数据库设计如下:

CREATE TABLE agent_memory (
    memory_id        TEXT PRIMARY KEY,
    memory_type      TEXT NOT NULL
        CHECK (memory_type IN ('semantic', 'episodic', 'procedural')),

    namespace        TEXT NOT NULL,
    tenant_id        TEXT NOT NULL,
    user_id          TEXT,
    project_id       TEXT,
    thread_id        TEXT,

    payload          JSONB NOT NULL,
    source           JSONB NOT NULL,
    confidence       NUMERIC(4,3) NOT NULL
        CHECK (confidence >= 0 AND confidence <= 1),

    valid_from       TIMESTAMPTZ,
    valid_to         TIMESTAMPTZ,
    status           TEXT NOT NULL DEFAULT 'active'
        CHECK (status IN ('active', 'superseded', 'retracted', 'expired')),

    supersedes       TEXT REFERENCES agent_memory(memory_id),
    created_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at       TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_memory_scope
ON agent_memory (tenant_id, user_id, project_id, memory_type, status);

这个表可以支持统一的审计和生命周期管理,但检索时仍然必须按类型分流:

语义记忆     → 属性过滤、实体查询、向量召回
情景记忆     → 时间、事件、结果、相似案例检索
程序记忆     → 目标、前置条件、版本、权限匹配

不能因为三者都使用 payload JSONB,就把它们当成同一种数据。


五、写入:从对话内容到记忆的判定过程

1. 不应把每条消息都写入长期记忆

一条消息进入长期记忆,至少要经过以下判定:

消息
 │
 ├─ 是否只对当前任务有效?
 │       └─ 是 → 保留在任务状态,不写长期记忆
 │
 ├─ 是否由用户明确陈述或可信系统确认?
 │       └─ 否 → 保留为低置信情景记忆,或不写入
 │
 ├─ 是否包含可执行步骤?
 │       └─ 是 → 写入程序候选,并进行规则校验
 │
 ├─ 是否具有跨任务复用价值?
 │       └─ 是 → 形成语义记忆候选
 │
 └─ 是否需要人工确认?
         └─ 是 → pending,不直接 active

可以把写入条件表达为:

write(m)=stable(m)reusable(m)authorized(m)evidenced(m)write(m) = stable(m) \land reusable(m) \land authorized(m) \land evidenced(m)

其中:

  • stable(m):预计不会只在当前瞬间成立;
  • reusable(m):未来任务使用它有明确价值;
  • authorized(m):写入行为得到用户、系统或业务授权;
  • evidenced(m):存在可追溯来源。

这个条件不是所有产品都必须采用的规范,而是一种工程判定模型。高风险系统还应增加:

writeactive(m)=write(m)review_passed(m)write_{active}(m) = write(m) \land review\_passed(m)

即只有审核通过后,记忆才进入可直接影响决策的 active 状态。

2. “明确偏好”和“行为推断”必须分开

以下两句话的来源强度不同:

用户说:“我偏好 Markdown。”
系统观察到:用户过去三次都选择了 Markdown。

前者是显式陈述,后者只是行为证据。可以分别存储:

{
  "predicate": "output_format",
  "value": "markdown",
  "source": {
    "kind": "explicit_user_statement",
    "event_id": "evt-2001"
  },
  "confidence": 0.98
}
{
  "predicate": "output_format",
  "value": "markdown",
  "source": {
    "kind": "behavioral_inference",
    "supporting_events": ["evt-1901", "evt-1902", "evt-1903"]
  },
  "confidence": 0.72
}

如果不区分来源,Agent 可能把一次推断当成用户已经确认的事实,造成不必要的个性化甚至错误决策。

3. 程序记忆不能由单次成功自动升级

一次操作成功只证明:

success(event,context)success(event, context)

它不等价于:

valid(procedure,all future contexts)valid(procedure, all\ future\ contexts)

例如,一次“先重启服务再清理缓存”成功,不足以推出这是所有故障的正确恢复流程。可能原因是:

  • 当时流量很低;
  • 当时没有长连接;
  • 当时服务版本不同;
  • 当时缓存可以安全丢失;
  • 真正原因并不是重启,而是另一个并发变化。

因此,程序记忆至少需要保存适用前提:

{
  "procedure_id": "recover.cache.v1",
  "preconditions": [
    "service_status != degraded_due_to_database",
    "cache_is_rebuildable",
    "maintenance_window = true"
  ]
}

六、检索:不是“相似度最高就使用”

1. 检索结果必须先按类型解释

假设用户询问:

为什么上次发布失败?这次怎么发布?

系统可能同时检索到:

语义记忆:
payment-service 使用 PostgreSQL。

情景记忆:
2026-08-31,staging 的 V42 迁移因重复索引失败。

程序记忆:
发布前先备份,再在 staging 执行迁移,最后请求人工审批。

这三条内容在提示词中的角色不同:

[事实]
payment-service 使用 PostgreSQL。

[历史事件]
上一次 staging 发布中,V42 迁移因重复索引失败。

[执行流程]
本次发布必须满足备份和审批前置条件。

如果全部拼接成:

payment-service 使用 PostgreSQL;
上次迁移失败;
先备份再迁移;

模型仍然需要自行推断哪一句是事实、哪一句是历史、哪一句是指令。数据边界已经丢失。

2. 一个可解释的排序函数

检索排序可以使用类型、范围、时间和来源共同计算:

score(m,q)=αsim(m,q)+βscope(m,q)+γfreshness(m,q)+δauthority(m)+ϵtypefit(m,q)λrisk(m)score(m,q)= \alpha sim(m,q) +\beta scope(m,q) +\gamma freshness(m,q) +\delta authority(m) +\epsilon typefit(m,q) -\lambda risk(m)

其中:

  • sim(m,q)sim(m,q):记忆内容与查询的语义相似度;
  • scope(m,q)scope(m,q):租户、用户、项目和线程范围是否匹配;
  • freshness(m,q)freshness(m,q):内容是否仍然新鲜;
  • authority(m)authority(m):来源是否可信;
  • typefit(m,q)typefit(m,q):记忆类型是否适合当前问题;
  • risk(m)risk(m):错误使用该记忆的潜在损失。

例如,对于“如何执行退款”,程序记忆的 typefit 应高于情景记忆;对于“上次为什么失败”,情景记忆的 typefit 应高于程序记忆。

高风险动作不能只看分数:

score 高
≠
可以直接执行

还必须经过:

权限检查
→ 当前状态检查
→ 前置条件检查
→ 幂等性检查
→ 人工审批(如需要)

3. 当前事实应优先于历史记忆

假设语义记忆中保存:

生产数据库版本为 PostgreSQL 15。

但当前配置中心返回:

生产数据库版本为 PostgreSQL 16。

二者冲突时,当前可信系统状态通常应优先于旧记忆。正确处理不是删除旧记录,而是建立替代关系:

旧记忆:PostgreSQL 15
状态:superseded
superseded_by:PostgreSQL 16
证据:配置中心 config revision 8841

这样既能保证当前决策正确,也能保留历史审计能力。


七、更新、冲突与遗忘

1. 更新不是覆盖,而是版本化

对语义记忆执行原地覆盖会损失:

  • 谁修改了它;
  • 修改前是什么;
  • 修改依据是什么;
  • 哪些任务使用过旧值。

推荐使用追加式版本:

m1: 用户偏好称呼 = 小王
m2: 用户偏好称呼 = 王工
    supersedes = m1

查询时只返回有效版本,审计时保留完整链路。

2. 冲突必须按范围解决

以下记录并不一定冲突:

用户在工作场景中偏好称呼“王工”。
用户在朋友群中使用昵称“小王”。

它们的 scope 不同:

[
  {
    "predicate": "preferred_name",
    "value": "王工",
    "scope": "work"
  },
  {
    "predicate": "preferred_name",
    "value": "小王",
    "scope": "friends"
  }
]

真正的冲突是:

同一用户、同一场景、同一有效时间内:
preferred_name = 王工
preferred_name = 林工

可采用以下决策顺序:

  1. 用户明确声明优先于行为推断;
  2. 当前有效时间优先于已过期时间;
  3. 更窄的适用范围优先于全局范围;
  4. 可信系统来源优先于模型生成内容;
  5. 无法判定时进入 conflict 或请求用户确认。

3. 情景记忆不应无限增长

情景记忆通常数量最多。若不做处理,会产生两个相反问题:

  • 旧事件过多,检索噪声升高;
  • 为了压缩历史而过早总结,导致关键上下文丢失。

可采用分层生命周期:

原始事件
   ↓
结构化情景记录
   ↓
相似事件聚合
   ↓
经过验证的语义事实或程序候选
   ↓
原始记录按保留策略归档或删除

但“总结”不能自动改变记忆类型。比如:

三次部署都在迁移阶段失败

可以作为聚合情景事实;它仍然不是:

迁移阶段必然失败

更不是:

以后跳过迁移阶段

4. 遗忘是数据治理,不只是删除向量

一条记忆的遗忘条件可以表示为:

forget(m)=expired(m)retracted(m)low_utility(m)privacy_request(m)superseded_and_not_audited(m)forget(m)= expired(m) \lor retracted(m) \lor low\_utility(m) \lor privacy\_request(m) \lor superseded\_and\_not\_audited(m)

其中:

  • expired:超过有效期;
  • retracted:用户或系统撤回;
  • low_utility:长期未被使用且价值很低;
  • privacy_request:用户要求删除;
  • superseded_and_not_audited:已被替代且不再需要保留审计版本。

程序记忆的遗忘尤其谨慎。旧流程可能已经不适用,但仍应保留版本和废弃原因,避免 Agent 因检索不到新版本而重新使用旧流程。


八、混用风险:同一个句子为什么会造成不同故障

风险一:把情景记忆当成语义记忆

原始事件:

2026-08-31,用户在会议中要求不要发送邮件。

错误升级:

用户不喜欢邮件。

后果:

  • 未来所有任务都避开邮件;
  • 用户无法收到必须通过邮件发送的通知;
  • Agent 无法解释这个偏好来自哪次会议。

修复方式是保留条件:

{
  "memory_type": "episodic",
  "event": "declined_email_notification",
  "scope": "meeting-2026-08-31"
}

如果用户之后明确说“以后都不要发邮件”,再写入全局语义记忆。

风险二:把语义记忆当成程序记忆

事实:

生产服务使用蓝绿部署。

错误执行:

Agent 直接切换流量。

“使用蓝绿部署”只描述架构事实,不说明:

  • 当前是否存在可切换的绿色环境;
  • 健康检查是否通过;
  • 是否满足审批条件;
  • 流量切换是否可回滚;
  • 当前操作者是否有权限。

事实可以帮助选择程序,但不能替代程序步骤和执行前验证。

风险三:把程序记忆当成无条件命令

程序:

数据库迁移失败后执行回滚。

错误理解:

任何迁移失败都直接回滚。

真实流程可能要求先判断:

  • 迁移是否在事务中;
  • 是否已经产生不可逆副作用;
  • 回滚脚本是否存在;
  • 是否有其他服务已经依赖新结构;
  • 当前故障是否需要人工接管。

程序记忆应包含条件分支,而不是只有线性步骤:

if migration_is_transactional:
    rollback()
elif rollback_script_verified:
    request_approval()
    rollback()
else:
    stop_and_escalate()

风险四:把线程状态当成跨线程记忆

线程 A:

用户正在讨论项目 alpha。

错误写入全局用户记忆:

用户当前负责项目 alpha。

之后线程 B 讨论项目 beta,Agent 可能把 alpha 的上下文带入 beta,产生:

  • 项目数据串线;
  • 错误的工具参数;
  • 权限边界失效;
  • 用户对 Agent 失去信任。

线程级状态必须绑定 thread_id 或任务标识。跨线程使用时,必须显式提升作用域。

风险五:把工具输出当成可信事实

工具返回:

订单状态:已退款

这通常是一次情景证据,不一定适合作为永久语义记忆。订单状态是一个随业务变化的动态属性,正确做法通常是:

  • 需要当前状态时重新查询;
  • 历史状态作为事件保留;
  • 不把旧工具输出当成永远有效的事实。

九、端到端示例:一次发布任务如何使用三种记忆

假设用户输入:

把 payment-service 发布到生产。上次 staging 的迁移失败了,这次先检查原因。

系统中已有三类数据:

语义记忆:
payment-service 的生产环境使用 PostgreSQL。

情景记忆:
2026-08-31,staging 的 V42 迁移因重复索引失败。

程序记忆:
发布前备份;在 staging 验证迁移;生产发布需要人工审批。

正确的数据流如下:

flowchart TD
    A[用户请求发布] --> B[读取当前线程状态]
    B --> C[检索语义记忆]
    B --> D[检索相关情景记忆]
    B --> E[检索发布程序记忆]
    C --> F[构造任务上下文]
    D --> F
    E --> F
    F --> G[检查当前环境与前置条件]
    G -->|迁移原因未确认| H[执行只读诊断]
    H --> I[写入本次诊断情景]
    I --> G
    G -->|备份完成且测试通过| J[请求人工审批]
    J -->|批准| K[执行生产发布]
    J -->|拒绝| L[结束任务并保存状态]
    K --> M[记录结果与证据]
    M --> N[更新情景记忆]

关键路径是:

  1. 语义记忆提供“系统是什么”;
  2. 情景记忆提供“上次发生了什么”;
  3. 程序记忆提供“这次应该如何行动”;
  4. 当前工具查询确认“现在是什么状态”;
  5. 检查点保存任务进展;
  6. 只有满足前置条件且获得授权,才执行副作用操作。

诊断阶段可以写入:

{
  "memory_type": "episodic",
  "event_id": "deploy-2026-09-01-diagnosis",
  "subject": "payment-service",
  "context": {
    "environment": "staging",
    "migration": "V42"
  },
  "actions": [
    "查询数据库索引元数据",
    "确认目标索引已存在"
  ],
  "outcome": "root_cause_identified",
  "evidence": [
    {
      "kind": "database_query",
      "query_digest": "sha256:..."
    }
  ]
}

只有在明确验证后,才可能更新程序记忆,例如:

V42 迁移脚本必须在执行前检查目标索引是否存在。

但这条程序更新仍应经过代码审查或发布流程批准,而不是因为一次 Agent 诊断成功就自动激活。


十、与 OpenAI 会话状态和 LangGraph 持久化的对应关系

1. OpenAI 会话状态的边界

OpenAI 文档将多轮状态分成几种管理方式:

  • 手动传入历史消息;
  • 使用 previous_response_id 链接前后响应;
  • 使用 Conversations API 保存具有持久标识的会话对象。

previous_response_id 适合表达响应之间的线程连续性;Conversations API 适合需要跨会话、设备或任务继续访问的会话状态。(developers.openai.com)

这类状态解决的是:

模型如何看到之前的对话和工具交互?

它没有自动解决:

哪些内容应该成为用户长期事实?
哪些事件应该过期?
哪些流程应该经过审批?
哪些记忆可以跨项目使用?

此外,Responses API 即使使用 previous_response_id,链中的历史输入仍会作为输入 token 计费;上下文窗口也同时受到输入、输出和推理 token 的限制。长对话需要通过压缩或显式摘要管理上下文,而不能把无限历史当成无限记忆。(developers.openai.com)

2. LangGraph 的 checkpointer 与 store

LangGraph 的典型配置是:

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

checkpointer = InMemorySaver()
store = InMemoryStore()

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

result = graph.invoke(
    {"messages": [{"role": "user", "content": "Hi, my name is Bob."}]},
    {"configurable": {"thread_id": "thread-1"}},
)

这个例子中:

  • thread_id="thread-1" 标识当前线程;
  • checkpointer 保存该线程的图状态;
  • store 用于应用自己定义的跨线程数据;
  • 二者可以同时使用,但用途不同。(docs.langchain.com)

映射到三种记忆时,可以这样设计:

checkpointer
└── 当前发布线程的消息、工具调用、审批状态、临时变量

store / memory repository
├── semantic: 用户和项目的稳定事实
├── episodic: 历史发布、故障和诊断事件
└── procedural: 发布流程、恢复流程和版本化 runbook

不要把所有内容都放在 checkpointer 中。检查点适合恢复当前线程,不适合承担跨线程用户画像;也不要把所有内容都放入 store,否则当前任务的临时状态会污染长期知识。

LangGraph 文档还指出,内存型 MemorySaverInMemorySaver 在进程重启后会丢失数据,生产环境应使用持久化检查点;长期运行的线程还需要处理检查点无限增长带来的延迟和存储成本。(docs.langchain.com)


十一、并发与故障路径

1. 并发更新会制造“最后写入获胜”错误

假设两个请求同时更新用户称呼:

请求 A:用户说“叫我王工”
请求 B:用户说“改叫林工”

如果系统直接执行:

UPDATE user_profile SET preferred_name = :name;

最终结果取决于提交顺序,而不是信息可信度、时间或用户意图。

更安全的做法是:

  1. 为每条记忆生成唯一版本;
  2. 记录来源事件;
  3. 使用乐观锁或事务;
  4. 检测同一作用域中的冲突;
  5. 必要时保留 conflict 状态。

伪代码:

def update_semantic_memory(candidate, expected_version):
    current = load_current(candidate.key)

    if current.version != expected_version:
        raise ConcurrentUpdate("memory changed after read")

    if conflicts(current, candidate):
        return mark_conflict(current, candidate)

    return append_version(
        candidate,
        supersedes=current.memory_id,
    )

2. 工具成功不等于任务成功

一个工具调用可能返回成功,但后续步骤失败:

备份成功
→ staging 迁移成功
→ 人工审批成功
→ 生产部署请求超时

此时不能把整个程序记忆标记为“成功”,也不能把“生产已发布”写入语义记忆。应按事件粒度记录:

backup: succeeded
staging_migration: succeeded
approval: granted
production_deploy: unknown

状态为 unknown 时,恢复流程应先查询真实环境,而不是盲目重试。否则可能把一个已经成功但响应丢失的部署重复执行。

3. 检查点恢复必须区分可重放和不可重放步骤

程序记忆中的步骤可能具有不同副作用:

查询状态        可重复
生成计划        通常可重复
创建备份        可能重复但成本较高
扣款            不可随意重复
发送邮件        可能产生重复通知
切换生产流量    需要幂等键或状态确认

因此,检查点恢复时应保存:

{
  "step_id": "charge",
  "status": "submitted",
  "idempotency_key": "payment-42-order-991",
  "tool_call_id": "tool-123",
  "result_digest": "sha256:..."
}

恢复逻辑不是简单地“从上一个节点重新运行”,而是:

读取检查点
→ 判断步骤状态
→ 查询外部系统
→ 根据幂等键确认是否已生效
→ 决定跳过、补偿或重试

十二、诊断:如何判断是哪一种记忆出了问题

当 Agent 行为异常时,可以先按症状分类。

症状一:跨会话一直重复同一个错误

优先检查语义记忆:

错误内容是否被写成 active?
来源是否只是一次模型推断?
是否缺少 valid_to?
是否存在更新但旧版本仍被召回?

症状二:只在某个线程中出错

优先检查线程状态和检查点:

thread_id 是否复用?
不同任务是否错误共用了同一个线程?
恢复时是否加载了旧的工具结果?
是否把临时变量写入了共享 store?

症状三:Agent 知道流程,却跳过关键步骤

优先检查程序记忆的执行边界:

流程是否只是提示词文本?
required 步骤是否由编排器强制?
前置条件是否由代码验证?
工具是否允许绕过审批?

症状四:回答“上次发生了什么”时内容失真

优先检查情景记忆:

事件时间是否保存?
参与对象和环境是否完整?
工具输出是否有证据引用?
多个相似事件是否被错误合并?
摘要是否丢失了否定条件?

症状五:不同项目之间出现数据串线

优先检查命名空间和作用域:

tenant_id
user_id
project_id
thread_id

这些字段不能只用于展示;它们必须参与检索过滤和写入授权。仅依赖向量相似度无法保证租户隔离。


十三、生产系统中的最小边界

一个可维护的 Agent 记忆系统,至少应满足以下边界:

当前任务指令
    不自动升级为长期偏好

单次历史事件
    不自动升级为普遍事实

事实记录
    不自动变成可执行步骤

操作流程
    不自动绕过权限、审批和前置条件

线程检查点
    不自动跨线程共享

工具输出
    不自动成为永久可信事实

可以把最终决策写成一个类型安全的接口:

from dataclasses import dataclass
from typing import Literal, Any

MemoryType = Literal["semantic", "episodic", "procedural"]

@dataclass
class MemoryCandidate:
    memory_type: MemoryType
    scope: dict[str, str]
    payload: dict[str, Any]
    source: dict[str, Any]
    confidence: float
    requires_review: bool

写入流程:

def accept_candidate(candidate: MemoryCandidate) -> str:
    if candidate.memory_type == "semantic":
        validate_semantic(candidate)
    elif candidate.memory_type == "episodic":
        validate_event_time_and_evidence(candidate)
    elif candidate.memory_type == "procedural":
        validate_preconditions_and_side_effects(candidate)

    if candidate.requires_review:
        return save_as_pending(candidate)

    return save_as_active(candidate)

这里最重要的不是 Python 类型本身,而是让“记忆类型”成为系统中的显式分支。若所有记忆都只是 text: str,类型差异最终会被隐藏在提示词里,导致错误只能通过线上行为发现。


结语

语义、情景和程序记忆的区别,本质上是三种不同的时间和行动关系:

语义记忆:什么通常是真的?
情景记忆:什么曾经发生过?
程序记忆:在什么条件下应该怎么做?

它们可以共享统一的元数据、权限、审计和检索基础设施,但不能共享未经区分的解释方式。

上下文解决当前推理,线程状态解决任务连续性,检查点解决执行恢复,长期记忆解决跨任务复用。只有把这些层次分开,Agent 才能在“记得更多”的同时避免“把所有事情都记成同一种东西”。

对于工程实现,最有价值的约束不是选择哪一种向量数据库,而是确保每条记忆都能回答:

它是什么类型?
它来自哪里?
对谁、对什么项目、在哪个线程有效?
从什么时候到什么时候有效?
它能否影响行动?
如果错了,如何撤回、替代和审计?

如果这些问题没有数据模型上的答案,系统即使拥有很强的检索能力,也只是在更快地召回无法解释的错误。


系列导航与关联阅读

官方资料

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