目录
一、了解核心组件(Components)
二、消息(Messages)
LLM 大模型的消息结构
LangChain 消息
BaseMessage 抽象消息类
对话模式
三、缓存历史消息
多轮对话
内存缓存
说明
四、管理历史消息
前置概念
上下文窗口
Token
消息裁剪
基于输入 Token 数的修剪
基于消息数的修剪
消息过滤
按类型进行筛选
按类型 + ID 进行筛选:
消息合并
一、了解核心组件(Components)
从本篇文章开始,我们将学习大量的概念、术语,及其具体用法和对应的接口方法。
但不用担心,它们本质上都是围绕大模型调用衍生出来的一系列功能扩展,因为大模型也不是随随便便就能被很好的调用起来。因此,为了让整个交互流程更规范、更高效,LangChain 定义了一系列的组件使我们与大模型的交互更规范。
因此在学习过程中,有几点要提前强调:
二、消息(Messages)
消息是聊天模型中的通信单位,用于表示聊天模型的输入和输出,以及可能与对话关联的任何其他上下文或元数据。
LLM 大模型的消息结构
每条消息都有一个角色和内容,以及因 LLM 的不同而不同的附加元数据。
1. 消息角色 (Role):用来区分对话中不同类型的消息,并帮助聊天模型了解如何响应给定的消息序列。

2. 消息内容 (Content):表示多模态数据 (例如,图像、音频、视频) 的消息文本或字典列表的内容。内容的具体格式可能因底层不同的 LLM 而异。目前大多数模型都支持文本作为主要内容类型,对多模态数据的支持仍然有限。
3. 消息其他元数据 (Additional metadata)

下面展示一个 OpenAI 的格式消息列表:

代码讲解: 这是 OpenAI API 原生的对话消息格式,是一个列表,列表里面每一个字典代表一条对话消息。
整体逻辑:把一轮一轮对话,按用户提问→模型回答→用户再提问顺序放到列表里,完整保存上下文传给大模型,大模型就知道完整聊天历史,不会丢失前面对话信息。
LangChain 接受下面的格式作为聊天模型的输入如下:

代码讲解:
简单说:LangChain 聊天模型可以直接兼容 OpenAI 原生字典消息格式,把完整对话历史列表丢进 invoke(),这样就可以调用大模型继续对话。
后面我们还会见到 LangChain 封装好的 HumanMessage、AIMessage 类对象,这是 LangChain 自己的消息对象,和这种原始字典可以互相转换。
LangChain 消息
LangChain 提供了一种统一的消息格式,可以跨聊天模型使用,允许用户使用不同的聊天模型,而无需担心每个模型提供商使用的消息格式的具体细节。例如:

代码讲解:
这些模型提供商不同,但对于其输入和输出,统一使用 LangChain 的消息格式。LangChain 消息格式主要分为五种,分别是:

这几个消息类型,我们已经全部见过!它们都是 LangChain BaseMessage 的子类,全部是作为 LangChain 聊天模型的输入和输出!!
BaseMessage 抽象消息类
class langchain_core.messages.base.BaseMessage 是作为 LangChain 聊天模型的输入和输出。
参数如下:
- content:消息的字符串内容。
- additional_kwargs:与消息关联的其他有效负载数据。对于来自 AI 的消息,可能包括模型提供程序编码的工具调用。
- response_metadata:响应元数据。例如:响应标头、logprobs、令牌计数、模型名称。
- type:消息的类型。必须是消息类型唯一的字符串。此字段的目的是在对消息进行反序列化时方便地识别消息类型。
- name:消息名称,为消息提供一个人类可读的名称。该字段的使用是可选的,是否使用它取决于模型实现。
- id:消息的可选唯一标识符。理想情况下,这应该由创建消息的提供者 / 模型提供。
内置方法:
- pretty_print() → None:打印消息的漂亮表示。
- pretty_repr(html: bool = False) → str:获得消息的漂亮表示。
- 请求:是否将消息格式化为 HTML。如果为 True,则消息将使用 HTML 标记进行格式化。默认值为 False。
- 响应:这是消息的漂亮表示。
- text() → str:获取消息的文本内容。
对话模式
大多数对话都以设置对话上下文的系统消息开始。接下来是包含用户输入的用户消息,然后是包含模型响应的助手消息。如下图所示:
左侧普通对话流程:
- Turn 1:System message → Human message → AI message
- Turn 2:Human message → AI message
三、缓存历史消息
多轮对话
在与大型语言模型交互的过程中,我们常常体验到与智能助手进行连贯多轮对话的便利性。但目前我们的系统还不支持此功能,代码如下:

