目录

一、消息(Messages):LLM 交互的通用语言

1.1 LLM 原生消息结构

1.2 LangChain 统一消息格式

1.3 BaseMessage 抽象类

二、缓存历史消息:实现多轮对话的关键

2.1 手动管理对话历史

2.2 使用 RunnableWithMessageHistory 自动管理

三、管理历史消息:在 Token 限制下高效对话

3.1 理解上下文窗口与 Token

四、消息裁剪:在 Token 限制下的智能对话管理

4.1 基于 Token 数的智能裁剪

核心参数解析

实战示例

4.2 基于消息条数的简易裁剪

4.3 对话模式的完整性

五、消息过滤与合并:复杂场景下的消息流优化

5.1 消息过滤(Filtering)

按类型筛选

按类型和 ID 组合筛选

5.2 合并连续消息(Merging)


在大语言模型(LLM)的应用开发中,如何高效、规范地与模型交互是构建可靠应用的关键。LangChain 作为 LLM 应用开发的主流框架,通过一系列精心设计的核心组件,将复杂的交互流程抽象化、标准化。本文将深入解析其中最基础也最重要的部分 ——消息(Messages)历史消息管理,带你掌握构建连贯、智能对话系统的核心能力。


一、消息(Messages):LLM 交互的通用语言

消息是聊天模型的通信单位,它不仅承载了用户的输入和模型的输出,还包含了引导对话的上下文信息。理解消息的结构和类型,是使用 LangChain 的第一步。

1.1 LLM 原生消息结构

在与 LLM 交互时,每条消息都包含一个角色(Role)和内容(Content),以及因模型而异的附加元数据。

角色 (Role) 描述
system (系统角色) 用于告诉聊天模型如何行为并提供额外的上下文,例如设定角色或对话基调。
user (用户角色) 表示用户与模型交互的输入,通常是文本或其他交互式输入。
assistant (助理角色) 表示来自模型的响应,其中可以包含文本或调用工具的请求。
tool (工具角色) 用于在检索外部数据或将工具调用的结果传递回模型的消息。

以 OpenAI API 为例,一个典型的消息列表格式如下:

[
  {"role": "user", "content": "Hello, how are you?"},
  {"role": "assistant", "content": "I'm doing well, thank you for asking."},
  {"role": "user", "content": "Can you tell me a joke?"}
]

1.2 LangChain 统一消息格式

为了解决不同 LLM 提供商消息格式不统一的问题,LangChain 提供了一套抽象的消息类型,让开发者可以无缝切换模型,而无需关心底层细节。

LangChain 消息类型 对应角色 描述
SystemMessage system 用于启动 AI 模型的行为并提供额外的上下文。
HumanMessage user 表示用户与模型交互的输入。
AIMessage assistant 来自模型的响应,可包含文本或工具调用请求。
AIMessageChunk assistant 用于流式响应,在生成聊天模型时逐块传输响应。
ToolMessage tool 表示工具角色的消息,包含调用工具的结果。

这些消息类型都是 BaseMessage 抽象类的子类,它们共同构成了 LangChain 聊天模型的输入和输出。

1.3 BaseMessage 抽象类

langchain_core.messages.base.BaseMessage 是所有消息的基类,它定义了消息的核心属性和方法:

  • 核心参数

    • content: 消息的字符串内容。
    • additional_kwargs: 与消息关联的其他有效负载数据,如模型提供的工具调用。
    • response_metadata: 响应元数据,如响应标头、令牌计数、模型名称等。
    • type: 消息的类型,用于反序列化时识别消息类。
    • name: 可选的消息名称,提供人类可读的标识。
    • id: 可选的唯一标识符。
  • 内置方法

    • pretty_print(): 打印消息的漂亮表示。
    • pretty_repr(): 获得消息的漂亮表示。
    • text(): 获取消息的文本内容。

二、缓存历史消息:实现多轮对话的关键

LLM 本身是无状态的,每次调用都是独立的。要实现连贯的多轮对话,就必须将历史消息作为上下文,在每次新请求时重新发送给模型。

2.1 手动管理对话历史

最直接的方法是手动维护一个消息列表,每次交互后将新的 HumanMessageAIMessage 添加进去。

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage

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

