LangChain:消息

1. 消息(Messages)

消息是聊天模型中的通信单位,用于表示聊天模型的输入和输出,以及可能与对话关联的任何其他上下文或元数据。

1.1 LLM 消息结构

每条消息都有一个角色内容,以及因 LLM 的不同而不同的附加元数据。

消息角色(Role):用来区分对话中不同类型的消息,并帮助聊天模型了解如何响应给定的消息序列。

角色 描述
system(系统角色) 用于告诉聊天模型如何行为并提供额外的上下文,并非所有聊天模型都提供系统角色。
user(用户角色) 表示用户与模型交互的输入,通常以文本或其他交互式输入的形式。
assistant(助理角色) 表示模型响应,其中可以包括文本或调用工具的请求。

(系统角色:比如,某些早期的开源模型可能只区分"用户"和"助手",没有专门设置"系统"角色的地方。LangChain 提炼出这个概念,是为了统一接口,遇到不支持的模型时,它可能会自动把系统消息转换成用户消息来兼容处理。)


1.2 LangChain 消息

在没有统一抽象层的情况下,每个大模型服务商(如 OpenAI、Anthropic、Google)都定义了自己独特的 API 数据格式。

这就像一个多国语言交流的场景:

  • 给英国朋友写信,必须用英文。

  • 给法国朋友写信,必须用法文。

  • 给日本朋友写信,必须用日文。

映射到开发中就是:

  • 调用 OpenAI 的 GPT,消息要写成 {"role": "system", "content": "..."} 这样的字典。

  • 调用 Anthropic 的 Claude,格式可能要求 HumanAssistant 角色严格交替出现。

  • 调用 Google 的 Gemini,参数和结构可能又是另一套规则。

开发者的痛点: 每接入或切换一个模型,都必须去查阅该模型的专属文档,并重写消息构建的代码。这就是"需要担心每个模型提供的消息格式的具体细节"。

而:

LangChain为我们提供了 SystemMessageHumanMessageAIMessage 等基础消息类,开发者只需学习并采用这一种格式编写代码,剩下的"翻译"工作,由 LangChain 内部的集成模块自动完成。也就是说:LangChain 提供了一种统一的消息格式,可以跨聊天模型使用,允许用户使用不同的聊天模型,无需担心每个模型提供的消息格式的具体细节。

代码对比:

没有LangChain:每换一次模型,都是一次重构。

python 复制代码
# ---- 调用 OpenAI 的写法 ----
# 必须按照 OpenAI 要求的字典格式来
openai_messages = [
    {"role": "system", "content": "你是一个助手。"},
    {"role": "user", "content": "你好"}
]
# response = openai.ChatCompletion.create(model="gpt-4", messages=openai_messages)


# ---- 如果有一天要换成 Anthropic ----
# 之前的格式就失效了!必须重写。
# Anthropic 可能要求 Human/Assistant 角色交替,结构截然不同。
# anthropic_messages = [...]
# response = anthropic_client.completion(prompt=anthropic_messages)

有了LangChain:

python 复制代码
# 1. 导入 LangChain 定义好的"普通话"消息组件
from langchain_core.messages import SystemMessage, HumanMessage

# 2. 用统一格式构建消息列表(这就是你需要关心的全部!)
my_messages = [
    SystemMessage(content="你是一个助手。"),
    HumanMessage(content="你好")
]

# 3. 想用 OpenAI?
from langchain_openai import ChatOpenAI
model_openai = ChatOpenAI(model="gpt-4o-mini")
# 直接传入统一格式,LangChain 内部自动转换成 OpenAI 的"方言"
response_openai = model_openai.invoke(my_messages)
print(response_openai.content)

# 4. 想换成 Anthropic?只需改一行实例化代码!
from langchain_anthropic import ChatAnthropic
model_anthropic = ChatAnthropic(model="claude-3-haiku")
# 还是那个 my_messages!这次 LangChain 会自动翻译成 Claude 的"方言"
response_anthropic = model_anthropic.invoke(my_messages)
print(response_anthropic.content)

消息构建逻辑(my_messages)完全不用修改。切换模型的成本从"重写整个消息处理模块"骤降为"更改一行模型类名"。这就是"无需担心具体细节"的真正含义。


1.3 缓存历史消息

LangChain 提供了缓存消息的功能,可以避免重复计算,从而提高效率。例如:

  • cacheKey:表示缓存键值对,当模型返回相同的值时,会直接返回缓存结果。

  • cacheTime:表示缓存的有效时间,超过该时间的缓存将被删除。

1.3.2 内存缓存

大语言模型本身是没有记忆的,就像你每次跟它说话,都是你们这辈子第一次见面。它不记得你上一句说过什么。比如这个场景:

  • 你:我叫张三。

  • AI:好的,张三。

  • 你:我叫什么名字?

  • AI(一脸懵):不知道啊,你刚才没告诉我。

要让AI"记住"对话,就必须在每次提问时,把之前所有的聊天记录重新发给AI,就像每次说话前,先把你们从认识开始的聊天记录打印出来拍在它面前。

