> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

# 02 · 消息类数据字典：模型输入输出的"统一信封"

# 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 篇看到它有 `content`、`response_metadata`、`usage_metadata`、`tool_calls`…… 为什么一个回答要带这么多字段？

答案都在"消息类"里。**消息是 LangChain 里所有对话的原子单位**——输入是消息列表，输出是 AIMessage。

***

## 2. 数据字典：消息类体系（关键类的数据）

### 2.1 类层次

```
BaseMessage（抽象基类，6 字段）
 ├── SystemMessage   ← 系统指令（你是谁、怎么回答）
 ├── HumanMessage    ← 用户输入
 ├── AIMessage       ← 模型输出（9 字段，特殊）
 ├── AIMessageChunk  ← 流式输出碎片（第 04 篇）
 └── ToolMessage     ← 工具返回结果（第 11 篇工具篇深入用）
```

### 2.2 BaseMessage 六字段（所有消息的公共部分）

| 字段                  | 数据类型                 | 说明                                                    |                  |
| ------------------- | -------------------- | ----------------------------------------------------- | ---------------- |
| `content`           | `str` / `list[dict]` | 消息内容。字符串是常见情况；Anthropic 协议的思考/工具轮是块列表（第 06 篇细讲）       |                  |
| `additional_kwargs` | `dict`               | 协议私货。如 DeepSeek 的思考内容放 `reasoning_content`            |                  |
| `response_metadata` | `dict`               | 模型返回的元信息（id/model/stop\_reason/usage/model\_provider） |                  |
| `type`              | `str`                | 消息类型标识：`"system"` / `"human"` / `"ai"`                |                  |
| `name`              | \`str                | None\`                                                | 可选的发言者名字         |
| `id`                | \`str                | None\`                                                | 消息唯一 ID（用于追踪/删除） |

**序列化后的形态**（给模型的 JSON）——这是网络层真正发送的：

```json theme={null}
{ "content": "你是公司智能助手", "type": "system", "id": null }
{ "content": "我叫小林", "type": "human", "id": null }
```

### 2.3 AIMessage：9 字段（模型输出的完整信封）

| 字段                   | 数据类型                    | 说明                                                                              |                                          |
| -------------------- | ----------------------- | ------------------------------------------------------------------------------- | ---------------------------------------- |
| `content`            | `str / list[dict]`      | 答案文本（或块列表）                                                                      |                                          |
| `additional_kwargs`  | `dict`                  | 协议私货                                                                            |                                          |
| `response_metadata`  | `dict`                  | **实测**：`{"id","model","stop_reason","usage","model_name","model_provider",...}` |                                          |
| `type`               | `str`                   | 恒为 `"ai"`                                                                       |                                          |
| `name`               | \`str                   | None\`                                                                          | —                                        |
| `id`                 | \`str                   | None\`                                                                          | 形如 `"lc_run--xxx"`（LangChain 生成的 run id） |
| `tool_calls`         | `list[ToolCall]`        | 模型请求调用的工具（结构见 2.5，第 11 篇深入）                                                     |                                          |
| `invalid_tool_calls` | `list[InvalidToolCall]` | 解析失败的工具调用（args 不是合法 JSON）                                                       |                                          |
| `usage_metadata`     | \`UsageMetadata         | None\`                                                                          | token 用量                                 |

**实测 AIMessage.model\_dump() 完整结构**（智谱 glm-4.7，第 01 篇运行输出，脱敏）：

```json theme={null}
{
  "content": "你好！我是Z.ai训练的GLM大语言模型...",
  "additional_kwargs": {},
  "response_metadata": {
    "id": "msg_20260805...", "container": null, "model": "glm-4.7",
    "stop_details": null, "stop_reason": "end_turn", "stop_sequence": null,
    "usage": {
      "cache_creation": null, "cache_creation_input_tokens": null,
      "cache_read_input_tokens": 0, "inference_geo": null,
      "input_tokens": 6, "output_tokens": 45, "output_tokens_details": null,
      "server_tool_use": {"web_fetch_requests": null, "web_search_requests": 0},
      "service_tier": "standard"
    },
    "model_name": "glm-4.7", "model_provider": "anthropic"
  },
  "type": "ai",
  "name": null,
  "id": "lc_run--9d5c...",
  "tool_calls": [],
  "invalid_tool_calls": [],
  "usage_metadata": {
    "input_tokens": 6, "output_tokens": 45, "total_tokens": 51,
    "input_token_details": {"cache_read": 0}
  }
}
```

### 2.4 UsageMetadata（token 用量，三家协议统一成这三键）

| 键               | 类型    | 说明         |
| --------------- | ----- | ---------- |
| `input_tokens`  | `int` | 输入 token 数 |
| `output_tokens` | `int` | 输出 token 数 |
| `total_tokens`  | `int` | 合计         |

> 三家的原始 usage 结构完全不同（Anthropic 是 `input_tokens/output_tokens`，OpenAI 是 `prompt_tokens/completion_tokens`），LangChain 统一成一套——这就是"统一信封"在元数据层的体现。

### 2.5 ToolCall（工具调用结构，第 11 篇深入）

```python theme={null}
ToolCall(
    name="get_weather",        # str，工具名
    args={"city": "北京"},      # dict！永远解析成 dict（OpenAI 系 json.loads，Anthropic 系透传）
    id="call_1",               # str，调用 ID
    type="tool_call",
)
```

***

## 3. 关键方法（本片主角：怎么构造和检查消息）

| 方法/构造                         | 签名                                | 说明                            |
| ----------------------------- | --------------------------------- | ----------------------------- |
| `SystemMessage(content)`      | `SystemMessage(content="...")`    | 系统指令                          |
| `HumanMessage(content)`       | `HumanMessage(content="...")`     | 用户输入                          |
| `AIMessage(content=..., ...)` | 全部字段可传                            | 模型输出（一般由框架构造，你很少手动建）          |
| `invoke(messages)`            | `model.invoke(list[BaseMessage])` | 传消息列表给模型                      |
| `.model_dump()`               | `msg.model_dump()`                | Pydantic 序列化 → dict（打印看结构用这个） |
| `.pretty_print()`             | `msg.pretty_print()`              | 彩色打印消息（调试用）                   |
| `msg.content`                 | 属性                                | 取文本答案                         |

***

## 4. 三协议差异（为什么需要"统一信封"的实证）

|                     | Anthropic 协议（智谱/Claude）                          | OpenAI 协议（DeepSeek/通义/Kimi）              |
| ------------------- | ------------------------------------------------ | ---------------------------------------- |
| role 名              | `system` / `user` / `assistant`                  | `system` / `user` / `assistant`          |
| 消息列表                | `messages: [...]`                                | `messages: [...]`                        |
| **base\_url 规则**    | **自动拼 `/v1/messages`**（不带 /v1）                   | **不自动拼**（自己带 /v1）                        |
| **content 响应结构**    | `content` 是**列表**：`[{"type":"text","text":...}]` | `choices[0].message.content` 是字符串        |
| **usage 键名**        | `input_tokens/output_tokens`                     | `prompt_tokens/completion_tokens`        |
| **思考内容**            | `content` 块列表里 `{"type":"thinking"}`             | `additional_kwargs["reasoning_content"]` |
| **model\_provider** | `"anthropic"`                                    | `"openai"`                               |

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

***

## 5. 验证：跑起来

配套代码 `code/02_messages.py`：

1. 构造 SystemMessage + HumanMessage，`invoke` 传给模型；
2. 打印 AIMessage 的 `model_dump()` 全字段 JSON；
3. 打印 `usage_metadata` 和 `model_provider`，验证协议统一。

```powershell theme={null}
cd enterprise-rag-course\code
python 02_messages.py
```

**预期输出（节选）**：

```
✅ 消息类型: system / human
✅ model_provider: anthropic   ← 智谱走 Anthropic 协议
✅ usage_metadata: {'input_tokens': ..., 'output_tokens': ..., 'total_tokens': ...}
📦 AIMessage 完整结构（9 字段）:
{
  "content": "...",
  "response_metadata": {...},
  "usage_metadata": {...},
  "type": "ai",
  ...
}
```

***

## 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"` 的块。

***

## 推荐资料（延伸阅读）

* [LangChain 官方文档 · 消息](https://docs.langchain.com/oss/python/langchain/messages) —— 消息类完整数据字典（官方维护）
* [langchain-core API 参考 · messages](https://reference.langchain.com/python/langchain-core/messages/) —— 全部消息类的签名与字段

***

## 7. 未完待续

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

→ [03 · ChatPromptTemplate：模板与历史槽位](/doc/doc/enterprise-rag-course/03-模板与历史槽位)
