Transformers documentation

Análisis de la respuesta

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.2).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Análisis de la respuesta

Es cada vez más común que los modelos de chat generen salidas estructuradas, en lugar de una sola cadena de respuesta. Por ejemplo, un modelo de razonamiento puede emitir una cadena de pensamiento que contiene su trazado de razonamiento, mientras que un modelo de llamada a herramientas puede emitir nombres de funciones y argumentos.

El problema con las salidas estructuradas, sin embargo, es que las salidas de los LLM no son inherentemente estructuradas. Las APIs de LLM suelen aceptar y devolver diccionarios de mensajes, con claves como role y content y reasoning_content, pero internamente, los LLM simplemente continúan una única secuencia de tokens. Utilizamos una capa de conexión para vincular la API orientada al usuario con el flujo de tokens real del modelo. Para convertir las entradas en un flujo de tokens, usamos chat_templates, que se abordan en otros documentos. Este documento trata sobre la otra mitad de esa capa de conexión: Plantillas de respuesta, el sistema para convertir los tokens generados por el modelo de vuelta en un diccionario de respuesta estructurado.

En muchos sentidos, las plantillas de respuesta realizan la operación inversa a las plantillas de chat. Con las plantillas de chat, se introduce una lista de mensajes y se obtienen tokens listos para ingresar al modelo. Con las plantillas de respuesta, se introducen los tokens de salida crudos del modelo y se obtiene un mensaje estructurado. Al igual que las plantillas de chat, las plantillas de respuesta permiten a los usuarios ignorar los detalles incómodos de los formatos específicos y los tokens de control que un modelo espera, y utilizar una API universal de diccionarios de mensajes que funciona con cualquier modelo.

La mejor manera de entender las plantillas de respuesta es verlas en acción. El punto de entrada principal es el método parse_response(), que acepta una sola secuencia o un lote:

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": "..."}

Cuando un tokenizador tiene un response_template, el método parse_response convierte limpiamente un mensaje de salida en un diccionario estructurado, listo para añadirse al chat. Tenga en cuenta que también es necesario pasar el prefix (los tokens del prompt) a este método. Esto se debe a que muchas plantillas de chat inician mensajes o abren bloques de pensamiento antes de permitir que el modelo comience su respuesta, por lo que nuestro analizador necesita ver el prompt para comprender el mensaje. Todo el prefijo anterior al turno final se descarta; solo analizamos un mensaje a la vez. Solo necesitamos el prefijo para asegurarnos de que estamos viendo el mensaje final completo y no perder ningún campo prellenado.

Dado que un prefijo ausente puede provocar un análisis incorrecto de un mensaje prellenado, prefix es obligatorio. Omitirlo genera un error. En el caso poco frecuente en que estés seguro de que la generación ya contiene el mensaje completo y no se necesite contexto de prefijo, pasa prefix="" (o una lista vacía de ids de tokens) para excluirlo explícitamente.

Si el tokenizador no tiene una plantilla de respuesta configurada, parse_response lanzará un error. ¡Estamos trabajando para añadir plantillas a más modelos lo antes posible!

Análisis de respuestas en streaming

En el ejemplo anterior, analizamos la respuesta del modelo de una sola vez después de que termina la generación. Sin embargo, a menudo podemos querer analizar mensajes parciales a medida que se generan, especialmente en aplicaciones orientadas al usuario donde no solo queremos mostrar una página estática durante uno o dos minutos hasta que el modelo termine.

Cuando desee un análisis de flujo, llame a tokenizer.get_response_parser(), que devuelve un ResponseParser. Al igual que con parse_response, pase el prompt de chat como prefix= para que el analizador sepa sobre las partes del mensaje que fueron prellenadas por la plantilla de chat. El objeto devuelto es un analizador con estado al que puede alimentar texto a medida que el modelo lo genera:

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)

Si la solicitud incluye herramientas, pásalas también (get_response_parser(..., tools=tools)). Los argumentos de la llamada a la herramienta se tipan desde el JSON Schema de la herramienta que realiza la llamada a medida que cada región se cierra, por lo que los consumidores de streaming ven argumentos tipados por el esquema en region_close en lugar de solo después de finalize(). Consulta Typing tool-call arguments para más detalles.

