Agent 工程体系 · 第 25/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agent 语义、情景与程序记忆:数据模型、用途和混用风险
Agent 的“记忆”不是一个统一的数据结构。用户偏好、某次故障的经过、如何执行一次部署,虽然都可以被称为“记忆”,但它们的生命周期、可信度、检索方式和失败代价完全不同。
把这些内容全部写入一个向量库,再在提示词中统一检索,短期内可以工作;一旦系统进入多用户、多线程、长任务或高风险操作场景,问题通常会表现为:
- Agent 把一次性的事件误当成用户长期偏好;
- Agent 把“曾经这样做过”误当成“以后都应该这样做”;
- Agent 把一段操作步骤当成事实陈述,或者把事实陈述当成操作指令;
- 新旧记忆冲突时,没有依据、时间和适用范围可供判断;
- 当前对话状态、长期记忆和程序规则相互污染,导致错误持续扩大。
因此,首先要区分三种长期记忆:
- 语义记忆:关于人、对象、规则和稳定事实的知识;
- 情景记忆:某一次具体经历、任务或事件的记录;
- 程序记忆:完成某类任务时可执行的步骤、策略和约束。
它们都可能由自然语言表示,也都可以存入关系数据库、文档库或向量库,但“存储形式相同”不意味着“数据模型相同”。
一、先区分上下文、状态和长期记忆
1. 上下文不是长期记忆
**上下文(context)**是当前一次模型调用可见的信息集合。它可能包含:
- 系统指令;
- 当前用户输入;
- 最近几轮消息;
- 工具调用及其结果;
- 当前任务的中间变量;
- 被检索出来的文档和记忆。
可以形式化为:
其中:
- :系统级指令;
- :当前线程的历史消息;
- :当前任务状态;
- :本轮检索结果;
- :本轮用户输入和外部输入;
- :第 次模型调用实际看到的上下文。
上下文的关键特征是:它服务于当前推理,不天然代表未来仍然成立的知识。
例如:
用户说:“这次发布先不要执行数据库迁移,我要先做备份。”
这句话应该进入当前任务状态,可能成为本次发布的约束;但不能直接写成长期语义记忆:
用户不允许数据库迁移。
因为原话只表达了“这次发布”的暂时要求,而不是永久偏好。
2. 对话状态是可恢复的工作状态
**对话状态(conversation state)**是为了让一次连续交互能够继续进行而保存的状态。它通常包括消息、工具调用、工具输出以及任务中的中间结果。
OpenAI 的 Responses API 可以通过手动传入历史消息、previous_response_id 链接前一响应,或使用具有持久标识的 Conversations API 来维护多轮状态。Conversations 可以跨会话、设备和任务使用,并保存消息、工具调用和工具输出等项目。(developers.openai.com)
但这仍然是线程或会话连续性,不等于用户画像或跨任务长期记忆。
可以用下列关系表示:
当前请求
│
├── 当前输入
├── 当前线程历史
├── 当前任务状态
└── 检索到的长期记忆
其中只有最后一项通常来自语义、情景或程序记忆存储。
3. 检查点也不等于长期记忆
在图式 Agent 中,常见的持久化对象是检查点(checkpoint)。检查点保存某个线程在某个执行时刻的图状态,用于:
- 对话连续性;
- 人工介入后继续执行;
- 故障恢复;
- 时间旅行或从历史状态分叉。
LangGraph 将 checkpointer 与 store 明确区分:前者持久化单个 thread 的图状态快照,后者保存跨 thread 的应用级键值数据。前者适合短期、线程范围的记忆,后者适合用户偏好、事实和共享知识等长期记忆。(docs.langchain.com)
因此,一个更准确的分层是:
请求级上下文
↓
线程级状态 / 检查点
↓
长期记忆
├── 语义记忆
├── 情景记忆
└── 程序记忆
二、三种记忆的核心区别
1. 语义记忆:回答“是什么”
**语义记忆(semantic memory)**保存脱离单次事件后仍具有复用价值的事实、概念、属性和关系。
典型内容包括:
用户的稳定称呼是“林工”。
用户所在时区是 Asia/Shanghai。
项目 payment-service 使用 PostgreSQL。
生产环境发布必须经过人工审批。
团队的默认日志级别是 INFO。
语义记忆的核心不是“内容看起来像事实”,而是:
该内容能否脱离产生它的具体事件,在未来多个任务中作为事实使用?
可以将一条语义记忆建模为:
例如:
{
"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 执行退款前发现订单状态为已关闭,因此请求人工确认。
可以将情景记忆建模为:
示例:
{
"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. 观察错误率和延迟。
程序记忆不能只写成一段“经验文字”,否则模型很难知道:
- 何时适用;
- 哪些步骤必须执行;
- 哪些步骤可以跳过;
- 哪些步骤具有破坏性;
- 失败时如何恢复;
- 哪些动作需要审批。
更适合使用结构化模型:
示例:
{
"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
可以把写入条件表达为:
其中:
stable(m):预计不会只在当前瞬间成立;reusable(m):未来任务使用它有明确价值;authorized(m):写入行为得到用户、系统或业务授权;evidenced(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. 程序记忆不能由单次成功自动升级
一次操作成功只证明:
它不等价于:
例如,一次“先重启服务再清理缓存”成功,不足以推出这是所有故障的正确恢复流程。可能原因是:
- 当时流量很低;
- 当时没有长连接;
- 当时服务版本不同;
- 当时缓存可以安全丢失;
- 真正原因并不是重启,而是另一个并发变化。
因此,程序记忆至少需要保存适用前提:
{
"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. 一个可解释的排序函数
检索排序可以使用类型、范围、时间和来源共同计算:
其中:
- :记忆内容与查询的语义相似度;
- :租户、用户、项目和线程范围是否匹配;
- :内容是否仍然新鲜;
- :来源是否可信;
- :记忆类型是否适合当前问题;
- :错误使用该记忆的潜在损失。
例如,对于“如何执行退款”,程序记忆的 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 = 林工
可采用以下决策顺序:
- 用户明确声明优先于行为推断;
- 当前有效时间优先于已过期时间;
- 更窄的适用范围优先于全局范围;
- 可信系统来源优先于模型生成内容;
- 无法判定时进入
conflict或请求用户确认。
3. 情景记忆不应无限增长
情景记忆通常数量最多。若不做处理,会产生两个相反问题:
- 旧事件过多,检索噪声升高;
- 为了压缩历史而过早总结,导致关键上下文丢失。
可采用分层生命周期:
原始事件
↓
结构化情景记录
↓
相似事件聚合
↓
经过验证的语义事实或程序候选
↓
原始记录按保留策略归档或删除
但“总结”不能自动改变记忆类型。比如:
三次部署都在迁移阶段失败
可以作为聚合情景事实;它仍然不是:
迁移阶段必然失败
更不是:
以后跳过迁移阶段
4. 遗忘是数据治理,不只是删除向量
一条记忆的遗忘条件可以表示为:
其中:
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[更新情景记忆]
关键路径是:
- 语义记忆提供“系统是什么”;
- 情景记忆提供“上次发生了什么”;
- 程序记忆提供“这次应该如何行动”;
- 当前工具查询确认“现在是什么状态”;
- 检查点保存任务进展;
- 只有满足前置条件且获得授权,才执行副作用操作。
诊断阶段可以写入:
{
"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 文档还指出,内存型 MemorySaver 或 InMemorySaver 在进程重启后会丢失数据,生产环境应使用持久化检查点;长期运行的线程还需要处理检查点无限增长带来的延迟和存储成本。(docs.langchain.com)
十一、并发与故障路径
1. 并发更新会制造“最后写入获胜”错误
假设两个请求同时更新用户称呼:
请求 A:用户说“叫我王工”
请求 B:用户说“改叫林工”
如果系统直接执行:
UPDATE user_profile SET preferred_name = :name;
最终结果取决于提交顺序,而不是信息可信度、时间或用户意图。
更安全的做法是:
- 为每条记忆生成唯一版本;
- 记录来源事件;
- 使用乐观锁或事务;
- 检测同一作用域中的冲突;
- 必要时保留
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 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent 长期记忆:写入策略、检索、更新、冲突和遗忘
- 下一篇:Agent 身份与用户画像:稳定标识、偏好、称呼和可信来源
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论