> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

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

> 模型没有记忆，记忆在你的消息列表里：滚雪球式回传历史的真相。

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

## 开头的现象

小林花了三天时间，终于让"统一插口"跑通了。他兴冲冲地跟模型聊了一轮：

```python theme={null}
model.invoke("我叫小林，是后端工程师")
# → "你好小林！很高兴认识你！"
```

然后又问了一句：

```python theme={null}
model.invoke("我叫什么名字？")
# → "我不知道您的名字……我们好像是第一次对话。"
```

小林愣住了。他对着屏幕喊：**"我不是三秒钟前刚告诉过你吗？！"**

模型当然没理他。小林盯着那两行输出，心里涌上一种被背叛的感觉——**"这模型是不是有健忘症？还是它在装傻？"**

## 第一幕：小林试了"多给几条消息"，发现了一个开关

小林的第一反应是去查文档。他翻到一行关键说明：`model.invoke()` 可以接受一个**消息列表**，不只是一句话。他隐隐觉得，答案可能藏在这。

他改成了这样：

```python theme={null}
from langchain_core.messages import HumanMessage, SystemMessage

# 把"人设"和"对话"都塞进列表里
history = [
    SystemMessage(content="你是小林公司的客服助理。"),   # 系统消息：设定角色
    HumanMessage(content="我叫小林，是后端工程师。"),     # 用户消息：自我介绍
]

r1 = model.invoke(history)
print(r1.content)   # "你好小林！有什么可以帮你？"

# 把上一轮的回复也塞回去
history.append(r1)

history.append(HumanMessage(content="我叫什么名字？"))
r2 = model.invoke(history)
print(r2.content)   # "小林，你是个后端工程师。"  ← 记住了！
```

**记住了！** 小林盯着第二行输出，脑子转得飞快：刚才单独调用的时候它失忆，现在把历史列表传回去它就记得——**所以模型的"记忆"根本不藏在模型里，而是藏在"你每次传回去的消息列表"里！**

他赶紧验证反向实验：同一句话，不带历史直接问：

```python theme={null}
model.invoke([HumanMessage(content="我叫什么名字？")])
# → "我不知道您的名字……"    ← 又是金鱼脑！
```

小林把两次输出并排放在屏幕上，像破了一桩案子一样兴奋。**同一个模型，同一个问题，唯一的区别就是"消息列表里有没有历史"**——这就是"模型没有记忆"的全部真相：**模型是一个无状态的函数，你给它什么输入，它就基于什么回答。所谓"记忆"，就是你把历史滚动着传回去的"滚雪球"过程。**

> 想验证这个"开关"吗？跑 `04_ai_memory.py`，看三轮输出：第 1 轮自我介绍、第 2 轮带历史（答对）、第 3 轮不带历史（失忆）。三行代码之差，就是"金鱼脑"和"记事本"之差。

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

小林把整个对话过程画在纸上，他称之为"滚雪球图"：

```text theme={null}
第 1 轮：  [system 人设] \+ [human 我叫小林]
                  │
                  ▼  model.invoke()
          [system] \+ [human] \+ [ai 你好小林！]
                  │
第 2 轮：  [system] \+ [human] \+ [ai] \+ [human 我叫什么名字？]
                  │
                  ▼  model.invoke()
          [system] \+ [human] \+ [ai] \+ [human] \+ [ai 小林，你是后端工程师]
                  │
第 3 轮：  [system] \+ [human] \+ [ai] \+ [human] \+ [ai] \+ [human ...] \+ [ai ...]
```

每一轮，就是把用户的话追加到列表末尾，调模型，再把模型的回答也追加进去——**下一个问题来时，整段历史都被装进请求，模型"看到"了全部过去。**

小林盯着这张图，忽然明白了三个名字的来历：

* `SystemMessage`（系统消息）——角色的"人设"，永远在列表最前面
* `HumanMessage`（人类消息）——你（用户）说的话
* `AIMessage`（AI 消息）——模型的回答，**每一轮都要追加回历史**，这就是"记忆"的全部秘密

他想起网上那句流传的话"模型有上下文窗口"——现在他懂了：**上下文窗口不是"记忆"，是一张可以反复写的纸。每次对话，你把以前的字都抄到新纸上再给模型看。**

## 第三幕：小林发现 AIMessage 里藏着"额外信息"——该不该存

小林用 `show_full()` 打印了模型返回的 AIMessage 完整结构，发现里面除了文本，还塞着一堆东西：

```json theme={null}
{
  "content": "你好小林！",
  "response_metadata": { "stop_reason": "end_turn", "usage": { "input_tokens": 27 } },
  "usage_metadata": { "input_tokens": 27, "output_tokens": 15, "total_tokens": 42 },
  "type": "ai",
  "id": "lc_run--abc",
  "tool_calls": []
}
```

他第一反应是："历史里不能有这些垃圾信息！我得把它们删了再存！"——于是他想写"净化代码"：提取 content，造一个新的干净 AIMessage。

但他刚要动手，忽然想起一句话（那是他读源码时看到的）：**发送前，协议适配层 `_format_messages` 会自动把额外字段剥掉，只发 role + content。**

他验证了一遍：把一个塞满 `response_metadata`/`usage_metadata` 的 AIMessage 丢给 `_format_messages`，输出只剩 `{"role": "assistant", "content": "你好小林！"}`——**额外信息在发送时自动被挡在门外了。**

小林拍了一下桌子："那我净化它干嘛？白扔！而且——"他忽然想到一个更严重的问题："如果模型这轮调了工具，`tool_calls` 在 content 里，我提取文本重建，tool\_calls 就丢了！agent 直接断！"

他彻底放弃了"净化"的想法，定下了一条铁律：

```python theme={null}
history.append(resp)   # 直接 append 模型返回的 AIMessage 原对象，一个字不改
```

> 想验证"为什么要直接 append 吗？"跑一遍：把模型返回的 AIMessage 提取成 `AIMessage(content=resp.content)` 再存历史，第二轮问"你刚才调了什么工具"——模型一脸茫然。直接 append 原对象，它记得清清楚楚。

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

**① `BaseChatMessageHistory`——历史存储抽象（langchain\_core.chat\_history）**

```python theme={null}
from langchain_core.chat_history import BaseChatMessageHistory

history.messages                    # list[BaseMessage]：所有历史消息
history.add_message(msg)            # 加一条消息
history.add_messages(msgs)          # 加一批消息
history.add_user_message(text)      # 快捷加用户消息（str 也行）
history.add_ai_message(text)        # 快捷加 AI 消息
history.clear()                     # 清空
```

**② `InMemoryChatMessageHistory`——内存实现（最简单）**

```python theme={null}
InMemoryChatMessageHistory(
    messages: list[BaseMessage] = [],   # 初始消息
)
# 用法：按 session_id 各存一份，互不干扰
store = {}
def get_history(session_id):
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]
```

**③ 多轮对话的完整数据流（含签名）：**

```python theme={null}
# 模板 → 消息列表 → 模型 → AIMessage → 追加历史
messages = prompt.invoke({"history": history, "question": q}).to_messages()
ai_msg = model.invoke(messages)           # AIMessage
history.append(HumanMessage(content=q))   # 用户消息进历史
history.append(ai_msg)                    # ⚠️ 直接 append 原对象，不要重建！
```

> 想验证吗？`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 篇：打字机与结构化](/doc/doc/narrative-course/05-流式与结构化)。