El parser emitirá eventos a medida que se introduzca el texto del proceso de generación. Esto indica qué región se está generando actualmente. Cuando la región esté completa, se emitirá en un evento separado con el contenido completamente analizado. Al final de la generación, el método finalize() descarta cualquier texto restante y emite los eventos finales, así como el diccionario de mensaje completo.

Tenga en cuenta que, aunque parse_response puede aceptar lotes, el análisis en streaming siempre es de una sola secuencia: cada ResponseParser rastrea el estado de una generación. Si desea transmitir varias generaciones a la vez, cree un ResponseParser por secuencia.

Eventos de streaming

Cada evento de análisis transmitido es un diccionario con una clave type. Hay tres tipos:

TipoDescripciónContenido
region_openIndica que el modelo ha iniciado una nueva región, como content o reasoning_content.field (str): el nombre del campo.
region_chunkUn fragmento de texto para la región actual.field (str): el nombre del campo. text (str): el nuevo fragmento. dirty (bool): True si el fragmento es texto sin procesar que necesita análisis.
region_closeIndica que una región ha finalizado y que esa clave ahora está finalizada.field (str): el nombre del campo. value (any): el valor completamente analizado para la región

Los eventos region_chunk se emiten para cada región a medida que llegan los bytes, por lo que una interfaz de usuario en streaming puede mostrar el progreso incluso para regiones estructuradas. Para las regiones de tipo texto (text, int, float, bool), los fragmentos se marcan como dirty=False: cada fragmento ya forma parte del valor final (salvo el espacio en blanco final que se elimina al cerrar). Para las regiones estructuradas, como las llamadas a herramientas en formato JSON, los fragmentos se marcan como dirty=True. Esto significa que el texto es el cuerpo sin procesar; es seguro mostrarlo de forma incremental, pero el valor parsed (un diccionario, una lista, etc.) solo llega en el evento correspondiente de region_close. En cualquier caso, el valor finalizado de una región siempre lo lleva region_close, por lo que los consumidores que no se preocupan por la renderización intermedia pueden simplemente ignorar los eventos region_chunk.

Si el prefix del chat escribió algo en el mensaje (por ejemplo, la plantilla abrió un bloque de pensamiento, o un prefill del asistente inició una respuesta antes de ceder el control al modelo), el parser expone esos eventos como parser.initial_events, una lista que puedes reproducir en tu renderizador antes de procesar cualquier salida del modelo. Las regiones que se abrieron y cerraron dentro del prefijo generan una secuencia completa de region_open / region_chunk / region_close y su valor analizado se incluye en el diccionario de salida, exactamente como si el propio modelo los hubiera escrito.

Un flujo de eventos típico podría verse así:

{"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!"}}}}

Fíjate en cómo reasoning_content se emite con dirty=False, porque campos como reasoning_content y content suelen ser simplemente texto crudo. Esto significa que puedes tratar los fragmentos como una “salida parcial” válida. Sin embargo, tool_calls se marca como dirty porque el texto crudo necesita una limpieza considerable: las llamadas a herramientas a menudo deben analizarse como JSON u otro formato y luego reestructurarse para generar el diccionario final de la llamada a la herramienta. Como resultado, la salida final de estas regiones suele verse muy, muy diferente del texto crudo. Este análisis final solo ocurrirá cuando se alcance region_close. Depende de ti qué quieras hacer con los fragmentos dirty hasta entonces: puedes mostrarlos tal cual para mostrar al usuario la salida “cruda”, o simplemente esperar hasta tener algo limpio para mostrar.

Con esto se cubre la mayor parte de lo que necesitas saber para usar plantillas de respuesta. El resto de este documento se centra en los detalles internos del sistema de análisis y en cómo escribir plantillas de respuesta. Esto es relevante principalmente para desarrolladores y autores de modelos. ¡La mayoría de las personas pueden detenerse aquí con seguridad!

Avanzado: Escribir una plantilla de respuesta

La mejor manera de entender cómo escribir una plantilla de respuesta es elegir un ejemplo concreto. Así es como podría verse una respuesta cruda de SmolLM:

