创见博客
LangChain 里的消息(Message):结构、种类与工具调用流转
七崽爱吃小饼干2026/10/14阅读 0

在用 LangChain 构建对话应用时,你几乎一直在和「消息」(Message)打交道。它是对话的基本单位:用户说的话是一条消息,模型的回复是一条消息,系统设定是一条消息,工具返回的结果也是一条消息。理解消息的结构和种类,是写出多轮对话、工具调用、Agent 等复杂能力的前提。这篇文章就来讲清楚:消息是什么、长什么样、有哪些种类,以及它有哪几种表示形式。

一、消息是什么

一个朴素的想法是:对话不就是一串字符串吗?用户发一句,模型回一句,拼起来就行。但真实场景远不止于此——你需要区分谁在说话(系统 / 用户 / 助手)、携带工具调用信息、附带 token 用量和元数据……用裸字符串根本表达不了。

于是 LangChain 用「消息」来建模一次对话中的每一条内容。整段对话就是一个消息数组:

js
const messages = [
  new SystemMessage("你是一个简洁的中文助手。"),
  new HumanMessage("今天天气怎么样?"),
  new AIMessage("今天北京晴,气温 25℃。"),
];

模型接收这个数组,然后返回一条新的消息作为回复。多轮对话,就是把新的消息不断追加进这个数组。

二、消息的结构

每一条消息本质上都是一个对象,主要字段如下:

字段说明
content消息内容,通常是字符串,也可以是内容块(content blocks)数组
type消息类型,如 human、ai、system、tool
name可选,消息的名称
additional_kwargs附加参数(厂商特有的字段)
response_metadata响应元信息(模型名、finish_reason、token 用量等)
id消息的唯一标识

例如一条 HumanMessage 序列化出来大致是这样:

json
{
  "content": "你好,请介绍一下你自己",
  "type": "human",
  "additional_kwargs": {},
  "response_metadata": {}
}

可以看出,content 加上 type 就已经勾勒出一条消息的基本轮廓,其余字段则在更复杂的场景中发挥作用。

三、消息的几种表示形式

同一条消息,在 LangChain 里可以有多种「长相」。理解它们的区别,能帮你少踩很多坑。常见的有四种:

1. 类实例(对象格式):这是运行时的主要形式,带有 .content、.concat()、.contentBlocks 等方法。

js
new HumanMessage("你好");

2. MessageLike(简写输入):很多时候你不需要手动 new,LangChain 提供了 coerceMessageLikeToMessage 把简写自动转成消息对象。以下写法都合法:

js
"你好"                             // 字符串,默认当作人类消息
["human", "你好"]                  // [type, content] 元组
{ role: "user", content: "你好" }  // role 字典

3. lc 序列化 JSON:对消息对象做 JSON.stringify,得到的是 LangChain 的序列化格式(带 lc、id、kwargs),用于框架内部的序列化 / 反序列化:

json
{
  "lc": 1,
  "type": "constructor",
  "id": ["langchain_core", "messages", "HumanMessage"],
  "kwargs": { "content": "你好", "additional_kwargs": {}, "response_metadata": {} }
}

4. StoredMessage(存储格式):用于业务持久化,结构更扁平。用 mapChatMessagesToStoredMessages() 得到,用 mapStoredMessageToChatMessage() 还原:

json
{
  "type": "human",
  "data": { "content": "你好", "additional_kwargs": {}, "response_metadata": {} }
}

简单区分:对象是运行时形态;JSON 其实有两种——lc 序列化格式用于框架内部存取,StoredMessage 用于业务持久化;而 MessageLike 则是书写时的快捷方式。日常开发大多直接传字符串或消息对象即可,只有在做持久化或框架间传递时才会接触到后两者。

四、消息的种类

LangChain 提供了多种消息类,对应对话中不同的角色和用途:

类type作用
SystemMessagesystem设定模型的角色与行为规范,通常放在对话最前面
HumanMessagehuman代表用户输入
AIMessageai代表模型回复,可能携带 tool_calls
AIMessageChunkaistream 流式返回时的片段,可 concat 合并
ToolMessagetool携带工具执行结果,用 tool_call_id 关联对应的调用
ChatMessagegeneric通用消息,可指定任意 role
RemoveMessageremove用于在消息历史中删除某条消息