所以对于历史消息的管理尤为重要,在 LangChain 老版本中,可以使用 RunnableWithMessageHistory 消息历史类来包装另一个 Runnable 并为其管理聊天消息历史记录,它将跟踪模型的输入和输出,并将其存储在某个数据存储中,未来的交互将加载这些消息,并将其作为输入的一部分传递给链。

代码如下:

python 复制代码
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()
# 模型回复:Hi Bob! How can I assist you today?
# 同时,"Hi! I'm Bob" 和模型回复都被自动存入了store

# 第二轮对话
with_message_history.invoke(
    [HumanMessage(content="What's my name?")],
    config=config,
).pretty_print()
# 模型回复:Your name is Bob!

RunnableWithMessageHistory 类初始化参数说明:

  • runnable:被包装的 Runnable 实例,这里就是我们定义的聊天模型。

  • get_session_history:返回类型为 BaseChatMessageHistory 的函数,传入后作为回调函数。此函数接受一个 session_id 字符串类型,并返回相应的聊天消息历史记录实例。

RunnableWithMessageHistory 类方法说明:

  • .invoke():此方法与其他 Runnable 实例的 .invoke() 方法相同。只不过注意其 config 配置,需要配置成 config={"configurable": {"session_id": ""}},以便 RunnableWithMessageHistory 可以读取到会话 ID。
说明

从 LangChain v0.3 版本开始,官方建议用户不要使用 RunnableWithMessageHistory,而是利用 LangGraph 持久性 来完成。原因是它们的功能有限,不太适合现实世界的对话式 AI 应用程序,这些内存抽象缺乏对多用户、多对话场景的内置支持,而这对于实际的对话式人工智能系统至关重要。这些实现中的大多数已在 LangChain 0.3.x 中被正式弃用,取而代之的是 LangGraph 持久性,LangGraph 持久性非常灵活,可以支持比 RunnableWithMessageHistory 更广泛的用例。因此,RunnableWithMessageHistory 这部分了解的并不深入,在之前,对于生产环境,还需要使用聊天消息历史记录的持久化实现,例如 RedisChatMessageHistory(),而不是 InMemoryChatMessageHistory(),但现在也已不推荐新应用使用它们了。


1.4 管理历史消息

1.4.1 前置概念
1.4.1.1 上下文窗口

管理历史消息,无非就是理解如何"管理","管理"无非也就是一些"CRUD"(增删改查)。那么在了解如何管理消息之前,需要先了解下多轮对话的核心概念:上下文窗口。上下文窗口可以理解为模型的"短期工作记忆区",即 LLM 在一次处理请求时,所能查看和处理的最大 Token 数量,它包含了:

  • 用户的输入

  • 大模型的输出

  • 有时还包括系统指令(SystemMessage)和对话历史。

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

  • OpenAI 下 GPT-5 模型上下文窗口为 400000(最大 Token 数)

  • GPT-4.1 模型上下文窗口为 1047576(最大 Token 数)

  • 其他模型上下文窗口可参考对应模型官网说明,如 OpenAI 下模型可以参考这里

把大语言模型想象成一个手艺高超的工匠。把Token想象成一个个积木零件:

正是因为上下文窗口这个"工作台"大小有限,而不断累积的聊天记录又会迅速把它占满,我们才需要:

  • 消息裁剪:把旧的、不重要的对话零件从工作台上清理出去,腾出空间。

  • 消息过滤:只从历史里挑选特定类型的消息发给模型。

  • 消息合并:把零碎的同类消息压缩成一个,节省空间。

1.4.1.2 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,生僻字或复杂词可能会被拆分成多个。


1.4.2 消息裁剪

有了上下文窗口和 Token 的认知,再来看多轮对话的实现原理,其实就是:

  • 输入 = 系统消息 + 对话历史 + 最新用户问题

  • 对于模型来说,并不真正"记忆",而是每次都将完整的上下文重新输入。

所有模型的上下文窗口大小都是有限的,这意味着作为输入的 Token 也是有限的,如果有累积很长的消息历史记录,则需要管理传递给模型的消息的长度。

trim_messages 可用于将聊天历史记录的大小减小为指定的令牌计数或指定的消息计数。

1.4.2.1 基于输入 Token 数的修剪

下面演示一个通过 trim_messages 裁剪消息的示例(基于输入 Token 数的修剪)。

先来看看不做任何输入限制的聊天:

python 复制代码
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"),
    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!"),
]

print(model.invoke(messages))

打印结果:

Ai Message

...

token_usage: {

completion_tokens: 5,

prompt_tokens: 88,

total_tokens: 93

}

...

从打印结果看来,LLM 还认识我们,且共输入了 88 tokens。接下来让我们对消息进行裁剪,我们希望将来输入时,最多输入 65 tokens,超出的需要按照一定的"规则"进行裁剪,代码如下:

python 复制代码
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?"),
]

# 使用 trim_messages 对消息进行裁剪
trimmer = trim_messages(
    max_tokens=65,          # 最大 token 数,超出会被裁剪
    strategy="last",        # 保留策略:"last" 保留最后的消息;"first" 保留最前的消息
    token_counter=model,    # 传入模型以计算 token 数
    include_system=True,    # 是否始终包含 system message
    allow_partial=False,    # 是否允许拆分消息的内容
    start_on="human",       # 保证裁剪后的消息以 human 开头(或符合对话模式)
)