<think>
I should greet the user
</think>

<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>

Cuando analizamos esta salida en el formato estándar de diccionario de mensajes, debería verse así:

{
    "role": "assistant",
    "reasoning_content": "I should greet the user",
    "tool_calls": [
        {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}
    ]
}

Y aquí está la plantilla que lo analiza. No se dejen intimidar: ¡gran parte de ella es bastante autoexplicativa!

{
    "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",
        },
    },
}

En esencia, la plantilla define campos y delimitadores. Cada campo corresponde a una clave en el diccionario de salida. Los campos también incluyen información para analizar el texto dentro de sus delimitadores. Hay un matiz: el campo content no tiene open, porque en SmolLM (y varios otros modelos) no está marcado por un token especial. En su lugar, content se almacena en el espacio después de las otras regiones, pero antes del final de la secuencia. En nuestra plantilla, representamos esto como un campo implícito / residual que recoge cualquier texto no reclamado por otra región.

Los nombres de los campos deben coincidir con las claves que la plantilla de chat del modelo lee, para que un mensaje analizado pueda agregarse al chat y volver a renderizarse. En particular, nombre el campo de razonamiento reasoning_content. Esta es la clave estándar para el razonamiento en los diccionarios de mensajes y la que casi todas las plantillas de chat leen para el razonamiento.

Además de fields, la plantilla admite dos claves de nivel superior:

  • defaults (opcional) — Un diccionario de valores predefinidos en la salida (p. ej. {"role": "assistant"}). Las claves aquí se conservan siempre en la salida analizada, incluso si ningún campo escribió en ellas; las demás claves se descartan cuando su campo no capturó nada.
  • start_anchor (str) / start_anchor_pattern (str regex) — Marca dónde comienza el mensaje actual del asistente dentro de un prompt de chat. Cuando se pasa prefix= a parse_response o get_response_parser, el parser recorta el prefijo por la derecha a partir de la última aparición de esta ancla antes de procesarlo, de modo que los turnos anteriores en una conversación de varios turnos no contaminen el estado del mensaje actual. La ancla se aplica solo al prefix, nunca a la respuesta/generación que se analiza: algunos formatos la emiten legítimamente de nuevo a mitad del mensaje (la salida harmony de gpt-oss abre cada canal con <|start|>assistant), por lo que eliminar la respuesta a partir de la ancla haría perder el propio razonamiento del modelo y sus llamadas a herramientas. Por eso la generación por sí sola nunca es suficiente para evitar la filtración del historial: pase el prompt como prefix=. Para los modelos de estilo ChatML, la ancla suele ser "<|im_start|>assistant\n". Debe establecerse exactamente uno de start_anchor o start_anchor_pattern.

Por ejemplo, dado este prefijo de varios turnos (fíjate en los dos turnos del asistente):

<|im_start|>user
Hi<|im_end|>
<|im_start|>assistant
Hello!<|im_end|>
<|im_start|>user
Again?<|im_end|>
<|im_start|>assistant

el parser trunca todo hasta el último <|im_start|>assistant\n, descartando el turno anterior "Hello!". La regla es que todo excepto el turno final del asistente siempre se descarta.

Al igual que las plantillas de chat, las plantillas de respuesta se almacenan como atributos del tokenizador y se guardan junto con él. A diferencia de las plantillas de chat, las guardamos dentro de tokenizer_config.json y no como un archivo separado, porque su formato encaja de forma natural en JSON, a diferencia de un script Jinja de plantilla de chat.

tokenizer.response_template = template
tokenizer.save_pretrained(...)  # Written as a key in tokenizer_config.json

Avanzado: Referencia de la API de campos

Cada campo admite varias claves. Podemos dividirlas en dos tipos. En primer lugar, están las claves que definen cómo debe capturarse el campo:

ClaveTipoPropósito
openstr o list[str]Cadena literal que abre esta región. Una lista de cadenas significa “coincidir con cualquiera de estas”.
open_patternstr (regex)Alternativa con expresiones regulares a open. Los grupos nombrados se convierten en variables de captura disponibles para transform.
closestr o list[str]Cadena literal (o lista de cadenas) que cierra esta región. Omita para ejecutar hasta el final del flujo.
close_patternstr (regex)Alternativa con expresiones regulares a close. Los grupos nombrados se convierten en variables de captura disponibles para transform.
repeatsboolSi es true, el campo es una lista y cada coincidencia se añade. El valor predeterminado es false.
joinstrCon repeats, concatena las coincidencias en una sola cadena con este separador en lugar de una lista.
optionalboolSi es false y la región nunca coincide, se genera un error. Valor predeterminado true.

Un campo debe tener ya sea open o open_pattern, pero no ambos, y lo mismo aplica para close y close_pattern.

Un campo puede omitir por completo close/close_pattern, en cuyo caso la región permanece abierta hasta el final del texto generado. Esto es útil para un campo final que se extiende hasta el final del mensaje.

Un campo con neither open ni open_pattern es el campo implicit: está activo siempre que no haya ninguna región explícita abierta, por lo que captura el texto restante. Como máximo, un campo puede ser implícito. Esto se usa con más frecuencia cuando content no tiene etiquetas de tokens especiales, ya que se escribe simplemente como texto plano después de los otros campos.

Además de abrir y cerrar delimitadores, también puede especificar repeats, lo que indica que el campo es una lista y que los delimitadores pueden coincidir varias veces. Esto es más común para la invocación paralela de herramientas, cuando un modelo emite varias llamadas a herramientas simultáneamente:

'<tool_call>{"name": "a", ...}</tool_call><tool_call>{"name": "b", ...}</tool_call>'
# Returns `"tool_calls": [{... "a" ...}, {... "b" ...}]` in a template with repeats: true

Sin embargo, no todos los campos repetidos deben convertirse en listas. Algunos modelos emiten varios bloques de reasoning_content o content en un solo mensaje, y la salida natural para esos es una sola cadena concatenada. Establecer join (una cadena separadora, a menudo solo "") cambia un campo de repeats de recopilar una lista a concatenar sus coincidencias:

field = {"reasoning_content": {"open": "<think>", "close": "</think>", "repeats": True, "join": "\n"}}
input = "<think>first</think>...<think>second</think>"
# Returns: {"reasoning_content": "first\nsecond"}

Cada coincidencia de un campo join debe analizarse como una cadena. Al transmitir en streaming, region_close sigue llevando el valor propio de cada bloque; el separador solo aparece en el diccionario del mensaje final.

Por último, puedes especificar optional: false para los campos que deben estar presentes. Si falta uno de estos campos, lanzamos un error en lugar de devolver simplemente un diccionario de mensajes sin él.

El final de la generación cerrará y finalizará cualquier región abierta, incluso si no se vio su delimitador de cierre.

Analizar el contenido de un campo

Una vez que definimos cómo capturar un campo, también necesitamos especificar cómo analizar el texto crudo dentro de esa captura. Hay cuatro claves que controlan esto:

ClaveTipoPropósito
contentstrEl tipo de contenido dentro de esta región. El valor predeterminado es "text". Cada tipo tiene su propio analizador.
content_argsdictArgumentos que se pasarán al analizador de contenido para esta región.
transformdict/listPlantilla opcional posterior al análisis que reorganiza el cuerpo analizado (consulte Transform).
transform_eachboolSi es verdadero, el contenido analizado debe ser una lista y transform se aplica elemento por elemento.

La primera (y más importante) clave es content. Esta indica el tipo de contenido del campo, lo que determina el analizador que se utilizará para convertir el texto sin procesar capturado en el campo en la salida final. content_args se utilizan para configurar el analizador y nos permiten soportar varias particularidades de formato sin necesidad de código personalizado. Examinaremos cada tipo de analizador y sus argumentos a continuación.

Tipos básicos

text, int, float y bool son los tipos básicos. Todos estos tipos de contenido simplemente eliminan los espacios en blanco y, si es necesario, realizan una conversión de tipo simple. No tienen ningún content_args, excepto text, que admite el argumento strip, que elimina los espacios en blanco del inicio y el final del texto capturado, y cuyo valor predeterminado es true.

