Skip to content

消息协议

要点

  • 对 Agent 来说,真正的输入单位不是单个字符串,而是带角色的消息数组。
  • 消息数组的价值在于把上下文拆清楚:系统规则、用户输入、模型回复、工具结果各有位置。
  • 前期只需要理解四种角色:systemuserassistanttool
  • systemPrompt 是 Agent 的长期设定,messages 是当前请求的上下文。两者职责分开,代码结构更清晰。
  • 多轮对话和工具调用之所以依赖消息协议,是因为它们需要把中间状态按顺序回传给模型。

1. 背景:从字符串到消息数组

上一篇里,最小 Agent 的调用代码大致如下:

typescript
const stream = await agent.stream(
  {
    messages: [
      {
        role: "user",
        content: "请用一句话说明当前是 Agent 流式调用验证。",
      },
    ],
  },
  {
    streamMode: "messages",
  },
);

这里最值得注意的地方不是 stream(),而是 messages。传给 Agent 的不是一段字符串,而是一组消息。这说明在 LangChain 里,到了 Agent 这一层,真正重要的输入单位已经不是「一句话」,而是「一组带角色的上下文」。

2. 为什么 Agent 需要消息数组

如果只是一次最简单的生成,模型当然可以直接吃字符串:

typescript
const response = await model.invoke("帮我生成一段测试日志的示例");

但只要开始做 Agent,很快就会遇到这些内容:

  • 系统设定
  • 用户这一轮输入
  • 模型上一轮说过的话
  • 工具执行结果
  • 多轮对话历史

如果把这些内容全都揉成一段长字符串,代码也能跑,但会有两个明显问题:

第一,角色容易混。

你很难稳定区分哪些是系统规则,哪些是用户输入,哪些又是模型自己上一轮说的话。字符串没有结构,只能靠分隔符或约定格式,既不鲁棒又难维护。

第二,上下文难维护。

一旦接上工具、记忆、历史消息,这段大字符串会越来越长,也越来越难改。任何一个新角色加入,都需要重新调整拼接逻辑。

所以 LangChain 会把这些内容都整理成消息。从 Agent 的角度看,这样更自然:

  • Agent 接收的是一组消息。
  • Agent 根据消息判断下一步怎么做。
  • Agent 再把新的消息加回这条上下文里。

3. 四种核心角色

这一阶段不需要把所有消息类名都背下来,先把下面四种角色理解透就够了:

角色谁发出的作用
system开发者 / 系统给 Agent 立规则、定身份
user用户表达这一轮的问题或要求
assistant模型 / Agent表示 Agent 自己给出的回复
tool工具执行层把工具运行结果回给 Agent

可以用更直白的方式记它们:

  • system:告诉 Agent 你是谁、该遵守什么规则。
  • user:告诉 Agent 用户现在要什么。
  • assistant:告诉 Agent 你刚刚说过什么。
  • tool:告诉 Agent 工具刚刚做完了什么。

tool 角色在前面三种里用得最少,但一讲工具调用,它就变得必不可少。

4. 对象字面量与消息类

在 LangChain 里,消息有两种常见写法:

  • 直接写对象字面量。
  • 显式使用消息类。

当前阶段建议先用对象字面量,因为它最短、最直观。直接调模型时可以这样写:

typescript
const response = await model.invoke([
  {
    role: "system",
    content: "你是一名说话简洁的技术助手。",
  },
  {
    role: "user",
    content: "请解释一下为什么 Agent 要用消息数组。",
  },
]);

console.log(response.text);

Agent 调用时也类似:

typescript
const stream = await agent.stream(
  {
    messages: [
      {
        role: "user",
        content: "解释一下消息协议。",
      },
    ],
  },
  {
    streamMode: "messages",
  },
);

这两段代码都在做同一件事:把输入整理成消息,再交给模型或 Agent。

如果后面开始显式操作消息对象,也可以这样导入:

typescript
import { HumanMessage, SystemMessage } from "langchain";
// 或者:import { HumanMessage, SystemMessage } from "@langchain/core/messages"

消息类和对象字面量的功能等价,只是前者提供了更多类型安全和便利方法。在简单场景里,对象字面量足够。

5. systemPrompt 与 messages 的职责边界

把消息放回 Agent 运行过程里,会更清楚。一个最小 Agent 通常这样定义:

typescript
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  apiKey: process.env.MODEL_API_KEY,
  model: process.env.MODEL_NAME ?? "deepseek-chat",
  configuration: {
    baseURL: process.env.MODEL_BASE_URL ?? "https://api.deepseek.com/v1",
  },
});

const agent = createAgent({
  model,
  tools: [],
  systemPrompt: "你是一名说话自然、简短的技术助手。",
});

这里的 systemPrompt 是 Agent 的默认设定。真正运行时,当前这一轮的输入再通过 messages 传进去:

typescript
const stream = await agent.stream(
  {
    messages: [
      {
        role: "user",
        content: "解释一下消息协议。",
      },
    ],
  },
  {
    streamMode: "messages",
  },
);

所以这一层的关系可以先这样理解:

  • systemPrompt:Agent 自带的长期设定,每次请求都会带上。
  • messages:当前这轮请求带进来的上下文,动态变化。

前期写代码时,一个稳的做法是:

  • 人设、规则、语气要求,放进 systemPrompt
  • 用户输入、多轮上下文、工具结果,放进 messages

这样代码结构最清楚,也方便后续把 systemPrompt 抽成配置、把 messages 交给记忆模块管理。

6. 多轮对话与工具调用为什么离不开消息

单轮调用时,消息协议的价值还不算特别明显。一旦进入多轮对话,它就立刻变得很重要。

比如下面这组消息:

typescript
const messages = [
  {
    role: "system",
    content: "你是一名专业、简洁的技术助手。",
  },
  {
    role: "user",
    content: "我今天被需求折腾得很烦。",
  },
  {
    role: "assistant",
    content: "先别急,和我说说最卡的是哪一段。",
  },
  {
    role: "user",
    content: "明明昨天定好的方案,今天又全改了。",
  },
];

如果没有中间这条 assistant 消息,模型看到的只是两条用户输入。带上它之后,Agent 才能知道刚才回复过什么,从而接着往下说。消息的顺序和角色共同决定了对话的连续性。

工具调用也是一样。当 Agent 判断要调用工具时,流程通常会变成:

  1. 用户发来请求。
  2. Agent 判断要不要调工具。
  3. 工具执行。
  4. 工具结果回到消息流里。
  5. Agent 继续生成最终回复。

工具结果并不是「额外塞回模型」的一段文本,它也是消息流的一部分,通常以 tool 角色出现。因此,消息协议是工具调用和多轮对话能够稳定工作的基础。

如果你在旧资料里看到 FunctionMessage,可以把它当成早期 function calling 语义里的历史概念。当前这条主线里,更值得优先理解的是 systemuserassistanttool

7. 总结

消息协议是 LangChain Agent 的基础约定。先记住三件事:

  1. 对 Agent 来说,真正的输入单位是 messages,不是单个字符串。
  2. 前期最常用的四种角色是 systemuserassistanttool
  3. 消息协议不是为了让写法更复杂,而是为了把上下文分清楚,让 Agent 能稳定继续工作。

接下来要考虑的问题是:既然输入已经是结构化消息了,那 Prompt 模板是怎么把这些消息稳定组织起来的呢?

基于 MIT 协议开源