精读 LangChain 官方文档(三):

精读 LangChain 官方文档(三):Messages 消息首讲

本篇对应的官方文档

  • LangChain Messages:支撑 role、content、metadata、四类 Message 与 content_blocks 的概念主线。
  • Messages API Reference:支撑消息对象、内容块和程序可读取字段的接口边界。
  • ToolMessage API Reference:支撑 tool_call_id 的请求---结果配对,以及 artifact 的应用侧数据边界。

本篇讲解范围

本篇集中讲清 Message 的上下文职责、四类消息、工具结果配对与 content_blocks 标准视图;完整 Agent 自动循环和 Tool Calling 留给第 05、06 篇。

会初始化 Chat Model,只解决了"模型从哪里来"。真正把模型放进多轮对话或工具链时,应用还要回答另一个问题:每段上下文以什么身份进入模型,模型返回的结果又怎样被程序继续处理。

LangChain 用 Message 承担这层交接。官方文档把它称为模型上下文的基本单位:一条 Message 不只保存文字,还保存角色、内容结构和程序需要的元数据。它既是模型看到的一段上下文,也是应用判断下一步动作时可以读取的对象。

于是,应用与模型之间传递的不再只是一段文本:rolecontent 组织模型看到的上下文,metadata 与 AIMessage 的返回字段则留给程序判断下一步。模型给出直接回答还是返回工具请求,应用都不必再从一段字符串里猜执行意图。

Message 因此不能只按普通聊天气泡理解。聊天气泡主要解决界面怎么显示,Message 解决的是一段信息如何进入调用、如何留在历史中,以及后续代码能否识别它的来源和用途。相同一句话放进不同角色,或者出现在工具请求前后,对模型产生的上下文意义并不相同。

本篇只打牢这层基础:先看字符串在复杂调用中会丢掉什么,再拆开 role、content 和 metadata,随后沿 SystemMessageHumanMessageAIMessageToolMessage 的顺序走完一次消息往返。

模型怎样自动选择和执行工具属于后续 Agent 与 Tools 主题。这里处理的是工具请求已经产生以后,消息怎样保存请求、结果和对应关系。

后文始终使用同一个问题:"北京今天适合带伞吗?"它从用户输入开始,可能触发天气工具,再带着查询结果回到模型。读完后需要得到的不是四个类名,而是一条可以落到代码里的判断:什么时候字符串已经够用,什么时候必须显式管理 Message。

1. 一次调用变复杂后,纯文本会丢掉什么

最简单的模型调用只关心一件事:把输入送进去,再取回回答。这时传字符串很方便。LangChain 也支持这种写法,并会把单个字符串当作一次单独的用户输入处理。

但"内容一样"不等于"上下文作用一样"。例如"回答不超过三句话"既可能是开发者设置的系统约束,也可能只是用户临时提出的要求。纯字符串只留下字面内容,无法单独表达它是长期行为约束,还是本轮用户输入。

天气场景再多走一步,程序马上遇到几个仅靠文本无法稳定回答的问题:

  • "北京今天适合带伞吗"是谁说的?
  • "调用天气工具"是普通回答,还是模型发出的结构化请求?
  • 天气工具返回的结果应该接到哪一次请求后面?
  • 模型这次没有返回文字,是调用失败,还是正在等待工具结果?
  • token 用量、停止原因和响应 ID 应该保存在哪里?

可以自行约定文本前缀,例如给每一段加上 USER:ASSISTANT:。这种办法在演示代码里看似可行,问题是应用开始承担一套隐式解析协议:前缀需要转义,工具参数需要从文本里重新解析,并发结果还得依赖列表位置配对。一旦消息变多或供应商结构变化,代码就会到处出现"猜这段文本是什么意思"的分支。

Message 的价值由此变得具体:文本仍然负责表达内容,角色、历史位置、工具调用和响应信息则进入明确字段。应用不再从句子里猜执行语义,而是直接读取对象。