field = {"count": {"open": "<n>", "close": "</n>", "content": "int"}}
input = "<n> 42 </n>"
# Returns: {"count": 42}

json

El parser json analiza el texto capturado como JSON. Es la herramienta principal para los argumentos de llamadas a herramientas y cualquier otra cosa con estructura anidada. Acepta un puñado de content_args opcionales para manejar las diversas formas en que los modelos alteran el JSON en la práctica:

  • unquoted_keys (bool, valor predeterminado false): Actívalo cuando los nombres de las claves no están entre comillas (p. ej. {name: "foo"}). Útil para modelos que emiten literales de objetos de estilo Javascript en lugar de JSON estricto.
  • string_delims (lista de pares [open, close], opcional): para modelos que envuelven los valores de cadena en delimitadores personalizados en lugar de "...". Cada par proporciona un marcador de apertura y uno de cierre.
  • allow_non_json (bool, valor predeterminado false): si el análisis falla, devuelve el texto crudo sin espacios en blanco en lugar de lanzar una excepción. Útil como alternativa para campos donde el modelo suele emitir JSON pero a veces se queda en texto plano.

unquoted_keys y string_delims existen para manejar modelos que emiten una salida no estándar, casi pero no del todo JSON, por lo que solo deberías necesitarlos para unos pocos modelos.

field = {"args": {"open": "<args>", "close": "</args>", "content": "json", "content_args": {"unquoted_keys": True}}}
input = '<args>{city: "London"}</args>'
# Returns: {"args": {"city": "London"}}

xml-inline

El parser xml-inline está diseñado para regiones compuestas por una secuencia plana de etiquetas similares a XML, donde cada etiqueta se convierte en una entrada de un diccionario. Se utiliza con mayor frecuencia dentro de un campo tool_calls para modelos que emiten cada argumento como su propia etiqueta en lugar de como un bloque JSON:

  • tag_pattern (str, obligatorio): expresión regular que coincide con una única etiqueta. Debe contener los grupos nombrados key (la clave resultante del diccionario) y value (el texto sin procesar que se convierte en el valor del diccionario).
  • value_parser (dict, opcional): analizador de contenido anidado aplicado a cada value capturado. Un dict con name (el analizador, p. ej. "json", "int") y args opcional (sus content_args). Si se omite, los valores se mantienen como cadenas de texto sin procesar.
  • merge_duplicates (bool, valor predeterminado false): cuando la misma clave aparece varias veces, se recogen los valores en una lista en lugar de permitir que las coincidencias posteriores sobrescriban a las anteriores.

Por ejemplo, Qwen3 emite cada argumento de llamada a herramienta como su propia etiqueta <parameter>, y lo analizamos de la siguiente manera:

"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}"}},
}

Tenga en cuenta el value_parser anidado: cada valor de parámetro se procesa a su vez con el parser json (con allow_non_json para que las cadenas simples sigan pasando). Al pasar el campo tool_calls anterior como entrada:

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

El parser kv-lines maneja pares key: value delimitados por líneas (piense en metadatos similares a YAML o archivos .env). Cada línea se convierte en una entrada del diccionario resultante. Todos los argumentos son opcionales:

  • line_sep (str, por defecto "\n"): separador entre pares.
  • kv_sep (str, por defecto ":"): separador entre una clave y su valor dentro de una sola línea. Solo se utiliza la primera aparición como punto de división, por lo que los valores pueden contener el separador.
  • strip (bool, valor predeterminado true): elimina el espacio en blanco circundante de cada clave y valor.
  • value_parser (dict, opcional): analizador de contenido anidado aplicado a cada valor, en el mismo formato {"name": ..., "args": ...} que para xml-inline. Si se omite, los valores se mantienen como cadenas de texto sin procesar.

Las líneas vacías o que no contienen kv_sep se omiten silenciosamente, por lo que se toleran las líneas en blanco sueltas en la región capturada.

field = {"metadata": {"open": "<meta>", "close": "</meta>", "content": "kv-lines"}}
input = "<meta>name: alice\nage: 30</meta>"
# Returns: {"metadata": {"name": "alice", "age": "30"}}

