【LangChain 开发】LangChain核心组件——消息


🚀 欢迎来到我的CSDN博客:Optimistic _ chen
一名热爱技术与分享的全栈开发者,在这里记录成长,专注分享编程技术与实战经验,助力你的技术成长之路,与你共同进步!


🚀我的专栏推荐

专栏内容特色适合人群
🔥C语言从入门到精通系统讲解基础语法、指针、内存管理、项目实战零基础新手、考研党、复习
🔥Java基础语法系统解释了基础语法、类与对象、继承Java初学者
🔥Java核心技术面向对象、集合框架、多线程、网络编程、新特性解析有一定语法基础的开发者
🔥Java EE 进阶实战Servlet、JSP、SpringBoot、MyBatis、项目案例拆解想快速入门Java Web开发的同学
🔥Java数据结构与算法图解数据结构、LeetCode刷题解析、大厂面试算法题面试备战、算法爱好者、计算机专业学生
🔥Redis系列从数据类型到核心特性解析项目必备

🚀我的承诺:
✅ 文章配套代码:每篇技术文章都提供完整的可运行代码示例

✅ 持续更新:专栏内容定期更新,紧跟技术趋势

✅ 答疑交流:欢迎在文章评论区留言讨论,我会及时回复(支持互粉)


🚀 关注我,解锁更多技术干货!
⏳ 每天进步一点点,未来惊艳所有人!✍️ 持续更新中,记得⭐收藏关注⭐不迷路 ✨

📌 标签:#技术博客#编程学习#Java#C语言#算法#程序员

核心组件

开始学习LangChain的时候,就说过它是统一封装一套标准化的组件,本质上都是围绕⼤模型调⽤衍⽣出来的⼀系列功能扩展
因此LangChain 的核心组件围绕其模块化和可组合的设计理念构建,旨在让开发者能像“搭积木”一样,灵活地构建强大的 AI 应用。它的核心组件可以归纳为以下几个关键部分:模型与提示词模板,链(工作流),记忆组件,检索器与向量存储,Agents,工具组件,回调系统

LLM消息结构

消息是聊天模型中的通信单位,用于表示聊天模型的输入、输出和对话相关的上下文数据

LLM的消息结构:每条消息都有角色和内容,以及附加元数据

  • 消息角色:区分对话中不同类型的消息,帮助聊天模型了解如何响应给定的消息序列
角色描述
system(系统角色)告诉聊天模型如何⾏动并提供额外的上下⽂
user(用户角色)用户与大模型交互的输入
assistant(助理角色)来自模型的响应,包括文本或调用工具的请求
tool(工具角色)检索外部数据或将⼯具调⽤的结果传递回模型的消息
  • 消息内容:表示数据的消息文本或者字典表的内容;内容的具体格式因LLM底层而异。目前大部分LLM支持文本作为主要内容类型,对多模态数据支持有限。
  • 消息元数据:消息标识符,有关消息的其他信息,例如时间戳、令牌使⽤情况等。模型发出的⼀个或多个⼯具的调⽤请求。

LangChain消息结构

由于不同的大语言模型(LLM)具有各自独特的消息结构,**当 LangChain 调用不同的大模型时,就需要处理来自不同 LLM 的响应消息。**为此,LangChain 提供了一套统一的消息格式,使得开发者可以跨聊天模型使用,无需关心各个模型提供商之间的消息格式差异。

deepseek_model=init_chat_model("deepseek-v4-pro",model_provider="deepseek")

LangChain的消息格式:

消息类型对应角色描述
SystemMessage对应system系统角色⽤于启动AI模型的⾏为并提供额外的上下⽂
HumanMessage对应user用户角色⽤⼾与模型交互的输⼊
AIMessage对应assistant助理角色LLM的响应
AIMessageChunk对应assistant助理角色(流式响应)⽣成聊天模型时流式传输响应,⽤⼾可以实时看到响应
ToolMessage对应tool工具角色调用工具的结果

对话模式

⼤多数对话都以设置对话上下⽂的系统消息开始。接下来是包含⽤⼾输⼊的⽤⼾消息,然后是包含模型响应的助⼿消息。

缓存历史消息

在与⼤型语⾔模型交互的过程中,我们常常体验到与智能助⼿进⾏连贯多轮对话的便利性,想要实现多轮对话,必须获得历史消息:
最简单的方式就是把在发送新消息时,将历史消息重新发送给大模型,就可以实现多轮对话。
其次就是内存缓存历史消息的方法,使用RunnableWithMessageHistory消息历史类来包装另一个Runnable并为其管理聊天消息历史记录。

