从工程视角看,Messages 不是聊天 UI 的附属物,而是整个模型上下文的统一数据总线。LangChain 在这页文档里把 Message 定义为模型交互的基本单元:里面包含 rolecontentmetadata,并且跨 provider 保持统一接口。也就是说,你传给模型的,不只是“几句对话”,而是一组带身份、内容载体、工具调用结果、token 统计、流式分片信息的结构化上下文。(LangChain Docs)

如果把这个抽象吃透,很多工程决策会更清晰:


一、真正的分界线,不是 Prompt vs Chat,而是“单次生成” vs “可管理上下文”

文档里给了两种最基础的调用方式:

  1. 直接传字符串:model.invoke("Write a haiku about spring")
  2. 传消息列表:model.invoke([SystemMessage(...), HumanMessage(...)]) (LangChain Docs)

很多人会把这两者理解为“简洁写法”和“完整版写法”的区别,但从工程上看,真正的分界线是:

工程建议

如果你的场景满足下面任意一条,就不要再继续用裸字符串:

换句话说,一旦你的应用要上线,Messages 几乎就是默认选项。因为上线应用真正要处理的不是“让模型回答一句话”,而是“持续维护一个可演化的上下文”。


二、SystemMessage 不是“设定人设”,而是你系统边界的第一层防线

文档把 SystemMessage 描述为:用于给模型设定行为方式、角色和回复规范。它既可以是简单指令,也可以是详细 persona。(LangChain Docs)

但在工程里,SystemMessage 的价值远不止“你是一名 Python 专家”。

更实用的理解:SystemMessage 承载三类约束

1. 产品级约束

例如回答风格、输出语言、禁止暴露内部字段、遵守公司话术。

2. 任务级约束

例如“先判断用户意图,再输出 JSON”、“找不到信息时明确说不知道”。

3. 安全级约束

例如不要泄露工具返回的敏感原始数据,不要把内部 trace 回给用户。

一个更接近生产环境的写法

python
from langchain.chat_models import init_chat_model
from langchain.messages import SystemMessage, HumanMessage
 
model = init_chat_model("gpt-5-nano")
 
messages = [
    SystemMessage(
        content=(
            "你是企业内部智能助手。\n"
            "回答使用简体中文。\n"
            "优先给结论,再给依据。\n"
            "如果涉及工具结果,不要直接暴露原始内部字段。\n"
            "当信息不足时,明确说明不确定,不要编造。"
        )
    ),
    HumanMessage("帮我总结这份故障报告的核心结论")
]
 
resp = model.invoke(messages)
print(resp.text)

一个常见误区

很多团队把大量动态业务上下文也塞进 SystemMessage。这会带来两个问题:

更稳妥的做法是:

这样分层以后,日志、回放、裁剪上下文都更容易做。这个分层思路,与 LangChain 对消息类型的划分是完全一致的。(LangChain Docs)


三、HumanMessage 的重点不在“用户说了什么”,而在“用户输入如何被结构化”

文档里提到 HumanMessage 可以包含文本、图片、音频、文件等多模态内容,也支持 nameid 这类元数据。name 是否生效依赖 provider,id 可以用于追踪。(LangChain Docs)

这背后的工程价值很大。

1)id 不是可有可无,它应该进入你的 trace 体系

如果你的应用有这些需求:

那就应该主动给 HumanMessage 生成业务 ID,而不是完全依赖 provider 自动返回。

python
from langchain.messages import HumanMessage
import uuid
 
msg = HumanMessage(
    content="请分析这个工单附件里的错误原因",
    id=f"user_msg_{uuid.uuid4().hex}",
    name="frontend_user"
)

2)多模态输入不要再单独设计一套“附件协议”

这页文档已经给出方向:消息 content 不只可以是字符串,也可以是内容块列表;图像、PDF、音频、视频都能作为 block 塞进同一条消息里。(LangChain Docs)

这意味着在工程上,你可以把用户输入统一建模成:

python
user_input = {
    "text": "...",
    "attachments": [...],
    "metadata": {...}
}

最后映射成一个 HumanMessage,而不是“文本走 messages,附件走另一个接口,OCR 结果再拼第三套上下文”。

统一入口,后续做缓存、审计、重放时会轻松很多。


四、AIMessage 最容易被低估:它不是“模型回复文本”,而是完整响应对象

文档里明确说,model.invoke(...) 返回的是 AIMessage,它除了文本外,还能携带:

这点很关键,因为很多业务代码还停留在:

python
answer = model.invoke(messages).content

这样写虽然能跑,但你其实把一大半高价值信息都丢了。

工程里应该优先保留的几个字段

1. tool_calls

如果模型要调工具,这就是它的“行动计划”。不是附加信息,而是 agent loop 的核心输入。(LangChain Docs)

2. usage_metadata

这能拿到 token 使用情况,文档示例里包含 input_tokensoutput_tokenstotal_tokens,甚至细分到 reasoning 等 token 维度。(LangChain Docs)

这在工程上的直接用途:

3. response_metadata

这里通常适合留给底层 provider 相关信息,用于日志和调试。(LangChain Docs)