Nota: age mantiene "30" como una cadena de texto; añade un value_parser de {"name": "int"} para parsearlo a 30.

Tipado de argumentos de llamadas a herramientas

A veces, el analizador de respuestas puede interpretar las salidas del modelo con el tipo incorrecto. Por ejemplo, podría interpretar el float 1.5 como “1.50”. Esto puede causar problemas con la invocación de herramientas, si las herramientas esperan un argumento de un tipo pero lo reciben de otro. Para evitarlo, puedes pasar las tools de la solicitud a parse_response() o ResponseParser para convertirlos usando los parameters del JSON Schema de cada herramienta. Las herramientas se aceptan en el mismo formato que apply_chat_template(): esquemas JSON, o funciones de Python con type hints y docstrings que se convierten automáticamente en esquemas.

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

Para la mayoría de los campos, la clave transform no es necesaria. Se utiliza cuando el cuerpo analizado debe reestructurarse para convertirse en la salida final, o cuando la información de los delimitadores debe combinarse en el resultado. Aparece con mayor frecuencia en los campos tool_calls, ya que estos suelen tener una estructura compleja.

transform es una plantilla: un diccionario (o lista) que describe la forma de la salida, donde cualquier cadena de la forma "{name}" se reemplaza con el valor correspondiente. Los valores se pueden acceder desde content (el cuerpo analizado de esta región) y de cualquier grupo nombrado capturado por open_pattern / close_pattern. Un caso de uso muy común es envolver un diccionario de llamada a herramienta en un diccionario externo con una clave function, ya que forman parte de nuestro formato estándar de llamadas a herramientas:

"tool_calls": {
    "open": "<tool_call>",
    "close": "</tool_call>",
    "repeats": True,
    "content": "json",
    "transform": {"type": "function", "function": "{content}"},
},

Entonces, esta salida cruda:

<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>

se convierte en (fíjate en que repeats: True convierte tool_calls en una lista):

[{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}]

Un marcador de posición de cadena completa como "{content}" devuelve el valor consultado con su tipo preservado, por lo que, en el ejemplo anterior, el diccionario JSON analizado se inserta directamente como el valor de function. El marcador de posición debe ser la cadena completa: no se permite mezclar texto y marcadores de posición ("abc {name} def"). ¡No son f-strings!

Los marcadores de posición también pueden tomar una ruta con puntos, como "{content.args}", que busca content y luego desciende a la clave args del diccionario analizado. Esta es la forma de reestructurar los cuerpos de llamadas a herramientas cuyos nombres de clave no coinciden con nuestro formato estándar. Por ejemplo, un modelo que emite {"name": ..., "args": ...} necesita que args se renombre a arguments:

"tool_calls": {
    "open": "<tool>",
    "close": "</tool>",
    "repeats": True,
    "content": "json",
    "transform": {"type": "function", "function": {"name": "{content.name}", "arguments": "{content.args}"}},
},

Una ruta que no se resuelve (aquí, una llamada a una herramienta cuyo cuerpo carece de args) genera un error en lugar de emitir una llamada a la herramienta malformada.

transform es bastante versátil, lo cual resulta necesario cuando la salida del modelo tiene un formato muy diferente al de nuestra API estándar. GPT-OSS es un buen ejemplo: incrusta el nombre de la función en la cabecera del canal en lugar de en el cuerpo JSON, por lo que debemos capturarlo con un grupo nombrado en open_pattern y fusionarlo con content dentro del transform. Todos los grupos nombrados en open_pattern y close_pattern quedan disponibles como variables junto a 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}"}},
},

El nombre de la función se encuentra en la cabecera del canal, no en el cuerpo JSON, por lo que esto:

<|channel|>commentary to=functions.get_current_weather <|constrain|>json<|message|>{"location": "San Francisco, CA"}<|call|>

se convierte en:

