ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

记忆机制:对话内与跨对话的记忆

记忆机制:对话内与跨对话的记忆

很多人对大模型记忆有一个根深蒂固的误解:以为对话多了,模型就"认识"你了,就"记得"你上周说过什么。这个错觉很危险,因为它把工程的归工程的、模型的归模型的,全搅在了一起。今天我把这件事讲清楚:上下文窗口本身是无状态的,所谓的"记忆"从来不是模型的本事,而是一套被你显式管理的状态——存起来,再注入回去。理解了这一点,你才不会在设计对话系统时在错误的方向上越走越远。

一、先拆穿一个幻觉:模型根本不"记得"任何事

先把话说绝一点:上下文窗口是无状态的。你每调一次 Chat Completions 接口,传过去的是一份完整的 messages,模型基于这份输入算出下一个 token,返回回复,然后这次计算就结束了。没有后台线程在默默记笔记,没有"上次聊到哪了"这种东西。下一次调用,如果你不主动把历史塞回去,模型对上一轮一无所知。

这跟传统软件里"用户登录态存在 session 里"是两码事。模型的"记忆"不是它长出来的能力,而是你替它维护、再喂给它的外部状态。区别在于:session 是框架帮你管的,而对话记忆,从消息列表到摘要到长期事实库,全得你自己设计。

  • 别神化模型模型不会"积累经验",每次请求都是一张白纸,白纸上有没有字,取决于你画了什么。
  • 别混淆层次上下文工程归你,梯度更新归训练,两者不互通。推理时的记忆和训练时的权重无关。
  • 别假设连续性多轮对话之所以看起来连贯,是因为应用层每轮都把旧消息重新拼进请求,不是模型在"接着说"。

所以记忆机制的第一个本质命题就是:记忆是被显式管理的状态,不是模型的隐式能力。下面两类记忆,区别只在于这份状态存在哪里、活多久。

二、thread-in:单次对话内,靠一张不断变长的 message 列表

thread-in 指的是同一会话内部的记忆。它最简单,也最容易理解:你维护一个 list,每轮把用户消息 append 进去,把模型回复也 append 进去,下一轮连着整张列表一起发。

fromopenaiimportOpenAI client=OpenAI()messages=[{"role":"system","content":"你是一个耐心的订单助手"}]defchat(user_input:str)->str:messages.append({"role":"user","content":user_input})resp=client.chat.completions.create(model="gpt-4o",messages=messages)reply=resp.choices[0].message.content messages.append({"role":"assistant","content":reply})returnreply

这段代码的全部"记忆"就在那个messages变量里。它活在进程内存中,对话连续是因为列表连续。

thread-in 的好处是零成本、零外部依赖,原汁原味。坏处也同样直白:它是易失的。服务重启、会话超时、用户退出,这个 list 说没就没。而且它没法跨会话——你周一和用户的对话,周四再开一个 session,内存早清空了。

  • 存活范围仅限当前连接 / 当前 session,关掉就归零。
  • 保真度完整保留原文,不丢一字,适合需要精确回溯的短对话。
  • 天花板列表线性增长,迟早撞上上下文窗口上限,逼你做裁剪或摘要。

当你需要"这次关掉,下次还能接上",thread-in 就不够了,得把状态搬出内存。这就是 thread-between。

三、thread-between:跨对话,必须把状态搬出进程内存

thread-between 解决的是会话与会话之间的记忆。核心动作只有一个词:持久化。会话结束时,把有价值的事实或摘要写进外部存储(关系库、KV、文件、向量库都行);下次会话启动时,再把它读回来,注入到 system 提示或上下文开头。

关键在于,两次会话彼此不直接共享任何内存。它们只通过中间那个持久化存储间接连接。中间的进程可能重启过十次,内存里的临时状态全死了,但只要落库的那份事实还在,对话就能接上。

最常见的落地形态是用户事实库:从对话里抽取"用户偏好、背景、已确认信息",按 user_id 存起来。

importjson# 会话结束:抽取事实并落库defpersist_facts(user_id:str,facts:list[str]):# 这里用文件举例,生产用数据库 / KVwithopen(f"facts/{user_id}.json","w")asf:json.dump(facts,f,ensure_ascii=False)# 会话启动:加载事实并注入defload_system_with_facts(user_id:str)->str:try:withopen(f"facts/{user_id}.json")asf:facts=json.load(f)exceptFileNotFoundError:facts=[]base="你是一个了解用户背景的助手。"iffacts:base+="已知用户事实:\n"+"\n".join(f"-{x}"forxinfacts)returnbase
  • 跨会话存活状态在外部存储,跟进程生命周期脱钩,重启无碍。
  • 按需加载不是把全部历史灌回来,而是加载精炼过的事实,避免污染当前任务。
  • 隔离与权限跨会话就意味着多用户,必须按 user_id 隔离,否则 A 的记忆跑到 B 的对话里就是事故。