推荐做法:把 AIMessage 原样入库,而不是只存 answer 文本

python
response = model.invoke(messages)
 
record = {
    "answer_text": response.text,
    "message_id": response.id,
    "tool_calls": response.tool_calls,
    "usage": response.usage_metadata,
    "response_metadata": response.response_metadata,
    "raw_content": response.content,
}

这样以后你才能做:


五、Tool Calling 的关键不在“怎么绑定工具”,而在“消息闭环是否完整”

文档给出了典型流程:

  1. 模型输出一个 AIMessage,其中带 tool_calls
  2. 应用执行工具
  3. 把工具执行结果包装成 ToolMessage
  4. 再把这个 ToolMessage 连同前序消息一起送回模型继续推理 (LangChain Docs)

这其实是 LLM 工程里一个非常核心的设计点:

工具调用不是一次函数调用,而是一段消息闭环。

一个最小可运行模式

python
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, ToolMessage
 
def get_weather(location: str) -> str:
    return f"{location} 晴,23°C"
 
model = init_chat_model("gpt-5-nano").bind_tools([get_weather])
 
messages = [HumanMessage("帮我查一下上海天气")]
ai_msg = model.invoke(messages)
 
messages.append(ai_msg)
 
for call in ai_msg.tool_calls:
    result = get_weather(**call["args"])
    messages.append(
        ToolMessage(
            content=result,
            tool_call_id=call["id"],
            name=call["name"],
        )
    )
 
final_msg = model.invoke(messages)
print(final_msg.text)

生产里最容易踩的坑:tool_call_id 对不上

文档强调 ToolMessage.tool_call_id 必须匹配 AIMessage.tool_calls 里的调用 ID。(LangChain Docs)

这意味着:

更进一步:把 artifact 当成“给程序看的结果”,content 当成“给模型看的结果”

文档特别提到 ToolMessage 有一个 artifact 字段:它不会发给模型,但程序可以读取。官方举的例子是检索工具把文档元信息放进 artifact。(LangChain Docs)

这在 RAG 或企业应用里特别有用:

python
ToolMessage(
    content="订单 1024 在 2026-03-10 已完成支付。",
    tool_call_id="call_123",
    name="query_order",
    artifact={
        "order_id": 1024,
        "db_latency_ms": 18,
        "source_table": "payments",
        "trace_id": "trace_xxx"
    }
)

这样你就能做到:

这是非常典型的工程分层:

给模型看的内容要短,给系统保留的数据可以全。


六、contentcontent_blocks:LangChain 真正想解决的是“跨模型格式不统一”

这页文档有一个非常重要但容易被忽略的点:

为什么这对工程重要?

因为不同模型厂商对“富内容”定义不一样。

比如文档举例:

这意味着如果你在做:

那你最好不要直接面向 provider 原始格式写业务逻辑,而应该尽量读 content_blocks

一个更稳妥的处理模式

python
resp = model.invoke(messages)
 
for block in resp.content_blocks:
    if block["type"] == "text":
        handle_text(block["text"])
    elif block["type"] == "reasoning":
        handle_reasoning(block["reasoning"])

一个很实用的细节

文档提到,如果你希望在 LangChain 外部系统里也拿到标准 content block,可以设置环境变量 LC_OUTPUT_VERSION=v1,或者初始化模型时传 output_version="v1"。(LangChain Docs)

这对接外部系统很有帮助,比如:


七、多模态设计的关键不是“支持图片”,而是“同一条消息里混合不同载体”

文档明确说明:message content 可以承载图像、PDF、音频、视频等,并展示了 URL、base64、provider-managed file id 等不同形式;同时也提醒,不是所有模型都支持所有文件类型。某些 provider 甚至会要求 PDF 带文件名。(LangChain Docs)

这说明 LangChain 的设计重点并不是“给你一套图片 API”,而是:

把多模态输入统一纳入 Message 体系,而不是把它做成外挂接口。

这对业务系统意味着什么?

1. 一个用户请求可以是“文本 + 图片 + PDF”混合体

例如:

python
message = {
    "role": "user",
    "content": [
        {"type": "text", "text": "请帮我总结这份 PDF 的第 3 页,并结合这张图解释"},
        {"type": "file", "url": "https://example.com/report.pdf", "mime_type": "application/pdf"},
        {"type": "image", "url": "https://example.com/chart.png"},
    ]
}

2. 你的服务端应该做“附件到内容块”的统一转换层

不要在 controller 里 if/else 写死:

更合理的架构是先转成统一 message block,再由模型能力和 provider 适配层决定怎么发。

3. 兼容性判断应该前置

文档提醒不同 provider 支持的格式和大小限制不同。(LangChain Docs)

所以生产里最好有一层校验:


八、Streaming 真正的工程价值,是“渐进构建最终消息对象”

文档里提到 streaming 时会收到 AIMessageChunk,这些 chunk 可以累加合并成完整消息。(LangChain Docs)

这个设计非常值得注意,因为它告诉你:

为什么这很重要?