角色、历史位置、工具调用、响应元数据和多模态结构共同为内容补上可执行身份:角色标明来源,历史保留交接顺序,工具调用记录下一步动作,metadata 支撑计费与追踪,多模态结构避免把图片或文件伪装成字符串。Message 处理的不是"怎样存一句话",而是"怎样让一句话进入模型上下文后仍能被程序可靠处理"。要把这些职责落到对象里,下一步要继续拆开 role、content 和 metadata 的分工。

2. role、content 和 metadata 分别管什么

官方文档把 Message 的核心信息归纳为三层:role、content 和 metadata。三者不是并列术语,而是分别服务模型理解、内容承载和程序判断。

role 决定这条信息以什么身份进入上下文。 系统约束、用户输入、模型响应和工具结果即使文字相同,也不属于同一种消息。模型供应商会按照各自规则处理不同角色,因此角色不能靠正文语气替代。

把"根据天气数据回答,不要猜测"写进 SystemMessage,表达的是本次调用的行为约束;把同一句话放进 HumanMessage,表达的是用户请求。两者内容相同,但在消息历史中的职责不同。还要注意,SystemMessage 只能影响模型行为,不能代替后端权限校验、参数校验或工具访问控制。

content 是真正的消息载荷。 最简单时它是一段字符串;需要多模态输入时,也可以是文本、图片、音频或文件等内容块。Message 能表达某种内容,不等于当前模型供应商一定支持这种类型,格式和大小限制仍需核对具体供应商。

metadata 服务程序,而不是用来偷偷扩写提示词。 id 可以追踪消息,usage_metadata 可以保存供应商返回的 token 统计,response_metadata 可以保存停止原因等响应信息。

某些消息还会带 nametool_calls 或其他专用字段。字段是否存在以及具体内容,取决于消息类型和供应商返回值。

例如,HumanMessage(content="北京今天适合带伞吗?", name="weather_page_user", id="msg_weather_question_001") 在保存问题的同时增加了名称和消息 ID。

这里的 content 会进入模型上下文,id 方便应用追踪这条消息。name 在不同供应商中的处理方式可能不同,因此不能把它当成跨供应商都可靠的鉴权字段。

到了这里,真正需要同时守住的是两条边界:模型只接收完成推理所需的上下文,应用侧的追踪、计费与控制信息则留在可稳定读取的字段中。三层结构怎样分开这两类信息,决定了 Message 能否同时服务模型与程序。

三层信息只有各守住职责,Message 才能同时成为模型上下文和程序对象:role 确定来源,content 承载模型需要处理的内容,metadata 保留应用侧追踪与判断所需的信息。把它们混在一起,模型会读到不必要的控制数据,程序也会失去稳定字段。单条消息的边界明确以后,下一步就是看四类 Message 如何沿时间顺序完成交接。

3. 四类 Message 是四个交接位置

LangChain 常用的四类消息对象分别承担不同职责:

消息对象 在天气场景中的位置 主要边界
SystemMessage 规定回答简洁,并要求根据天气数据判断是否带伞 控制模型行为,不等于权限控制
HumanMessage 用户询问北京天气和带伞建议 保存用户输入,不承载系统规则
AIMessage 模型回答,或发出天气工具调用 除文本外还可能带 tool_calls 和响应元数据
ToolMessage 天气工具返回温度、降水概率等结果 必须对应模型发出的某次工具调用

四类对象并不是四种随意替换的写法,而是消息链中的四个交接位置。SystemMessageHumanMessage 先进入模型;如果模型能够直接回答,返回一个普通 AIMessage

如果模型需要天气数据,第一次 AIMessage 会携带 tool_calls。工具执行后再追加 ToolMessage,模型读取更新后的历史,最后生成新的 AIMessage

因此,消息历史也不是一个"把所有内容塞进去"的容器。列表顺序表达调用发生的先后,消息类型表达每一步的职责,专用字段表达跨步骤的连接关系。删掉中间对象或打乱次序,改变的不是显示效果,而是模型收到的上下文。

天气场景的基础历史可以先写成这样:

python 复制代码
from langchain.messages import HumanMessage, SystemMessage