几个要点值得展开:

  • 系统消息(SystemMessage):定义“助手是谁、要遵守什么规则”。它不会出现在用户的输入里,但会深刻影响模型的行为,比如“你是一个专业的法律顾问,回答需严谨”。
  • 用户消息(HumanMessage):就是用户的提问或指令。
  • 助手消息(AIMessage):模型返回的内容。普通的文本回复之外,它还可能出现 tool_calls 字段——表示模型希望调用某个工具。
  • 工具消息(ToolMessage):当模型决定调用工具后,由你的代码执行工具,把结果包成 ToolMessage 再送回给模型。它必须带上 tool_call_id,与之前那次工具调用一一对应,这样模型才知道“这是哪次调用的结果”。
  • 流式片段(AIMessageChunk):用 stream 时,回复是一小片一小片返回的,每一片就是一个 AIMessageChunk。
  • 通用消息(ChatMessage):当内置角色不够用时,可以自定义角色。
  • 删除消息(RemoveMessage):在管理较长的消息历史时,用来移除某条消息。

五、工具调用中的消息流转

消息最能体现价值的地方,是工具调用(Tool Calling)。整个流程就是消息在几种类型之间流转:

js
import { SystemMessage, HumanMessage, ToolMessage } from "@langchain/core/messages";
import { initChatModel, tool } from "langchain";
import { z } from "zod";

// 1. 定义一个工具
const getWeather = tool(
  ({ city }) => `${city} 今天晴,气温 25℃`,
  {
    name: "get_weather",
    description: "查询指定城市的天气",
    schema: z.object({ city: z.string().describe("要查询的城市名") }),
  }
);

// 2. 给模型绑定工具
const model = await initChatModel("deepseek:deepseek-chat", { apiKey: process.env.DEEPSEEK_API_KEY });
const modelWithTools = model.bindTools([getWeather]);

// 3. 构造对话历史
const messages = [
  new SystemMessage("你是一个助手,需要实时信息时请调用工具。"),
  new HumanMessage("北京今天天气怎么样?"),
];

// 4. 第一次调用:模型返回带有 tool_calls 的 AIMessage
const aiResp = await modelWithTools.invoke(messages);
// aiResp.content === ""
// aiResp.tool_calls === [{ name: "get_weather", args: { city: "北京" }, id: "call_xxx", type: "tool_call" }]
messages.push(aiResp);

// 5. 执行工具,把结果包成 ToolMessage 放回历史
for (const call of aiResp.tool_calls ?? []) {
  const result = await getWeather.invoke(call.args);
  messages.push(
    new ToolMessage({
      content: String(result),
      tool_call_id: call.id,
      name: call.name,
    })
  );
}

// 6. 再次调用:模型结合工具结果给出最终答复
const finalResp = await modelWithTools.invoke(messages);
console.log(finalResp.content);
// 北京今天天气晴朗,气温约 25℃,比较舒适,适合外出活动。

这段代码结束后,消息历史的 type 序列是:

system -> human -> ai -> tool -> ai

可以看到,一次工具调用其实是「助手消息提出请求 → 工具消息返回结果 → 助手消息给出答案」的三段式,而每一步都用消息来表达。这也解释了为什么 ToolMessage 必须带 tool_call_id:一次对话里可能有好几个工具调用,得靠 id 把它们各自的结果对上号。

六、为什么用消息而不是字符串

  • 能区分角色:系统、用户、助手、工具各司其职,模型据此理解上下文结构;
  • 能承载结构化信息:工具调用、token 用量、元数据都挂在消息上;
  • 能支持多轮与流式:历史是一个消息数组,流式回复是一个个消息片段。

字符串能表达“说了什么”,消息则能表达“谁、对谁、说了什么、调用了什么”,这是构建真实应用的基础。

七、小结

消息是 LangChain 对话模型的输入输出单位。一条消息由 content、type 以及若干可选字段组成;常见类型包括系统消息、用户消息、助手消息、工具消息,以及流式片段、通用消息和删除消息。其中最值得掌握的是工具调用场景下的消息流转:AIMessage(带 tool_calls)→ ToolMessage(带 tool_call_id)→ AIMessage(最终答复)。理解了这一点,你就理解了 LangChain 对话与 Agent 的底层骨架。

评论
0/100