Transformers 聊天消息内容格式实战:从文本、多模态到工具调用与批量推理的统一 Message 模式
Transformers 中所有聊天模型(chat model)都以「角色 + 内容」的消息列表作为对话输入,而 chat_content_patterns.md 是官方为这套统一格式编写的模式指南。本文基于该文档完整展开各类内容模式(纯文本、工具调用、图片/视频/音频、混合模态、批量、多轮)的写法,并结合 processing_utils.py 中 apply_chat_template 的源码实现,说明每条消息在框架内部是如何被解析、渲染和送入模型的。读完本文,你可以直接写出可运行的多模态聊天消息结构,并理解从消息字典到模型张量的完整处理链路。
统一的消息结构:role 与 content
聊天模型期望接收一个字典列表作为对话。每个字典使用 role 和 content 两个键:role 标识说话方(system、user、assistant、tool),content 承载实际内容。文本语言模型只接受文本与工具,多模态模型则在文本之外结合图片、视频和音频。
Transformers 采用统一格式:每种模态都通过显式的 "type" 字段声明,因此同一条消息中可以自由混排多种输入。这也是后续所有模式(工具、多模态、批量、多轮)的基础约定。
文本消息
文本是最基础的内容类型,是所有其他模式的基础。最简单的做法是把消息直接以字符串形式传给 "content":
message = [
{
"role": "user",
"content": "Explain the French Bread Law."
}
]
也可以使用显式的 "type": "text" 格式,这样日后在同一份代码中追加图片、视频或音频时,消息结构保持一致,无需改动已有逻辑:
message = [
{
"role": "user",
"content": [{"type": "text", "text": "Explain the French Bread Law."}]
}
]
从源码看,字符串形式的 content 与列表形式的 content 都会被接受:在 ProcessorMixin.apply_chat_template 中解析媒体时,源码先判断 isinstance(content, str),若是字符串则直接跳过媒体提取(processing_utils.py),再对列表形式的各内容块按 type 分别处理。两种写法对纯文本模型效果相同,显式 type 写法则让多模态与纯文本消息使用同一种数据结构。
工具调用消息(Tools)
工具(Tools)是聊天模型可以调用的函数,例如获取实时天气数据,而不是让模型自行生成答案。工具的定义、传给 apply_chat_template 的 tools 参数、JSON schema 生成等完整流程在 chat_extras.md 中详细说明,本节聚焦消息列表本身如何表达一次工具调用。
工具调用涉及两个角色。assistant 角色承载模型的「工具请求」:在 "tool_calls" 键中设置 "type": "function",并把你的工具放入 "function" 键,然后把该请求追加到消息列表:
weather = {"name": "get_current_temperature", "arguments": {"location": "Paris, France", "unit": "celsius"}}
message.append(
{
"role": "assistant",
"tool_calls": [{"type": "function", "function": weather}]
}
)
tool 角色承载工具的执行结果,追加到 "content" 中。注意:这个值必须始终是字符串(tool 角色结果即使返回数字 22 也要写成 "22"):
message.append({"role": "tool", "content": "22"})
几个容易踩坑的点,均出自 chat_extras.md 的官方警告:
- 模型本身并不能真正调用工具,它只是「请求」一次调用,执行工具、把请求与结果追加回对话历史的职责在你的应用代码;
tool_calls中的function值在 Transformers 里必须是 dict,而 OpenAI API 使用 JSON 字符串。把 OpenAI 风格的 JSON 字符串直接放进 Transformers 会导致报错或模型行为异常;- 大多数模型一次只发出单个工具调用,少数老模型会并发发出多个,此时需要借助 tool call ID 做区分,具体格式应以对应模型卡为准。
完整的调用链是:tokenizer.apply_chat_template(messages, tools=tools, add_generation_prompt=True, return_dict=True, return_tensors="pt") → 模型生成 → 用 [~PreTrainedTokenizer.parse_response] 提取工具调用(支持 response parsing 的模型可自动完成)→ 按上述格式追加回 messages → 再次调用模板渲染,让模型读取工具结果并作答。
多模态消息
多模态模型扩展了同一套格式来处理图片、视频和音频。每个输入显式指定其 "type",并通过 "url" 或 "path" 提供媒体本体。
图片
设置 "type": "image",链接用 "url",本地文件用 "path":
message = [
{
"role": "user",
"content": [
{"type": "image", "url": "https://assets.bonappetit.com/photos/57ad4ebc53e63daf11a4ddc7/master/w_1280,c_limit/kouign-amann.jpg"},
{"type": "text", "text": "What pastry is shown in the image?"}
]
}
]
视频
设置 "type": "video",同样用 "url" 或 "path":
message = [
{
"role": "user",
"content": [
{"type": "video", "url": "https://static01.nyt.com/images/2019/10/01/dining/01Sourdough-GIF-1/01Sourdough-GIF-1-superJumbo.gif"},
{"type": "text", "text": "What is shown in this video?"}
]
}
]
音频
设置 "type": "audio",同样用 "url" 或 "path":
message = [
{
"role": "user",
"content": [
{"type": "audio", "url": "https://huggingface.co/datasets/Narsil/asr_dummy/resolve/main/mlk.flac"},
{"type": "text", "text": "Transcribe the speech."}
]
}
]
混合多模态
content 列表接受任意类型的组合,模型会一起处理全部输入,从而支持跨模态比较与推理:
message = [
{
"role": "user",
"content": [
{"type": "image", "url": "https://assets.bonappetit.com/photos/57ad4ebc53e63daf11a4ddc7/master/w_1280,c_limit/kouign-amann.jpg"},
{"type": "video", "url": "https://static01.nyt.com/images/2019/10/01/dining/01Sourdough-GIF-1/01Sourdough-GIF-1-superJumbo.gif"},
{"type": "text", "text": "What does the image and video share in common?"},
],
},
{
"role": "user",
"content": [
{"type": "image", "url": "https://assets.bonappetit.com/photos/57ad4ebc53e63daf11a4ddc7/master/w_1280,c_limit/kouign-amann.jpg"},
{"type": "image", "url": "https://assets.bonappetit.com/photos/57e191f49f19b4610e6b7693/master/w_1600%2Cc_limit/undefined"},
{"type": "text", "text": "What type of pastries are these?"},
],
}
]
源码视角:媒体块是如何被提取的
上述 url/path 的取值并非只有文档写的这两种键。在 ProcessorMixin.apply_chat_template 的媒体提取逻辑中(processing_utils.py):
- 图片块会按
image、url、path、base64四个候选键依次取值——也就是说图片除了 URL 和本地路径,还支持base64字段直接内嵌媒体数据; - 视频块按
video、url、path取值; - 音频块按
audio、url、path取值,并通过load_audio以指定采样率加载(未显式传入时取音频处理器的sampling_rate,否则默认 16 000 Hz,见 processing_utils.py); - 若设置了
load_audio_from_video=True,则从视频文件中直接抽取音轨,并向消息中追加{"type": "audio"}块以确保模板渲染出音频占位符(processing_utils.py)。
另外,apply_chat_template 会自动把 OpenAI 风格的 {"type": "image_url", "image_url": {"url": "..."}} 内容块归一化为 HuggingFace 风格的 {"type": "image", "url": "..."}(processing_utils.py),因此从 OpenAI 兼容代码迁移过来的图片消息可以直接使用。
在媒体进入模型之前,还有一步关键的文本侧处理:处理器按顺序扫描模板渲染出的文本,将每个模态的占位符 token(如 image_token)逐一替换为对应媒体的展开字符串,并记录每个占位符的原始与替换后偏移(processing_utils.py)。同时,create_mm_token_type_ids 会为每个 token 位置生成模态类型 ID:0 表示普通文本、1 表示图片、2 表示视频、3 表示音频(processing_utils.py),供模型的 mm_token_type_ids 输入使用。这解释了为什么消息中媒体与文本的相对顺序必须与占位符顺序一致。
批量推理消息(Batched)
批量推理把多段对话放入单次前向传播,以提升吞吐与效率。做法是把每段对话各自包在一个列表里,再整体作为「列表的列表」传入:
messages = [
[
{"role": "user",
"content": [
{"type": "image", "url": "https://assets.bonappetit.com/photos/57ad4ebc53e63daf11a4ddc7/master/w_1280,c_limit/kouign-amann.jpg"},
{"type": "text", "text": "What type of pastry is this?"}
]
},
],
[
{"role": "user",
"content": [
{"type": "image", "url": "https://assets.bonappetit.com/photos/57e191f49f19b4610e6b7693/master/w_1600%2Cc_limit/undefined"},
{"type": "text", "text": "What type of pastry is this?"}
]
},
],
]
从源码看,apply_chat_template 会自动识别这种结构:当输入是列表且第一个元素本身也是列表(或带有 content 属性)时,is_batched 置为 True,否则把单个对话包装成单元素列表走非批量路径(processing_utils.py)。模板渲染完成后,非批量输入会拆回单个 prompt,批量输入则保持多 prompt 列表,tokenize=True 时各段对话的图像、视频、音频分别构成 batch_images、batch_videos、batch_audios 交由处理器统一批处理(processing_utils.py)。
多轮对话(Multi-turn)
对话跨越多轮交互,在 "user" 与 "assistant" 角色之间交替。每一轮都向列表追加一条新消息,使模型能够看到完整对话历史,从而生成更贴合上下文的回复:
message = [
{
"role": "user",
"content": [
{"type": "image", "url": "https://assets.bonappetit.com/photos/57ad4ebc53e63daf11a4ddc7/master/w_1280,c_limit/kouign-amann.jpg"},
{"type": "text", "text": "What pastry is shown in the image?"}
]
},
{
"role": "assistant",
"content": [{"type": "text", "text": "This is kouign amann, a laminated dough pastry (i.e., dough folded with layers of butter) that also incorporates sugar between layers so that during baking the sugar caramelizes."}]
},
{
"role": "user",
"content": [
{"type": "image", "url": "https://static01.nyt.com/images/2023/07/21/multimedia/21baguettesrex-hbkc/21baguettesrex-hbkc-videoSixteenByNineJumbo1600.jpg"},
{"type": "text", "text": "Compare it to this image now."}
]
}
]
多轮场景中每轮都可以携带不同的媒体,模型能结合前文(如上一轮的助手回答与更早的图片)进行比较式回答。
消息模式的最终去向:apply_chat_template 处理链
把上述所有模式串起来,消息从字典列表到模型输入要经过 apply_chat_template。多模态处理器上的方法签名为(processing_utils.py):
def apply_chat_template(
self,
conversation: list[dict[str, str]] | list[list[dict[str, str]]], # 单段或批量
chat_template: str | None = None,
tools: list[dict] | None = None,
documents: list[dict[str, str]] | None = None,
add_generation_prompt: bool = False,
continue_final_message: bool | str = False,
return_assistant_tokens_mask: bool = False,
tokenize: bool = False,
return_tensors: str | TensorType | None = None,
return_dict: bool = False,
load_audio_from_video: bool = False,
processor_kwargs: dict | None = None,
**kwargs,
) -> str:
关键参数与本文各模式的对应关系:
| 参数 | 作用 |
|---|---|
conversation |
接受单段对话或「列表的列表」批量对话,即上文 Batched 模式的入口 |
tools |
工具调用模式所需的函数/JSON schema 列表(纯文本 tokenizer 上同名参数) |
add_generation_prompt |
生成前追加 assistant 提示头;与 continue_final_message 互斥 |
tokenize / return_dict / return_tensors |
控制是否直接产出张量与处理器字典(含 pixel_values 等媒体张量) |
load_audio_from_video |
从视频消息中抽取音轨并追加 {"type": "audio"} 块 |
典型执行链为:① 识别是否批量 → ② 归一化 OpenAI 风格 image_url 块 → ③ 按各模态的候选键提取并加载媒体 → ④ 用 Jinja chat template 渲染出带占位符的 prompt(模板写法见 chat_templating.md)→ ⑤ 占位符按序展开为各媒体的替换串并记录偏移 → ⑥ 处理器输出 input_ids、pixel_values 等可直接送入 model.generate 的张量。
小结:模式速查
| 模式 | 关键写法 | 注意事项 |
|---|---|---|
| 文本 | content 为字符串,或 [{"type": "text", "text": ...}] |
两种写法等价,后者结构更统一 |
| 工具调用 | assistant + tool_calls(type: "function");tool 角色回传结果 |
tool 的 content 必须是字符串;function 值必须是 dict 而非 JSON 串 |
| 图片 | {"type": "image", "url"/"path": ...} |
源码还支持 base64 键 |
| 视频 | {"type": "video", "url"/"path": ...} |
可用 load_audio_from_video 抽音轨 |
| 音频 | {"type": "audio", "url"/"path": ...} |
加载采样率默认 16 000 Hz |
| 混合 | 同一条 content 中混排任意类型 |
媒体顺序须与模板占位符顺序一致 |
| 批量 | 列表的列表,每段对话一个内层列表 | apply_chat_template 自动识别批量结构 |
| 多轮 | user/assistant 交替追加,保留完整历史 |
每一轮均可携带新的媒体块 |
所有以上消息结构的最终消费者是 apply_chat_template,因此掌握「消息字典怎么构造」与「模板与处理器怎么消费」两侧,就能在 Transformers 中正确使用任何聊天模型的多模态对话功能。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00