from operator import itemgetter
from langchain_openai import ChatOpenAI
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import BaseMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_core.runnables.history import RunnableWithMessageHistory
from pydantic import BaseModel, Field

# 1. 定义内存消息历史存储类
class InMemoryHistory(BaseChatMessageHistory, BaseModel):
    """内存中的聊天消息历史实现[reference:0]"""
    messages: list[BaseMessage] = Field(default_factory=list)

    def add_messages(self, messages: list[BaseMessage]) -> None:
        """向存储中添加消息列表[reference:1]"""
        self.messages.extend(messages)

    def clear(self) -> None:
        """清空历史记录[reference:2]"""
        self.messages = []

# 2. 创建会话历史存储工厂
# 使用全局字典存储不同会话的历史记录[reference:3]
store = {}

def get_by_session_id(session_id: str) -> BaseChatMessageHistory:
    """根据 session_id 获取或创建对应的消息历史对象[reference:4]"""
    if session_id not in store:
        store[session_id] = InMemoryHistory()
    return store[session_id]

# 3. 构建带历史记录的对话链
# 定义提示模板,包含历史消息占位符[reference:5]
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一位乐于助人的助手。"),
    MessagesPlaceholder(variable_name="history"),  # 历史消息将插入此处
    ("human", "{question}"),
])

# 创建基础链:提示模板 + 模型
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model

# 4. 使用 RunnableWithMessageHistory 包装链[reference:6]
chain_with_history = RunnableWithMessageHistory(
    chain,
    get_by_session_id,                    # 会话历史工厂函数
    input_messages_key="question",        # 输入中代表用户问题的键
    history_messages_key="history",       # 提示模板中历史消息的变量名
)

# 5. 调用示例
# 第一次对话(session_id="session_1")
result1 = chain_with_history.invoke(
    {"question": "我叫小明,我喜欢吃苹果。"},
    config={"configurable": {"session_id": "session_1"}},  # 指定会话ID[reference:7]
)
print("第一次回复:", result1.content)

# 第二次对话,使用相同 session_id,模型会记住之前的对话
result2 = chain_with_history.invoke(
    {"question": "我喜欢吃什么水果?"},
    config={"configurable": {"session_id": "session_1"}},
)
print("第二次回复:", result2.content)

# 新会话(session_id="session_2"),历史记录独立
result3 = chain_with_history.invoke(
    {"question": "我喜欢吃什么水果?"},
    config={"configurable": {"session_id": "session_2"}},
)
print("新会话回复:", result3.content)

在这里插入图片描述

注意:LangChain弃用警告:RunnableWithMessageHistory 已被弃用。请改用 LangGraph 内置的持久化功能。

管理历史消息

管理历史消息之前,我们知道多轮对话的核⼼是上下⽂窗⼝:短期工作记忆区也就是LLM在一次处理请求时,所能处理的最大Token(增量文本块)数量。(不同⼤模型⽀持的上下⽂窗⼝⼤⼩不同)
多轮对话的核心输入=系统消息+对话历史+新用户消息,对于模型来说,每一次请求都是将完整的上下文重新输入。由于所有模型的上下⽂窗⼝⼤⼩都是有限的,这意味着作为输⼊的Token也是有限的。如果有累积了很⻓的消息历史记录,则需要管理传递给模型的消息的⻓度。

消息裁剪

trim_messages可⽤于将聊天历史记录的⼤⼩减⼩为指定的令牌计数或指定的消息计数。

import os
import warnings
from typing import List, Union
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage
from langchain_core.messages import trim_messages
from transformers import AutoTokenizer

# 1. 忽略 PyTorch 未找到的警告(不影响 tokenizer)
warnings.filterwarnings("ignore", message="PyTorch was not found")

# 2. 设置 Hugging Face 镜像(解决网络问题,按需启用)
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"  # 如果仍无法下载,可换为 "https://hf-mirror.com"