messages = [
    SystemMessage("你是天气助手。只能根据提供的天气数据给出带伞建议。"),
    HumanMessage("北京今天适合带伞吗?"),
]

这两条消息已经把行为约束与用户问题分开。模型返回后,应用不能只复制回答文本,而应把完整 AIMessage 追加到历史;如果其中包含工具请求,相应的 ToolMessage 还要跟在它后面。这样下一次调用拿到的不是几段失去身份的文字,而是一段可复原的交接过程。

LangChain 的 chat model 也接受带 rolecontent 的字典格式。它便于接入 OpenAI-compatible 数据结构,但进入调用后仍然对应 Message 语义。字典只是输入表示法,不意味着角色、顺序和工具配对可以省略。

字符串、Message 对象和字典最终都可以进入模型,区别在于应用希望显式管理多少上下文语义。真正需要继续处理模型输出时,返回的 AIMessage 会把这个区别放大。

4. 调用模型后,别急着只取 content

先看一段只包含系统约束和用户问题的调用。代码的重点不是模型连接字段,而是 messages 如何进入 invoke,以及返回值为什么需要保留为 AIMessage

python 复制代码
import os

from langchain.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="qwen3.7-plus",
    api_key=os.environ["DASHSCOPE_API_KEY"],
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

messages = [
    SystemMessage("你是天气助手。回答要简洁,并明确给出是否建议带伞。"),
    HumanMessage("北京今天适合带伞吗?"),
]

response = model.invoke(messages)

print(response.text)
print(response.tool_calls)
print(response.usage_metadata)
print(response.response_metadata)
print(response.id)

输入是两条职责明确的 Message。invoke 把整个序列交给模型,返回的 responseAIMessage。业务代码可以先看 tool_calls 判断模型是否要求执行工具,再根据当前产品需要读取文本、token 用量、停止原因或消息 ID。

几个常用字段的读取目的并不相同:

  • text 提供便捷的文本视图,适合界面展示或普通回答处理。
  • content 保留原始载荷,可能是字符串,也可能包含供应商原生内容块。
  • content_blocks 提供标准化内容视图,适合跨供应商处理不同类型的块。
  • tool_calls 保存模型发出的结构化工具请求;没有调用时通常为空。
  • usage_metadataresponse_metadata 面向计费、调试和运行判断,不应拼进回答文本。
  • id 用于追踪消息,但是否由供应商返回、格式如何,不能由业务代码凭空假定。

这意味着 AIMessage.content 为空不一定是失败。模型可能没有准备给用户最终文本,而是先返回了工具请求。可靠的分支顺序通常是先判断调用是否成功,再检查 tool_calls,最后才决定把文本展示给用户还是推进工具流程。

错误边界也由此变得清楚:网络或供应商错误发生在 invoke;调用成功但没有文本时,应检查工具请求或其他内容块;用量字段缺失时,应确认供应商是否提供相应数据,而不是直接把空值解释为零消耗。

读取 AIMessage 时,字段选择取决于应用准备推进哪条执行路径:展示答案读取 text 或标准内容块,处理工具请求读取 tool_calls,计费与调试读取 usage 和 response metadata,消息追踪则读取 id。把返回对象压缩成字符串,会同时丢掉动作、观测和关联信息。当天气问题需要实时数据时,执行路径就从 AIMessage.tool_calls 转向工具执行;接下来必须保证工具结果回到正确请求。

5. ToolMessage 靠 tool_call_id 回到正确请求

模型请求工具时,AIMessage.tool_calls 中的每次调用都有 ID。应用执行工具后创建 ToolMessage,并把同一个 ID 写入 tool_call_id。这个字段不是备注,而是请求与结果之间的连接键。

下面手工构造一次天气工具往返。完整的自动工具执行会在第 06 篇展开,这里只观察消息怎样保存请求和结果。

python 复制代码
from langchain.messages import AIMessage, HumanMessage, ToolMessage

question = HumanMessage("北京今天适合带伞吗?")

tool_request = AIMessage(
    content="",
    tool_calls=[
        {
            "name": "get_weather",
            "args": {"city": "北京"},
            "id": "call_weather_001",
        }
    ],
)

