04 · 金鱼脑的真相:模型为什么记不住你说过的话
开头的现象
小林花了三天时间,终于让”统一插口”跑通了。他兴冲冲地跟模型聊了一轮:第一幕:小林试了”多给几条消息”,发现了一个开关
小林的第一反应是去查文档。他翻到一行关键说明:model.invoke() 可以接受一个消息列表,不只是一句话。他隐隐觉得,答案可能藏在这。
他改成了这样:
想验证这个”开关”吗?跑 04_ai_memory.py,看三轮输出:第 1 轮自我介绍、第 2 轮带历史(答对)、第 3 轮不带历史(失忆)。三行代码之差,就是”金鱼脑”和”记事本”之差。
第二幕:小林画出了对话的全貌——消息列表在”滚雪球”
小林把整个对话过程画在纸上,他称之为”滚雪球图”:SystemMessage(系统消息)——角色的”人设”,永远在列表最前面HumanMessage(人类消息)——你(用户)说的话AIMessage(AI 消息)——模型的回答,每一轮都要追加回历史,这就是”记忆”的全部秘密
第三幕:小林发现 AIMessage 里藏着”额外信息”——该不该存
小林用show_full() 打印了模型返回的 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 剪枝。
复现信号:什么时候你会想起这一章
- 模型”忘记”了你说过的话——不是它坏了,是你没把历史传回去。检查你的 invoke 是不是只传了一句话。
- 你想”记住用户”——不要找模型要记忆,去维护你的消息列表。
- 历史越传越长、越来越慢——是时候剪枝了(
trim_messages或后面的 checkpointer)。 - 你想”净化”AIMessage 再存历史——停!直接 append 原对象,发送时协议层会自动剥离,你手动净化只会丢掉 tool_calls。