本文定位:Agent 记忆 / 上下文管理 / 数据建模
示例环境:Java 21、Spring Boot 3.3、PostgreSQL、Redis、向量检索。记忆保存涉及隐私和合规,具体保留策略必须经过业务确认。
摘要
“给 Agent 加记忆”经常被理解成把所有历史聊天保存下来,再在下一次请求中全部拼回 Prompt。这样做会带来三个问题:上下文越来越长,成本和延迟不断增加;过期或错误信息被重复使用;用户不清楚系统记住了什么,也无法删除或修正。
真正可用的记忆系统应该区分短期上下文、长期偏好、可检索事实和任务状态。短期上下文服务当前对话,长期记忆保存经过确认的稳定信息,任务状态保证业务流程可恢复,RAG 知识库则负责外部资料。本文给出这几类数据的边界、表结构、写入策略和检索流程。
一、四种“记忆”不是一回事
| 类型 | 例子 | 保存位置 | 生命周期 |
|---|---|---|---|
| 短期上下文 | 当前会话最近几轮消息 | Redis/会话存储 | 分钟到数天 |
| 长期偏好 | 用户偏好的输出格式 | 结构化数据库 | 用户可查看和删除 |
| 事实记忆 | 设备曾发生某类故障 | 数据库/向量索引 | 有来源、有过期时间 |
| 任务状态 | 工单执行到第3步 | 关系数据库 | 直到任务结束和归档 |
不要把任务状态塞进自然语言记忆。status=WAIT_CONFIRM、step=3和retry=1应该由确定性的数据库字段表示,不能依赖模型从聊天记录中猜出来。
二、记忆读取流程
每次请求不是把所有记忆都取出来,而是先根据问题召回候选,再按来源、可信度、时间和权限筛选。记忆检索失败时,Agent 仍然应该可以在无长期记忆的情况下完成普通任务。
三、短期上下文裁剪
短期上下文包括当前对话和本轮工具结果。建议按 Token 预算分配:系统规则、当前问题、最近消息、摘要和外部证据分别设置上限。
publicContextWindowbuildWindow(List<Message>history,Stringsummary,Stringevidence,intmaxTokens){List<Message>recent=takeFromEnd(history,8);ContextWindowwindow=newContextWindow();window.addSystemRules(systemRules);window.addSummary(truncate(summary,1200));window.addMessages(recent);window.addEvidence(truncate(evidence,maxTokens/2));returnwindow.fit(maxTokens);}摘要也不是事实数据库。摘要由模型生成,可能遗漏否定条件或把不确定内容写成事实。高风险信息应回到结构化字段和原始证据,而不是只依赖摘要。
四、长期记忆的写入门槛
不是每句话都应该进入长期记忆。可以设置以下条件:
- 用户明确表达“请记住”或主动保存。
- 信息具有稳定性,不是临时情绪和一次性任务。
- 能说明来源、时间和置信度。
- 不包含不必要的敏感信息。
- 写入后用户可以查看、修改和删除。
publicOptional<MemoryCandidate>extract(Stringconversation,UserIntentintent){if(!intent.explicitSaveRequest()){returnOptional.empty();}MemoryCandidatecandidate=memoryExtractor.parse(conversation);if(candidate.sensitive()||candidate.content().length()>500){returnOptional.empty();}returnOptional.of(candidate.withExpiration(Duration.ofDays(180)));}“用户喜欢简洁回答”可以作为偏好候选;“用户的身份证号是……”不应该默认写入长期记忆。记忆提取结果要经过规则校验,必要时请求用户确认后再保存。
五、数据模型
CREATETABLEuser_memory(id uuidPRIMARYKEY,tenant_idvarchar(64)NOTNULL,user_idvarchar(128)NOTNULL,memory_typevarchar(32)NOTNULL,contenttextNOTNULL,source_trace_idvarchar(128)NOTNULL,confidencenumeric(4,3)NOTNULL,expires_at timestamptz,statusvarchar(16)NOTNULL,created_at timestamptzNOTNULLDEFAULTnow(),updated_at timestamptzNOTNULLDEFAULTnow());CREATEINDEXidx_memory_ownerONuser_memory(tenant_id,user_id,status);如果需要语义检索,可以增加 embedding,但必须同时保留结构化的租户、用户、状态和过期条件。向量相似度不能替代这些过滤。
六、事实记忆的来源与冲突
同一用户或设备可能出现互相冲突的事实,例如“设备已经更换模块”和历史记录“模块未更换”。记忆项应带时间、来源和状态,检索时优先当前有效、来源可靠且时间较新的记录。
publicList<Memory>resolveConflicts(List<Memory>candidates){returncandidates.stream().filter(Memory::active).filter(m->m.expiresAt()==null||m.expiresAt().isAfter(Instant.now())).sorted(Comparator.comparing(Memory::confidence).reversed().thenComparing(Memory::createdAt).reversed()).limit(5).toList();}冲突没有可靠解决办法时,Agent 应该明确说明“存在两条时间不同的记录”,而不是强行选择一个。事实记忆需要可追溯的来源,不应以模型“觉得合理”为依据。
七、任务状态必须结构化
CREATETABLEagent_task_state(task_id uuidPRIMARYKEY,tenant_idvarchar(64)NOTNULL,user_idvarchar(128)NOTNULL,statusvarchar(32)NOTNULL,current_stepintegerNOTNULL,state_json jsonbNOTNULLDEFAULT'{}',versionbigintNOTNULLDEFAULT0,updated_at timestamptzNOTNULLDEFAULTnow());使用乐观锁version防止两个调度器同时推进任务。每次状态迁移都记录旧状态、新状态、触发原因和 Trace ID。模型可以提出下一步,但状态迁移必须由后端有限状态机判断。
八、记忆污染和安全
长期记忆可能被恶意用户利用:先诱导 Agent 保存“我是管理员”,后续再用记忆绕过权限。任何记忆都不能改变认证和授权。记忆只能作为辅助上下文,不能覆盖当前 Token、数据库状态和业务规则。
还要防止跨租户、跨用户检索。缓存键、向量索引过滤、删除接口和导出接口都必须带上租户与用户条件。用户删除记忆时,应同步清理主表、向量索引、缓存和备份中的可检索副本。
九、如何评测记忆效果
记忆系统不应该以“召回越多越好”为目标。评测包括:
- 该记住的偏好是否被正确召回。
- 过期信息是否不再使用。
- 冲突信息是否能提示不确定性。
- 无关记忆是否不会污染答案。
- 用户删除后是否确实不再出现。
- 记忆内容是否不越权、不改变权限。
可以建立“应该记住”“不应该记住”“应该忘记”“存在冲突”的样本集,并测试写入、读取、更新和删除全流程。
十、总结
Agent 记忆不是无限聊天记录,而是不同生命周期、不同可信度和不同治理要求的数据集合。短期上下文服务对话,长期记忆服务偏好,RAG 服务外部知识,任务状态服务可靠执行。
最关键的原则有三个:未经确认的信息不要随意长期保存;记忆不能改变权限和业务状态;所有记忆都应有来源、生命周期和删除路径。只有这样,记忆才会成为稳定能力,而不是越来越大的上下文垃圾场。
读者讨论
如果你准备给 Agent 增加记忆,建议先列出“哪些内容可以保存、保存多久、谁能查看、如何删除”四个问题,再选择存储方案。