tool_result = ToolMessage(
    content='{"rain_probability": 70, "temperature_c": 22}',
    tool_call_id="call_weather_001",
    name="get_weather",
    artifact={
        "provider": "example-weather-service",
        "raw_response_id": "weather_20260716_001",
    },
)

final_response = model.invoke([question, tool_request, tool_result])
print(final_response.text)

这里的输入不是三段互不相关的文本,而是一段连续历史:用户问题产生工具请求,ToolMessage.tool_call_id 指回 call_weather_001,模型读取对应结果后生成最终回答。

ToolMessagecontentartifact 也有清晰分工。content 放模型需要看到的结果,例如降水概率和温度;artifact 保存原始响应 ID、文档编号或调试信息,供日志、前端或下游程序使用,但不必占用模型上下文。

当模型一次请求两个工具时,ID 的价值更明显。假设一个调用查询北京降水,另一个调用查询上海降水,两个结果即使返回顺序相反,也应分别通过自己的 tool_call_id 找回对应请求。若应用只按列表第一个、第二个硬拼,网络延迟就可能让城市与天气结果串线。

工具消息常见的失败并不是 Python 语法错误,而是历史关系被破坏:

  • tool_call_id 写错,结果无法对应模型发出的请求。
  • 只追加 ToolMessage,却漏掉此前携带 tool_callsAIMessage
  • 裁剪历史时删除工具请求,只保留工具结果。
  • 把原始大对象全部放进 content,导致模型上下文被无关数据挤占。
  • 并发执行后按完成顺序猜配对关系,而不是按调用 ID 组装消息。

把这些错误放回一条往返链,最值得盯住的是请求 AIMessage 与结果 ToolMessage 之间那条由 tool_call_id 建立的对应关系。

tool_call_id 在这条往返中承担关联键,而不是展示编号:消息顺序还原请求、执行和回传的时间过程,ID 则把每个 ToolMessage 精确连回发起它的 AIMessage.tool_calls。即使多个工具并发完成,应用也不需要根据返回先后猜测配对关系。请求与结果能够稳定对齐后,消息内容本身还存在供应商格式差异,content_blocks 正是这一层的统一视图。

6. content_blocks 是标准视图,不是新的供应商能力

Message 的 content 很灵活。官方文档列出了三种常见形态:字符串、供应商原生内容块列表,以及 LangChain 标准内容块列表。灵活让不同模型能力能够进入同一个 Message 接口,也意味着直接读取原始 content 时可能遇到不同结构。

content_blocks 提供标准、类型化的读取视图。它会尝试把原始 content 解析成统一的文本、推理、图片、音频、文件或工具调用等块;创建 Message 时,也可以直接传入标准块。

python 复制代码
from langchain.messages import HumanMessage

message = HumanMessage(
    content_blocks=[
        {"type": "text", "text": "请概括这份天气报告。"},
        {
            "type": "file",
            "url": "https://example.com/beijing-weather.pdf",
            "mime_type": "application/pdf",
        },
    ]
)

print(message.content)
print(message.content_blocks)

初始化时传入 content_blocks,LangChain 仍会填充 content;读取 message.content_blocks 时得到标准视图。二者不是新旧 API 的替代关系,而是原始载荷与标准读取接口的关系。

如果供应商返回自己的 thinkingreasoning 或多模态结构,标准视图可以把已知结构解析成 LangChain 内容块。应用因此可以优先按 type 处理文本、图片或推理摘要,不必为每个供应商都从零编写一套读取分支。

需要把标准块序列化给 LangChain 之外的应用时,可以考虑 output_version="v1"LC_OUTPUT_VERSION=v1。这个选项决定标准块如何存回消息内容,不是访问 content_blocks 属性的前提。

最重要的边界仍在供应商一侧:标准化解决"应用怎样统一表达和读取",不负责扩展模型能力。Message 可以表达 PDF、音频或视频,不代表当前 qwen3.7-plus 接口一定接受示例中的文件。真正发送前仍需核对供应商支持的类型、MIME type、大小限制和 URL、base64 或文件 ID 等传输方式。

