> ## 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.

# 06 · 记忆与会话：模型的金鱼脑与你的药方

# 06 · 记忆与会话：模型的金鱼脑与你的药方

> **本片目标**：理解"模型无状态"的本质，学会三种让对话连贯的方式：① list 手搓（零依赖，看懂本质）；② `InMemoryChatMessageHistory`（官方，会话隔离）；③ 知道为什么不用弃用的 `RunnableWithMessageHistory`。为第 09\~10 篇"对话式 RAG"打底。
> **新增规定性：6**（会话状态：你要自己管理历史消息 + session 隔离）
> **数据字典**：BaseChatMessageHistory / InMemoryChatMessageHistory。
> **进程线程模型**：单会话串行；多会话靠"会话 id → 独立历史存储"隔离。
> **网络模型**：无新增（历史拼进消息列表一起发）。

***

## 1. 上集回顾

第 05 篇输入输出都齐了。但有个致命问题从第 01 篇就存在：

```python theme={null}
model.invoke("我叫小林")
# → "你好小林！"
model.invoke("我叫什么名字？")
# → "抱歉，我不知道你的名字。"   ← 金鱼脑！
```

**模型每次调用都是无状态的**——它不记得说过什么。对话 API 之所以能"记住"，是因为**每次调用都把完整历史放进消息列表**：

```
第 1 次: [system, "我叫小林"]
第 2 次: [system, "我叫小林", "你好小林！", "我叫什么名字？"]   ← 历史全带上了
```

**所以"记忆"不是模型的能力，是你的责任**——把历史消息存起来、每次拼进去。

***

## 2. 三种历史方案：从手搓到官方

核心问题只有一个：**历史消息存在哪、怎么取、怎么写回**。三种方案的区别就是这三件事的实现方式不同。

### 2.1 方案一：list 手搓（零依赖，理解本质）

**最原始的方案：一个 `list` 就够了。** 你手动 append 消息对象：

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

class ListHistory:
    """自己实现的历史：一个 list 就够。BaseChatMessageHistory 的三个接口手搓版。"""
    def __init__(self):
        self.messages = []                     # 就是普通 list
    def add_user_message(self, content):
        self.messages.append(HumanMessage(content=content))
    def add_ai_message(self, content):
        self.messages.append(AIMessage(content=content))

h = ListHistory()
h.add_user_message("我叫小林")
h.add_ai_message("你好小林！")
h.messages
# → [HumanMessage("我叫小林"), AIMessage("你好小林！")]
```

**为什么先讲它**：它证明"记忆"没有任何魔法——就是一个列表，你往里放消息。官方方案只是在它外面包了一层统一接口（方便以后换持久化实现时接口不变）。

### 2.2 方案二：`InMemoryChatMessageHistory`（官方，langchain\_core 内置，**不依赖 LangGraph**）

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

history = InMemoryChatMessageHistory()
history.add_user_message("我叫小林")
history.add_ai_message("你好小林！")
history.messages
# → [HumanMessage("我叫小林"), AIMessage("你好小林！")]
```

> **为什么它能用**：`InMemoryChatMessageHistory` 在 `langchain_core.chat_history` 模块，1.x 官方活跃维护（第 00 篇已实测导入成功）。它不碰 LangGraph、不碰弃用包——符合本系列原则。接口和上面手搓的 `ListHistory` 一模一样（`messages` / `add_user_message` / `add_ai_message`），所以你的对话函数**换存储不用改逻辑**。

### 2.3 方案三（弃用）：`RunnableWithMessageHistory` —— 为什么不用它

旧教程的经典写法：把"历史存储 + 模板槽位"**封装成链的一层**，链自己管历史。实测（本机 1.5.1）它在**实例化时**立刻抛出弃用警告：

```
[LangChainDeprecationWarning] RunnableWithMessageHistory is deprecated. Use LangGraph's built-in persistence instead.
```

**弃用理由指向 LangGraph**——它把记忆绑定到 LangGraph 生态。而本系列不用 LangGraph，所以也不用它。方案一/二已覆盖全部功能（存、取、拼进消息、写回），接口更透明、不依赖 LangGraph、无弃用风险。

### 2.4 三方案对比表

| 维度   | ① list 手搓                   | ② InMemoryChatMessageHistory  | ③ RunnableWithMessageHistory |
| ---- | --------------------------- | ----------------------------- | ---------------------------- |
| 状态   | ✅ 官方活跃维护                    | ✅ 官方活跃维护                      | ❌ **弃用**（实例化警告）              |
| 依赖   | 零（langchain\_core.messages） | langchain\_core.chat\_history | 弃用 + 导向 LangGraph            |
| 接口   | 自定（list 足够）                 | `BaseChatMessageHistory` 标准   | 封装进链（黑盒）                     |
| 换持久化 | 自己改                         | 换实现即可（接口不变）                   | 绑定 LangGraph 持久化             |
| 透明性  | 完全透明                        | 透明                            | 历史如何拼进消息被封装                  |
| 本系列  | 理解本质用                       | **推荐**                        | 不用                           |

### 2.5 会话隔离模式（多用户的关键）

```python theme={null}
store = {}   # session_id → history

def get_history(session_id: str) -> InMemoryChatMessageHistory:
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# 用户 A 的会话和用户 B 的会话互不干扰
hist_a = get_history("user_A")
hist_b = get_history("user_B")
```

