响应解析
聊天模型生成结构化输出(而非仅返回单个回复字符串)的情况越来越普遍。例如,推理模型可能会输出一条包含其推理过程的思维链,而工具调用模型则可能会输出函数名称和参数。
然而,结构化输出的问题在于,LLM 的输出本身并不具备结构化特征。LLM API 通常接受并返回消息字典,其中包含诸如 role、content 和 reasoning_content 等键,但在内部,LLM 实际上只是继续生成单一的 token 序列。我们使用一个粘合层来连接面向用户的 API 与模型实际的 token 流。为了将输入转换为 token 流,我们使用 chat_templates,这在其他文档中已有介绍。本文档介绍的是该粘合层的另一半:Response templates,即用于将模型生成的 token 输出重新转换为结构化响应字典的系统。
在许多方面,响应模板执行的是与聊天模板相反的操作。使用聊天模板时,你输入一个消息列表,得到的是可直接输入模型的 token。而使用响应模板时,你输入的是模型的原始输出 token,得到的是一个结构化的消息。与聊天模板类似,响应模板允许用户忽略模型所期望的具体格式和控制 token 等繁琐细节,转而使用一种通用的消息字典 API,该 API 适用于任何模型。
理解响应模板的最佳方式是查看其实际用法。主要入口点是 parse_response() 方法,它接受单个序列或一个批次:
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]))
# Outputs a structured dict: {"role": "assistant", "reasoning_content": "...", "content": "..."}当分词器具有 response_template 时,parse_response 方法会将输出消息干净地转换为结构化的字典,以便直接追加到对话中。请注意,我们还需要向该方法传入 prefix(即提示词标记)。这是因为许多对话模板会在让模型开始生成响应之前,先启动消息或打开思考块,因此解析器需要查看提示词才能理解该消息。最终轮次之前的所有前缀都会被丢弃;我们一次只解析一条消息。我们只需要前缀来确保看到的是完整的最终消息,从而不会遗漏任何预填充的字段!
由于缺失的前缀可能导致已预填充的消息被静默地错误解析,因此 prefix 是必需的。省略该参数会引发错误。在极少数情况下,如果您确定生成内容已包含完整消息且无需前缀上下文,请传入 prefix=""(或一个空的 token id 列表)以显式选择退出。
如果 tokenizer 未设置响应模板,parse_response 将抛出错误。我们正在尽快为更多模型添加模板!
流式响应解析
在上面的示例中,我们在生成完成后一次性解析模型响应。不过,我们通常希望在消息生成的过程中就对其进行部分解析,尤其是在面向用户的应用中,我们并不希望在模型完成之前让用户盯着一个静态页面等待一两分钟。
当您需要流式解析时,请调用 tokenizer.get_response_parser(),它会返回一个 ResponseParser。
与 parse_response 一样,请将聊天提示作为 prefix= 传入,以便解析器了解消息中由聊天模板预填充的部分。返回的对象是一个有状态的解析器,您可以在模型生成文本时向其输入文本:
parser = tokenizer.get_response_parser(prefix=input_ids[0])
for event in parser.initial_events:
render(event) # Display the partial message to the user however you want to
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)如果请求中包含工具,请将其一并传递(get_response_parser(..., tools=tools))。
随着每个区域关闭,工具调用参数会根据调用工具的 JSON Schema 进行类型标注,因此流式消费者在 region_close 时即可看到带有类型标注的参数,而无需等到 finalize() 之后。
有关详细信息,请参阅 工具调用参数的类型标注。
解析器会在生成过程的文本输入时发出事件,以指示当前正在生成的区域。当该区域生成完毕时,它会以单独的事件形式发出,其中包含完全解析后的内容。在生成结束时,finalize() 方法会刷新所有剩余文本并发出最终事件,同时返回完整的消息字典。
请注意,尽管 parse_response 可以接受批次,但流式解析始终针对单个序列:每个 ResponseParser 只跟踪一个生成的状态。
如果您想同时流式处理多个生成,请为每个序列创建一个 ResponseParser。
流式事件
每个流式解析事件都是一个包含 type 键的字典。共有三种类型:
| 类型 | 描述 | 目录 |
|---|---|---|
region_open | 表示模型已开始新的区域,例如 content 或 reasoning_content。 | field(str):字段名称。 |
region_chunk | 当前区域的文本块。 | field (str):字段名称。text (str):新的文本块。dirty (bool):如果该文本块是需要解析的原始文本,则为 True。 |
region_close | 表示某个区域已完成,且该键现已最终确定。 | field(str):字段名称。value(any):该区域的完整解析值。 |
region_chunk 事件会在每个 region 接收到字节时发出,因此流式 UI 即使针对结构化 region 也能渲染进度。对于文本类 region(text、int、float、bool),chunk 会被标记为 dirty=False:每个 chunk 已经是最终值的一部分(除了关闭时去除的尾部空白)。对于结构化 region(例如 JSON 格式的 tool call),chunk 会被标记为 dirty=True。这意味着 text 是未经解析的原始正文;可以安全地增量显示,但 parsed 值(dict、list 等)仅在对应的 region_close 事件中到达。无论如何,region 的最终值始终由 region_close 携带,因此不关心中间渲染的消费者可以简单地忽略 region_chunk 事件。
如果聊天 prefix 向消息中写入了任何内容(例如,模板打开了一个思考块,或者 assistant prefill 在移交给模型之前已开始响应),解析器会将这些事件暴露为 parser.initial_events,这是一个列表,您可以在输入任何模型输出之前将其重放到渲染器中。在 prefix 内部打开并关闭的区域会生成完整的 region_open / region_chunk / region_close 序列,其解析后的值会进入输出字典,效果与模型本身写入这些内容完全相同。
典型的事件流可能如下所示:
{"type": "region_open", "field": "reasoning_content"}
{"type": "region_chunk", "field": "reasoning_content", "text": "I should ", "dirty": False}
{"type": "region_chunk", "field": "reasoning_content", "text": "greet the user", "dirty": False}
{"type": "region_close", "field": "reasoning_content", "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!"}}}}请注意,reasoning_content 以 dirty=False 的形式输出,因为诸如 reasoning_content 和 content 之类的字段通常只是原始文本。这意味着你可以将这些块视为有效的“部分输出”。然而,tool_calls 被标记为 dirty,因为原始文本需要大量清理——工具调用通常需要解析为 JSON 或其他格式,然后重新构建以生成最终的工具调用字典。因此,这些区域的最终输出往往与原始文本大相径庭。这种最终解析只有在到达 region_close 时才会发生。在此之前,如何处理 dirty 块由你决定——你可以直接显示它们以向用户展示“原始”输出,或者简单地等待直到有干净的内容可供显示。
至此,您已了解使用响应模板所需的大部分知识。本文档的其余部分将聚焦于解析系统的内部机制以及如何编写响应模板。这部分内容主要面向开发者和模型作者。大多数人可以安全地在此处停止阅读!
高级:编写响应模板
理解如何编写响应模板的最佳方式是选择一个具体的示例。SmolLM 的原始回复可能如下所示:
<think>
I should greet the user
</think>
<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>当我们以标准消息字典格式解析此输出时,其结构应如下所示:
{
"role": "assistant",
"reasoning_content": "I should greet the user",
"tool_calls": [
{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}
]
}下面是用于解析它的模板。别被它吓到——其中大部分内容其实相当直观!
{
"defaults": {"role": "assistant"},
"start_anchor": "<|im_start|>assistant\n",
"fields": {
"reasoning_content": {"open": "<think>", "close": "</think>", "content": "text"},
"tool_calls": {
"open": "<tool_call>",
"close": "</tool_call>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": "{content}"},
},
"content": {
"close": "<|im_end|>",
"content": "text",
},
},
}本质上,模板定义了字段和分隔符。每个字段对应输出字典中的一个键。字段还包含用于解析其分隔符内文本的信息。这里有一个细微之处:content 字段没有 open,因为在 SmolLM(以及另外几个模型)中,它不由特殊标记来标识。相反,content 存储在其他区域之后、序列结束之前的空白中。在我们的模板中,我们将此表示为一个隐式 / 剩余字段,用于捕获未被其他区域占用的任何文本。
字段名称应与模型的聊天模板所读取的键相匹配,以便解析后的消息可以追加到聊天中并再次渲染。特别是,请将推理字段命名为 reasoning_content。这是消息字典中推理的标准键,也是几乎所有聊天模板读取推理内容的键。
除了 fields 之外,该模板还支持两个顶级键:
defaults(可选)— 一个字典,包含预填充到输出中的值(例如{"role": "assistant"})。此处的键始终保留在解析后的输出中,即使没有字段写入它们;其他键在其对应字段未捕获到任何内容时会被丢弃。start_anchor(str) /start_anchor_pattern(str regex) — 标记当前助手消息在聊天提示词中的起始位置。当向parse_response或get_response_parser传入prefix=时,解析器会在处理前,将该前缀从该锚点的最后一次出现位置之后进行右截断,从而避免多轮对话中较早的轮次污染当前消息的状态。该锚点仅应用于prefix,绝不应用于所解析的响应/生成内容——某些格式会在消息中间合法地重新输出该锚点(例如 gpt-oss harmony 输出的每个通道都以<|start|>assistant开头),因此如果将响应中锚点之后的部分剥离,就会丢失模型自身的推理和工具调用。这就是为什么仅凭生成内容不足以防止历史污染:请将提示词作为prefix=传入。对于 ChatML 风格的模型,该锚点通常为"<|im_start|>assistant\n"。必须且只能设置start_anchor或start_anchor_pattern中的一个。
例如,给定以下多轮前缀(注意有两个助手轮次):
<|im_start|>user Hi<|im_end|> <|im_start|>assistant Hello!<|im_end|> <|im_start|>user Again?<|im_end|> <|im_start|>assistant
解析器会截断到最后一个 last <|im_start|>assistant\n 之前的所有内容,从而丢弃较早的 "Hello!" 轮次。规则是:除最后一个 assistant 轮次外的所有内容都会被丢弃。
与聊天模板类似,响应模板作为分词器的属性进行存储,并随分词器一起保存。与聊天模板不同的是,我们将响应模板保存在 tokenizer_config.json 中,而不是作为单独的文件,因为其格式天然适合 JSON,而聊天模板的 Jinja 脚本则不然。
tokenizer.response_template = template
tokenizer.save_pretrained(...) # Written as a key in tokenizer_config.json高级:字段 API 参考
每个字段支持多个键。我们可以将这些键分为两类。第一类是定义字段应如何被捕获的键:
| 密钥 | 类型 | 目的 |
|---|---|---|
open | str 或 list[str] | 用于打开此区域的字面字符串。字符串列表表示“匹配其中任意一个”。 |
open_pattern | str(正则表达式) | open 的正则表达式替代方案。命名组会成为可供 transform 使用的捕获变量。 |
close | str 或 list[str] | 用于关闭此区域的字面字符串(或字符串列表)。省略则运行至流末尾。 |
close_pattern | str(正则表达式) | close 的正则表达式替代方案。命名组会成为可供 transform 使用的捕获变量。 |
repeats | bool | 如果为 true,该字段是一个列表,每次匹配都会追加。默认值为 false。 |
join | str | 使用 repeats 时,将匹配结果用此分隔符拼接为一个字符串,而不是返回一个列表。 |
optional | bool | 如果为 false 且区域始终不匹配,则会引发错误。默认值为 true。 |
字段应仅包含 open 或 open_pattern 其中之一,不能同时包含两者;close 和 close_pattern 也是如此。
某个字段可以完全省略 close/close_pattern,在这种情况下,该区域将保持开放,直到生成的文本结束。这对于一直延伸到消息末尾的最后一个字段非常有用。
一个既没有 neither open 也没有 open_pattern 的字段是 implicit 字段:只要没有显式区域处于打开状态,它就处于激活状态,因此会捕获剩余的文本。最多只能有一个字段是隐式的。当 content 没有特殊标记标签时,通常会使用这种字段,它只是作为纯文本写在其他字段之后。
除了开始和结束分隔符之外,你还可以指定 repeats,它表示该字段是一个列表,并且分隔符可以匹配多次。这在并行工具调用中最为常见,即模型同时发出多个工具调用:
'<tool_call>{"name": "a", ...}</tool_call><tool_call>{"name": "b", ...}</tool_call>'
# Returns `"tool_calls": [{... "a" ...}, {... "b" ...}]` in a template with repeats: true不过,并非每个重复字段都应转换为列表。某些模型会在单条消息中输出多个 reasoning_content 或 content 块,对于这些情况,自然的输出形式是一个拼接后的字符串。设置 join(一个分隔符字符串,通常仅为 "")可以将 repeats 字段从收集列表切换为拼接其匹配项:
field = {"reasoning_content": {"open": "<think>", "close": "</think>", "repeats": True, "join": "\n"}}
input = "<think>first</think>...<think>second</think>"
# Returns: {"reasoning_content": "first\nsecond"}join 字段的每个匹配项都必须解析为字符串。在流式处理时,region_close 仍然携带每个块自身的值;分隔符仅出现在最终的消息字典中。
最后,对于必须存在的字段,您可以指定 optional: false。如果缺少此类字段,我们会引发错误,而不是仅返回一个不包含该字段的消息字典。
生成结束时,会关闭并结束所有未关闭的区域,即使未看到其结束分隔符也是如此。
解析字段的内容
一旦我们定义了如何捕获某个字段,还需要指定如何解析该捕获内的原始文本。控制此行为的有四个键:
| 密钥 | 类型 | 目的 |
|---|---|---|
content | str | 此区域内部的内容类型。默认为 "text"。每种类型都有各自的解析器。 |
content_args | dict | 要传递给此区域内容解析器的参数。 |
transform | dict/list | 可选的解析后模板,用于重塑解析后的主体(参见 Transform)。 |
transform_each | bool | 如果为 true,则解析后的内容必须是一个列表,并且 transform 会逐元素应用。 |
第一个(也是最重要的)键是 content。它指示该字段的内容类型,从而决定用于将字段中捕获的原始文本转换为最终输出的解析器。content_args 用于配置解析器,使我们无需编写自定义代码即可支持各种格式上的特殊要求。
接下来,我们将依次介绍每种解析器及其参数。
基本类型
text、int、float 和 bool 是基本类型。这些内容类型都会先去除空白字符,然后在必要时进行简单的类型转换。它们没有 content_args,text 除外,它支持参数 strip,用于去除捕获文本首尾的空白字符,默认值为 true。
field = {"count": {"open": "<n>", "close": "</n>", "content": "int"}}
input = "<n> 42 </n>"
# Returns: {"count": 42}json
json 解析器会将捕获的文本解析为 JSON。它是处理工具调用参数以及其他具有嵌套结构内容的核心组件。它接受若干可选的 content_args,以应对模型在实际使用中破坏 JSON 格式的多种情况:
unquoted_keys(bool,默认值为false):当键名是未加引号的原始形式(例如{name: "foo"})时启用。适用于输出 JavaScript 风格对象字面量而非严格 JSON 的模型。string_delims([open, close]对的列表,可选):用于那些使用自定义分隔符而非"..."来包裹字符串值的模型。每一对包含一个开始标记和一个结束标记。allow_non_json(bool,默认值为false):如果解析失败,则返回去除空白后的原始文本,而不是抛出异常。适用于模型通常输出 JSON 但偶尔会退化为纯文本的字段,作为回退方案。
unquoted_keys 和 string_delims 均用于处理输出非标准、接近但并非完全符合 JSON 格式内容的模型,因此通常只需在少数几个模型中使用它们。
field = {"args": {"open": "<args>", "close": "</args>", "content": "json", "content_args": {"unquoted_keys": True}}}
input = '<args>{city: "London"}</args>'
# Returns: {"args": {"city": "London"}}xml-inline
xml-inline 解析器适用于由扁平的类 XML 标签序列组成的区域,其中每个标签都会成为字典中的一个条目。它最常用于 tool_calls 字段中,适用于那些将每个参数作为独立标签而非 JSON 数据块输出的模型:
tag_pattern(str, 必填):用于匹配单个标签的正则表达式。必须包含命名组key(作为结果字典的键)和value(作为字典值的原始文本)。value_parser(dict,可选):应用于每个捕获的value的嵌套内容解析器。一个字典,包含name(解析器,例如"json"、"int")以及可选的args(其content_args)。如果省略,值将保持为原始字符串。merge_duplicates(bool,默认值为false):当同一个键多次出现时,将对应的值收集到一个列表中,而不是让后面的匹配项覆盖前面的值。
例如,Qwen3 会将每个工具调用参数作为独立的 <parameter> 标签输出,我们按如下方式对其进行解析:
"tool_calls": {
"open_pattern": r"<tool_call>\s*<function=(?P<name>\w+)>",
"close": "</tool_call>",
"repeats": True,
"content": "xml-inline",
"content_args": {
"tag_pattern": r"<parameter=(?P<key>\w+)>\s*(?P<value>.*?)\s*</parameter>",
"value_parser": {"name": "json", "args": {"allow_non_json": True}},
},
"transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
}注意嵌套的 value_parser:每个参数值本身都会经过 json 解析器处理(并设置 allow_non_json,以便普通字符串也能通过)。将上述 tool_calls 字段作为输入:
input = "<tool_call><function=get_weather><parameter=city>London</parameter><parameter=units>celsius</parameter></function></tool_call>"
# Returns: {"tool_calls": [{"type": "function", "function": {"name": "get_weather", "arguments": {"city": "London", "units": "celsius"}}}]}kv-lines
kv-lines 解析器用于处理以换行符分隔的 key: value 键值对(类似于 YAML 风格的元数据或 .env 文件)。每一行都会成为结果字典中的一个条目。所有参数均为可选:
line_sep(str, 默认值为"\n"):用于分隔各对的字符。kv_sep(str, 默认值为":"):单行内键与其值之间的分隔符。仅使用第一次出现的位置作为分割点,因此值本身可以包含该分隔符。strip(bool,默认值为true):去除每个键和值两侧的空白字符。value_parser(dict,可选):应用于每个值的嵌套内容解析器,格式与xml-inline的{"name": ..., "args": ...}相同。如果省略,值将保持为原始字符串。
空行或不含 kv_sep 的行会被静默跳过,因此捕获区域中出现的多余空行是可以接受的。
field = {"metadata": {"open": "<meta>", "close": "</meta>", "content": "kv-lines"}}
input = "<meta>name: alice\nage: 30</meta>"
# Returns: {"metadata": {"name": "alice", "age": "30"}}注意 age 会将 "30" 保留为字符串;请添加 value_parser 为 {"name": "int"} 以将其解析为 30。
为工具调用参数添加类型
有时,响应解析器可能会以错误的类型解析模型输出。例如,它可能将浮点数 1.5 解析为 “1.50”。如果工具期望某种类型的参数,但实际接收到另一种类型,这可能会导致工具调用出现问题。为避免这种情况,您可以将请求的 tools 传递给 parse_response() 或 ResponseParser,以便使用每个工具的 JSON Schema parameters 进行类型转换。
工具接受的格式与 apply_chat_template() 相同:JSON schema,或带有类型提示和文档字符串的 Python 函数(会自动转换为 schema)。
tools = [{
"type": "function",
"function": {
"name": "set_alarm",
"parameters": {
"type": "object",
"properties": {
"hour": {"type": "integer"},
"enabled": {"type": "boolean"},
"label": {"type": "string"},
},
},
},
}]
message = parse_response(model_out, template, prefix="", tools=tools)
# message["tool_calls"][0]["function"]["arguments"] ==
# {"hour": 7, "enabled": True, "label": "wake up"}转换
对于大多数字段,transform 键并非必需。当解析后的主体需要重塑为最终输出,或者需要将分隔符中的信息合并到结果中时,才会用到它。它最常出现在 tool_calls 字段中,因为这些字段通常具有复杂的结构。
transform 是一个 模板:一个描述输出形状的字典(或列表),其中任何形如 "{name}" 的字符串都会被替换为对应的值。这些值可以从 content(该区域的解析正文)以及由 open_pattern / close_pattern 捕获的任何命名组中获取。一个非常常见的用例是将工具调用字典包裹在一个带有 function 键的外层字典中,因为这是我们的标准工具调用格式的一部分:
"tool_calls": {
"open": "<tool_call>",
"close": "</tool_call>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": "{content}"},
},因此,这个原始输出:
<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>变为(注意 repeats: True 会使 tool_calls 成为一个列表):
[{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}]像 "{content}" 这样的整串占位符会返回查找到的值,并保留其类型——因此在上例中,解析后的 JSON 字典会直接作为 function 的值。占位符必须是整个字符串:不允许将文本与占位符混用(如 "abc {name} def")。它们不是 f-string!
占位符还可以采用点分路径的形式,例如 "{content.args}",它会先查找 content,然后深入解析后的字典中的 args 键。这是重塑工具调用主体的方式,适用于键名与我们标准格式不匹配的情况。例如,一个输出 {"name": ..., "args": ...} 的模型需要将 args 重命名为 arguments:
"tool_calls": {
"open": "<tool>",
"close": "</tool>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": {"name": "{content.name}", "arguments": "{content.args}"}},
},如果某个路径无法解析(在此处,指工具调用的主体中缺少 args),则会引发错误,而不是发出格式错误的工具调用。
transform 非常灵活,当模型输出的格式与我们的标准 API 差异较大时,这一点就显得尤为必要。GPT-OSS 就是一个很好的例子——它将函数名嵌入到通道头部,而不是 JSON 主体中,因此我们必须在 open_pattern 中使用命名组来捕获它,并在 transform 内部将其与 content 合并。open_pattern 和 close_pattern 中的所有命名组都会作为变量与 content 一起可用:
"tool_calls": {
"open_pattern": r"<\|channel\|>commentary to=functions\.(?P<name>\w+).*?<\|message\|>",
"close": "<|call|>",
"repeats": True,
"content": "json",
"transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
},函数名位于通道头部,而不是 JSON 主体中,因此:
<|channel|>commentary to=functions.get_current_weather <|constrain|>json<|message|>{"location": "San Francisco, CA"}<|call|>变为:
[{"type": "function", "function": {"name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}}}]有时,某个字段的解析内容本身就是一个记录列表,而您希望重塑其中的每一项。Cohere 模板就是一个很好的例子:它在单个 JSON 数组中输出所有工具调用,因此我们设置 transform_each: True 以按元素应用转换。每个数组元素的键会被解包到模板作用域中,因此 "{tool_name}" 会在当前元素中查找 tool_name:
"tool_calls": {
"open": "<|START_ACTION|>",
"close": "<|END_ACTION|>",
"content": "json",
"transform_each": True,
"transform": {"type": "function", "function": {"name": "{tool_name}", "arguments": "{parameters}"}},
},这将把如下输出进行转换:
[
{"tool_name": "greet_user", "parameters": {"greeting": "Hi!"}},
{"tool_name": "search", "parameters": {"query": "weather tomorrow"}}
]转换为如下输出,以符合我们的标准 API:
[
{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}},
{"type": "function", "function": {"name": "search", "arguments": {"query": "weather tomorrow"}}}
]transform_each 标志仅在 content 已经是列表时才需要;对于更常见的情况,即每个匹配项贡献一个元素(且 repeats: True 会累积它们),转换默认会应用于每个元素。
框架开发者:正则表达式可移植性
open_pattern、close_pattern 和 start_anchor_pattern 均为正则表达式字符串。对于大多数用户,甚至大多数模型作者而言,这通常不会构成问题;但如果您是一位正在其他语言中编写响应解析实现的开发者,则应了解我们的实现细节。本节专门面向那些不得不实现完整的 Jinja 解析器才能让非 Python 聊天模板正常工作的开发者——我们希望,如果您遵循以下简单指南,响应模板的实现将会轻松许多:
- 我们使用 Python 的
regex模块来处理聊天解析中使用的所有正则表达式。 由于所有 Python3 字符串都是 unicode,我们的所有正则表达式匹配都是 unicode 感知的。这尤其会影响\w等常见字符。请确保在您的引擎中设置了相关的 unicode 标志。 - 我们编译所有正则表达式时均启用
re.DOTALL并禁用re.MULTILINE,因此.会匹配\n,但^和$仅匹配整个输入的开头/结尾,而非换行符。 - 我们使用
(?P<name>...)语法来表示命名组。其他正则表达式实现中的命名捕获组语法差异很大,因此您可能需要在正则表达式中查找此模式,并将其重写以匹配您本地的实现。 - 我们使用部分正则匹配来决定何时可以输出数据。这是必要的,因为区域的末尾可能由多个 token 组成,而我们不希望输出该区域结束的分隔符,因此我们会暂存 token,直到确定它们位于区域内而非边界的一部分。这意味着,每当区域结束的正则表达式出现部分匹配时,我们都会暂存数据,直到该正则表达式完全匹配或匹配失败。如果你的正则表达式引擎不支持部分匹配,你仍然可以实现响应模板,但可能需要寻找其他解决方案来处理此问题。一种简单的做法是,在明确看到结束分隔符之前,始终暂存这些区域或将其作为
dirty输出。 - 你的正则表达式引擎可能(极少情况下)不支持类似
(?!...)的环视(lookaround)。虽然这些语法在响应模板中并不常用,但它们确实可能出现,而我们对此提供了支持!在这些情况下,你可能需要抛出错误,或者手动提取环视条件,并在正则表达式引擎找到可能的匹配项时,在代码中强制执行这些条件。 - 诸如反向引用、原子组、占有型量词、递归等高级功能通常不会在响应模板中使用。我们将尝试劝阻模型作者使用这些功能,因此您可以放心地忽略它们。