代码讲解:
核心问题:每一次单独调用 invoke,模型只会看到本次传入的消息,不会自动记住之前对话内容,因为模型本身没有记忆,所以第二次提问它不知道你叫 Bob。
打印结果:

原因:因为第二次 invoke 只给了新问题,没有把第一轮对话消息一并传给模型,模型看不到之前的聊天记录。
所以可以发现,聊天模型并不认识我们,更别说支持更多轮的对话了~
下面稍作修改,让我们将 AI 回复给我们的响应跟着新的用户消息一起发给聊天模型试试。代码如下:

代码讲解:
- 第一条:HumanMessage,用户说 “Hi! I'm Bob”,对应第一轮用户提问。
- 第二条:AIMessage,把上一轮 AI 返回的回答手动写进列表,代表模型之前输出过的内容。
- 第三条:新的 HumanMessage,用户继续提问 “What's my name?”。
核心逻辑:大模型本身没有记忆能力,记忆需要我们自己维护消息列表,把过去全部用户消息、AI 回复全部放进列表,每次调用都完整传给模型,模型才能看懂上下文。
打印结果:

输出讲解: 现在模型正确识别出来用户名字是 Bob。因为这一次调用的时候我们把之前完整对话全部交给模型,模型读取历史记录拿到名字信息,就可以正确回答后续问题。
从结果可知,只要将历史消息重新发送给聊天模型,那么就可以实现多轮对话的功能。
内存缓存
通过上述代码我们就能知道对于历史消息的管理就显得尤为重要。在 LangChain 老版本中我们可以使用 RunnableWithMessageHistory 消息历史类来包装另一个 Runnable 并为其管理聊天消息历史记录。它将跟踪模型的输入和输出,并将其存储在某个数据存储中。未来的交互将加载这些消息,并将其作为输入的一部分传递给链。
代码如下:

代码讲解:
1. 导入模块:
- ChatOpenAI:创建聊天大模型实例。
- HumanMessage、AIMessage:用户消息、AI 消息对象。
- BaseChatMessageHistory:聊天历史的抽象基类;InMemoryChatMessageHistory 是内存版实现,消息保存在程序内存里,程序关闭数据就消失。
- RunnableWithMessageHistory 专门用来给模型自动维护对话历史的包装器,不用我们手动拼接 messages 列表。
2. 创建模型对象 model,使用 gpt‑4o‑mini 模型。
3. store = {}:定义一个字典充当全局存储仓库,用来存放不同会话的聊天历史。字典的 key 就是 session_id,value 是对应会话的消息历史对象。
4. get_session_history 回调函数:接收 session_id 会话编号作为参数。
- 如果这个 session_id 不在 store 字典就新建一个内存聊天历史对象存入 store。
- 如果已经存在就直接返回已有的历史对象。作用:根据会话 ID 取出该会话全部聊天记录,不同 session_id 代表不同用户/不同对话,互相隔离互不干扰。
5. with_message_history = RunnableWithMessageHistory(model, get_session_history) 把原始 model 传入,绑定历史回调函数,得到包装后的对象。之后调用这个对象,框架内部自动帮我们追加、保存历史消息,不需要手动维护 messages 列表。
6. config = {"configurable": {"session_id": "1"}} 配置参数,指定当前使用会话编号为"1"。调用 invoke 的时候必须传入这个 config,框架才能知道该用哪一份聊天历史。
7. 第一次 invoke 调用:传入用户消息 Hi! I'm Bob,同时带上 config。框架内部自动保存本次用户消息和 AI 回复到 session_id=1 对应的内存历史。调用 pretty_print() 打印回复。
8. 第二次 invoke 调用:只传入新问题 What's my name?,不需要手动带上之前的对话。框架会自动读取 session_id=1 的全部历史消息,拼接完整消息列表传给大模型,同时保存新一轮对话。
注意:InMemoryChatMessageHistory 是内存存储,程序一旦重启 store 字典全部清空,记忆就丢失了,不能持久化保存。
RunnableWithMessageHistory 类初始化参数说明:
-
runnable:被包装 Runnable 实例,这里就是我们定义的聊天模型。
-
get_session_history:返回类型为 BaseChatMessageHistory 的回调函数。接收 session_id 字符串并返回对应会话的聊天消息历史实例。
RunnableWithMessageHistory 类方法说明:
-
.invoke()方法:用法和普通 Runnable 的 invoke 一致。但是必须传入config= {"configurable": {"session_id": "xxx"}} 配置,框架靠这个拿到会话 id 读取对应的聊天历史。
最终打印结果:

此时我们通过内存缓存就成功实现记忆功能!
说明
从 LangChain 的 v0.3 版本开始,官方建议 LangChain 用户不要使用 RunnableWithMessageHist ory,而是利用 LangGraph 持久性来完成。
原因是它们的功能有限,不太适合现实世界的对话式 AI 应用程序。这些内存抽象缺乏对多用户和多对话场景的内置支持,而这对于实际的对话式人工智能系统至关重要。这些实现中的大多数已在 LangChain 0.3.x 中被正式弃用,取而代之的是 LangGraph 持久性。LangGraph 持久性非常灵活,可以支持比 RunnableWithMessageHistory 接口更广泛的用例。我们会在 LangGraph 篇章中学习它!
因此,RunnableWithMessageHistory 这部分我们讲解的并不深入。在之前对于生产环境,我们还需要使用聊天消息历史记录的持久化实现,例如 RedisChatMessageHistory(),而不是InMemoryChatMessageHistory(),但现在也已不推荐新应用使用它们了。
四、管理历史消息
前置概念
管理历史消息,无非就是理解如何 “管理”,“管理” 无非也就是一些 “CRUD”。那么在了解如何管理消息之前,需要先了解下多轮对话的核心概念:
上下文窗口
上下文窗口 : 上下文窗口可以理解为模型的 “短期工作记忆区”,即 LLM 在一次处理请求时所能查看和处理的最大 Token 数量,它包含了:
- 用户的输入
- 大模型的输出
- 有时还包括系统指令(SystemMessage)和对话历史。
不同大模型支持的上下文窗口大小不同,例如:
- OpenAI 下 GPT‑5 模型上下文窗口为 400000(最大 Token 数量)
- GPT‑4.1 模型上下文窗口为 1047576(最大 Token 数量)
- 其他模型上下文窗口可参考对应模型官网说明,如 OpenAI 下模型可以参考官网文档。
Token
在自然语言处理(NLP)中,Token 是文本的基本单位。它不是完全等同于一个单词或一个汉字,而是一个更细粒度的划分。
为什么用 Token?
因为计算机无法直接理解文字,它需要将文本转换为数字向量。而 Tokenization 令牌化就是这个转换过程的第一步,将句子分解成模型可以理解和处理的碎片。
-
对于英文:1 个 Token ≈ 4 个字符或 0.75 个单词,1000 个 Tokens 约等于 750 个英文单词。 一个 Token 可以是一个单词 (如 "apple")、一个词根 (如 "un" 在 "unlikely" 中),或者一个标点符号 (如 ".")。例如 "ChatGPT is great!" 可能会被分成 ["Chat", "G", "PT", " is", " great", "!"] 这 6 个 Token。
-
对于中文:1 个汉字 ≈ 1.5‑2 个 Tokens,1000 个 Tokens 大约相当于 500‑700 个汉字。常见的词和字可能是一个 Token,生僻字或复杂词可能会被拆分成多个。
举个例子,上下文窗口就像一个固定大小的工作台,再把 Token 比作一个积木零件,把大模型比作一个工匠。工匠需要拼出模型,必须把所需的零件 (输入的 Token) 放在工作台上,一边拼装 (生成回复),一边把拼好的部分 (输出的 Token) 也放在工作台上。整个过程 (输入 + 输出) 中,工作台上的所有积木 (Tokens) 总数都不能超过工作台的最大容量 (上下文窗口大小)。

