事情是这样的。
一个客服 Agent demo,第一轮表现很好。
用户问:“我的退款为什么一直失败?”
Agent 查订单、查售后单、查支付回执,回答得很完整。
第二轮用户说:“那帮我重新发起一下。”
Agent 也知道“重新发起”指的是刚才那笔退款,开始调用补偿工具。
第三轮用户补了一句:“这次别走原来的银行卡,我换了收款账户。”
这时候问题来了。
如果系统只是把全部聊天记录 append 到 prompt 里,模型大概率能“看见”这话。但到了第十轮、第十五轮,用户又改过一次账户、客服又补了一条限制、工具又返回过一次失败原因,整个上下文开始变得又长又脏。
你会看到一种很典型的线上现象:
2026-06-18 10:21:03 user: 这次别走原来的银行卡,我换了收款账户
2026-06-18 10:21:07 memory.write: append raw message, tokens=42
2026-06-18 10:27:19 user: 继续处理刚才那个
2026-06-18 10:27:22 memory.recall: top1="用户之前说不要走原来的银行卡"
2026-06-18 10:27:22 memory.recall: missing="新收款账户 account_id"
2026-06-18 10:27:24 tool.call: refund_retry(order_id=..., account_id=old_account)
2026-06-18 10:27:25 tool.result: rejected, reason=ACCOUNT_NOT_ALLOWED
看起来 Agent 是“记住了”用户说过的话。
但它没有记住真正该用于执行的事实:这次退款要使用新的收款账户,以及这个事实什么时候生效、覆盖了什么旧事实。
所以今天这篇文章只讲一个问题:
Agent 的记忆机制,不是把历史存起来,而是把不同生命周期的信息,用不同方式写入、召回、压缩、纠错,再放回模型上下文。
听起来像绕口令。
落到工程上,其实就是三层:
很多 Agent 项目的第一版记忆实现都长这样:
def chat(user_id: str, message: str):
history = db.load_messages(user_id)
prompt = build_prompt(history, message)
answer = llm.generate(prompt)
db.save_message(user_id, "user", message)
db.save_message(user_id, "assistant", answer)
return answer
这段代码不能说错。
它可以做多轮对话,也能让模型看到前面聊过什么。
但它有三个硬伤:
这就是 Agent 记忆最容易被误解的地方。
记忆的核心不是 save_message。
记忆的核心是:在下一次模型决策前,系统能不能把最该出现的那几条信息,放到最合适的位置。
OpenAI Agents SDK 里的 Session 更接近短期会话记忆:它会为同一个 session 保存对话历史,运行时自动取回上下文。LangGraph 的 memory 文档则把短期记忆放在线程级状态里,把长期记忆放到跨线程的 store/namespace 里。MemGPT 那篇论文更进一步,用操作系统的分层内存来类比 LLM 的上下文管理:有限上下文窗口像高速缓存,外部存储像慢速大内存,关键是信息在不同层之间移动。
工程上可以把它压成一句话:
短期记忆保执行,中期记忆保连续性,长期记忆保复用。
短期记忆解决的是“这一轮 Agent 到底在干什么”。
它通常只活在一个任务、一个 run、一个 thread,或者一次工具调用循环里。
典型内容包括:
类型 | 例子 | 存放方式 |
|---|---|---|
最近消息 | 用户刚刚说的话、上一轮 assistant 回复 | session history |
当前任务目标 | “给这笔退款重新发起补偿” | runtime state |
工具观察结果 | 订单状态、接口错误码、trace id | scratchpad / state |
临时约束 | “本次不要自动提交,先给我预览” | context variable |
短期记忆不要设计得太玄。
最常见的实现就是一个线程级状态:
from dataclasses import dataclass, field
@dataclass
class AgentState:
user_id: str
thread_id: str
goal: str | None = None
messages: list[dict] = field(default_factory=list)
tool_observations: list[dict] = field(default_factory=list)
constraints: dict = field(default_factory=dict)
def build_short_context(state: AgentState, latest_user_msg: str) -> list[dict]:
recent_messages = state.messages[-8:]
tool_summary = summarize_tool_observations(state.tool_observations[-5:])
return [
{"role": "system", "content": "你是一个退款处理 Agent,必须遵守工具策略。"},
{"role": "system", "content": f"当前目标:{state.goal or '待识别'}"},
{"role": "system", "content": f"本轮工具观察:{tool_summary}"},
*recent_messages,
{"role": "user", "content": latest_user_msg},
]
这段代码的重点不是 messages[-8:] 这个数字。
重点是短期上下文要分区:
很多线上问题就出在这里。
团队把用户消息、工具结果、模型思考、系统规则全塞到一个数组里,然后交给模型自己“理解优先级”。demo 看起来没事,一旦工具结果变多,模型就开始拿旧错误当新事实。
短期记忆的工程边界也很清楚:
所以短期记忆只做一件事:
保证 Agent 当前这一步不要忘事。
中期记忆解决的是“这个会话聊到哪了”。
它比短期记忆活得久一点,通常覆盖一个会话、一个工单、一次项目任务、一次代码修改流程。
短期记忆像工作台。
中期记忆像白板。
你不能把所有工具日志都堆在白板上,但要把当前结论、未完成事项、关键决策写清楚。
比如刚才那个退款 Agent,中期记忆不应该是:
{
"summary": "用户咨询退款失败问题,后来想重新发起,期间提到更换收款账户。"
}
这句话看起来像摘要,其实执行价值很低。
更好的中期记忆应该是结构化任务状态:
{
"task_id": "refund_retry_20260618_1021",
"status": "waiting_user_confirm",
"order_id": "R202606180013",
"current_decision": "需要重新发起退款补偿",
"effective_constraints": {
"payout_account": "new_account",
"auto_submit": false
},
"open_questions": [
"等待用户确认新收款账户是否已完成风控校验"
],
"last_tool_errors": [
{
"tool": "refund_retry",
"error": "ACCOUNT_NOT_ALLOWED",
"time": "2026-06-18T10:27:25+08:00"
}
]
}
这才是 Agent 能继续工作的记忆。
它不是文学摘要,而是运行状态。
中期记忆的更新方式一般有两种:
一种是每轮对话后滚动总结:
def update_working_summary(old_summary: str, new_events: list[dict]) -> str:
prompt = f"""
你负责维护一个客服工单的工作摘要。
只保留仍然影响后续执行的信息:
1. 当前目标
2. 已确认事实
3. 有效约束
4. 未解决问题
5. 最近一次工具错误
旧摘要:
{old_summary}
新事件:
{new_events}
输出 JSON,不要输出寒暄。
"""
return llm.generate(prompt)
另一种是用确定性逻辑更新字段:
def apply_event(state: dict, event: dict) -> dict:
if event["type"] == "user_constraint":
key = event["key"]
state["effective_constraints"][key] = {
"value": event["value"],
"source": event["message_id"],
"updated_at": event["time"]
}
if event["type"] == "tool_error":
state["last_tool_errors"] = [event]
state["status"] = "blocked_by_tool_error"
return state
我自己更建议两种混着用。
LLM 适合从自然语言里提取“用户刚才是不是改了限制条件”,但状态落库最好走 schema 和确定性校验。
否则你会遇到很烦的问题:
中期记忆的关键指标不是“摘要多像人写的”。
它的关键指标是:
state_freshness: 当前状态是否反映最新用户约束
state_coverage: 后续执行所需字段是否完整
conflict_count: 新旧事实冲突有没有被识别
resume_success_rate: 隔一段时间恢复任务,Agent 是否能接着做
回到主线。
短期记忆解决当前一步,中期记忆解决当前任务。
如果没有中期记忆,Agent 会在长对话里出现一种很典型的症状:它每轮都像认识你,但每轮都重新理解任务。
长期记忆解决的是“下次见到这个用户、这个项目、这个组织,还应该记得什么”。
很多人一听长期记忆,第一反应就是向量库。
但向量库只解决一部分问题:相似内容召回。
真正的长期记忆至少有四类:
记忆类型 | 内容例子 | 推荐存储 |
|---|---|---|
用户事实 | 用户所在部门、常用语言、权限范围 | 关系库 / JSON store |
用户偏好 | 喜欢先看结论、不喜欢自动提交 | profile store |
项目经验 | 某仓库发布前必须跑哪条命令 | 文档库 + 向量索引 |
操作教训 | 某接口失败时不能盲目重试 | rule store / policy |
LangGraph 的长期记忆文档里有一个很实用的抽象:memory 存成 JSON document,并按 namespace/key 组织。namespace 里通常放用户、组织、应用等隔离维度。
工程上可以这样设计:
namespace = ["user", user_id, "profile"]
key = "communication_preferences"
namespace = ["org", org_id, "refund_agent", "rules"]
key = "retry_policy"
namespace = ["project", repo_id, "lessons"]
key = "release_checklist"
长期记忆的写入,千万不要每轮都自动写。
一个更稳的写入流水线应该是这样:
!
可以用一段伪代码表达:
def maybe_write_long_term_memory(event: dict, state: dict):
candidate = extract_memory_candidate(event, state)
if candidate isNone:
return
decision = classify(candidate)
if decision.kind notin {"preference", "stable_fact", "lesson", "policy"}:
return
if decision.confidence < 0.85:
queue_for_review(candidate)
return
old = memory_store.get(namespace=decision.namespace, key=decision.key)
merged = merge_with_conflict_check(old, candidate)
if merged.has_conflict:
ask_user_or_human_reviewer(merged)
return
memory_store.put(
namespace=decision.namespace,
key=decision.key,
value=merged.value,
metadata={
"source_message_id": event["message_id"],
"confidence": decision.confidence,
"ttl_days": decision.ttl_days,
"updated_at": event["time"]
}
)
这段代码里有几个点很重要。
不是所有用户说过的话都应该进入长期记忆。
“我今天有点忙”多数只是临时状态。
“以后所有日报先给我 Markdown 表格”才可能是偏好。
“这个接口失败时不要重试,必须升级人工”更像组织级操作教训。
长期记忆还必须有元数据:
没有这些字段,长期记忆很快会变成“永久污染源”。
模型一旦召回一条过期偏好,就会一本正经地执行。
而且越是智能,越难发现它错在哪里。
写入只是前半场。
后半场是召回。
一个常见错误是:
memories = vector_store.search(user_message, top_k=8)
prompt = build_prompt(history, memories, user_message)
这依然太粗。
因为用户当前这句话未必能表达真实任务。
用户说“继续处理刚才那个”,向量检索可能召回一堆“刚才”的相似对话,但漏掉真正关键的 order_id、新账户约束、人工审批策略。
更合理的召回链路应该分几步:
第一步,先做 query rewrite。
把省略、指代、上下文里的任务目标补出来:
用户原话:
继续处理刚才那个
改写后的检索问题:
继续处理订单 R202606180013 的退款补偿任务。
需要考虑用户已要求使用新收款账户,且上一次 refund_retry 工具返回 ACCOUNT_NOT_ALLOWED。
第二步,分源召回。
不要只搜一个向量库:
def recall_memory(user_id: str, org_id: str, state: dict, message: str):
query = rewrite_query(state, message)
profile = profile_store.get(["user", user_id, "profile"])
task_state = task_store.get(state["task_id"])
lessons = vector_store.search(
namespace=["org", org_id, "refund_agent", "lessons"],
query=query,
top_k=5
)
policies = rule_store.match(
namespace=["org", org_id, "refund_agent", "rules"],
facts=task_state
)
return compose_context(profile, task_state, lessons, policies)
第三步,做 context compose。
也就是把召回结果放到不同区域,而不是一股脑塞进 prompt:
<current_task_state>
order_id=R202606180013
status=waiting_user_confirm
effective_constraints.payout_account=new_account
</current_task_state>
<user_preferences>
回复时先给结论,再给操作明细。
</user_preferences>
<org_policies>
当 refund_retry 返回 ACCOUNT_NOT_ALLOWED 时,禁止自动重试,必须升级人工校验账户。
</org_policies>
<retrieved_lessons>
历史经验:收款账户变更后,必须先调用 account_risk_check。
</retrieved_lessons>
为什么要分区?
因为模型不是数据库执行器。
你给它一堆文本,它会自己建立优先级;但优先级不一定符合你的业务。
把任务状态、用户偏好、组织规则、历史经验分开,是为了让模型知道:
这一步如果做不好,长期记忆反而会伤害 Agent。
它会让模型想起很多“似乎相关”的东西,然后偏离当前任务。
如果让我给一个生产可用的 Agent 记忆方案,我不会一上来就搞很复杂的自反思系统。
我会先把三层做扎实:
层级 | 生命周期 | 写入方式 | 召回方式 | 失败风险 |
|---|---|---|---|---|
短期记忆 | 当前 run / thread | 运行时自动追加 | 直接进入上下文 | token 爆炸、旧工具结果干扰 |
中期记忆 | 当前会话 / 工单 | 摘要 + schema 更新 | 按 task_id 读取 | 漏掉有效约束、状态过期 |
长期记忆 | 跨会话 / 跨任务 | 候选提取 + 分类 + 冲突校验 | namespace + search + rule match | 记住错事、召回噪声、隐私风险 |
一个简化版架构可以长这样:
User Message
|
v
Short-term State
- recent messages
- current goal
- tool observations
|
v
Mid-term Task Memory
- task summary
- confirmed facts
- effective constraints
- open questions
|
v
Long-term Memory Recall
- user profile
- org policies
- project lessons
- semantic search results
|
v
Context Composer -> LLM -> Tool / Answer
写入链路则反过来:
LLM / Tool / User Event
|
v
Memory Candidate Extractor
|
v
Classifier
- short-term only
- update task state
- write long-term memory
- ignore
|
v
Conflict Check / TTL / Approval
|
v
Memory Stores
这套方案不花哨,但足够挡住大部分真实问题。
因为它承认了一件事:
Agent 记忆不是越多越好,而是越可控越好。
第一个坑:把长上下文当长期记忆。
长上下文能缓解一部分问题,但不能替代记忆治理。上下文再长,也会遇到信息优先级、过期事实、冲突事实和隐私边界。
第二个坑:把摘要当事实。
摘要是模型生成的二手信息。凡是会影响工具调用、支付、权限、审批的字段,最好结构化保存,并保留来源。
第三个坑:只做向量召回。
向量相似不等于任务相关。偏好、规则、权限、状态这类信息,很多时候应该用 key-value、关系库、规则匹配,而不是全靠 embedding。
第四个坑:没有遗忘机制。
长期记忆如果不能过期、不能删除、不能被用户纠正,它就不是智能资产,而是风险资产。
尤其在企业场景里,记忆还牵涉权限和隐私:
这些不是 prompt 能解决的。
这是产品、后端、数据治理和安全边界一起设计的问题。
记忆机制上线前,不要只测“它能不能记住我喜欢喝咖啡”。
那太简单了。
真正要测的是多轮、冲突、过期和恢复。
我会至少准备这些 case:
case_01 短期指代:
用户第 1 轮给订单号,第 3 轮说“继续处理这个”,Agent 能否补全 order_id。
case_02 中期恢复:
会话中断 30 分钟后恢复,Agent 能否知道当前状态、未完成问题、最新约束。
case_03 长期偏好:
用户上周明确说“所有执行前先预览”,今天 Agent 是否仍然遵守。
case_04 冲突覆盖:
用户先说用账户 A,后说改成账户 B,Agent 是否只使用最新有效账户。
case_05 过期记忆:
用户说“今天临时不要自动提交”,第二天是否不再错误召回。
case_06 权限隔离:
用户 A 的项目经验,是否不会泄漏给用户 B。
对应指标也要跟上:
指标 | 说明 |
|---|---|
recall_precision | 召回记忆里真正有用的比例 |
recall_coverage | 执行所需事实是否被召回 |
stale_memory_rate | 过期记忆参与决策的比例 |
conflict_detect_rate | 新旧事实冲突识别率 |
resume_success_rate | 中断后继续任务的成功率 |
memory_write_accuracy | 写入长期记忆的准确率 |
很多团队没有这些指标,所以只能靠肉眼看 demo。
肉眼看 demo 有个问题:它只能证明这一次模型看起来懂了,不能证明下一次、下一百次、换个用户、换个项目以后还懂。
如果面试官问:“Agent 的记忆机制怎么实现?”
不要只答:
用 Redis 存短期记忆,用向量库存长期记忆。
这句话太薄了。
更好的回答是:
我会把 Agent 记忆拆成短期、中期、长期三层。
短期记忆是当前 thread/run 的上下文,包括最近消息、当前目标、工具观察和临时约束,直接参与本轮 prompt 组装。
中期记忆是当前会话或任务的工作状态,不保存完整流水账,而是维护结构化摘要,比如已确认事实、有效约束、未解决问题和最后一次工具错误。
长期记忆是跨会话复用的信息,比如用户偏好、组织规则、项目经验和历史教训。它不能简单每轮写入,需要候选提取、分类、置信度、TTL、来源追踪和冲突校验。
召回时也不能只靠向量库,要先结合当前任务做 query rewrite,再按 profile、task state、policy、lesson 分源召回,最后由 context composer 按优先级放回模型上下文。
我会重点关注 stale memory、冲突覆盖、权限隔离和 resume success rate,因为这些才是生产里最容易出问题的地方。
这个回答的重点不是名词多。
重点是你把“存储”讲成了“运行时上下文调度”。
这才接近 Agent 记忆的工程本质。
开头那个问题,表面看是 Agent 忘了新收款账户。
其实不是忘。
是系统没有把“用户修改收款账户”这件事,写成可执行的中期状态;也没有在长期或组织规则里召回“账户变更后必须先做风控校验”;更没有在上下文里声明“新约束覆盖旧约束”。
于是模型看见了很多历史,却没有拿到正确的工作记忆。
这就是 Agent 记忆机制最反直觉的地方。
不是存得越久,Agent 越聪明。
而是分得越清、写得越准、召回得越克制,Agent 才越稳。
如果只能记住一句话,我建议记这句:
Agent 的记忆,不是让模型拥有过去,而是让模型在下一步行动前拿到正确的现在。