# 手动构建包含历史的消息列表
messages = [
    HumanMessage(content="Hi! I'm Bob"),
    AIMessage(content="Hello Bob! How can I assist you today?"),
    HumanMessage(content="What's my name?"),
]

# 将完整历史发送给模型
model.invoke(messages).pretty_print()

输出结果

==================================================
Ai Message
==================================================
Your name is Bob! How can I help you today, Bob?

这种方法虽然有效,但在复杂应用中会变得繁琐且容易出错。

2.2 使用 RunnableWithMessageHistory 自动管理

为了简化历史管理,LangChain 提供了 RunnableWithMessageHistory 包装器。它可以自动跟踪模型的输入和输出,并将其存储在指定的存储介质中,未来的交互会自动加载这些历史。

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

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

# 定义一个存储,用于区分不同会话
store = {}

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

# 包装模型,使其具备记忆能力
with_message_history = RunnableWithMessageHistory(model, get_session_history)

# 配置会话ID
config = {"configurable": {"session_id": "1"}}

# 第一次对话
with_message_history.invoke([HumanMessage(content="Hi! I'm Bob")], config=config).pretty_print()

# 第二次对话,模型会自动记住之前的信息
with_message_history.invoke([HumanMessage(content="What's my name?")], config=config).pretty_print()

输出结果

==================================================
Ai Message
==================================================
Hi Bob! How can I assist you today?

==================================================
Ai Message
==================================================
Your name is Bob! How can I help you today, Bob?

⚠️ 重要提示:从 LangChain v0.3 开始,官方建议使用 LangGraph 持久性 来替代 RunnableWithMessageHistory,因为它更灵活,能更好地支持多用户、多对话场景。


三、管理历史消息:在 Token 限制下高效对话

模型的上下文窗口(Context Window)是有限的,它决定了模型一次能处理的最大 Token 数量。如果历史消息过长,就会超出 Token 限制,导致请求失败。因此,管理历史消息的核心在于在保持对话连贯性的同时,有效控制 Token 数量

3.1 理解上下文窗口与 Token

  • 上下文窗口:可以理解为模型的 “短期工作记忆区”,是 LLM 在一次处理请求时,所能查看和处理的最大 Token 数量。它包含了用户输入、模型输出、系统指令和对话历史。
  • Token:是文本的基本单位。对于英文,1 个 Token ≈ 4 个字符或 0.75 个单词;对于中文,1 个汉字 ≈ 1.5-2 个 Token。

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

  • OpenAI GPT-5: 400,000 Token
  • GPT-4 Turbo: 128,000 Token

四、消息裁剪:在 Token 限制下的智能对话管理

在上一部分中,我们了解了消息的基本结构和历史管理。但在实际应用中,模型的上下文窗口(Context Window)是有限的。当对话历史不断累积,Token 数量超过模型上限时,就会导致请求失败。因此,** 消息裁剪(Message Trimming)** 成为了在 Token 限制下维持对话连贯性的关键技术。

4.1 基于 Token 数的智能裁剪

LangChain 提供了强大的 trim_messages 函数,它允许我们根据指定的 Token 数量,智能地裁剪历史消息列表,确保每次发送给模型的上下文都在安全范围内。

核心参数解析
  • max_tokens: 裁剪后消息列表的最大 Token 数。
  • strategy: 裁剪策略,决定保留哪些消息。
    • "last" (默认): 保留列表中最后的消息,即最新的对话。
    • "first": 保留列表中最早的消息。
  • token_counter: 用于计算 Token 数的函数。可以传入模型本身(如 model),或简单的 len 函数(基于消息条数裁剪)。
  • include_system: 是否始终保留 SystemMessage,默认为 True
  • allow_partial: 是否允许拆分消息,默认为 False
  • start_on: 指定裁剪后列表的起始消息类型,确保对话结构完整。
实战示例

假设我们有一个较长的对话历史,模型的输入 Token 数为 88,而我们希望将其限制在 65 Token 以内:

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, trim_messages

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,
    strategy="last",
    token_counter=model,
    include_system=True,
    allow_partial=False,
    start_on="human",
)