但光落库还不够聪明。如果每次都把全部历史原样读回来,你迟早又撞上那个窗口上限。于是需要第三招:压缩。

四、summary memory:用压缩换窗口,但别迷信压缩

summary memory 的思路是:超出窗口的旧消息,不再原样保留,而是交给模型压成一段摘要。近期消息保留原文,远期消息变成一段话,两者一起注入。这样既保留了"发生过什么"的脉络,又把 token 用量压了下来。

defsummarize(old_messages:list[dict])->str:convo="\n".join(f"{m['role']}:{m['content']}"forminold_messages)prompt=[{"role":"system","content":"把下面这段对话压缩成不超过 200 字的摘要,保留决策、偏好和待办"},{"role":"user","content":convo}]r=client.chat.completions.create(model="gpt-4o-mini",messages=prompt)returnr.choices[0].message.contentdeftrim(messages:list[dict],keep_recent:int=6)->list[dict]:iflen(messages)<=keep_recent:returnmessages old,recent=messages[:-keep_recent],messages[-keep_recent:]summary=summarize(old)return[{"role":"system","content":f"历史摘要:{summary}"}]+recent

trim就是个简单的滑动窗口加摘要:保留最近的若干轮,把更早的压缩掉。本质上你是在用有损压缩换上下文空间。

这里有个必须说清楚的交易:摘要会丢细节。模型压缩时,它认为不重要的东西就真的没了。如果用户两周前说过一句"我对青霉素过敏",而那句话恰好落在被摘要掉的区间,压缩后变成"用户聊过健康问题",危险就埋下了。

  • 适合长程闲聊、开放式助手,脉络比逐字准确更重要。
  • 不适合强约束场景(医疗、金融、合同),摘要丢一字就是事故。
  • 补丁摘要 + 长期事实库双轨:摘要管"聊过什么脉络",事实库管"硬约束",后者不参与压缩。

讲到这,三类正经手段(消息列表、摘要、长期记忆)都齐了。下面专门点几个反例,这些都是生产环境里真实踩过的坑。

五、反模式:把历史无脑塞进 context,以及以为模型"记得"

先说最蠢但最常见的反例:把所有历史原样无脑塞进 context。有人图省事,messages 只增不减,跑到几万 token 还在 append。后果是确定的:

  • token 爆炸直接撞上下文上限,要么报错,要么被截断到一半。
  • 注意力稀释真相关的当前问题被淹没在几十条无关历史里,模型答非所问。
  • 成本失控每轮都把几万 token 重复发一遍,账单和延迟一起起飞。
# 反例:永不裁剪的 message 列表messages=[{"role":"system","content":"你是助手"}]defbad_chat(user_input):messages.append({"role":"user","content":user_input})# 危险:messages 无限增长,从不摘要、从不检索resp=client.chat.completions.create(model="gpt-4o",messages=messages)messages.append({"role":"assistant","content":resp.choices[0].message.content})returnresp.choices[0].message.content

第二个反例更隐蔽:以为模型"记得"上次聊过什么。产品经理拍脑袋说"让 AI 记住用户",开发就以为调一次接口模型就自动有了记忆。实际上每次都是全新请求,你不注入,它就什么都不知道。把"记忆"当模型的隐式能力,是这类 bug 的根源。

正确的心智模型是:像管理缓存一样管理记忆。设窗口、定期摘要、按需检索,而不是无脑堆积。具体选型时按下面的优先级走:

  • thread-in 够用就别上持久化单轮或短会话,维护 message 列表最简单。
  • 要跨会话就落库事实,而非落库全文存精炼过的事实和摘要,加载时按需注入。
  • 窗口超了就 trim + 摘要,强约束另存事实库别让摘要替你背医疗、合同这类硬责任的锅。
  • 长期记忆用向量检索,而不是全量回灌用户量大了,按相似度召回相关事实,上下文才可控。

记忆机制没有银弹,但有一条铁律:记忆是被显式管理的状态,不是模型的隐式能力。你把它当状态来设计——存在哪、活多久、怎么注入、何时丢弃——系统就稳;你把它当魔法来期待,系统就一定会翻车。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进