在用 LangChain 构建对话应用时,你几乎一直在和「消息」(Message)打交道。它是对话的基本单位:用户说的话是一条消息,模型的回复是一条消息,系统设定是一条消息,工具返回的结果也是一条消息。理解消息的结构和种类,是写出多轮对话、工具调用、Agent 等复杂能力的前提。这篇文章就来讲清楚:消息是什么、长什么样、有哪些种类,以及它有哪几种表示形式。
一、消息是什么
一个朴素的想法是:对话不就是一串字符串吗?用户发一句,模型回一句,拼起来就行。但真实场景远不止于此——你需要区分谁在说话(系统 / 用户 / 助手)、携带工具调用信息、附带 token 用量和元数据……用裸字符串根本表达不了。
于是 LangChain 用「消息」来建模一次对话中的每一条内容。整段对话就是一个消息数组:
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 序列化出来大致是这样:
{
"content": "你好,请介绍一下你自己",
"type": "human",
"additional_kwargs": {},
"response_metadata": {}
}
可以看出,content 加上 type 就已经勾勒出一条消息的基本轮廓,其余字段则在更复杂的场景中发挥作用。
三、消息的几种表示形式
同一条消息,在 LangChain 里可以有多种「长相」。理解它们的区别,能帮你少踩很多坑。常见的有四种:
1. 类实例(对象格式):这是运行时的主要形式,带有 .content、.concat()、.contentBlocks 等方法。
new HumanMessage("你好");
2. MessageLike(简写输入):很多时候你不需要手动 new,LangChain 提供了 coerceMessageLikeToMessage 把简写自动转成消息对象。以下写法都合法:
"你好" // 字符串,默认当作人类消息
["human", "你好"] // [type, content] 元组
{ role: "user", content: "你好" } // role 字典
3. lc 序列化 JSON:对消息对象做 JSON.stringify,得到的是 LangChain 的序列化格式(带 lc、id、kwargs),用于框架内部的序列化 / 反序列化:
{
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "HumanMessage"],
"kwargs": { "content": "你好", "additional_kwargs": {}, "response_metadata": {} }
}
4. StoredMessage(存储格式):用于业务持久化,结构更扁平。用 mapChatMessagesToStoredMessages() 得到,用 mapStoredMessageToChatMessage() 还原:
{
"type": "human",
"data": { "content": "你好", "additional_kwargs": {}, "response_metadata": {} }
}
简单区分:对象是运行时形态;JSON 其实有两种——lc 序列化格式用于框架内部存取,StoredMessage 用于业务持久化;而 MessageLike 则是书写时的快捷方式。日常开发大多直接传字符串或消息对象即可,只有在做持久化或框架间传递时才会接触到后两者。
四、消息的种类
LangChain 提供了多种消息类,对应对话中不同的角色和用途:
| 类 | type | 作用 |
|---|---|---|
SystemMessage | system | 设定模型的角色与行为规范,通常放在对话最前面 |
HumanMessage | human | 代表用户输入 |
AIMessage | ai | 代表模型回复,可能携带 tool_calls |
AIMessageChunk | ai | stream 流式返回时的片段,可 concat 合并 |
ToolMessage | tool | 携带工具执行结果,用 tool_call_id 关联对应的调用 |
ChatMessage | generic | 通用消息,可指定任意 role |
RemoveMessage | remove | 用于在消息历史中删除某条消息 |
几个要点值得展开:
- 系统消息(SystemMessage):定义“助手是谁、要遵守什么规则”。它不会出现在用户的输入里,但会深刻影响模型的行为,比如“你是一个专业的法律顾问,回答需严谨”。
- 用户消息(HumanMessage):就是用户的提问或指令。
- 助手消息(AIMessage):模型返回的内容。普通的文本回复之外,它还可能出现
tool_calls字段——表示模型希望调用某个工具。 - 工具消息(ToolMessage):当模型决定调用工具后,由你的代码执行工具,把结果包成
ToolMessage再送回给模型。它必须带上tool_call_id,与之前那次工具调用一一对应,这样模型才知道“这是哪次调用的结果”。 - 流式片段(AIMessageChunk):用
stream时,回复是一小片一小片返回的,每一片就是一个AIMessageChunk。 - 通用消息(ChatMessage):当内置角色不够用时,可以自定义角色。
- 删除消息(RemoveMessage):在管理较长的消息历史时,用来移除某条消息。
五、工具调用中的消息流转
消息最能体现价值的地方,是工具调用(Tool Calling)。整个流程就是消息在几种类型之间流转:
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 的底层骨架。