精读 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 不只保存文字,还保存角色、内容结构和程序需要的元数据。它既是模型看到的一段上下文,也是应用判断下一步动作时可以读取的对象。
于是,应用与模型之间传递的不再只是一段文本:role和content组织模型看到的上下文,metadata 与AIMessage的返回字段则留给程序判断下一步。模型给出直接回答还是返回工具请求,应用都不必再从一段字符串里猜执行意图。
Message 因此不能只按普通聊天气泡理解。聊天气泡主要解决界面怎么显示,Message 解决的是一段信息如何进入调用、如何留在历史中,以及后续代码能否识别它的来源和用途。相同一句话放进不同角色,或者出现在工具请求前后,对模型产生的上下文意义并不相同。
本篇只打牢这层基础:先看字符串在复杂调用中会丢掉什么,再拆开 role、content 和 metadata,随后沿SystemMessage、HumanMessage、AIMessage、ToolMessage的顺序走完一次消息往返。
模型怎样自动选择和执行工具属于后续 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可以保存停止原因等响应信息。
某些消息还会带name、tool_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 | 天气工具返回温度、降水概率等结果 | 必须对应模型发出的某次工具调用 |
四类对象并不是四种随意替换的写法,而是消息链中的四个交接位置。SystemMessage与HumanMessage先进入模型;如果模型能够直接回答,返回一个普通AIMessage。
如果模型需要天气数据,第一次AIMessage会携带tool_calls。工具执行后再追加ToolMessage,模型读取更新后的历史,最后生成新的AIMessage。
因此,消息历史也不是一个“把所有内容塞进去”的容器。列表顺序表达调用发生的先后,消息类型表达每一步的职责,专用字段表达跨步骤的连接关系。删掉中间对象或打乱次序,改变的不是显示效果,而是模型收到的上下文。
天气场景的基础历史可以先写成这样:
fromlangchain.messagesimportHumanMessage,SystemMessage messages=[SystemMessage("你是天气助手。只能根据提供的天气数据给出带伞建议。"),HumanMessage("北京今天适合带伞吗?"),]这两条消息已经把行为约束与用户问题分开。模型返回后,应用不能只复制回答文本,而应把完整AIMessage追加到历史;如果其中包含工具请求,相应的ToolMessage还要跟在它后面。这样下一次调用拿到的不是几段失去身份的文字,而是一段可复原的交接过程。
LangChain 的 chat model 也接受带role和content的字典格式。它便于接入 OpenAI-compatible 数据结构,但进入调用后仍然对应 Message 语义。字典只是输入表示法,不意味着角色、顺序和工具配对可以省略。
字符串、Message 对象和字典最终都可以进入模型,区别在于应用希望显式管理多少上下文语义。真正需要继续处理模型输出时,返回的AIMessage会把这个区别放大。
4. 调用模型后,别急着只取 content
先看一段只包含系统约束和用户问题的调用。代码的重点不是模型连接字段,而是messages如何进入invoke,以及返回值为什么需要保留为AIMessage。
importosfromlangchain.messagesimportHumanMessage,SystemMessagefromlangchain_openaiimportChatOpenAI 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把整个序列交给模型,返回的response是AIMessage。业务代码可以先看tool_calls判断模型是否要求执行工具,再根据当前产品需要读取文本、token 用量、停止原因或消息 ID。
几个常用字段的读取目的并不相同:
text提供便捷的文本视图,适合界面展示或普通回答处理。content保留原始载荷,可能是字符串,也可能包含供应商原生内容块。content_blocks提供标准化内容视图,适合跨供应商处理不同类型的块。tool_calls保存模型发出的结构化工具请求;没有调用时通常为空。usage_metadata和response_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 篇展开,这里只观察消息怎样保存请求和结果。
fromlangchain.messagesimportAIMessage,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,模型读取对应结果后生成最终回答。
ToolMessage的content与artifact也有清晰分工。content放模型需要看到的结果,例如降水概率和温度;artifact保存原始响应 ID、文档编号或调试信息,供日志、前端或下游程序使用,但不必占用模型上下文。
当模型一次请求两个工具时,ID 的价值更明显。假设一个调用查询北京降水,另一个调用查询上海降水,两个结果即使返回顺序相反,也应分别通过自己的tool_call_id找回对应请求。若应用只按列表第一个、第二个硬拼,网络延迟就可能让城市与天气结果串线。
工具消息常见的失败并不是 Python 语法错误,而是历史关系被破坏:
tool_call_id写错,结果无法对应模型发出的请求。- 只追加
ToolMessage,却漏掉此前携带tool_calls的AIMessage。 - 裁剪历史时删除工具请求,只保留工具结果。
- 把原始大对象全部放进
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 时,也可以直接传入标准块。
fromlangchain.messagesimportHumanMessage 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 的替代关系,而是原始载荷与标准读取接口的关系。
如果供应商返回自己的thinking、reasoning或多模态结构,标准视图可以把已知结构解析成 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_calls与ToolMessage.tool_call_id |
| 需要读取 token 或停止原因 | 返回的AIMessage | 元数据不应混进回答文本 |
| 需要跨供应商处理多模态内容 | content_blocks视图 | 使用标准类型读取,同时保留供应商能力边界 |
遇到消息相关问题时,也可以沿同一条链排查,而不是先怀疑模型“突然变笨”:
- 检查角色是否正确,系统约束有没有被误放进用户消息。
- 检查历史顺序,工具请求和结果之间是否缺少对象。
- 检查
AIMessage.tool_calls,不要把空文本直接判定为失败。 - 检查每个
ToolMessage.tool_call_id是否对应真实请求。 - 检查应用读取的是原始
content,还是更适合当前任务的text或content_blocks。 - 检查供应商是否真的返回用量、响应字段,并支持准备发送的多模态类型。
回到最初的问题,文章真正要建立的是一条边界:当应用只关心一次独立问答的文本结果时,字符串足够直接;当角色、历史、工具、多模态或响应元数据开始影响下一步执行时,Message 就不再是可有可无的包装,而是应用必须维护的上下文单位。
后续的 Structured Output、Agent、Tools、Memory 和 Context Engineering 会继续改变消息里保存什么、保留多久、何时裁剪,但它们都依赖这里的基础:信息以正确角色进入历史,执行结果回到正确请求,程序从明确字段决定下一步。