# 16 - 记忆与对话历史（含 Redis 基础）

---

**本章课程目标：**

- 理解本章所说的**记忆（Memory）**到底是什么，知道它为什么在多轮对话中不可缺少。
- 掌握“**读历史 → 拼入提示 → 调模型 → 写回历史**”这条最核心的实现主线，理解 `RunnableWithMessageHistory` 与 `BaseChatMessageHistory` 的职责分工。
- 对 Redis 在本章中的定位建立清晰认识：它不是让模型“变聪明”，而是让**会话历史可持久化、可跨进程、可跨实例共享**。

**学习建议：** 这章最好用两轮对话来学：先看没有记忆时模型为什么“忘事”，再看同一个 `session_id` 下历史消息如何被读出、拼进提示词、调用模型、写回存储。Redis 先别当重点，它只是把内存版的历史保存得更持久。只要能讲清“读历史 -> 拼提示 -> 调模型 -> 写历史”，后面的实现类就不散。

**官方文档与资源**：详见 [工具导航与参考资料索引 - LangGraph](工具导航与参考资料索引.md#LangGraph)。

---

## 1、记忆简介

### 1.1 为什么需要记忆

如果你只做单轮问答，那么每次请求都是一次独立调用，模型只看当前输入，答完就结束。  
但只要你开始做聊天机器人、客服助手、学习助手、企业知识问答，多轮上下文就会立刻成为刚需。

最典型的现象就是：

1. 第一轮你说：“我叫张三。”
2. 第二轮你再问：“我叫什么？”

如果系统没有保存并重新注入上一轮内容，模型就只能把第二轮当作一条全新的请求，于是很自然地回答：“我不知道。”这不是模型“太笨”，而是程序根本没有把上一轮信息带给它。

![无记忆时两轮对话相互独立，模型无法利用上一轮信息](images/16/16-1-1-1.jpeg)

所以从工程角度看，记忆并不是一个“可有可无的高级特性”，而是多轮对话系统最基础的能力之一。它至少解决三类问题：

- **上下文连续性**：让模型知道上一轮说过什么。
- **会话个性化**：让模型记住用户在当前会话里提供的信息，如名字、偏好、任务背景。
- **多步任务承接**：让模型把前一步结果作为后一步输入，而不是每轮都重新从零开始。

用一句最直白的话说：**没有记忆，聊天系统就只是“连续发了很多次单轮请求”；有了记忆，它才真正开始像“对话”。**

![LangChain 官方文档「Core components → Short-term memory」概述：记忆用于保存先前交互信息，支撑智能体在多轮交互中保持效率与体验](images/16/16-1-1-2.jpeg)

### 1.2 定义

本章里的**记忆（Memory）**，指的主要是**短期记忆（Short-Term Memory）**或**对话历史（Chat History）**。

它的本质不是“模型真的记住了”，而是：**程序把之前的消息保存下来，并在下一轮调用前，再把这些历史消息一并传给模型。**

所以它更像一种“外部会话状态管理机制”，而不是模型参数层面的学习。

### 1.3 这不是训练模型

这里容易混淆，所以单独强调。

本章所说的记忆：

- **不是**重新训练模型
- **不是**把信息写进模型参数
- **不是**让模型永久学会某些知识

它做的事情只是：

1. 把历史消息保存在外部
2. 下次调用前重新读出来
3. 和当前问题一起发给模型

也就是说，模型“记住你是谁”，不是因为它内部学会了你，而是因为程序每次都把“你叫张三”这条历史重新带给它。

这也是为什么：

- **内存版**：程序一关，历史就丢了
- **Redis 版**：程序重启后，历史还能读回来

### 1.4 本章的“记忆”能做什么

从项目角度看，本章的记忆主要能做这些事：

- **让多轮问答连续起来**：例如先自我介绍，再追问名字、偏好、之前说过的内容。

- **保存当前会话上下文**：例如用户正在做一份简历、正在学习 Python、正在整理某份报告，后续问题不必每轮都重述背景。

- **为链式流程提供上下文**：历史消息最终通常通过 [第 13 章](13-提示词与消息模板.md) 的 `MessagesPlaceholder` 注入模板，与 [第 15 章](15-LCEL与链式调用.md) 中的链组合在一起使用。

- **为后续 Agent / Tool / LangGraph 打基础**：你后面会发现，工具调用、Agent、多步决策几乎都离不开“当前线程 / 当前会话”的状态保存。

### 1.5 本章不重点讨论什么

为了避免和后面章节混在一起，这里也要明确本章边界：

- **不是长期知识记忆**：比如用户永久画像、跨很多天的长期偏好管理，这不属于本章核心。

- **不是 RAG 检索记忆**：把外部文档查出来再回答，属于 [第 19 章 RAG](19-RAG检索增强生成.md) 的主线。

- **不是 Agent 决策状态机**：更复杂的循环式状态与持久化，会在 [Agent](21-Agent智能体.md) 与 LangGraph 相关章节里更自然地出现。

所以你可以先把本章聚焦成一句话：**本章主要讲“当前会话里，怎么把历史消息保存起来并重新喂给模型”。**

---

## 2、「我不知道」演示：无记忆时的行为

在真正上记忆之前，最好的学习方式不是先背概念，而是先亲眼看到“没有记忆时会发生什么”。下面这个案例故意只保留最简单的链：Prompt、Model、Parser。

然后连续做两次调用：

1. 第一次告诉模型：“我叫张三，你叫什么？”
2. 第二次再问：“你知道我是谁吗？”

因为程序没有保存第一轮内容，也没有在第二轮重新注入历史，所以模型只能把第二个问题当作一条全新的请求处理，于是给出“我不知道”之类的回答。

【案例源码】`案例与源码-2-LangChain框架/07-memory/Memory_IDontKnow.py`

[Memory_IDontKnow.py](案例与源码-2-LangChain框架/07-memory/Memory_IDontKnow.py ":include :type=code")

这个案例用来建立一个基础直觉：**模型不是天然会记住多轮对话，是程序决定它能不能看见历史。**

---

## 3、实现原理

### 3.1 核心主线

先不急着看类名，记忆的实现过程可以拆成四步：

1. **读历史**
2. **把历史拼进当前提示**
3. **调用模型**
4. **把本轮输入和输出写回历史**

这就是本章最核心的链路。

![记忆在链中的位置：读历史 → 拼入 Prompt → 调模型 → 写回历史](images/16/16-3-1-1.jpeg)

### 3.2 工程化表达

如果用更接近程序的方式来描述，一次带记忆的调用通常是这样发生的：

**第一步：根据会话标识找到对应历史。**  
比如通过 `session_id="user-001"` 找到这位用户当前会话的消息列表。

**第二步：把历史消息插入 Prompt。**  
这一步通常会和 [第 13 章](13-提示词与消息模板.md) 里的 `MessagesPlaceholder("history")` 配合使用。

**第三步：把“历史 + 当前问题”一起交给模型。**  
这样模型看到的就不再只是当前一句话，而是完整上下文。

**第四步：把本轮消息写回历史存储。**  
也就是把：用户这一轮输入、模型这一轮回复，都追加进去，供下一轮再读取。

### 3.3 MessagesPlaceholder 与模板的关系

在对话历史场景里，你通常不会把历史消息一条条手写死在模板中，因为历史轮数与内容每轮都在变。更合理的做法是在模板里留一个“历史消息插槽”，运行时再把当前会话历史整块塞进去——这正是 [第 13 章](13-提示词与消息模板.md) 中 `MessagesPlaceholder` 的典型用法，与本章的“读写历史”前后衔接。

### 3.4 session_id 的意义

只要涉及记忆，就会涉及一个关键问题：**你怎么知道哪份历史属于哪位用户、哪次会话？**

最常见的做法就是使用 `session_id`，它就是“当前会话的编号”。

例如：

- `user-001`
- `user-002`
- `chat-room-a`

不同的 `session_id`，通常对应不同的历史记录。

在 `RunnableWithMessageHistory` 里，`session_id` 通常通过运行配置传入，例如 `config={"configurable": {"session_id": "user-001"}}`。不同的 `session_id` 会对应不同的历史对象。

这也是为什么本章 Redis 版和多 session 版案例里，`session_id` 都很重要。如果没有这层区分，系统就可能把 A 用户的历史错拿给 B 用户。

### 3.5 本章和 LangGraph 官方主线的关系

这里需要补一个版本背景。

在 LangChain / LangGraph 当前官方主线里，**短期记忆**越来越倾向于放在 **LangGraph persistence / checkpointer / thread** 这条体系里来讲。  
也就是说，官方现在更强调：

- 线程（thread）
- 持久化（persistence）
- 检查点（checkpointer）
- 图执行状态

但这并不意味着 `RunnableWithMessageHistory` 没价值了。对 LCEL 链来说，它更直观，也更容易看清“历史消息如何注入到当前调用里”。

所以本章采用这条学习顺序：

- 先用 `RunnableWithMessageHistory + BaseChatMessageHistory` 看清链级对话历史
- 再知道复杂 Agent 的状态持久化会继续走向 LangGraph persistence

> **版本说明：** 本章继续使用 `RunnableWithMessageHistory`，是为了把 LCEL 链里的“历史消息如何读写和注入”讲清楚。生产级 Agent 或复杂多轮状态管理，建议再对照 LangGraph 的 checkpointer、thread 和 persistence 机制来设计。

这样后面你切到更复杂的 Agent / LangGraph，就不会断层。

**补充：** 历史消息会占用模型上下文窗口；轮数过多时需要做截断、摘要或只保留最近 \(k\) 轮，否则可能触达 token 上限或带来额外成本。本章先把“注入历史”这件事讲清楚，具体压缩策略后面按产品需求再选。

---

## 4、记忆相关实现类

### 4.1 本章使用哪套写法

前面已经明确：本章先用 `RunnableWithMessageHistory + BaseChatMessageHistory` 讲清链级对话历史；复杂 Agent 的状态持久化，后续再放到 LangGraph 的 `thread / checkpointer / persistence` 里理解。

接下来先看几个相关类：哪些是早期写法，哪些适合作为本章重点。

### 4.2 ConversationChain（早期写法）

LangChain 早期有一个比较有代表性的类，叫 `ConversationChain`。

它内置了对话模板和记忆机制，所以很适合快速做简单演示；但它的灵活性有限，也不太贴合 LCEL / Runnable 体系，面对复杂对话流程时扩展性会比较差。

所以在今天的学习路径里，它不再适合作为本章的主线写法。

### 4.3 RunnableWithMessageHistory（更合适的写法）

`RunnableWithMessageHistory` 的作用很直接：**给一条已有的 Runnable / Chain 包上一层历史管理能力。**

它并不替代你的 Prompt、Model、Parser，而是站在它们外面，统一负责历史的读取、注入和写回。

这和第 15 章讲的 LCEL 很契合，因为它不是另起一套风格，而是在 Runnable 体系之上继续工作。所以从教学角度说，它特别适合这一章，因为它能把“记忆是在链外单独管理的一层会话能力”讲清楚。

### 4.4 BaseChatMessageHistory（历史存储接口）

如果 `RunnableWithMessageHistory` 解决的是“历史读写时机”，那么 `BaseChatMessageHistory` 解决的就是：

**历史到底存在哪里、怎么存。**它可以看作“聊天消息历史的统一抽象接口”。

不必先纠结源码细节，只要抓住这层分工：

- **RunnableWithMessageHistory**：控制历史何时读、何时写
- **BaseChatMessageHistory 及其实现类**：控制历史存到哪里

这就是为什么本章会同时讲它们两个，而不是只讲其中一个。

### 4.5 常见实现类

LangChain 提供了多种聊天历史实现（如内存、文件、Redis、Elasticsearch、DynamoDB 等），核心差别主要在于**是否持久化、是否适合多实例共享**。

常见实现的区别主要看存储位置：

| 组件名称                     | 存储方式     | 适合什么场景                   |
| ---------------------------- | ------------ | ------------------------------ |
| `InMemoryChatMessageHistory` | 进程内内存   | 本地学习、单进程演示、临时会话 |
| `FileChatMessageHistory`     | 本地文件     | 轻量持久化、小型脚本           |
| `RedisChatMessageHistory`    | Redis        | 持久化、跨进程、多实例共享     |
| 其他后端实现                 | ES、数据库等 | 和现有技术栈集成               |

对这一章来说，最重要的是前两个核心结论：

- **先学 InMemory**，因为它最直观
- **再学 Redis**，因为它最贴近真实项目

### 4.6 本章案例结构对应关系

下面按学习顺序列出本章案例文件：

| 文件                                     | 作用                              |
| ---------------------------------------- | --------------------------------- |
| `Memory_IDontKnow.py`                    | 先看无记忆时的问题                |
| `Memory_RunnableWithMessageHistory.py`   | 最基础的内存版带历史对话          |
| `Memory_RunnableWithMessageHistoryV2.py` | 多 session 版，对应真实多用户场景 |
| `Memory_InMemoryChatMessageHistory.py`   | 不走包装器，直接手动操作 history  |
| `RedisEnvCheck.py`                       | 跑 Redis 版前先做环境校验         |
| `Memory_RedisChatMessageHistory.py`      | Redis 持久化主案例                |
| `Memory_RedisStackChatMessageHistory.py` | Redis Stack 可选案例              |

这样安排其实很合理：

- 先让你看到问题
- 再给出最简单解决方案
- 再扩展到多 session
- 最后再讲持久化

---

## 5、案例代码

### 5.1 内存版（进程内，重启即丢失）

内存版最适合入门，因为它把“记忆的本质”暴露得最清楚：

- 历史就是一个消息列表
- 每轮都从这份列表里读取
- 每轮结束再把新消息写回去

缺点也很明显：

- 只在当前 Python 进程有效
- 程序一停，历史就没了
- 不适合多实例共享

但正因为足够简单，它适合用来建立第一层理解。

#### 5.1.1 最基础写法：RunnableWithMessageHistory

【案例源码】`案例与源码-2-LangChain框架/07-memory/Memory_RunnableWithMessageHistory.py`

[Memory_RunnableWithMessageHistory.py](案例与源码-2-LangChain框架/07-memory/Memory_RunnableWithMessageHistory.py ":include :type=code")

这个案例最值得看懂的点有三个：

- `MessagesPlaceholder("history")` 负责接收历史
- `RunnableWithMessageHistory` 负责包住整条链
- `session_id` 负责告诉系统“当前该读哪份历史”

#### 5.1.2 多 session 写法：按 session_id 维护多份历史

【案例源码】`案例与源码-2-LangChain框架/07-memory/Memory_RunnableWithMessageHistoryV2.py`

[Memory_RunnableWithMessageHistoryV2.py](案例与源码-2-LangChain框架/07-memory/Memory_RunnableWithMessageHistoryV2.py ":include :type=code")

这个案例比上一个更贴近真实项目，因为真实系统里不会只有一个用户，也不会只有一份历史。

它用一个 `store` 字典按 `session_id` 存放不同的 `InMemoryChatMessageHistory`，本质上是在演示：

**同一套链逻辑，如何根据不同会话编号切换到不同历史。**如果你后面要做 Web 聊天、客服系统、多人会话，这个思路会反复用到。

#### 5.1.3 直接操作 InMemoryChatMessageHistory

【案例源码】`案例与源码-2-LangChain框架/07-memory/Memory_InMemoryChatMessageHistory.py`

[Memory_InMemoryChatMessageHistory.py](案例与源码-2-LangChain框架/07-memory/Memory_InMemoryChatMessageHistory.py ":include :type=code")

这个案例不再通过 `RunnableWithMessageHistory` 自动管理，而是手动：

- `add_user_message(...)`
- `add_message(...)`
- `history.messages`

它的价值在于让你更“看见底层”：**所谓记忆，说到底就是在维护一份消息历史列表。**

从工程角度说：

- **想快速搭多轮链**：优先用 `RunnableWithMessageHistory`
- **想完全掌控读写时机**：可以直接操作 `InMemoryChatMessageHistory`

### 5.2 持久化：Redis 存储

当你已经理解内存版后，就会自然遇到一个问题：**程序一重启，历史全没了，怎么办？**

这就是 Redis 出场的原因。Redis 在本章里的角色非常明确：

- 它不是模型
- 它不是 Prompt
- 它不是“更高级的记忆算法”
- 它只是一个更适合持久化保存会话历史的存储后端

也就是说，本章从内存版切到 Redis 版，变化的不是“记忆原理”，而是“历史保存的位置”。

#### 5.2.1 设计要求与参考文档

这一节的目标非常朴素：**把对话历史从进程内存，换成 Redis 持久化存储。**这样做之后：程序重启后还能恢复历史，多个实例可以共享同一份历史，更贴近真实线上系统。

同时结合本项目当前依赖，你也要知道：

- 本仓库 `requirements.txt` 中已经包含 `redis>=5.3,<6`
- 也包含 `langchain-redis>=0.2`

所以如果你已经执行过 `pip install -r requirements.txt`，通常不需要再单独补装基础依赖。

#### 5.2.2 Redis 与 Redis Stack 简介

先说最重要的结论：**本章对话历史案例，原生 Redis 就够用了。**

原因是 `RedisChatMessageHistory` 存储对话历史时，使用的只是 Redis 的基础数据结构能力，不依赖 Redis Stack 才有的高级模块。

所以：

- **原生 Redis**：完全能跑本章主案例
- **Redis Stack**：可以跑，而且更方便用 RedisInsight 可视化查看数据

![Redis Stack 与原生 Redis 的关系](images/16/16-5-2-1.jpeg)

如果你是第一次学，可以直接把它们区分成这样：`Redis` 是高性能键值存储本体，`Redis Stack` 则是在 Redis 基础上补上更多增强能力，并带来更友好的工具链。

本章之所以还保留 Redis Stack 小节，一方面是为了让你能用 RedisInsight 更直观地观察数据，另一方面也顺手为后面向量存储、搜索等更复杂的 Redis 用法做一点环境铺垫。

| 功能维度 | 原生 Redis                 | Redis Stack 增强功能                                  |
| :------- | :------------------------- | :---------------------------------------------------- |
| 数据结构 | 字符串、列表、集合、哈希等 | 增加 JSON、图、时间序列、概率结构等高级类型           |
| 查询能力 | 仅限键值查询               | 支持全文搜索、向量搜索、图查询、JSON 查询             |
| 使用场景 | 缓存、消息队列、计数器等   | 实时推荐、时序分析、知识图谱、文档数据库、AI 向量检索 |
| 开发体验 | 命令行操作，需手动拼装逻辑 | 提供 RedisInsight 和对象映射库，开发效率更高          |

#### 5.2.3 Docker 启动与宿主机连接

如果你本机没单独装 Redis，用 Docker 是最省事的办法。

**原生 Redis：**

```bash
docker run -d --name docker-redis-1 -p 6379:6379 redis
```

这样本机就可以通过：

```bash
redis://localhost:6379
```

来连接 Redis。

**Redis Stack：**

```bash
docker run -d --name redis-stack -p 26379:6379 -p 8001:8001 redis/redis-stack
```

这里要区分两个端口：

- `26379`：本机访问 Redis 服务用（映射到容器内 `6379`）
- `8001`：浏览器访问 RedisInsight 的常用映射端口

也就是说，Redis Stack 版案例里如果默认连接 `redis://localhost:26379`，是完全正常的，因为它连的是**宿主机映射端口**，不是容器内的原始 `6379`。若你本地镜像或 compose 将 Insight 映到 **8002** 等其他端口，以实际 `docker ps` 或文档为准即可。

#### 5.2.4 Redis 基本命令与查看对话历史

这一节不用学成 Redis 专家，只要会看本章案例写进去的数据就够了。

最常用的命令有这些：

| 命令                 | 说明                       | 示例                             |
| :------------------- | :------------------------- | :------------------------------- |
| `PING`               | 检测服务是否存活           | `PING` → 返回 `PONG`             |
| `SET key value`      | 设置字符串键值             | `SET mykey "hello"`              |
| `GET key`            | 获取字符串值               | `GET mykey`                      |
| `DEL key [key ...]`  | 删除一个或多个键           | `DEL mykey`                      |
| `KEYS pattern`       | 按模式查找键（慎用于生产） | `KEYS *`、`KEYS message_store:*` |
| `EXISTS key`         | 判断键是否存在             | `EXISTS mykey` → 1 或 0          |
| `TTL key`            | 查看键的剩余过期时间（秒） | `TTL mykey`，-1 表示永不过期     |
| `EXPIRE key seconds` | 设置键的过期时间           | `EXPIRE mykey 3600`              |
| `DBSIZE`             | 当前数据库的键数量         | `DBSIZE`                         |
| `FLUSHDB`            | 清空当前数据库（慎用）     | `FLUSHDB`                        |

本章 Redis 对话历史案例里，最值得你关注的是这一点：**LangChain 会按 `session_id` 写入不同的键。**

例如你运行过某个 `session_id=user-001` 的会话后，可能会看到类似：

```redis
KEYS *
TYPE message_store:user-001
LRANGE message_store:user-001 0 -1
```

你看到的每个元素，本质上就是一条序列化后的消息记录。这里可以建立一个基础直觉：

**对话历史并不神秘，落到 Redis 里，本质上就是按 session 管理的一组消息数据。**

#### 5.2.5 环境验证

在真正运行 Redis 对话历史前，建议先检查两个问题：

1. Python 里的 `redis` 包是否安装正常
2. Redis 服务是否真的能连上

【案例源码】`案例与源码-2-LangChain框架/07-memory/RedisEnvCheck.py`

[RedisEnvCheck.py](案例与源码-2-LangChain框架/07-memory/RedisEnvCheck.py ":include :type=code")

这个脚本建议作为“跑 Redis 案例前的第一步”，因为很多问题其实不是 LangChain 的问题，而是 Redis 环境本身没就绪。

#### 5.2.6 案例：Redis 对话历史

【案例源码】`案例与源码-2-LangChain框架/07-memory/Memory_RedisChatMessageHistory.py`

[Memory_RedisChatMessageHistory.py](案例与源码-2-LangChain框架/07-memory/Memory_RedisChatMessageHistory.py ":include :type=code")

这是本章最重要的持久化案例。它和内存版相比，真正变化的核心只有一处：

- `get_session_history(...)` 不再返回 `InMemoryChatMessageHistory`
- 而是返回 `RedisChatMessageHistory`

也就是说：**链的读写逻辑没变，只是历史的存储后端换了。**

这个认识特别重要，因为它让你真正理解：

- `RunnableWithMessageHistory` 负责“什么时候读写”
- `RedisChatMessageHistory` 负责“历史存到哪里”

两者是配合关系，不是替代关系。

#### 5.2.7 案例：使用 Redis Stack（可选）

【案例源码】`案例与源码-2-LangChain框架/07-memory/Memory_RedisStackChatMessageHistory.py`

[Memory_RedisStackChatMessageHistory.py](案例与源码-2-LangChain框架/07-memory/Memory_RedisStackChatMessageHistory.py ":include :type=code")

这个案例和上一节主案例的逻辑基本一致，只是默认连接到了 Redis Stack 常见端口。

它最大的教学价值不是“换了一个全新方案”，而是：让你确认 Redis Stack 也能兼容跑本章历史存储；让你能用 RedisInsight 直观看到会话数据。

![RedisInsight 中查看 LangChain 写入的会话：键 message_store:user-001 类型为 LIST，元素为序列化后的 human/ai 消息（JSON），便于对照代码理解持久化结构](images/16/16-5-2-2.png)

所以这节更适合看作：**主案例的一个更方便观察数据的变体。**

---

**章节思考题：**

1. 如果用户说“模型记住了我”，你会怎样解释本章里的“记忆”到底是什么？

   **参考思路：** 这里的记忆不是模型参数更新，而是系统把历史消息重新取出来，作为上下文交给模型。模型看起来记得，是因为应用帮它带上了历史。

2. `RunnableWithMessageHistory` 和具体存储类的边界为什么要分清？

   **参考思路：** 前者负责把历史接入调用链，决定何时读写；后者负责消息真正存在哪里。边界分清后，内存、Redis、数据库等存储可以替换，而调用链逻辑不用大改。

3. 多用户场景里，`session_id` 设计不好会出现什么问题？

   **参考思路：** 可能串会话、串用户、误用历史，甚至泄露隐私。`session_id` 应该和用户、会话、业务租户等隔离策略一起设计，而不是随便写一个字符串。

4. 为什么完整历史不能无限塞回模型？

   **参考思路：** 上下文窗口、成本、延迟和噪声都会增加。历史越多不一定越好，真实系统要做窗口截断、摘要、重要信息提取和过期清理。

**本章小结：**

- **记忆是什么**：这里讲的是短期记忆 / 对话历史，不是训练模型，也不是更新模型参数，而是把历史消息保存下来，并在下一轮调用前重新交给模型。
- **基本流程**：读历史 → 拼入提示 → 调模型 → 写回历史。`MessagesPlaceholder` 负责给历史消息留位置，`RunnableWithMessageHistory` 负责管理读写时机，`BaseChatMessageHistory` 及其实现类负责具体存储。
- **存储怎么选**：`InMemoryChatMessageHistory` 适合单进程学习和演示，进程一停就丢；`RedisChatMessageHistory` 适合持久化、多实例共享和跨进程会话恢复。
- **版本关系**：本章用 `RunnableWithMessageHistory + BaseChatMessageHistory` 讲清链级记忆；复杂 Agent 的长期运行状态，后面再看 LangGraph 的 thread、persistence 和 checkpointer。

**建议下一步：** 先把无记忆、内存版、多 session 版、Redis 版案例都跑一遍，重点观察“同一个 `session_id` 下历史是怎么被延续的”；然后继续学习 [第 17 章 Tools 工具调用](17-Tools工具调用.md)，你会更容易理解为什么 Agent 场景不仅需要工具，还需要会话状态与历史管理。
