你有没有过这种经历:用Agent写方案,写到一半要开会,你关掉窗口就走。三天后你再打开,Agent礼貌地打招呼:“你好,请问需要我做什么?” 你盯着屏幕,心想:“我们不是已经在做那个项目了吗?周报呢?数据呢?” 然后你不得不把三天前说过的需求又重新讲一遍。那一刻,你忍不住怀疑:Agent的记忆只有七秒吗?

这个问题表面上是“Agent没记住对话”,实际上是会话恢复设计的缺失。恢复并不只是把聊天记录存一下那么简单,它牵扯到LLM的无状态本质、上下文窗口限制,以及“三天”带来的时间语义变化。今天我就把自己在这个问题上踩过的坑和想明白的事,一次性讲清楚。
为什么Agent会失忆?
先搞清楚一个基础事实:大模型本身没有记忆。你发给它的每一条消息,都像是第一次见面。它之所以能跟你聊下去,是因为你的客户端在每次请求里,默默把之前所有的消息都附上了。ChatGPT多轮对话那么流畅,不是因为它记得,而是因为每一轮都把整段聊天记录重新发了一遍。这意味着,所谓的“恢复会话”,本质上就是“重新构造一段足够完整的聊天历史,再次喂给模型。
最简单的方案,为什么撑不过三天
那最简单的方法就是把历史全部存起来,下次原样发回去。我最早确实这么干过。那时候我在做一个客服Agent,用户隔天回来继续问,我们就从数据库里取出一整天的聊天记录拼成上下文,直接调用模型。效果出奇地好,直到用户连续聊了一个小时,消息数量超过一百条,模型直接报错:请求太大了。
后来我改成只保留最近的二十条,结果用户回来说一句“我之前提到的那个报错”,Agent一脸懵,因为报错细节在第二十三条。
这就是会话恢复的第一个死结:全量重放撑不到三天,滑动窗口又会失忆。你需要在“记住”和“放得下”之间找平衡。
三个方案,三个代价
目前工程上流行的方案大概有三种,我把它们放在一张表里:
| 方案 | 核心思路 | 恢复效果 | 代价 |
|---|---|---|---|
| 滑动窗口 | 只保留最近N条消息 | 短期连续性好,但丢掉细节 | 实现简单,一个队列搞定 |
| 摘要压缩 | 把旧对话定期加工成摘要保存 | 能找回大方向,但细节失真 | 需要额外调用LLM生成摘要 |
| 外部记忆+状态跟踪 | 把任务进度、用户偏好、关键实体存成结构化数据 | 最可靠,能精确恢复关键信息 | 需要设计存储和更新逻辑 |
滑动窗口不用多说,最直接的方案。摘要压缩是目前很多框架默认的做法,比如LangChain里的ConversationSummaryBufferMemory,就是一边滚动窗口一边把旧的对话总结成摘要,恢复时先看摘要再看最近几轮。但摘要有个致命问题:它是在“压缩”,而压缩必然会丢信息。用户随口提的一句“价格敏感,别推荐太贵的”,可能被摘要省略成“注意预算”,甚至干脆没写进去。等用户三天后回来,Agent推荐了一个旗舰套餐,然后就没有然后了。
真正让我想通的一刻,是看到外部记忆方案。所谓外部记忆,就是把对话历史当成“原始日志”,再额外维护一张“状态卡”。状态卡里记的不是说了什么,而是“任务进展到哪一步了”“上次确认了哪些决定”“还有哪些未决问题”。恢复会话时,不是把三天前的话一字不差地念给模型听,而是把这张状态卡交给模型,让它快速进入角色。这就像你休了三天假回到公司,不会把过去一个月每封邮件都重读一遍,而是打开项目进度表看一眼,就知道接哪儿了。状态卡就是你Agent的项目进度表。
恢复时的提示词,顺序比内容更重要
有了状态卡,是不是直接把它丢给模型就行了?不是。恢复时的提示词构造,顺序非常关键。我自己踩过坑:一开始我把最近消息放在最前面,结果模型完全被“刚才聊到哪里”带偏,连用户最初要什么目标都忘了。后来我总结出下面这个固定顺序:
- 加载系统提示词:定义Agent的角色和任务边界。
- 加载用户长期记忆:比如偏好、历史画像,来自独立的用户档案。
- 加载会话摘要:把之前对话总结出来的要点放在这里,越精炼越好。
- 加载结构化状态:当前任务的进度、待办、关键参数。
- 加载最近消息:最后几条原始对话,保证衔接的语感自然。
这个顺序背后的逻辑是:先给模型一个“你是谁”的框架,再告诉它“用户是谁”,然后再说“你们之前聊了什么”,最后才是“刚才聊到哪儿了”。如果顺序颠倒,模型很可能被最近的细枝末节带偏,忘记了更重要的长期目标。
状态卡长什么样
状态卡不一定要很复杂,但必须结构化。我项目里用的是一份JSON,每次对话结束时让LLM根据新消息更新它:
{"task": "整理季度报告", "progress": "数据部分已完成,待写结论", "decisions": ["预算定在1万以内", "用柱状图展示增长"], "open_questions": ["是否加入竞品对比?"]}
你可能会问,把原始对话直接存下来不好吗?当然也存,但那是“日志”,用来留底和审计。恢复时真正喂给模型的核心是这张状态卡。原始对话太长,且大部分是噪音;状态卡短小精悍,每一条都直指任务关键。
为什么不能只存状态卡,不存原始对话?
因为状态卡是压缩后的结果,不可能包含所有细节。比如用户当初随口说的一句“我喜欢北欧风格”,可能在状态卡里被记为“喜欢简约”。如果之后用户强烈反对,你需要能回溯原始记录来澄清。所以原始对话作为日志必须保留,只是不参与每次恢复时的上下文构造。
三天不只是长度问题,还是时间语义问题
但即便你把顺序调对了,还有一个隐藏的坑,就是“三天”本身。时间不是中性的。用户三天前说“这周给我初稿”,你恢复会话时,如果原样把这个需求丢给模型,模型不知道今天是周五还是周一,很可能认为“这周”还很充裕。你需要对时间相关的内容做归一化处理。我在项目里加了一个小步骤:恢复时扫描摘要和状态卡,把所有相对时间(比如“明天”“下周”“三天后”)全部换算成绝对日期,或者标注上“消息发出时间”和“当前时间”。这样模型才不会犯低级错误。
别急着恢复,先确认用户还想不想继续
还有一个问题经常被忽略:用户回来之后,到底是想继续原任务,还是已经改了主意?如果Agent一上来就自作主张地继续之前的话题,可能会惹恼用户。所以恢复会话的第一步不是恢复任务,而是确认意图。你可以这样设计:Agent先问一句“上次我们正在整理季度报告,已经完成了数据部分,需要继续吗?” 这句话既证明了它还记得,又给了用户重新选择的机会。这比闷头继续讲好得多。
会话恢复的本质是语义压缩
我总结一下,如果你要设计一个能支撑“三天后无缝接上”的Agent,你的核心工作不是存储,而是“语义压缩”。你需要把长长的对话历史,提炼成状态卡上的几个要点。这件事本身也需要LLM来完成,而LLM在做摘要时,可能会丢弃你认为重要的信息。因此,一定要有机制让用户随时能纠正或补充,而不是完全相信摘要。
这个设计的边界也很明显。如果用户消失了三个月而不是三天,或者任务本身已经过期(比如用户问的是“明天的天气”),那再完美的状态卡也无法让这段对话有意义。这时候你需要有“会话过期”策略,比如主动询问是否开启新任务。对于时间敏感的任务,恢复后第一件事应该是检查截止时间。
最后说点实在的。这个设计没有银弹,摘要、窗口、状态卡各司其职,经常要组合使用。我现在的做法是:短期直接用滑动窗口,中期用摘要,长期用结构化状态和用户画像。三级分层,每一级都有不同的更新策略。用户三天后回来,我只需要把三层内容按优先级拼接起来,再做一个时间归一化处理,就能让Agent像从未离开过一样继续工作。
Agent的记忆终究不是人的记忆。它不是一个连续的意识流,而是一个可以被反复“唤醒”的档案夹。你要做的,是让这个档案夹的内容组织得足够清晰,让模型每次打开,都能在三秒钟内重新回到角色。这就是会话恢复设计的本质。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/485.html