[{"type": "function", "function": {"name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}}}]

A veces, el contenido analizado de un campo es en sí mismo una lista de registros y se desea reestructurar cada uno de ellos. La plantilla de Cohere es un buen ejemplo: emite todas las llamadas a herramientas dentro de un único array JSON, por lo que se establece transform_each: True para aplicar la transformación por elemento. Las claves de cada elemento del array se descomponen en el ámbito de la plantilla, por lo que "{tool_name}" busca tool_name en el elemento actual:

"tool_calls": {
    "open": "<|START_ACTION|>",
    "close": "<|END_ACTION|>",
    "content": "json",
    "transform_each": True,
    "transform": {"type": "function", "function": {"name": "{tool_name}", "arguments": "{parameters}"}},
},

Esto convertirá una salida como esta:

[
    {"tool_name": "greet_user", "parameters": {"greeting": "Hi!"}},
    {"tool_name": "search", "parameters": {"query": "weather tomorrow"}}
]

A una salida como esta, que se ajusta a nuestra API estándar:

[
    {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}},
    {"type": "function", "function": {"name": "search", "arguments": {"query": "weather tomorrow"}}}
]

La bandera transform_each solo es necesaria cuando content ya es una lista; para el caso más común en el que cada coincidencia aporta un elemento (y repeats: True los acumula), la transformación se aplicará a cada elemento de forma predeterminada.

Desarrolladores de frameworks: portabilidad de expresiones regulares

open_pattern, close_pattern y start_anchor_pattern son cadenas de expresiones regulares. Para la mayoría de los usuarios, e incluso para la mayoría de los autores de modelos, esto no debería ser un problema, pero si eres un desarrollador que está escribiendo una implementación de análisis de respuestas en otro lenguaje, debes ser consciente de los detalles de nuestra implementación. Esta sección está dedicada a todos los que tuvieron que implementar un parser completo de Jinja para que la plantillado de chat no en Python funcionara: esperamos que, si sigues las simples pautas a continuación, las plantillas de respuesta sean mucho menos dolorosas:

  • Utilizamos el módulo regex de Python para todas las expresiones regulares utilizadas en el análisis de chat. Dado que todas las cadenas de Python3 son unicode, todas nuestras coincidencias de expresiones regulares son conscientes de unicode. Esto afecta en particular a caracteres comunes como \w. Asegúrate de establecer las banderas de unicode relevantes en tu motor.
  • Compilamos todas las expresiones regulares con re.DOTALL habilitado y re.MULTILINE deshabilitado, por lo que . coincide con \n, pero ^ y $ solo coinciden con el inicio/final de toda la entrada, no con los saltos de línea.
  • Utilizamos la sintaxis (?P<name>...) para grupos nombrados. Otras implementaciones de expresiones regulares tienen una sintaxis muy diferente para los grupos de captura nombrados, por lo que es posible que necesites buscar este patrón en las expresiones regulares y reescribirlo para que coincida con tu implementación local.
  • Utilizamos coincidencia parcial de expresiones regulares para decidir cuándo podemos emitir datos. Esto es necesario porque el final de una región puede consistir en varios tokens, y no queremos emitir ese delimitador de fin de región, por lo que retenemos los tokens hasta estar seguros de que están dentro de la región y no forman parte del límite. Esto significa que, cada vez que la expresión regular de fin de región tiene una coincidencia parcial, retenemos los datos hasta que la expresión regular coincida o falle. Si su motor de expresiones regulares no admite la coincidencia parcial, aún puede implementar plantillas de respuesta, pero es posible que necesite encontrar otra solución para este problema. Un enfoque simple es simplemente retener estas regiones / emitirlos como dirty hasta que vea definitivamente el delimitador final.
  • Es posible (aunque es poco frecuente) que su motor de expresiones regulares no admita miradas hacia adelante o hacia atrás, como (?!...). Aunque estos no se usan comúnmente en las plantillas de respuesta, pueden aparecer y ¡los admitimos! En esos casos, es posible que necesite lanzar un error o extraer manualmente las miradas y aplicarlas en su código cuando el motor de expresiones regulares encuentre una coincidencia posible.
  • Otros recursos avanzados, como referencias inversas, grupos atómicos, cuantificadores posesivos, recursión, etc., generalmente no se utilizan en las plantillas de respuesta. Intentaremos disuadir a los autores de modelos de usarlos, por lo que, con suerte, podrás ignorarlos con seguridad.
Update on GitHub