消息裁剪
有了上下文窗口和 Token 的认知,我们再来看多轮对话的实现原理,其实就是:
- 输入 = 系统消息 + 对话历史 + 最新用户问题
- 对于模型来说,并不真正 “记忆”,而是每次都将完整的上下文重新输入。
由于所有模型的上下文窗口大小都是有限的,这意味着作为输入的 Token 也是有限的。如果有累积了很长的消息历史记录,则需要管理传递给模型的消息的长度。
而下面我们要讲的 trim_messages 就能用于将聊天历史记录的大小减小为指定的令牌计数或指定的消息计数。
基于输入 Token 数的修剪
下面演示一个通过 trim_messages 裁剪消息的示例(基于输入 Token 数的修剪)。
先来看看不做任何输入限制的聊天:

代码讲解:
打印结果:

模型正确回答出名字是 Bob。prompt_tokens:88 代表输入一共消耗 88 个 token。completion_tokens:5 是模型输出消耗的 token,合计 93 个 token。
从打印结果看来,LLM 还认识我们,且共输入了 88 tokens。接下来让我们对消息进行裁剪,我们只希望将来输入时,最多输入 65 tokens,超出的需要按照一定的 “规则” 进行裁剪。

代码讲解
1. trimmer = trim_messages(…) 构造消息修剪器。
- max_tokens=65:修剪后输入消息总 token 上限设为 65。
- strategy="last":从旧消息开始丢弃,优先保留靠后最新的对话。
- token_counter=model:直接使用模型本身做 token 计数,不同模型 token 计算规则不一样,交给模型计算最准确。
- include_system=True:强制保留 SystemMessage 系统提示词,不会把系统消息裁掉。
- allow_partial=False:不允许把单条消息拆开截断,只能整条消息丢弃。
- start_on="human":修剪完成之后,第一条非系统消息必须是用户 HumanMessage 类型,避免修剪完开头残留 AI 消息。
2. chain = trimmer | model,LangChain 的管道写法:先跑消息裁剪,把裁剪完毕的消息列表送入大模型。
3. chain.invoke(messages),传入完整原始消息列表,链内部先裁剪,再调用模型。
打印结果:

结果解读
- input_tokens 变成 60,被控制在设置的 max_tokens=65 以内。
- 模型回答 “我不知道你的名字”,代表早期对话 hi! I'm bob 这一轮消息已经被裁剪丢弃,模型看不到最早自我介绍的内容。
- 裁剪策略 last 代表旧历史被删掉,只留下靠后的部分对话,系统消息被保留。
- 这就是长对话的常见问题:消息裁剪会丢失久远历史信息,是权衡上下文长度与记忆保留的取舍。
下面我们查看裁剪后实际消息内容:
我们直接调用 trimmer.invoke(messages) 打印裁剪器输出,就能看见到底哪些消息被删掉、哪些被保留。

输出结果:

从结果来看,确实是按照我们给定的裁剪 “规则” 来完成的。修剪聊天记录后,生成的聊天记录 (输入) 应该有效,需遵循对话模式原则:

- 聊天记录以 HumanMessage 或 SystemMessage 开头,后跟 HumanMessage。这可以通过设置 start_on="human" 来实现。
- 聊天记录以 HumanMessage 或 ToolMessage 结尾。这可以通过设置 ends_on=("human", "tool") 来实现。
- ToolMessage 只能出现在涉及工具调用的 AIMessage 之后。如果原始聊天历史记录中存在 SystemMessage,则新聊天历史记录应包括 SystemMessage,因为 SystemMessage 包含对聊天模型的特殊说明。SystemMessage 总是历史记录中的第一条消息 (如果存在)。这可以通过设置 include_system=True。
基于消息数的修剪
除了基于 token 的修剪,还可以通过设置 token_counter=len 根据消息数修剪聊天记录。在这种情况下,max_tokens 将控制最大消息数。示例如下:

- max_tokens=11:最大消息数,注意这里不是 token 数量,当 token_counter=len 开启按消息条数裁剪时,该参数代表最多保留多少条消息。
- strategy="last":修剪策略
- "last" (默认) :保留最后的消息。可获取消息列表中的最后一个 max_tokens
- "first":保留最早的消息。
- token_counter=len:根据消息数裁剪,用 Python 内置 len() 统计消息列表长度,不再调用大模型做 token 计数。
- include_system=True:如果想始终保留初始系统消息,可以指定
- include_system=True,强制把 SystemMessage 算进保留列表。
- allow_partial=False:是否允许拆分消息的内容,False 代表不能把单条消息拆开截断,只能整条消息丢弃。
- start_on="human":如果需要确保我们的第一条消息(不包括系统消息)始终是特定类型,可以指定start_on。
结果如下:

运行结果现象
区分两种模式关键点:
消息过滤
在更复杂的场景下,我们可能会使用消息列表来跟踪状态,例如我们可能只想将这个完整消息列表的子集传递模型调用,而不是所有的历史记录。
filter_messages 方法则可以轻松地按类型、ID 或名称过滤 message。
下面演示相关过滤示例,首先准备消息列表:
代码讲解
导入需要的消息类与 filter_messages 过滤工具;构造消息列表,每条消息设置 id 属性,后续可以依据 id 做过滤。
按类型进行筛选
我们可以按类型进行筛选:

代码讲解
include_types="human" 只保留 HumanMessage 用户消息,过滤掉系统消息和 AI 回复消息。有两种调用形式:
按类型 + ID 进行筛选:
也可以按类型 + ID 进行筛选:
代码讲解
- include_types=[HumanMessage, AIMessage]:保留用户消息与 AI 消息,排除 SystemMessage 系统消息。
- exclude_ids=["3"]:把 id 等于"3"的消息剔除,也就是 id=3 那一条 AI 消息 “示例输出” 会被过滤掉,不会出现在返回结果。
消息合并
若我们的消息列表存在连续某种类型相同的消息,但实际上某些模型不支持传递相同类型的连续消息。因此对于这种情况,我们可以使用 merge_message_runs 方法轻松合并相同类型的连续消息。
示例如下:
代码讲解
合并结果:
合并之后,连续两条 SystemMessage 合并为一条,内容中间使用换行符 \\n 拼接;连续两条 HumanMessage、连续两条 AIMessage 同样完成合并;非连续同类型消息不会合并,最后的单条 HumanMessage 保持原样。
下面调用大模型:
代码讲解
本文介绍了LangChain中管理大模型对话的核心组件与方法,重点讲解了消息历史的管理技术。主要内容包括:1. 消息结构解析:详细说明消息角色、内容和元数据的组成,展示OpenAI和LangChain两种消息格式的转换方法。2. 多轮对话实现:通过代码示例演示如何通过维护完整消息列表来保持对话上下文,解决模型无状态的问题。3. 消息管理技术:- 内存缓存:使用InMemoryChatMessageHistory实现对话记忆(已不推荐用于生产环境)- 消息裁剪:基于token数或消息数的修剪方法(trim_messages)- 消息过滤:按类型或ID筛选消息(filter_messages)- 消息合并:处理连续同类消息(merge_message_runs)4. 上下文窗口概念:解释token计数和模型内存限制对对话长度的影响。文章强调理解组件功能比死记接口更重要,并提供了实际的代码示例说明各种消息管理技术的应用场景和效果。

![[LangChain RAG] 06 文档加载、Chroma 向量库与检索增强问答-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260829115912-6a92c990605f7-220x150.png)