首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >短期、中期、长期:一套生产级 Agent 记忆设计

短期、中期、长期:一套生产级 Agent 记忆设计

作者头像
java金融
发布2026-06-24 14:15:14
发布2026-06-24 14:15:14
1460
举报
文章被收录于专栏:java金融java金融

事情是这样的。

一个客服 Agent demo,第一轮表现很好。

用户问:“我的退款为什么一直失败?”

Agent 查订单、查售后单、查支付回执,回答得很完整。

第二轮用户说:“那帮我重新发起一下。”

Agent 也知道“重新发起”指的是刚才那笔退款,开始调用补偿工具。

第三轮用户补了一句:“这次别走原来的银行卡,我换了收款账户。”

这时候问题来了。

如果系统只是把全部聊天记录 append 到 prompt 里,模型大概率能“看见”这话。但到了第十轮、第十五轮,用户又改过一次账户、客服又补了一条限制、工具又返回过一次失败原因,整个上下文开始变得又长又脏。

你会看到一种很典型的线上现象:

代码语言:javascript
复制
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 项目的第一版记忆实现都长这样:

代码语言:javascript
复制
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

短期记忆不要设计得太玄。

最常见的实现就是一个线程级状态:

代码语言:javascript
复制
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 看起来没事,一旦工具结果变多,模型就开始拿旧错误当新事实。

短期记忆的工程边界也很清楚:

  • 它快,因为直接进 prompt
  • 它贵,因为消耗上下文 token
  • 它不稳定,因为窗口满了就要裁剪
  • 它不适合保存跨会话事实

所以短期记忆只做一件事:

保证 Agent 当前这一步不要忘事。

中期记忆:把一坨聊天记录压成可执行状态

中期记忆解决的是“这个会话聊到哪了”。

它比短期记忆活得久一点,通常覆盖一个会话、一个工单、一次项目任务、一次代码修改流程。

短期记忆像工作台。

中期记忆像白板。

你不能把所有工具日志都堆在白板上,但要把当前结论、未完成事项、关键决策写清楚。

比如刚才那个退款 Agent,中期记忆不应该是:

代码语言:javascript
复制
{
  "summary": "用户咨询退款失败问题,后来想重新发起,期间提到更换收款账户。"
}

这句话看起来像摘要,其实执行价值很低。

更好的中期记忆应该是结构化任务状态:

代码语言:javascript
复制
{
  "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 能继续工作的记忆。

它不是文学摘要,而是运行状态。

中期记忆的更新方式一般有两种:

一种是每轮对话后滚动总结:

代码语言:javascript
复制
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)

另一种是用确定性逻辑更新字段:

代码语言:javascript
复制
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 和确定性校验。

否则你会遇到很烦的问题:

  • 摘要把“不要自动提交”漏掉了
  • 摘要把旧账户当成新账户
  • 摘要生成了一句看似合理但没有来源的事实

中期记忆的关键指标不是“摘要多像人写的”。

它的关键指标是:

代码语言:javascript
复制
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 里通常放用户、组织、应用等隔离维度。

工程上可以这样设计:

代码语言:javascript
复制
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"

长期记忆的写入,千万不要每轮都自动写。

一个更稳的写入流水线应该是这样:

!

可以用一段伪代码表达:

代码语言:javascript
复制
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 表格”才可能是偏好。

“这个接口失败时不要重试,必须升级人工”更像组织级操作教训。

长期记忆还必须有元数据:

  • 来源:这条记忆从哪句话、哪个工具结果来
  • 置信度:是用户明确表达,还是模型推断
  • 生效范围:用户级、组织级、项目级,还是任务级
  • 过期时间:偏好可能长期有效,临时约束必须过期
  • 冲突关系:新事实是否覆盖旧事实

没有这些字段,长期记忆很快会变成“永久污染源”。

模型一旦召回一条过期偏好,就会一本正经地执行。

而且越是智能,越难发现它错在哪里。