**为什么必须隔离**：生产环境有 N 个用户同时聊天。如果不隔离，所有人的历史混在一个列表里，模型会乱。`session_id`（会话 id）= 用户身份的钥匙。这个模式对方案一/二都适用——`store` 里装 `ListHistory` 或 `InMemoryChatMessageHistory` 都一样。

***

## 3. 关键方法（手动滚雪球，完整流程）

```python theme={null}
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是公司智能助手，说话简洁。"),
    MessagesPlaceholder("history"),
    ("human", "{question}"),
])

history = get_history("user_A")

def chat(question: str) -> str:
    # ① 取历史 → 拼消息
    messages = prompt.invoke({
        "history": history.messages,   # ← 从历史存储取（方案一/二都一样）
        "question": question,
    }).to_messages()
    # ② 调模型
    ai_msg = model.invoke(messages)
    # ③ 写回历史（用户问 + 模型答都要存）
    history.add_user_message(question)
    history.add_ai_message(ai_msg.content)
    return ai_msg.content

chat("我叫小林，是后端工程师")
chat("我叫什么名字？")   # → "你叫小林，是后端工程师" ← 记住了！
```

**三个环节缺一不可**：**取历史 → 拼进消息 → 写回历史**。漏掉任何一个，对话就"失忆"。而你的 `chat` 函数只依赖 `history.messages` / `add_user_message` / `add_ai_message` 三个接口——**方案一换方案二，chat 函数一行都不用改**。

***

## 4. 进程线程模型

```
main thread
  │
  ├─ chat("我叫小林") → 取历史(内存) → invoke(阻塞1~3s) → 写回历史
  ├─ chat("我叫什么名字？") → 取历史(内存) → invoke(阻塞) → 写回历史
  │
  └─ 多个 user 会话 = store 字典里有多个 history 对象，互不相干
```

* **`InMemoryChatMessageHistory` 是纯内存**——进程重启就丢。生产要持久化（第 14、15 篇讲 Redis/DB 方案思路）。
* **并发注意**：多个线程同时写同一个 session 的 history 有竞态。单用户单线程（FastAPI 异步单协程）下没问题；多线程共享时要加锁（第 11、13 篇）。

***

## 5. 网络模型

```
chat("我叫什么名字？")
  └─ messages = [SystemMessage, HumanMessage(我叫小林), AIMessage(你好小林), HumanMessage(我叫什么名字？)]
       └─ POST .../v1/messages
            Body.messages = [ {system}, {user:我叫小林}, {assistant:你好小林}, {user:我叫什么名字？} ]
```

**历史消息通过网络逐条发给模型**——这就是"记忆"的物理真相：不是模型记住了，是**每次把历史完整地发一遍**。代价是历史越长，token 越多、越贵、越慢（第 15 篇讲裁剪）。

***

## 6. 验证：跑起来

配套代码 `code/06_memory.py`：

1. 无记忆演示（模型真的不记得）；
2. **方案一** list 手搓（零依赖，看消息列表增长）；
3. **方案二** InMemory 官方 + 会话隔离（两个 session 互不干扰）；
4. **方案三** 弃用的 `RunnableWithMessageHistory`——实例化时捕获真实弃用警告；
5. 打印每次发送的消息列表，看历史如何增长。

```powershell theme={null}
cd enterprise-rag-course\code
python 06_memory.py
```

**预期输出（节选）**：

```
===== ① 无记忆 =====
问: 我叫小林
答: 你好小林！
问: 我叫什么名字？
答: 抱歉，我不知道。       ← 金鱼脑

===== ② 方案一：list 手搓 =====
A答: 小林。               ← 记住名字
list 历史条数: 4  ← 就是个普通 list
  ['HumanMessage', 'AIMessage', 'HumanMessage', 'AIMessage']

===== ③ 方案二：InMemory + 会话隔离 =====
[user_A] 我叫什么名字？ → 小林
[user_B] 我叫什么名字？ → 我不知道你的名字   ← B 的历史是空的，不受 A 影响

===== ④ 方案三（弃用） =====
[LangChainDeprecationWarning] RunnableWithMessageHistory is deprecated. Use LangGraph's...
```

***

## 7. 边界

* **历史无限增长会爆上下文**——长对话要裁剪（`trim_messages`）或摘要（第 15 篇）。
* **`RunnableWithMessageHistory` 已弃用**——本机 1.5.1 实测：实例化时抛 `LangChainDeprecationWarning`，弃用理由明确指向 LangGraph。本系列不用 LangGraph，用方案一/二的手动管理。
* **别 append 重建的消息**——写回历史时直接存原始 `AIMessage` 对象（含 tool\_calls/usage\_metadata），重建会丢字段。
* **内存历史只适合开发**——生产必须换持久化存储（Redis/DB），接口不变（方案二实现 `BaseChatMessageHistory`，换持久化实现即可，chat 函数不动）。

***

## 推荐资料（延伸阅读）

* [LangChain 官方文档 · 短期记忆](https://docs.langchain.com/oss/python/langchain/short-term-memory) —— 消息历史的官方概念页
* [langchain-core API 参考 · chat\_history](https://reference.langchain.com/python/langchain-core/chat_history/) —— BaseChatMessageHistory 家族

***

## 8. 未完待续

对话连贯了。但新的问题出现：**模型只知道你教它的，不知道你们公司的新人培训手册、政策文件**。让模型回答"转正需要什么条件"这种公司内部问题，它只能瞎编。

怎么把公司文档变成模型可用的知识？第一步：把文档变成一个个"块"——这就是第 07 篇：文档与切分。

→ [07 · 文档与切分](/doc/doc/enterprise-rag-course/07-文档与切分)
