【LangChain】消息系统全解析:从原生结构到多轮对话管理


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言
  • [一. LLM 原生消息结构:对话的基本单位](#一. LLM 原生消息结构:对话的基本单位)
    • [1.1 四大核心消息角色](#1.1 四大核心消息角色)
    • [1.2 消息的完整组成](#1.2 消息的完整组成)
  • [二. LangChain 统一消息体系:跨模型的对话抽象](#二. LangChain 统一消息体系:跨模型的对话抽象)
  • [三. BaseMessage 抽象基类:所有消息的父类](#三. BaseMessage 抽象基类:所有消息的父类)
    • [3.1 核心属性](#3.1 核心属性)
    • [3.2 常用内置方法](#3.2 常用内置方法)
  • [四. 标准对话模式:消息的交互流程](#四. 标准对话模式:消息的交互流程)
  • 五、历史消息缓存:实现多轮对话的基础
    • [5.1 大模型的 "失忆" 问题](#5.1 大模型的 "失忆" 问题)
    • [5.2 手动管理历史消息](#5.2 手动管理历史消息)
    • [5.3 RunnableWithMessageHistory(已弃用,推荐 LangGraph)](#5.3 RunnableWithMessageHistory(已弃用,推荐 LangGraph))
  • [六. 历史消息管理:突破上下文窗口限制](#六. 历史消息管理:突破上下文窗口限制)
    • [6.1 前置概念:上下文窗口与 Token](#6.1 前置概念:上下文窗口与 Token)
    • [6.2 消息裁剪:保留最相关的上下文](#6.2 消息裁剪:保留最相关的上下文)
      • [6.2.1 基于 Token 数的裁剪](#6.2.1 基于 Token 数的裁剪)
      • [6.2.2 基于消息数的裁剪](#6.2.2 基于消息数的裁剪)
    • [6.3 消息过滤:只传递必要的消息](#6.3 消息过滤:只传递必要的消息)
    • [6.4 消息合并:解决连续同类型消息问题](#6.4 消息合并:解决连续同类型消息问题)
  • 结尾:

前言

在构建任何对话式 AI 应用时,消息系统都是最基础也最核心的组件。大语言模型(LLM)本身是无状态的,它不会记住之前的对话内容,每一次调用都是独立的。要实现连贯的多轮对话,我们必须手动管理所有的历史消息,并将它们作为上下文传递给模型。LangChain 作为最流行的 LLM 应用开发框架,提供了一套完整且统一的消息抽象体系。它不仅屏蔽了不同模型提供商(OpenAI、Anthropic、DeepSeek 等)的消息格式差异,还内置了历史消息的缓存、裁剪、过滤和合并等实用工具,让开发者可以专注于业务逻辑,而不用纠结于底层的消息格式转换和上下文管理。本文将从 LLM 原生消息结构讲起,深入解析 LangChain 的五种核心消息类型,然后通过实战代码演示如何实现多轮对话,以及如何高效管理历史消息以突破上下文窗口的限制。


一. LLM 原生消息结构:对话的基本单位

在了解 LangChain 的消息系统之前,我们首先需要理解大语言模型本身是如何接收和发送消息的。所有现代聊天模型都采用基于角色 - 内容的消息格式。

1.1 四大核心消息角色

每条消息都必须指定一个角色(Role),用来区分对话中不同类型的参与者:

角色 描述
system(系统角色) 用于告诉聊天模型如何行为并提供额外的上下文。例如:"你是一个后端开发专家,回答要简洁专业"。并非所有模型都支持系统消息。
user(用户角色) 表示用户与模型交互的输入,通常是文本形式。
assistant(助理角色) 表示来自模型的响应,可以包含文本或工具调用请求。
tool(工具角色) 用于将工具调用的结果传递回模型,仅在支持工具调用的模型中使用。

1.2 消息的完整组成

一条完整的 LLM 消息除了角色和内容外,还包含一些重要的元数据:

  • content(内容):消息的主体,可以是文本或多模态数据(如图像、音频)。
  • id(消息标识符):每条消息的唯一 ID。
  • name(名称):允许区分具有相同角色的不同实体,例如不同的工具调用者。
  • metadata(元数据):有关消息的其他信息,如时间戳、令牌使用情况等。
  • tool_calls(工具调用):模型发出的一个或多个工具调用请求。

下面是一个标准的 OpenAI API 请求示例,展示了原生的消息格式:

python 复制代码
import openai

client = openai.OpenAI()
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Hello!"}
    ],
    stream=True
)

二. LangChain 统一消息体系:跨模型的对话抽象

不同的模型提供商(OpenAI、Anthropic、Google 等)的消息格式存在细微差异。如果直接使用原生格式,当我们需要切换模型时,就需要修改大量的代码。

LangChain 解决了这个问题,它提供了一套统一的消息格式,可以跨所有聊天模型使用。无论你使用的是 GPT-4o、Claude 3.5 还是 DeepSeek,都可以用同一套代码来构建消息。

LangChain 定义了五种核心消息类型,分别对应 LLM 的不同角色:

消息类型 对应角色 描述
SystemMessage system 用于设置 AI 模型的行为和上下文,例如指定模型的角色、回答风格等。
HumanMessage user 表示人类用户的输入消息。
AIMessage assistant 表示 AI 模型生成的响应消息,可以包含文本或工具调用。
AIMessageChunk assistant(流式) 用于流式响应,表示 AIMessage 的一部分。当使用流式传输时,模型会逐块返回内容,每一块都是一个 AIMessageChunk。
ToolMessage tool 表示工具调用的结果消息,包含工具执行后的返回值。

这五种消息类型都是BaseMessage抽象类的子类,它们共同构成了 LangChain 消息系统的基础。


三. BaseMessage 抽象基类:所有消息的父类

BaseMessage是 LangChain 中所有消息类型的抽象基类,它定义了所有消息都必须具备的属性和方法。

3.1 核心属性

python 复制代码
class BaseMessage:
    content: str  # 消息的字符串内容
    additional_kwargs: dict  # 与消息关联的其他有效负载数据,如工具调用
    response_metadata: dict  # 响应元数据,如响应头、令牌计数、模型名称
    type: str  # 消息的类型,唯一标识消息类型
    name: Optional[str] = None  # 消息的人类可读名称
    id: Optional[str] = None  # 消息的可选唯一标识符

3.2 常用内置方法

  • pretty_print():以美观的格式打印消息,非常适合调试。
  • pretty_repr(html: bool = False):获取消息的美观表示,支持 HTML 格式。
  • text():获取消息的文本内容。

实战示例:创建并打印不同类型的消息

python 复制代码
from langchain_core.messages import (
    SystemMessage,
    HumanMessage,
    AIMessage,
    ToolMessage
)

# 创建系统消息
system_msg = SystemMessage(content="你是一个后端开发专家,回答要简洁专业。")
# 创建用户消息
human_msg = HumanMessage(content="什么是RESTful API?")
# 创建AI响应消息
ai_msg = AIMessage(content="RESTful API是一种基于HTTP协议的软件架构风格,用于设计网络应用程序接口。")
# 创建工具消息
tool_msg = ToolMessage(content="查询结果:Python 3.13发布于2024年10月", tool_call_id="call_123")

# 美观打印消息
print("=== 系统消息 ===")
system_msg.pretty_print()
print("\n=== 用户消息 ===")
human_msg.pretty_print()
print("\n=== AI消息 ===")
ai_msg.pretty_print()

四. 标准对话模式:消息的交互流程

大多数对话都遵循一个标准的模式:

  1. 以一条系统消息开始,设置对话的上下文和模型的行为。
  2. 然后是用户消息AI 响应消息的交替。
  3. 如果涉及工具调用,流程会变成:用户消息 → AI 工具调用请求 → 工具执行结果(ToolMessage) → AI 最终响应。
Plain 复制代码
System message
    ↓
Human message  →  Turn 1
    ↓
AI message
    ↓
Human message  →  Turn 2
    ↓
AI message (tool call)
    ↓
Tool message
    ↓
AI message (final response)

五、历史消息缓存:实现多轮对话的基础

5.1 大模型的 "失忆" 问题

大语言模型本身是无状态的,它不会记住之前的对话内容。每一次调用模型都是独立的,模型只能看到当前请求中传递的消息列表。

错误示例:无法实现多轮对话

python 复制代码
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")

# 第一次对话
model.invoke(HumanMessage(content="我是小明,你好!")).pretty_print()
# 输出:你好,小明!很高兴认识你!今天有什么想聊的呢?

# 第二次对话
model.invoke(HumanMessage(content="你知道我是谁吗?")).pretty_print()
# 输出:抱歉,我无法知道您是谁,因为我没有访问个人信息的能力。

可以看到,第二次调用时模型完全忘记了我们之前说过的话。

5.2 手动管理历史消息

要实现多轮对话,最简单的方法是手动维护一个消息列表,每次对话都将新的用户消息和 AI 响应添加到列表中,然后将整个列表传递给模型。

正确示例:手动实现多轮对话

python 复制代码
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage

model = ChatOpenAI(model="gpt-4o-mini")

# 维护历史消息列表
messages = [
    HumanMessage(content="我是小明,你好!")
]

# 第一次对话
ai_response = model.invoke(messages)
ai_response.pretty_print()
messages.append(ai_response)  # 将AI响应添加到历史

# 第二次对话
messages.append(HumanMessage(content="你知道我是谁吗?"))
model.invoke(messages).pretty_print()
# 输出:我只知道你叫小明,但并不了解更多关于你的信息。

5.3 RunnableWithMessageHistory(已弃用,推荐 LangGraph)

LangChain 曾经提供了RunnableWithMessageHistory类来自动管理历史消息,但从v0.3 版本开始,官方已正式弃用该方法 ,推荐使用LangGraph 持久性来实现。

原因是RunnableWithMessageHistory功能有限,缺乏对多用户、多对话场景的内置支持,而 LangGraph 持久性更加灵活,能够支持更广泛的生产级用例。我们将在后续的 LangGraph 专题中详细讲解。


六. 历史消息管理:突破上下文窗口限制

随着对话的进行,历史消息会越来越长,最终会超过模型的上下文窗口限制。因此,我们需要对历史消息进行有效的管理。

6.1 前置概念:上下文窗口与 Token

  • 上下文窗口:模型在一次处理请求时所能查看和处理的最大 Token 数量,包含用户输入和模型输出。
  • Token:文本的基本单位。对于中文,1 个汉字约等于 1.5-2 个 Tokens;对于英文,1 个 Token 约等于 4 个字符或 0.75 个单词。

不同模型的上下文窗口大小不同:

  • GPT-5:400,000 Tokens
  • GPT-4.1:1,047,576 Tokens
  • GPT-4o-mini:128,000 Tokens

6.2 消息裁剪:保留最相关的上下文

LangChain 提供了trim_messages函数,可以根据指定的规则裁剪历史消息,确保其不超过上下文窗口的限制。

6.2.1 基于 Token 数的裁剪

最常用的裁剪方式是根据Token 数来裁剪,保留最新的消息。

python 复制代码
from langchain_core.messages import trim_messages, SystemMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")

# 模拟一段较长的历史消息
messages = [
    SystemMessage(content="you're a good assistant"),
    HumanMessage(content="hi! I'm bob"),
    AIMessage(content="hi!"),
    HumanMessage(content="I like vanilla ice cream"),
    AIMessage(content="nice"),
    HumanMessage(content="whats 2 + 2"),
    AIMessage(content="4"),
    HumanMessage(content="thanks"),
    AIMessage(content="no problem!"),
    HumanMessage(content="having fun?"),
    AIMessage(content="yes!"),
    HumanMessage(content="What's my name?")
]

# 创建裁剪器
trimmer = trim_messages(
    max_tokens=65,  # 最大Token数
    strategy="last",  # 保留最后的消息
    token_counter=model,  # 使用模型来计算Token数
    include_system=True,  # 始终保留系统消息
    allow_partial=False,  # 不允许拆分消息内容
    start_on="human"  # 确保第一条非系统消息是用户消息
)

# 裁剪消息
trimmed_messages = trimmer.invoke(messages)
print("裁剪后的消息数量:", len(trimmed_messages))
print("\n裁剪后的消息:")
for msg in trimmed_messages:
    msg.pretty_print()

运行结果分析:裁剪后,早期的消息(如 "hi! I'm bob")被移除了,只保留了最新的几条消息。由于上下文不足,模型将无法回答 "What's my name?" 这个问题。

6.2.2 基于消息数的裁剪

我们也可以根据消息数 来裁剪,只需将token_counter设置为len

python 复制代码
trimmer = trim_messages(
    max_tokens=11,  # 最大消息数
    strategy="last",
    token_counter=len,  # 按消息数计算
    include_system=True,
    allow_partial=False,
    start_on="human"
)

6.3 消息过滤:只传递必要的消息

在复杂场景中,我们可能只想将消息列表的一个子集传递给模型。LangChain 提供了filter_messages函数,可以按类型、ID 或名称过滤消息。

python 复制代码
from langchain_core.messages import filter_messages

messages = [
    SystemMessage("你是一个聊天助手", id="1"),
    HumanMessage("示例输入", id="2"),
    AIMessage("示例输出", id="3"),
    HumanMessage("真实输入", id="4"),
    AIMessage("真实输出", id="5"),
]

# 只保留用户消息
human_messages = filter_messages(messages, include_types="human")
print("只保留用户消息:")
for msg in human_messages:
    msg.pretty_print()

# 排除ID为3的消息
filtered_messages = filter_messages(messages, exclude_ids=["3"])
print("\n排除ID为3的消息:")
for msg in filtered_messages:
    msg.pretty_print()

6.4 消息合并:解决连续同类型消息问题

某些模型不支持传递连续相同类型的消息。例如,连续两条用户消息或连续两条 AI 消息可能会导致模型报错。

LangChain 提供了merge_message_runs函数,可以自动合并相同类型的连续消息:

python 复制代码
from langchain_core.messages import merge_message_runs

messages = [
    SystemMessage("你是一个聊天助手。"),
    SystemMessage("你总是以笑话回应。"),
    HumanMessage("为什么要使用LangChain?"),
    HumanMessage("为什么要使用LangGraph?"),
    AIMessage("因为当你试图让你的代码更有条理时,LangGraph会让你感到'节点'是个好主意!"),
    AIMessage("不过别担心,它不会'分散'你的注意力!"),
    HumanMessage("选择LangChain还是LangGraph?"),
]

# 合并连续同类型消息
merged_messages = merge_message_runs(messages)
print("合并后的消息:")
for msg in merged_messages:
    print(f"{type(msg).__name__}: {msg.content}")

运行结果

Plain 复制代码
SystemMessage: 你是一个聊天助手。
你总是以笑话回应。
HumanMessage: 为什么要使用LangChain?
为什么要使用LangGraph?
AIMessage: 因为当你试图让你的代码更有条理时,LangGraph会让你感到'节点'是个好主意!
不过别担心,它不会'分散'你的注意力!
HumanMessage: 选择LangChain还是LangGraph?

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:LangChain 的消息系统是构建所有对话式 AI 应用的基石。它的核心价值体现在三个方面:统一跨模型接口 :屏蔽了不同模型提供商的消息格式差异,实现了模型的无缝切换。完整的消息类型体系 :提供了五种核心消息类型,覆盖了所有对话场景,包括工具调用和流式响应。丰富的历史消息管理工具 :内置了裁剪、过滤和合并等功能,帮助开发者高效管理上下文,突破模型的上下文窗口限制。需要特别注意的是,虽然RunnableWithMessageHistory曾经是实现多轮对话的常用方法,但在 LangChain v0.3 及以上版本中,官方已推荐使用LangGraph 持久性来替代。LangGraph 提供了更强大、更灵活的状态管理能力,能够更好地支持生产级的复杂对话系统。在下一篇文章中,我们将深入探讨 LangChain 的提示词模板系统,学习如何构建高效、可复用的提示词,进一步提升 AI 应用的质量和稳定性。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど