Skip to main content

02 · 消息类数据字典:模型输入输出的”统一信封”

本片目标:彻底搞懂 LangChain 的”消息”体系——为什么对话不是”字符串进字符串出”,而是”消息对象进、AIMessage 出”。这是全系列最重要的一篇数据字典。 新增规定性:2(对话的原子单位是消息对象,有角色之分、有 9 个字段) 数据字典:BaseMessage / SystemMessage / HumanMessage / AIMessage / ToolCall / UsageMetadata。 进程线程模型:无变化(仍是同步阻塞)。 网络模型invoke(messages) 把消息对象序列化成各家协议的 JSON 结构。

1. 上集回顾

第 01 篇我们只用了 model.invoke("你好")——传字符串。但你有两个问题:
  1. 多轮对话怎么传?我需要同时告诉模型”你是助手”(system)+ 用户历史问题(user)+ 之前回答(assistant)。一个字符串表达不了这种结构。
  2. 模型返回的 AIMessage 到底有什么?第 01 篇看到它有 contentresponse_metadatausage_metadatatool_calls…… 为什么一个回答要带这么多字段?
答案都在”消息类”里。消息是 LangChain 里所有对话的原子单位——输入是消息列表,输出是 AIMessage。

2. 数据字典:消息类体系(关键类的数据)

2.1 类层次

2.2 BaseMessage 六字段(所有消息的公共部分)

序列化后的形态(给模型的 JSON)——这是网络层真正发送的:

2.3 AIMessage:9 字段(模型输出的完整信封)

实测 AIMessage.model_dump() 完整结构(智谱 glm-4.7,第 01 篇运行输出,脱敏):

2.4 UsageMetadata(token 用量,三家协议统一成这三键)

三家的原始 usage 结构完全不同(Anthropic 是 input_tokens/output_tokens,OpenAI 是 prompt_tokens/completion_tokens),LangChain 统一成一套——这就是”统一信封”在元数据层的体现。

2.5 ToolCall(工具调用结构,第 11 篇深入)


3. 关键方法(本片主角:怎么构造和检查消息)


4. 三协议差异(为什么需要”统一信封”的实证)

这就是第 01 篇说的”换厂商只改 import + 构造参数”的底层原因:LangChain 的消息对象把上表右边的全部差异吸收掉了。你写的消息列表,对三家是同一份 Python 代码。

5. 验证:跑起来

配套代码 code/02_messages.py
  1. 构造 SystemMessage + HumanMessage,invoke 传给模型;
  2. 打印 AIMessage 的 model_dump() 全字段 JSON;
  3. 打印 usage_metadatamodel_provider,验证协议统一。
预期输出(节选)

6. 边界

  • AIMessage 的 id 不是模型返回的 id——它是 LangChain 给 run 生成的 id(lc_run--xxx)。模型原始 id 在 response_metadata["id"]msg_xxx)。
  • tool_calls 平时是空列表——只有你让模型”可以调工具”时它才有内容(第 11 篇工具篇用 bind_tools 激活它)。
  • 不要把消息对象和普通 dict 混用——model.invoke() 里要么全是消息对象,要么是符合协议的 dict 列表(第 01 篇那种)。混用会报错。
  • content 可能是列表——Anthropic 协议在思考/工具场景下 content 是 [{"type":"text","text":...}, ...]。判断答案要遍历找 type=="text" 的块。

推荐资料(延伸阅读)


7. 未完待续

我们已经能手工构造消息列表并调用模型。但问题来了:每次都要手写 SystemMessage(...) + HumanMessage(...) 列表,还穿插历史消息,代码会变得很啰嗦、很容易写错(比如漏了 system、历史消息位置不对)。有没有一种方式,把”消息怎么拼”变成”模板”? 03 · ChatPromptTemplate:模板与历史槽位