# 执行裁剪并调用模型
chain = trimmer | model
print(chain.invoke(messages))

输出结果

content='I don't know your name. Would you like to share it with me?' 
...
usage_metadata={
    'input_tokens': 60,  # Token 数已被成功裁剪
    'output_tokens': 16,
    'total_tokens': 76
}

从结果可以看出,模型不再认识 “Bob”,因为早期的对话历史(包括自我介绍)已被裁剪,只保留了最近的交互。这证明了裁剪策略 "last" 正在生效。

4.2 基于消息条数的简易裁剪

除了基于 Token 数,我们还可以使用 token_counter=len 来根据消息条数进行裁剪,这在快速原型开发中非常方便:

trimmer = trim_messages(
    max_tokens=1,  # 这里的“tokens”指消息条数
    strategy="last",
    token_counter=len,
    include_system=True,
)

print(trimmer.invoke(messages))

结果

[
    SystemMessage(content="You're a good assistant", ...),
    HumanMessage(content="what's my name?", ...)
]

4.3 对话模式的完整性

无论采用何种裁剪策略,都必须遵循对话模式的基本原则,以确保模型能正确理解上下文:

  • 对话开头:聊天记录必须以 HumanMessageSystemMessage 开头,后跟 HumanMessage。可通过 start_on="human" 强制。
  • 对话结尾:聊天记录必须以 HumanMessageToolMessage 结尾。可通过 ends_on="human" 强制。
  • ToolMessage:只能出现在调用工具的 AIMessage 之后。

五、消息过滤与合并:复杂场景下的消息流优化

在更复杂的应用场景中,我们不仅需要裁剪消息,还需要对消息流进行更精细的控制。LangChain 提供了 filter_messagesmerge_message_runs 两个强大的工具。

5.1 消息过滤(Filtering)

filter_messages 允许我们根据消息的类型(type)、ID 或名称(name)来筛选消息,只将我们关心的部分传递给模型。

按类型筛选
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, filter_messages

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

# 只保留 HumanMessage
filtered = filter_messages(messages, include_types="human")
for msg in filtered:
    print(f"{msg.type}: {msg.content} (id={msg.id})")

输出

human: 示例输入 (id=2)
human: 真实输入 (id=4)
按类型和 ID 组合筛选
# 保留 HumanMessage 和 AIMessage,但排除 id="3"
filtered = filter_messages(messages, include_types=[HumanMessage, AIMessage], exclude_ids=["3"])
for msg in filtered:
    print(f"{msg.type}: {msg.content} (id={msg.id})")

输出

human: 示例输入 (id=2)
human: 真实输入 (id=4)
ai: 真实输出 (id=5)

5.2 合并连续消息(Merging)

某些模型不支持连续的相同类型消息(例如,连续两条 HumanMessage)。merge_message_runs 可以轻松地将连续的同类型消息合并为一条,避免模型报错。

from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, merge_message_runs

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

merged = merge_message_runs(messages)
for x in merged:
    print(f"\n{x.type}:")
    print(x.content)

合并结果

system:
你是一个聊天助手。
你总是以笑话回应。

human:
为什么要使用 LangChain?
为什么要使用 LangGraph?

ai:
因为当你试图让你的代码更有条理时,LangGraph 会让你感到"节点"是个好主意!
不过别担心,它不会"分散"你的注意力!

human:
选择LangChain还是LangGraph?

合并后的消息列表更加整洁,符合大多数模型的输入要求。我们可以将 merge_message_runs 作为预处理步骤,直接集成到我们的处理链中:

merger = merge_message_runs()
chain = merger | model
chain.invoke(messages).pretty_print()

六、总结与实践路径

通过对消息裁剪、过滤和合并的学习,我们已经掌握了在复杂场景下管理对话历史的完整工具箱:

  1. 消息裁剪 (trim_messages):核心是控制 Token 数量,确保在模型上下文窗口内。通过 strategystart_on 等参数,我们可以灵活地保留关键信息,同时丢弃冗余内容。
  2. 消息过滤 (filter_messages):用于精确控制哪些信息进入模型,保护敏感数据或简化模型输入。
  3. 消息合并 (merge_message_runs):用于处理模型兼容性问题,确保消息流格式正确。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