因为一旦你的应用支持 tool call、reasoning block、多模态 block,流式过程里就不只是普通文本 token 了,还可能出现:

工程建议

前端展示时可以按 token 流式刷新,但后端一定要维护一个“最终消息聚合器”:

python
full_message = None
 
for chunk in model.stream(messages):
    # 实时推送给前端
    if chunk.text:
        push_to_client(chunk.text)
 
    # 后端聚合完整消息
    full_message = chunk if full_message is None else full_message + chunk
 
# 落库、统计、进入下一轮 tool loop 的应该是 full_message
save(full_message)

否则你会遇到这些问题:


九、消息类型背后,其实是一条标准化的 Agent 状态流

把这页文档串起来看,LangChain 想表达的不只是“有四种消息”:

如果从系统设计角度理解,它们对应的是一个非常清晰的执行流:

SystemMessage
   ↓
HumanMessage
   ↓
AIMessage(可能带 tool_calls)
   ↓
ToolMessage(按 tool_call_id 回填)
   ↓
AIMessage(基于工具结果继续推理)

这条流最大的价值是:可回放、可裁剪、可观测、可迁移

而文档最后也明确提到,chat models 接受消息序列输入、返回 AIMessage 输出,实际交互往往是无状态的,因此会随着对话增长形成不断扩大的消息列表,这也自然引出“持久化、上下文裁剪、消息总结”等工程问题。(LangChain Docs)


十、一个更像生产代码的消息层设计

如果只基于这页文档,我会建议把 LangChain Messages 这一层设计成下面这样。

1. 统一的消息构建层

负责把前端输入、附件、业务元数据转换成 LangChain message 对象。

2. 统一的消息存储层

存完整 AIMessage / HumanMessage / ToolMessage,不要只存纯文本。

3. 工具执行中间层

专门处理:

4. 输出适配层

对外可只返回纯文本,但内部保留:

参考骨架

python
from langchain.chat_models import init_chat_model
from langchain.messages import SystemMessage, HumanMessage, ToolMessage
 
def run_turn(user_text: str, history: list, tools: list):
    model = init_chat_model("gpt-5-nano")
    model = model.bind_tools(tools)
 
    messages = [
        SystemMessage(
            "你是企业级智能助手。回答简洁,信息不足时明确说明。"
        ),
        *history,
        HumanMessage(content=user_text)
    ]
 
    ai_msg = model.invoke(messages)
    messages.append(ai_msg)
 
    if ai_msg.tool_calls:
        for call in ai_msg.tool_calls:
            tool_name = call["name"]
            tool_args = call["args"]
 
            result, artifact = dispatch_tool(tool_name, tool_args)
 
            messages.append(
                ToolMessage(
                    content=result,              # 给模型看的
                    tool_call_id=call["id"],     # 关联本次调用
                    name=tool_name,
                    artifact=artifact,           # 给程序看的
                )
            )
 
        ai_msg = model.invoke(messages)
        messages.append(ai_msg)
 
    return {
        "answer": ai_msg.text,
        "messages": messages,
        "usage": ai_msg.usage_metadata,
        "response_metadata": ai_msg.response_metadata,
    }

这个骨架没有引入别的页面知识,完全是从这页 Messages 文档能推出来的最实用工程落地方式。它利用了文档中强调的几个核心能力:消息序列输入、AIMessage 结构化输出、tool_calls、ToolMessage 回填、usage metadata、artifact 分层。(LangChain Docs)


十一、最容易踩的 6 个坑

1. 只把 Messages 当“聊天历史”

结果是工具结果、token 使用、流式 chunk、附件信息全散落在别处,后续维护很痛苦。文档其实已经说明,消息包含 role、content、metadata,是完整上下文单元。(LangChain Docs)

2. 只取 response.text,不保存 AIMessage

这样你等于主动放弃 tool_calls、usage_metadata、response_metadata。(LangChain Docs)

3. 工具执行后没正确回填 ToolMessage

尤其是 tool_call_id 不匹配,会直接破坏后续推理链路。(LangChain Docs)

4. 直接写死 provider 原始 content 格式

切模型后就容易崩。更稳妥的是尽量消费 content_blocks。(LangChain Docs)

5. 流式输出只做前端展示,不做后端聚合

最后你会有“看起来输出了,但系统里没有完整消息对象”的问题。文档明确说 chunk 可以拼回完整消息。(LangChain Docs)

6. 把工具原始大结果直接塞进 content

更好的做法是:摘要放 content,全量数据放 artifact。(LangChain Docs)


结语:Messages 不是 LangChain 的基础概念,而是 LLM 应用的工程接口

如果只看表面,Messages 像是在教你:

但从工程实现看,这页真正讲的是一件更重要的事:

如何把模型交互过程,组织成可组合、可观测、可跨 provider 迁移的结构化消息流。 (LangChain Docs)

所以,做 LangChain 应用时,最好不要把 Messages 当成“Prompt 的另一种写法”。

更准确的理解是:

Messages 是你的上下文协议、工具协议、多模态协议、流式协议,以及运行时审计协议。

当你这样看它,很多设计都会自然变对。