# 3. 加载 DeepSeek-V4-Pro 的 tokenizer
MODEL_NAME = "deepseek-ai/DeepSeek-V4-Pro"
print(f"正在加载 tokenizer: {MODEL_NAME} ...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
print("加载成功!")

# 4. 自定义计数函数(不使用 chat_template,手动拼接消息格式)
def count_tokens_deepseek(messages: Union[str, List[BaseMessage]]) -> int:
    """
    为 DeepSeek 模型计算 token 数量。
    如果传入字符串,直接编码;
    如果传入 BaseMessage 列表,则按照 DeepSeek 的对话格式拼接后编码。
    """
    if isinstance(messages, str):
        return len(tokenizer.encode(messages))

    # 按照 DeepSeek 官方格式拼接
    # 参考:<|startoftext|><|system|>...<|endoftext|><|user|>...<|endoftext|><|assistant|>...<|endoftext|>
    formatted = "<|startoftext|>"
    for msg in messages:
        if msg.type == "system":
            formatted += f"<|system|>{msg.content}<|endoftext|>"
        elif msg.type == "human":
            formatted += f"<|user|>{msg.content}<|endoftext|>"
        elif msg.type == "ai":
            formatted += f"<|assistant|>{msg.content}<|endoftext|>"
        else:
            # 其他类型(如 tool)按需添加,或作为普通文本处理
            formatted += f"<|{msg.type}|>{msg.content}<|endoftext|>"
    return len(tokenizer.encode(formatted))

# 5. 使用示例
if __name__ == "__main__":
    # 准备一段模拟对话
    messages = [
        SystemMessage("你是一位乐于助人的助手。"),
        HumanMessage("我很好奇它为什么叫 LangChain"),
        AIMessage('嗯,我猜是因为 "WordRope" 和 "SentenceString" 听起来不够酷吧!'),
        HumanMessage("那么 Harrison 到底在追什么呢"),
        AIMessage("让我想想...他可能是在追办公室里最后一杯咖啡!"),
        HumanMessage("不会说话的鹦鹉叫什么"),
    ]

    print(f"\n原始消息数量: {len(messages)}")

    # 使用自定义计数函数修剪到最多 45 个 token
    trimmed = trim_messages(
        messages,
        max_tokens=45,
        strategy="last",                     # 保留最后的消息
        token_counter=count_tokens_deepseek, # 传入自定义计数器
        include_system=True,                 # 始终保留系统消息
    )

    print(f"修剪后消息数量: {len(trimmed)}")
    print("\n修剪后的消息列表:")
    for msg in trimmed:
        print(f"  {msg.type}: {msg.content}")

在这里插入图片描述
注意:使用deepseek、MiMo 等国内LLM,因为没有实现get_num_tokens_from_messages ,所以使用自定义token计数函数:

  1. 模型提供商通常会开源其 Tokenizer。例如,Hugging Face (需要魔法)上一般有对应的 tokenizer 文件。
  2. 实现计数函数,函数需要接受两种输入:str类型:直接编码并返回长度;List[BaseMessage] 类型:需要将消息列表转换为模型所需的对话格式字符串,然后编码计数。

下图解释trim_messages的各个参数:
在这里插入图片描述

消息过滤

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

假设我们有如下的消息列表:

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

messages = [
    SystemMessage("你是一个乐于助人的助手。", id="1"),
    HumanMessage("你好,我叫小明。", id="2", name="user_xiaoming"),
    AIMessage("你好小明!很高兴认识你。", id="3", name="assistant_ai"),
    HumanMessage("我喜欢吃苹果。", id="4", name="user_xiaoming"),
    AIMessage("苹果很健康,我也很喜欢。", id="5", name="assistant_ai"),
    HumanMessage("你喜欢吃什么水果?", id="6", name="user_lili"),
    AIMessage("我喜欢吃香蕉。", id="7", name="assistant_ai"),
]

按照类型过滤:只保留用户消息

human_msgs = filter_messages(messages, include_types="human")

消息合并

若我们的消息列表存在连续某种类型相同的消息,但实际上某些模型不⽀持传递相同类型的连续消息。因此对于这种情况,我们可以使⽤merge_message_runs⽅法轻松合并相同类型的连续消息。
最简单的应用:合并后直接调用模型

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

# 准备有冗余消息的列表
messages = [
    HumanMessage("你好,我叫小明。"),
    HumanMessage("我喜欢吃苹果。"),  # 连续 HumanMessage
    AIMessage("你好小明!苹果很健康。"),
    AIMessage("我也很喜欢水果。"),   # 连续 AIMessage
]

# 构建链
merger = merge_message_runs()
model = ChatOpenAI(model="gpt-4o-mini")
chain = merger | model

# 调用
response = chain.invoke(messages)
print(response.content)

完结撒花!🎉

如果这篇博客对你有帮助,不妨点个赞支持一下吧!👍
你的鼓励是我创作的最大动力~

想获取更多干货? 欢迎关注我的专栏 → optimistic_chen
📌 收藏本文,下次需要时不迷路!

我们下期再见!💫 持续更新中……


悄悄说:点击主页有更多精彩内容哦~ 😊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Optimistic _ chen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值