首页
/ Transformers Response Parsing 完全指南:用 Response Template 把模型输出还原为结构化消息

Transformers Response Parsing 完全指南:用 Response Template 把模型输出还原为结构化消息

2026-09-06 12:53:21作者:裘晴惠Vivianne

本文围绕 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 层面看到的是 rolecontentthinking 等键组成的消息字典,而模型内部只是一条连续的 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_responseprefix 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(如 contentthinking field (str):字段名
region_chunk 当前 region 的一段文本块 field (str)、text (str):新文本块、dirty (bool):为 True 时表示该块是尚需解析的原始文本
region_close 该 region 结束,键值已最终确定 field (str)、value (any):完整解析后的值

dirty 语义的源码依据在 content_parsers.pySTREAMABLE_PARSERS = frozenset({"text", "int", "float", "bool"}),这些“文本型” region 的 chunk 标记 dirty=False——每个块本身就是最终值的一部分(仅收尾时可能去掉尾随空白);而 jsonxml-inlinekv-lines 等结构化 region 的 chunk 标记 dirty=Truetext 只是原始未解析正文(可增量展示),解析后的值(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,因为 thinkingcontent 这类字段通常只是纯文本,chunk 可视为合法的“部分输出”;tool_calls 标记为 dirty,因为其原始文本往往要解析为 JSON 并重新组织,最终值与原始文本差异很大,且该解析只在 region_close 时发生。在这之前如何展示 dirty chunk 由你决定:原样展示“原始输出”,或等待干净内容再展示。

编写 Response Template(进阶)

用一个具体例子理解。SmolLM 的原始回复可能长这样:

<think>
I should greet the user
</think>
登录后查看全文
热门项目推荐
相关项目推荐