Transformers Response Parsing 完全指南:用 Response Template 把模型输出还原为结构化消息
本文围绕 HuggingFace Transformers 仓库中的文档 chat_response_parsing.md 展开,讲解 Response Parsing(响应解析)机制:如何用 parse_response 与流式 ResponseParser 把 LLM 生成的原始 token 还原为带 role/thinking/tool_calls 等键的结构化消息字典,并深入 response template 的字段定义、内容解析器、transform 模板与类型强转的实现细节。读完后你可以直接在应用中对模型输出做一次性或逐 token 的结构化解析,也能为自己支持的模型编写并保存一份 response template。
为什么需要 Response Template
聊天模型越来越常产生结构化输出而非单一回复字符串:推理模型(reasoning model)会先输出思考链(chain of thought),工具调用(tool calling,参见 chat_extras)会输出函数名与参数。但 LLM 的输出本质上并不结构化——API 层面看到的是 role、content、thinking 等键组成的消息字典,而模型内部只是一条连续的 token 序列。
Transformers 用一个“胶水层”连接二者:
- 输入方向:用 chat template 把消息列表拼成模型可输入的真实 token 流;
- 输出方向(本文主题):用 Response template 把模型生成的 token 还原成结构化消息字典。
Response template 与 chat template 互为逆向操作:前者喂入原始输出文本,返回一条结构化消息。它的价值在于屏蔽不同模型各异的控制 token 与格式细节,让你始终使用统一的消息字典 API。
一次性解析:parse_response
主入口是 ~PreTrainedTokenizerBase.parse_response 方法,支持单序列或 batch:
from transformers import AutoModelForCausalLM, AutoTokenizer
checkpoint = "HuggingFaceTB/SmolLM3-3B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint, dtype="auto", device_map="auto")
messages = [{"role": "user", "content": "Summarize the end of the Cold War, very briefly."}]
input_ids = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt")["input_ids"].to(model.device)
outputs = model.generate(input_ids, max_new_tokens=1024)[0, input_ids.shape[1]:]
out_text = tokenizer.decode(outputs)
print(tokenizer.parse_response(out_text, prefix=input_ids[0]))
# 输出结构化字典:{"role": "assistant", "thinking": "...", "content": "..."}
prefix(提示词 token)是必传参数,从源码看这一点是强制的:parse_response 在 prefix is None 时直接抛出 ValueError(见 tokenization_utils_base.py)。原因是许多 chat template 会在模型开始生成之前就打开消息或思考块(例如预写 assistant 或 <think> 前缀),解析器必须看到提示词才能确定完整消息。parser 会把前缀截断到最后一个锚点、丢弃其余历史轮次,每次只解析一条消息。
两个边界行为值得注意:
- 若 tokenizer 未设置
response_template属性,parse_response会抛出AttributeError(源码见 tokenization_utils_base.py)。官方正在为更多模型补充模板; - 如果你确定生成文本已包含完整消息、不需要前缀上下文,可显式传
prefix=""(或空 token id 列表)opt out——源码中该空列表会被广播为""处理(见 tokenization_utils_base.py)。
batch 语义上:单个序列(str、一维 token id 列表、一维张量)返回单个消息字典;batch(字符串列表、二维张量等)返回字典列表。prefix 可传单条(广播到每个响应)或与响应一一对应,长度不匹配会报错。
流式解析:ResponseParser 与事件流
一次性解析要求等模型生成完毕。面向用户的交互应用更常需要边生成边解析。此时调用 get_response_parser,返回有状态的 ResponseParser:
parser = tokenizer.get_response_parser(prefix=input_ids[0])
for event in parser.initial_events:
render(event) # 以任意方式向用户展示部分消息
for chunk in model_output:
for event in parser.feed(chunk):
render(event)
message, final_events = parser.finalize()
for event in final_events:
render(event)
要点:
- 与
parse_response一样,需通过prefix=传入 chat prompt,让 parser 感知被模板预写的部分; - 若请求包含 tools,一并传入(
get_response_parser(..., tools=tools)),工具参数会在每个 region 关闭时按 JSON Schema 强转类型,流式消费方在region_close事件中即可拿到带类型的参数,而不用等到finalize(); - 虽然
parse_response支持 batch,流式解析始终是单序列的:每个ResponseParser只跟踪一条生成的状态,多路并发生成需为每条序列各建一个 parser; finalize()冲刷剩余文本、发出最后事件,并返回完整消息字典。源码上,即使某些 region 的关闭分隔符从未出现,finalize也会在序列结束时关闭并结算所有打开的 region(见 response_parser.py)。
三类流式事件
每个流式事件是带 type 键的字典,共三类:
| Type | 描述 | Contents |
|---|---|---|
region_open |
模型开始写一个新 region(如 content 或 thinking) |
field (str):字段名 |
region_chunk |
当前 region 的一段文本块 | field (str)、text (str):新文本块、dirty (bool):为 True 时表示该块是尚需解析的原始文本 |
region_close |
该 region 结束,键值已最终确定 | field (str)、value (any):完整解析后的值 |
dirty 语义的源码依据在 content_parsers.py:STREAMABLE_PARSERS = frozenset({"text", "int", "float", "bool"}),这些“文本型” region 的 chunk 标记 dirty=False——每个块本身就是最终值的一部分(仅收尾时可能去掉尾随空白);而 json、xml-inline、kv-lines 等结构化 region 的 chunk 标记 dirty=True,text 只是原始未解析正文(可增量展示),解析后的值(dict、list 等)只在对应的 region_close 事件中到达。不关心中间渲染的消费方可以完全忽略 region_chunk。
若 chat prefix 本身向消息写过内容(模板打开了 thinking 块,或 assistant prefill 提前开始了回复),parser 会把相应事件暴露在 parser.initial_events 中,供你在喂入任何模型输出之前回放给渲染器;在 prefix 内部既打开又关闭的 region 会产生完整的 region_open/region_chunk/region_close 序列,其解析值进入输出字典,如同模型自己写出的一样(源码见 response_parser.py 的 _consume_prefix)。
一条典型事件流:
{"type": "region_open", "field": "thinking"}
{"type": "region_chunk", "field": "thinking", "text": "I should ", "dirty": False}
{"type": "region_chunk", "field": "thinking", "text": "greet the user", "dirty": False}
{"type": "region_close", "field": "thinking", "value": "I should greet the user"}
{"type": "region_open", "field": "tool_calls"}
{"type": "region_chunk", "field": "tool_calls", "text": '{"name": "greet_user", ', "dirty": True}
{"type": "region_chunk", "field": "tool_calls", "text": '"arguments": {"greeting": "Hi!"}}', "dirty": True}
{"type": "region_close", "field": "tool_calls", "value": {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}}
thinking 的 chunk 是 dirty=False,因为 thinking、content 这类字段通常只是纯文本,chunk 可视为合法的“部分输出”;tool_calls 标记为 dirty,因为其原始文本往往要解析为 JSON 并重新组织,最终值与原始文本差异很大,且该解析只在 region_close 时发生。在这之前如何展示 dirty chunk 由你决定:原样展示“原始输出”,或等待干净内容再展示。
编写 Response Template(进阶)
用一个具体例子理解。SmolLM 的原始回复可能长这样:
<think>
I should greet the user
</think>
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