Transformers documentation
Разбор ответа
Разбор ответа
Всё чаще модели чата генерируют структурированные выводы, а не просто одну строку ответа. Например, модель рассуждений может выдать цепочку рассуждений, содержащую ход её мыслей, а модель с вызовами инструментов — имена функций и аргументы.
Проблема со структурированными выводами в том, что выходные данные LLM по своей природе не структурированы. API LLM обычно принимают и возвращают словари сообщений с ключами, такими как role, content и reasoning_content, но внутри LLM фактически просто продолжают единую последовательность токенов. Мы используем промежуточный слой для соединения пользовательского API с фактическим потоком токенов модели. Чтобы преобразовать входные данные в поток токенов, мы используем chat_templates, которые описаны в других документах. Этот документ посвящен другой половине этого промежуточного слоя: Response templates, системе, которая преобразует сгенерированные моделью токены обратно в структурированный словарь ответа.
Во многих отношениях шаблоны ответа выполняют операцию, обратную шаблонам чата. В случае с шаблонами чата вы передаёте список сообщений и получаете токены, готовые к вводу в модель. В случае с шаблонами ответа вы передаёте «сырые» выходные токены модели и получаете структурированное сообщение. Как и шаблоны чата, шаблоны ответа позволяют пользователям не беспокоиться о сложных деталях того, какие именно форматы и управляющие токены ожидает модель, и использовать универсальный 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="" (или пустой список идентификаторов токенов), чтобы отказаться от его использования.
Если для токенизатора не задан шаблон ответа, 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 излучаются для каждой области по мере поступления байтов, поэтому потоковый интерфейс может отображать прогресс даже для структурированных областей. Для текстовых областей (text, int, float, bool) фрагменты помечаются как dirty=False: каждый фрагмент уже является частью итогового значения (за исключением отступов в конце, которые удаляются при закрытии). Для структурированных областей, таких как вызовы инструментов в формате JSON, фрагменты помечаются как dirty=True. Это означает, что текст представляет собой необработанный корпус; его безопасно отображать по частям, но parsed значение (словарь, список и т. д.) поступает только в соответствующем событии region_close. В любом случае итоговое значение области всегда передаётся в событии region_close, поэтому потребители, которым не важно промежуточное отображение, могут просто игнорировать события region_chunk.
Если чат-prefix записал что-либо в сообщение (например, шаблон открыл блок размышлений, или префилл ассистента начал ответ перед передачей управления модели), парсер предоставляет эти события в виде parser.initial_events — списка, который можно воспроизвести в вашем рендерере до подачи любого вывода модели. Регионы, которые были открыты и закрыты внутри префикса, формируют полную последовательность 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) — Указывает, где внутри чат-промпта начинается текущее сообщение ассистента. При передачеprefix=вparse_responseилиget_response_parserпарсер обрезает префикс справа после последнего вхождения этого якоря перед обработкой, чтобы предыдущие реплики в многораундовом диалоге не загрязняли состояние текущего сообщения. Якорь применяется только к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
парсер обрезает всё до последнего <|im_start|>assistant\n, отбрасывая более ранний ход "Hello!". Правило состоит в том, что всегда отбрасывается всё, кроме последнего хода ассистента.
Как и шаблоны чата, шаблоны ответов хранятся в виде атрибутов токенизатора и сохраняются вместе с ним. В отличие от шаблонов чата, мы сохраняем их внутри tokenizer_config.json, а не в отдельном файле, поскольку их формат естественно вписывается в JSON, в отличие от Jinja-скрипта шаблона чата.
tokenizer.response_template = template
tokenizer.save_pretrained(...) # Written as a key in tokenizer_config.jsonДополнительно: Справочник по API полей
Каждое поле поддерживает несколько ключей. Их можно разделить на два типа. Во-первых, это ключи, определяющие способ захвата поля:
| Ключ | Тип | Назначение |
|---|---|---|
open | str or list[str] | Строковый литерал, открывающий эту область. Список строк означает «совпадение с любым из этих значений». |
open_pattern | str (regex) | Регулярное выражение как альтернатива open. Именованные группы становятся переменными захвата, доступными для transform. |
close | str or list[str] | Строковый литерал (или список строковых литералов), закрывающий эту область. Пропустите, чтобы выполнять до конца потока. |
close_pattern | str (regex) | Регулярное выражение, альтернатива close. Именованные группы становятся переменными захвата, доступными для transform. |
repeats | bool | Если значение равно true, поле является списком, и каждое совпадение добавляется в конец. По умолчанию false. |
join | str | С параметром repeats совпадения объединяются в одну строку с этим разделителем вместо списка. |
optional | bool | Если значение равно false, а регион никогда не совпадает, мы вызываем ошибку. По умолчанию true. |
Поле должно иметь либо open, или open_pattern, но не оба, и то же самое относится к close и close_pattern.
Поле может полностью опускать close/close_pattern, в этом случае область остаётся открытой до конца сгенерированного текста. Это полезно для последнего поля, которое продолжается до конца сообщения.
Поле, у которого нет ни open, ни open_pattern, является неявным полем: оно активно, когда не открыта ни одна явная область, поэтому перехватывает оставшийся текст. Неявных полей может быть не более одного. Чаще всего это используется, когда 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, optional): вложенный парсер содержимого, применяемый к каждому захваченному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, optional): вложенный парсер содержимого, применяемый к каждому значению, в том же формате{"name": ..., "args": ...}, что и дляxml-inline. Если не указан, значения остаются в виде необработанных строк.
Пустые строки или строки, не содержащие kv_sep, молча пропускаются, поэтому случайные пустые строки в захваченной области допускаются.
field = {"metadata": {"open": "<meta>", "close": "</meta>", "content": "kv-lines"}}
input = "<meta>name: alice\nage: 30</meta>"
# Returns: {"metadata": {"name": "alice", "age": "30"}}Note age сохраняет "30" как строку; добавьте value_parser с {"name": "int"}, чтобы преобразовать его в 30.
Типизация аргументов вызова инструмента
Иногда парсер ответов может интерпретировать выходные данные модели с неверным типом. Например, он может распарсить число с плавающей точкой 1.5 как строку “1.50”. Это может вызывать проблемы при вызове инструментов, если инструменты ожидают аргумент одного типа, но получают его в другом. Чтобы избежать этого, вы можете передать tools запроса в parse_response() или ResponseParser, чтобы привести их к нужным типам с помощью JSON Schema parameters каждого инструмента.
Инструменты принимаются в том же формате, что и в apply_chat_template(): JSON-схемы или функции Python с аннотациями типов и docstrings, которые автоматически преобразуются в схемы.
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-строки!
Плейсхолдеры также могут принимать точечный путь, например "{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 и объединить с content внутри transform. Все именованные группы в 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 — мы надеемся, что, следуя простым рекомендациям ниже, вы обнаружите, что работа с шаблонами ответов станет значительно менее болезненной:
- Мы используем модуль
regexPython для всех регулярных выражений, применяемых при разборе чата. Поскольку все строки в Python3 являются unicode, все наши совпадения регулярных выражений учитывают unicode. Это особенно влияет на распространённые символы, такие как\w. Убедитесь, что вы установили соответствующие unicode-флаги в вашем движке. - Мы компилируем все регулярные выражения с включённым
re.DOTALLи отключённымre.MULTILINE, поэтому.сопоставляется с\n, а^и$сопоставляются только с началом/концом всего входного текста, а не с переносами строк. - Мы используем синтаксис
(?P<name>...)для именованных групп. В других реализациях регулярных выражений синтаксис именованных групп захвата может сильно отличаться, поэтому вам, возможно, потребуется найти этот шаблон в регулярных выражениях и переписать его в соответствии с вашей локальной реализацией. - Мы используем частичное сопоставление регулярных выражений, чтобы определить, когда можно выдать данные. Это необходимо, потому что конец области может состоять из нескольких токенов, и мы не хотим выдавать разделитель конца области, поэтому удерживаем токены, пока не будем уверены, что они находятся внутри области, а не являются частью границы. Это означает, что каждый раз, когда регулярное выражение конца области имеет частичное совпадение, мы удерживаем данные, пока регулярное выражение не совпадёт полностью или не завершится ошибкой. Если ваш движок регулярных выражений не поддерживает частичное сопоставление, вы всё равно можете реализовать шаблоны ответов, но, возможно, вам потребуется найти другое решение для этой проблемы. Один простой подход — просто удерживать эти области / выдавать их как
dirty, пока вы не увидите окончательный разделитель. - Ваш движок регулярных выражений может (редко) не поддерживать конструкции lookaround, такие как
(?!...). Хотя они нечасто используются в шаблонах ответов, они могут встречаться, и мы их поддерживаем! В таких случаях вам, возможно, придётся либо выбрасывать ошибку, либо вручную извлекать lookaround и принудительно применять их в вашем коде, когда движок регулярных выражений находит возможное совпадение. - Другие продвинутые возможности, такие как обратные ссылки, атомарные группы, жадные квантификаторы, рекурсия и т. д., как правило, не используются в шаблонах ответа. Мы постараемся отговорить авторов моделей от их использования, поэтому, скорее всего, вы можете безопасно их игнорировать.