content_blocks 完成的是表示层统一,供应商能力边界并没有因此改变:原始 content 保留文本或供应商结构,标准块提供统一读取视图,真正能否发送 PDF、音频或视频仍由模型接口决定。标准化减少了应用对供应商返回格式的依赖,却不能绕过 MIME type、大小和传输方式等限制。

至此,Message 从单条结构、历史顺序、模型返回、工具配对到内容标准化已经连成一条路径。最后需要把这些机制收束成实际选择,而不是默认所有调用都使用最复杂的写法。

7. 从一次完整往返判断该不该显式管理 Message

先把天气问题重新走一遍。

用户的"北京今天适合带伞吗"进入 HumanMessage,系统行为约束由 SystemMessage 单独保存。模型第一次调用返回 AIMessage:如果它已经能回答,应用读取文本即可;如果它返回 tool_calls,应用按照调用名称与参数执行天气工具。

工具结果随后进入 ToolMessage,其中 tool_call_id 指回模型发出的那一次请求,content 保存模型需要读取的天气数据,artifact 可以保存不必进入上下文的原始响应。模型再次读取这段历史,生成最后的 AIMessage,应用再从文本、用量和响应字段中取出各自需要的信息。

这条链路里没有哪一个类只是为了让代码"更面向对象"。每个 Message 都守住一个交接位置,字段则让模型上下文与程序控制信息不必混成字符串。

选择输入形式时,可以按应用真正需要维护的语义判断:

场景 合适的输入或读取方式 原因
单轮、无历史的自由问答 字符串 只关心一次用户输入和文本回答
需要系统约束和多轮历史 Message 列表 每条信息的角色与顺序必须稳定
模型可能调用工具 Message 列表 需要保存 AIMessage.tool_callsToolMessage.tool_call_id
需要读取 token 或停止原因 返回的 AIMessage 元数据不应混进回答文本
需要跨供应商处理多模态内容 content_blocks 视图 使用标准类型读取,同时保留供应商能力边界

遇到消息相关问题时,也可以沿同一条链排查,而不是先怀疑模型"突然变笨":

  1. 检查角色是否正确,系统约束有没有被误放进用户消息。
  2. 检查历史顺序,工具请求和结果之间是否缺少对象。
  3. 检查 AIMessage.tool_calls,不要把空文本直接判定为失败。
  4. 检查每个 ToolMessage.tool_call_id 是否对应真实请求。
  5. 检查应用读取的是原始 content,还是更适合当前任务的 textcontent_blocks
  6. 检查供应商是否真的返回用量、响应字段,并支持准备发送的多模态类型。

回到最初的问题,文章真正要建立的是一条边界:当应用只关心一次独立问答的文本结果时,字符串足够直接;当角色、历史、工具、多模态或响应元数据开始影响下一步执行时,Message 就不再是可有可无的包装,而是应用必须维护的上下文单位。

后续的 Structured Output、Agent、Tools、Memory 和 Context Engineering 会继续改变消息里保存什么、保留多久、何时裁剪,但它们都依赖这里的基础:信息以正确角色进入历史,执行结果回到正确请求,程序从明确字段决定下一步。

相关推荐
zzzzzz3101 小时前
当老板说「数据库里加个字段就行了」时,我在想什么——一个后端开发的奇葩需求大赏
数据库·人工智能·产品经理
fanstuck7 小时前
1M 上下文能怎么用?我用 Seed Evolving 做了一个招标文件版本差异审查器
服务器·人工智能·数据分析·开源·aigc
FoldWinCard11 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
聆听。。花开雨落11 小时前
mybatis的typeHandler 作用
数据库·mybatis
pxzsky11 小时前
PG17数据库安装中分分词插件:pg_jieba
数据库·postgresql·pg_jieba
kirs_ur11 小时前
SSD 在 AI 训练中的角色
大数据·服务器·人工智能
她说可以呀12 小时前
Redis哨兵
数据库·redis·bootstrap
tedcloud12312 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频