消息管理与多轮对话Messages裁剪 / 过滤 / 合并消息Messages是聊天模型的通信单位表示模型的输入和输出。本篇系统讲解消息的结构与角色、LangChain 的五种消息类型、如何实现多轮对话、如何用历史缓存自动管理记忆以及消息的裁剪、过滤与合并。目录LLM 消息结构角色、内容、元数据LangChain 的五种消息类型BaseMessage 抽象基类多轮对话的原理内存缓存RunnableWithMessageHistory前置概念上下文窗口与 Token消息裁剪trim_messages消息过滤filter_messages消息合并merge_message_runs本篇小结1. LLM 消息结构角色、内容、元数据每条消息都有一个角色和内容以及因模型而异的附加数据。1.1 消息角色Role角色用来区分对话中不同类型的消息帮助模型理解如何响应给定的消息序列角色描述system系统角色告诉模型如何行为、提供额外上下文并非所有供应商都支持user用户角色表示用户与模型交互的输入通常是文本或其他交互式输入assistant助理角色表示来自模型的响应可包含文本或调用工具的请求tool工具角色用于把工具调用结果传回模型与支持工具调用的模型一起使用1.2 消息内容Content表示消息文本或多模态数据图像、音频、视频的字典列表。具体格式因底层模型而异目前大多数模型仍主要支持文本。1.3 消息其他元数据Additional metadata元数据描述ID消息标识符Name区分相同角色的不同实体并非所有模型都支持Metadata其他信息如时间戳、令牌使用情况Tool Calls模型发出的一个或多个工具调用请求OpenAI 原生格式的消息列表长这样[{role:user,content:Hello, how are you?},{role:assistant,content:Im doing well, thank you for asking.},{role:user,content:Can you tell me a joke?},]2. LangChain 的五种消息类型LangChain 提供了统一的消息格式可以跨模型使用换模型时不用关心各家格式差异。五种消息类型消息类型对应角色描述SystemMessagesystem启动 AI 行为、提供上下文如「你是一个后端开发专家」HumanMessageuser表示用户输入大多数模型希望用户输入是文本AIMessageassistant来自模型的响应可包含文本或工具调用请求AIMessageChunkassistant流式传输时的消息块让用户实时看到响应ToolMessagetool角色为 tool 的消息包含调用工具的结果它们都是BaseMessage的子类全部作为聊天模型的输入和输出。3. BaseMessage 抽象基类langchain_core.messages.base.BaseMessage是所有消息的基类也是聊天模型的输入输出。参数content消息的字符串内容。additional_kwargs与消息关联的其他负载数据AI 消息可能包含工具调用。response_metadata响应元数据如响应标头、logprobs、令牌计数、模型名称。type消息类型的唯一字符串用于反序列化时识别类型。name消息的人类可读名称可选。id消息的可选唯一标识符理想情况由提供者生成。内置方法pretty_print()漂亮地打印消息。pretty_repr(htmlFalse)获取消息的漂亮表示htmlTrue时用 HTML 标记。text()获取消息的文本内容。3.1 对话模式大多数对话以一条设置上下文的 SystemMessage 开始然后是用户的 HumanMessage再是模型的 AIMessage如此往复。涉及工具时AIMessage工具调用之后会跟一条 ToolMessage然后才是新的 AIMessage。4. 多轮对话的原理模型本身不会自动记住上一轮说了什么。下面这段代码第二次提问时模型并不认识我们fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportHumanMessage modelChatOpenAI(modelgpt-4o-mini)# 第一次对话model.invoke([HumanMessage(contentHi! Im Bob)])# 第二次对话resultmodel.invoke([HumanMessage(contentWhats my name?)])# 模型回答我没有关于你的个人信息...不记得 Bob解决方法很简单把历史消息一起重新发给模型fromlangchain_core.messagesimportHumanMessage,AIMessage messages[HumanMessage(contentHi! Im Bob),AIMessage(contentHello Bob! How can I assist you today?),HumanMessage(contentWhats my name?),]model.invoke(messages)# 模型回答Your name is Bob! ...记住核心原理模型并不真正「记忆」而是每次都把完整上下文重新输入。所谓多轮对话就是维护并传递一个不断增长的消息列表。5. 内存缓存RunnableWithMessageHistory手动维护消息列表比较麻烦。LangChain 老版本提供RunnableWithMessageHistory它包装另一个 Runnable 并自动管理聊天历史跟踪模型输入输出、存到某个数据存储中后续交互自动加载历史并作为输入的一部分传给链。fromlangchain_openaiimportChatOpenAIfromlangchain_core.chat_historyimportInMemoryChatMessageHistoryfromlangchain_core.runnables.historyimportRunnableWithMessageHistory modelChatOpenAI(modelgpt-4o-mini)# 用字典存储不同会话的历史store{}# 根据 session_id 返回对应的历史对象区分不同对话defget_session_history(session_id:str)-InMemoryChatMessageHistory:ifsession_idnotinstore:# 把消息存在内存列表中store[session_id]InMemoryChatMessageHistory()returnstore[session_id]# 包装 model自动管理历史with_message_historyRunnableWithMessageHistory(model,get_session_history)# 关键config 里要传 session_idconfig{configurable:{session_id:1}}with_message_history.invoke([HumanMessage(contentHi! Im Bob)],configconfig,)with_message_history.invoke([HumanMessage(contentWhats my name?)],configconfig,)# 模型能答出Your name is Bob!参数说明runnable被包装的 Runnable这里是聊天模型。get_session_history一个返回BaseChatMessageHistory的函数接收 session_id 并返回对应历史实例。invoke 时 config 必须配置成{configurable: {session_id: ...}}让包装器知道是哪个会话。5.1 重要说明版本变化从 LangChainv0.3 开始官方建议不要再用RunnableWithMessageHistory而是用LangGraph 持久性persistence来完成。原因这些内存抽象功能有限缺乏对多用户、多对话场景的内置支持不适合真实的对话式 AI 应用LangGraph 持久性更灵活、支持更广泛的用例。生产环境若要持久化历史原本可用RedisChatMessageHistory等而非纯内存的InMemoryChatMessageHistory但现在也不推荐在新应用中使用统一走 LangGraph。6. 前置概念上下文窗口与 Token6.1 上下文窗口上下文窗口可以理解为模型的「短期工作记忆区」即 LLM 一次请求能查看和处理的最大 Token 数量包含用户输入、模型输出有时还包括系统指令和对话历史。不同模型窗口大小不同例如 OpenAI GPT-5 上下文窗口为 400000GPT-4.1 为 1047576具体以官网为准。6.2 TokenToken 是文本的基本单位比单词或汉字更细粒度。计算机无法直接理解文字需要先做Tokenization令牌化把句子拆成模型能处理的碎片。英文1 个 Token ≈ 4 个字符或 0.75 个单词1000 Tokens ≈ 750 个英文单词。一个 Token 可以是单词、词根或标点。中文1 个汉字 ≈ 11.52 个 Tokens1000 Tokens ≈ 500~700 个汉字。常见词可能是一个 Token生僻字可能被拆成多个。形象比喻上下文窗口是固定大小的工作台Token 是积木零件模型是工匠。输入和输出的所有零件总数不能超过工作台容量零件太多占满了就要减少输入。7. 消息裁剪trim_messages多轮对话原理是「输入 系统消息 对话历史 最新问题」。历史越长输入 Token 越多可能超出上下文窗口因此需要管理消息长度——本质就是对消息做「CRUD」。trim_messages用于把聊天历史的大小减小为指定的令牌数或消息数。7.1 基于输入 Token 数裁剪不做限制时一段长历史输入可能用掉 88 个 Token。我们希望最多输入 65 个 Token超出的按规则裁掉fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportHumanMessage,SystemMessage,AIMessage,trim_messages modelChatOpenAI(modelgpt-4o-mini)messages[SystemMessage(contentyoure a good assistant),HumanMessage(contenthi! Im bob),AIMessage(contenthi!),HumanMessage(contentI like vanilla ice cream),AIMessage(contentnice),HumanMessage(contentwhats 2 2),AIMessage(content4),HumanMessage(contentthanks),AIMessage(contentno problem!),HumanMessage(contenthaving fun?),AIMessage(contentyes!),]# 配置裁剪器trimmertrim_messages(max_tokens65,# 最多保留的令牌数strategylast,# 裁剪策略last 保留最后的消息first 保留最早的token_countermodel,# 传入模型或函数来计算消息令牌数include_systemTrue,# 始终保留系统消息allow_partialFalse,# 是否允许拆分一条消息的内容start_onhuman,# 确保裁剪结果第一条非系统消息从 human 开始)chaintrimmer|model chain.invoke(messages)裁剪后实际保留的消息最早的几轮被删掉了[SystemMessage(contentyoure a good assistant),HumanMessage(contentwhats 2 2),AIMessage(content4),HumanMessage(contentthanks),AIMessage(contentno problem!),HumanMessage(contenthaving fun?),AIMessage(contentyes!),]7.2 裁剪结果要符合「对话模式原则」聊天记录以 HumanMessage 或 SystemMessage 开头后跟消息流 → 用start_onhuman保证。聊天记录以 HumanMessage 或 ToolMessage 结尾 → 可用ends_on(human, tool)保证。ToolMessage 只能出现在涉及工具调用的 AIMessage 之后。如果原始历史里有 SystemMessage裁剪结果应保留它它总是第一条→ 用include_systemTrue。7.3 基于消息数裁剪把token_counterlen就按消息条数裁剪此时max_tokens控制最大消息数trimmertrim_messages(max_tokens11,# 最多保留 11 条消息strategylast,token_counterlen,# 用 len 数消息条数include_systemTrue,allow_partialFalse,start_onhuman,)print(trimmer.invoke(messages))8. 消息过滤filter_messages复杂场景下可能只想把消息列表的某个子集传给模型filter_messages可以方便地按类型、ID、名称过滤fromlangchain_core.messagesimportHumanMessage,SystemMessage,AIMessage,filter_messages messages[SystemMessage(你是一个聊天助手,id1),HumanMessage(示例输入,id2),AIMessage(示例输出,id3),HumanMessage(真实输入,id4),AIMessage(真实输出,id5),]按类型筛选只保留人类消息resultfilter_messages(messages,include_typeshuman)# 等价写法filter_messages(include_typeshuman).invoke(messages)# 结果只保留 id2 和 id4 的 HumanMessage按类型 ID 筛选resultfilter_messages(messages,include_types[HumanMessage,AIMessage],exclude_ids[3],# 排除 id 为 3 的消息)# 保留 id 2、4、5排除 id 39. 消息合并merge_message_runs某些模型不支持连续传递多条相同类型的消息。如果消息列表里存在连续多条同类型消息可以用merge_message_runs把它们合并成一条fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportHumanMessage,SystemMessage,AIMessage,merge_message_runs messages[SystemMessage(你是一个聊天助手。),SystemMessage(你总是以笑话回应。),HumanMessage(为什么要使用 LangChain?),HumanMessage(为什么要使用 LangGraph?),AIMessage(因为当你试图让你的代码更有条理时LangGraph 会让你感到「节点」是个好主意),AIMessage(不过别担心它不会「分散」你的注意力),HumanMessage(选择LangChain还是LangGraph?),]mergedmerge_message_runs(messages)print(\n.join([repr(x)forxinmerged]))合并后连续的两条 SystemMessage 变成一条内容用换行拼接连续 HumanMessage、AIMessage 同理SystemMessage(content你是一个聊天助手。\n你总是以笑话回应。)HumanMessage(content为什么要使用 LangChain?\n为什么要使用 LangGraph?)AIMessage(content因为...好主意\n不过别担心它不会「分散」你的注意力)HumanMessage(content选择LangChain还是LangGraph?)调用模型有两种方式# 方式一先合并再调用model.invoke(merge_message_runs(messages))# 方式二把合并器本身做成链mergermerge_message_runs()chainmerger|model chain.invoke(messages)10. 本篇小结消息四要素角色system/user/assistant/tool、内容、元数据、ID。LangChain 五种消息SystemMessage、HumanMessage、AIMessage、AIMessageChunk、ToolMessage都继承BaseMessage。多轮对话原理模型不记忆每次把完整历史重新输入。RunnableWithMessageHistory可自动管理内存历史但v0.3 起官方推荐改用 LangGraph 持久性。上下文窗口是一次能处理的最大 Token 数Token 是文本的细粒度单位。三个消息工具trim_messages按 Token 数或消息数token_counterlen裁剪配合strategy / include_system / start_on。filter_messages按类型、ID、名称过滤。merge_message_runs合并连续同类型消息。裁剪结果必须符合对话模式开头是 system/human、ToolMessage 紧跟 AIMessage 等。下一篇我们讲提示词模板Prompt Template与少样本提示few-shotting让提示词可复用、可动态组装。
阅读完成 · 觉得有帮助?