Skip to main content

04 · 金鱼脑的真相:模型为什么记不住你说过的话

开头的现象

小林花了三天时间,终于让”统一插口”跑通了。他兴冲冲地跟模型聊了一轮:
然后又问了一句:
小林愣住了。他对着屏幕喊:“我不是三秒钟前刚告诉过你吗?!” 模型当然没理他。小林盯着那两行输出,心里涌上一种被背叛的感觉——“这模型是不是有健忘症?还是它在装傻?“

第一幕:小林试了”多给几条消息”,发现了一个开关

小林的第一反应是去查文档。他翻到一行关键说明:model.invoke() 可以接受一个消息列表,不只是一句话。他隐隐觉得,答案可能藏在这。 他改成了这样:
记住了! 小林盯着第二行输出,脑子转得飞快:刚才单独调用的时候它失忆,现在把历史列表传回去它就记得——所以模型的”记忆”根本不藏在模型里,而是藏在”你每次传回去的消息列表”里! 他赶紧验证反向实验:同一句话,不带历史直接问:
小林把两次输出并排放在屏幕上,像破了一桩案子一样兴奋。同一个模型,同一个问题,唯一的区别就是”消息列表里有没有历史”——这就是”模型没有记忆”的全部真相:模型是一个无状态的函数,你给它什么输入,它就基于什么回答。所谓”记忆”,就是你把历史滚动着传回去的”滚雪球”过程。
想验证这个”开关”吗?跑 04_ai_memory.py,看三轮输出:第 1 轮自我介绍、第 2 轮带历史(答对)、第 3 轮不带历史(失忆)。三行代码之差,就是”金鱼脑”和”记事本”之差。

第二幕:小林画出了对话的全貌——消息列表在”滚雪球”

小林把整个对话过程画在纸上,他称之为”滚雪球图”:
每一轮,就是把用户的话追加到列表末尾,调模型,再把模型的回答也追加进去——下一个问题来时,整段历史都被装进请求,模型”看到”了全部过去。 小林盯着这张图,忽然明白了三个名字的来历:
  • SystemMessage(系统消息)——角色的”人设”,永远在列表最前面
  • HumanMessage(人类消息)——你(用户)说的话
  • AIMessage(AI 消息)——模型的回答,每一轮都要追加回历史,这就是”记忆”的全部秘密
他想起网上那句流传的话”模型有上下文窗口”——现在他懂了:上下文窗口不是”记忆”,是一张可以反复写的纸。每次对话,你把以前的字都抄到新纸上再给模型看。

第三幕:小林发现 AIMessage 里藏着”额外信息”——该不该存

小林用 show_full() 打印了模型返回的 AIMessage 完整结构,发现里面除了文本,还塞着一堆东西:
他第一反应是:“历史里不能有这些垃圾信息!我得把它们删了再存!“——于是他想写”净化代码”:提取 content,造一个新的干净 AIMessage。 但他刚要动手,忽然想起一句话(那是他读源码时看到的):发送前,协议适配层 _format_messages 会自动把额外字段剥掉,只发 role + content。 他验证了一遍:把一个塞满 response_metadata/usage_metadata 的 AIMessage 丢给 _format_messages,输出只剩 {"role": "assistant", "content": "你好小林!"}——额外信息在发送时自动被挡在门外了。 小林拍了一下桌子:“那我净化它干嘛?白扔!而且——“他忽然想到一个更严重的问题:“如果模型这轮调了工具,tool_calls 在 content 里,我提取文本重建,tool_calls 就丢了!agent 直接断!” 他彻底放弃了”净化”的想法,定下了一条铁律:
想验证”为什么要直接 append 吗?“跑一遍:把模型返回的 AIMessage 提取成 AIMessage(content=resp.content) 再存历史,第二轮问”你刚才调了什么工具”——模型一脸茫然。直接 append 原对象,它记得清清楚楚。

🔧 技术细节:这一章涉及的关键类和签名

BaseChatMessageHistory——历史存储抽象(langchain_core.chat_history)
InMemoryChatMessageHistory——内存实现(最简单)
③ 多轮对话的完整数据流(含签名):
想验证吗?h = InMemoryChatMessageHistory(); h.add_user_message("hi"); print(h.messages)——[HumanMessage(content='hi')]

第四幕:边界——历史会无限膨胀

小林用”滚雪球”聊了五十轮,突然发现一个问题:请求越来越慢,token 花得越来越多。 他把整段历史都传回去了——可前二十轮的内容,早就没用了。 他查了查,发现 LangChain 有现成的”剪枝”工具:trim_messages()——把最旧的消息删掉,只留最近的几轮。还有更省事的:等学到第 6 章,他会用 LangGraph 的 checkpointer(那是后话)。 反例来了:不是所有对话都需要全量历史。一个客服机器人聊 100 轮,你不可能把 100 轮全塞回去——上下文窗口放不下,费用也爆炸。这时候”滚雪球”就要变成”留尾巴”:只保留最近几轮 + 一个压缩摘要。
想验证边界吗?把 04_ai_memory.py 的 history 一直 append 到 50 轮,观察每次 invoke 的 input_tokens 一路飙升——然后试试 trim_messages 只留最近 5 轮,token 立刻降下来。

结论(小林用三天换来的)

模型没有记忆,记忆在你的消息列表里。 所谓”多轮对话”,就是把 SystemMessage(人设)+ 历史消息 + HumanMessage(当前问题)滚雪球式地传回给模型;每一轮都要把模型返回的 AIMessage 直接 append 回历史(不要提取重建,会丢 tool_calls)。这个机制简单到让人怀疑,但它就是全部真相——代价是历史会无限膨胀,得靠 trim_messages 或 checkpointer 剪枝。

复现信号:什么时候你会想起这一章

  1. 模型”忘记”了你说过的话——不是它坏了,是你没把历史传回去。检查你的 invoke 是不是只传了一句话。
  2. 你想”记住用户”——不要找模型要记忆,去维护你的消息列表。
  3. 历史越传越长、越来越慢——是时候剪枝了(trim_messages 或后面的 checkpointer)。
  4. 你想”净化”AIMessage 再存历史——停!直接 append 原对象,发送时协议层会自动剥离,你手动净化只会丢掉 tool_calls。
小林学会了”怎么让模型记住”之后,下一个让他抓狂的问题是:为什么模型回答要憋好几秒才一次性蹦出来?能不能像打字机一样边想边出?——翻到第 5 篇:打字机与结构化