召回:真正难的是把长期记忆放回正确位置

写入只是前半场。

后半场是召回。

一个常见错误是:

代码语言:javascript
复制
memories = vector_store.search(user_message, top_k=8)
prompt = build_prompt(history, memories, user_message)

这依然太粗。

因为用户当前这句话未必能表达真实任务。

用户说“继续处理刚才那个”,向量检索可能召回一堆“刚才”的相似对话,但漏掉真正关键的 order_id、新账户约束、人工审批策略。

更合理的召回链路应该分几步:

第一步,先做 query rewrite。

把省略、指代、上下文里的任务目标补出来:

代码语言:javascript
复制
用户原话:
继续处理刚才那个

改写后的检索问题:
继续处理订单 R202606180013 的退款补偿任务。
需要考虑用户已要求使用新收款账户,且上一次 refund_retry 工具返回 ACCOUNT_NOT_ALLOWED。

第二步,分源召回。

不要只搜一个向量库:

代码语言:javascript
复制
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:

代码语言:javascript
复制
<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

记住错事、召回噪声、隐私风险

一个简化版架构可以长这样:

代码语言:javascript
复制
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

写入链路则反过来:

代码语言:javascript
复制
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。

第四个坑:没有遗忘机制。

长期记忆如果不能过期、不能删除、不能被用户纠正,它就不是智能资产,而是风险资产。

尤其在企业场景里,记忆还牵涉权限和隐私:

  • 离职员工的偏好是否还能被召回
  • A 项目的经验能不能给 B 项目用
  • 用户撤回的个人信息是否真的删除
  • 低置信度推断能不能进入长期档案

这些不是 prompt 能解决的。

这是产品、后端、数据治理和安全边界一起设计的问题。

生产上怎么验收 Agent 记忆

记忆机制上线前,不要只测“它能不能记住我喜欢喝咖啡”。

那太简单了。

真正要测的是多轮、冲突、过期和恢复。

我会至少准备这些 case:

代码语言:javascript
复制
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 的记忆机制怎么实现?”

不要只答:

代码语言:javascript
复制
用 Redis 存短期记忆,用向量库存长期记忆。

这句话太薄了。

更好的回答是:

代码语言:javascript
复制
我会把 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 才越稳。

如果只能记住一句话,我建议记这句:

Agent 的记忆,不是让模型拥有过去,而是让模型在下一步行动前拿到正确的现在。

参考资料

  • OpenAI Agents SDK:Sessions 会自动维护同一 session 下的对话历史,用于多轮上下文。https://openai.github.io/openai-agents-python/sessions/
  • OpenAI Agents SDK:Session memory API 说明。https://openai.github.io/openai-agents-python/ref/memory/
  • LangChain / LangGraph Memory:短期记忆与长期记忆、namespace/store 等设计。https://docs.langchain.com/oss/python/concepts/memory
  • LangGraph Long-term memory:长期记忆以 JSON document 存储,并按 namespace/key 组织。https://docs.langchain.com/oss/python/langchain/long-term-memory
  • MemGPT: Towards LLMs as Operating Systems,用操作系统分层内存类比 LLM 上下文管理。https://arxiv.org/abs/2310.08560
  • Generative Agents: Interactive Simulacra of Human Behavior,提出 memory stream、reflection、planning 等 Agent 记忆与行为架构。https://arxiv.org/abs/2304.03442
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java金融 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先把一句话说透:记忆不是数据库,是上下文调度系统
  • 短期记忆:别急着向量库,先管好当前上下文
  • 中期记忆:把一坨聊天记录压成可执行状态
  • 长期记忆:不是“永久保存”,而是可召回、可纠错、可删除
  • 召回:真正难的是把长期记忆放回正确位置
  • 一套能落地的三层实现
  • 最容易踩的四个坑
  • 生产上怎么验收 Agent 记忆
  • 面试或方案评审时怎么讲
  • 回到开头那个退款 Agent
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档