chain = trimmer | model
print(chain.invoke(messages))

打印结果:

复制代码
Ai Message
...
  token_usage: {
    completion_tokens: 16,
    prompt_tokens: 60,
    total_tokens: 76
  }
...

可以看见,此时我们的输入 message 已经被修剪了,被修剪了哪些消息呢?来看下:

python 复制代码
print(trimmer.invoke(messages))

结果如下:

复制代码
[
    SystemMessage(content="you're a good assistant"),
    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?")
]

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

  • 聊天记录以 HumanMessage 或 SystemMessage 开头,后跟 HumanMessage。这可以通过设置 start_on="human" 来实现。

  • 聊天记录以 HumanMessage 或 ToolMessage 结尾。这可以通过设置 ends_on=("human", "tool") 来实现。

  • ToolMessage 只能出现在涉及工具调用的 AIMessage 之后。

  • 如果原始聊天历史记录中存在 SystemMessage,则新聊天历史记录应包括 SystemMessage,因为 SystemMessage 包含对聊天模型的特殊说明。SystemMessage 总是历史记录中的第一条消息(如果存在)。这可以通过设置 include_system=True

1.4.2.2 基于消息数的修剪

除了基于 token 的修剪,还可以通过设置 token_counter=len 根据消息数修剪聊天记录,在这种情况下,max_tokens 将控制最大消息数。示例如下:

python 复制代码
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?"),
]

# 使用 trim_messages 基于消息数裁剪
trimmer = trim_messages(
    max_tokens=7,            # 现在控制最大消息数量
    token_counter=len,       # 将 len 作为计数器,计算消息条数
    strategy="last",         # 保留最后的消息
    include_system=True,     # 始终包含 system message
    allow_partial=False,
    start_on="human",        # 裁剪后的消息列表,除了 SystemMessage,第一条必须是 HumanMessage
)

print(trimmer.invoke(messages))

结果如下:

复制代码
[
    SystemMessage(content="you're a good assistant"),
    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?")
]

1.4.3 消息过滤

在更复杂的场景下,我们可能会使用消息列表来跟踪状态,例如我们可能只想将这个完整消息列表的子集传递模型调用,而不是所有的历史记录。

filter_messages 方法则可以轻松地按类型、ID 或名称过滤 message。

下面演示相关过滤示例,首先准备消息列表:

python 复制代码
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, filter_messages

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

按类型进行筛选:

python 复制代码
print(filter_messages(messages, include_types="human"))

# 结果:
# [
#     HumanMessage(content='示例输入', id='2'),
#     HumanMessage(content='真实输入', id='4')
# ]

按类型+ID进行筛选:

python 复制代码
print(filter_messages(messages, include_types=[HumanMessage, AIMessage], exclude_ids=["3"]))

# 结果:
# [
#     HumanMessage(content='示例输入', id='2'),
#     HumanMessage(content='真实输入', id='4'),
#     AIMessage(content='真实输出', id='5')
# ]

1.4.4 消息合并

若我们的消息列表存在连续某种类型相同的消息,但实际上某些模型不支持传递相同类型的连续消息。因此对于这种情况,我们可以使用 merge_message_runs 方法轻松合并相同类型的连续消息。

示例如下:

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

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

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

merged = merge_message_runs(messages)
# 打印合并后的结果
print("\n".join([repr(x) for x in merged]))

合并结果:

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

调用大模型:

python 复制代码
merged = merge_message_runs(messages)
model.invoke(messages).pretty_print()

# 或者使用链式调用
merger = merge_message_runs()
chain = merger | model
chain.invoke(messages).pretty_print()

打印结果:

Ai Message 这就像选择汉堡还是热狗!如果你想要一个多层次的体验,选择LangChain;如果你想要一个清晰的链接,LangGraph就像是你的"支架"!让你的代码加个"料"!

相关推荐
求知也求真佳1 小时前
S07---S11 | 系统加固闭环总结:让你的 AI Agent 从 “能跑” 到 “稳跑、安全跑、长期跑”
开发语言·agent
JAVA学习通1 小时前
开云集致 Java开发 实习 一面
java·开发语言
小陈工2 小时前
Python异步编程进阶:asyncio高级模式与性能调优
开发语言·前端·数据库·人工智能·python·flask·numpy
阿旭超级学得完2 小时前
C++11(初始化)
java·开发语言·数据结构·c++·算法
是有头发的程序猿2 小时前
竞品店铺拆解:1688店铺首页装修数据API Python实战教程
开发语言·python
一只大袋鼠2 小时前
SpringMVC全局异常处理
java·开发语言·springmvc·javaweb
JaydenAI2 小时前
[Deep Agents:LangChain的Agent Harness-12]利用create_deep_agent整合所有的Harness中间件
langchain·agent·deep agents·harness
rit84324992 小时前
基于 MATLAB 的坐标变换程序
开发语言·matlab
不知名的老吴2 小时前
C++中emplace函数的不适场景总结(一)
java·开发语言·c++