Transformers documentation

Analyse de la réponse

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

Analyse de la réponse

Il est de plus en plus courant que les modèles de chat génèrent des sorties structurées, plutôt qu'une simple chaîne de réponse. Par exemple, un modèle de raisonnement peut émettre une chaîne de pensée contenant sa trace de raisonnement, tandis qu'un modèle de tool calling peut émettre des noms de fonctions et des arguments.

Le problème avec les sorties structurées, c’est que les sorties des LLM ne sont pas intrinsèquement structurées. Les API de LLM acceptent et renvoient généralement des dictionnaires de messages, avec des clés comme role, content et reasoning_content, mais en interne, les LLM ne font en réalité que poursuivre une seule séquence de jetons. Nous utilisons une couche de liaison pour connecter l’API destinée à l’utilisateur au flux de jetons réel du modèle. Pour transformer les entrées en un flux de jetons, nous utilisons chat_templates, qui sont abordés dans d’autres documents. Ce document porte sur l’autre moitié de cette couche de liaison : les Response templates, le système qui permet de retransformer les jetons générés par le modèle en un dictionnaire de réponse structuré.

À bien des égards, les modèles de réponse effectuent l'opération inverse des modèles de chat. Avec les modèles de chat, vous fournissez une liste de messages et vous obtenez des jetons prêts à être entrés dans le modèle. Avec les modèles de réponse, vous fournissez les jetons de sortie bruts du modèle et vous obtenez un message structuré. Comme les modèles de chat, les modèles de réponse permettent aux utilisateurs d'ignorer les détails fastidieux des formats spécifiques et des jetons de contrôle attendus par un modèle, et d'utiliser une API universelle de dictionnaires de messages qui fonctionne avec n'importe quel modèle.

La meilleure façon de comprendre les modèles de réponse est de les voir en action. Le point d’entrée principal est la méthode parse_response(), qui accepte soit une séquence unique, soit un lot :

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

Lorsqu'un tokenizer possède un response_template, la méthode parse_response transforme proprement un message de sortie en un dictionnaire structuré, prêt à être ajouté à la conversation. Notez qu'il est nécessaire de passer le prefix (les jetons du prompt) à cette méthode également. Cela est dû au fait que de nombreux modèles de conversation commencent les messages ou ouvrent des blocs de réflexion avant de laisser le modèle démarrer sa réponse, et notre analyseur a donc besoin de voir le prompt pour comprendre le message. Tout le préfixe avant le dernier tour est ignoré ; nous n'analysons qu'un seul message à la fois. Nous avons juste besoin du préfixe pour nous assurer de voir le message final dans son intégralité, sans manquer de champs pré-remplis !

Étant donné qu'un préfixe manquant peut entraîner une analyse silencieuse erronée d'un message prérempli, prefix est obligatoire. Son omission provoque une erreur. Dans le cas rare où vous êtes certain que la génération contient déjà le message complet et qu'aucun contexte de préfixe n'est nécessaire, passez prefix="" (ou une liste vide d'identifiants de jetons) pour désactiver explicitement cette exigence.

Si le tokenizer n'a pas de template de réponse défini, parse_response lèvera une erreur. Nous travaillons à l'ajout de templates à un maximum de modèles dans les plus brefs délais !

Analyse des réponses en streaming

Dans l’exemple ci-dessus, nous analysons la réponse du modèle en une seule fois, après la fin de la génération. Souvent, toutefois, nous souhaitons analyser les messages partiels au fur et à mesure de leur génération, en particulier dans les applications destinées aux utilisateurs, où nous ne voulons pas simplement afficher une page statique pendant une ou deux minutes jusqu’à ce que le modèle ait terminé.

Lorsque vous souhaitez une analyse en flux, appelez tokenizer.get_response_parser(), qui renvoie un ResponseParser. Comme pour parse_response, passez le prompt de chat en tant que prefix= afin que l’analyseur connaisse les parties du message qui ont été pré-remplies par le modèle de chat. L’objet renvoyé est un analyseur à état que vous pouvez alimenter en texte au fur et à mesure que le modèle le génère :

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 requête inclut des outils, transmettez-les également (get_response_parser(..., tools=tools)). Les arguments des appels d’outils sont alors typés à partir du JSON Schema de l’outil appelant à chaque fermeture de région, de sorte que les consommateurs du streaming voient des arguments typés par schéma sur region_close plutôt qu’après finalize(). Voir Typage des arguments des appels d’outils pour plus de détails.

Le parseur émet des événements au fur et à mesure que le texte du processus de génération est fourni. Cela indique la région actuellement en cours de génération. Lorsque la région est complète, elle est émise dans un événement distinct contenant le contenu entièrement analysé. À la fin de la génération, la méthode finalize() vide tout texte restant et émet les événements finaux, ainsi que le dictionnaire de message complet.

Notez que bien que parse_response puisse accepter des lots, l’analyse en flux est toujours mono-séquence : chaque ResponseParser suit l’état d’une seule génération. Si vous souhaitez diffuser plusieurs générations simultanément, créez un ResponseParser par séquence.

Événements en streaming

Each streamed parsing event is a dict with a type key. There are three kinds:

TypeDescriptionSommaire
region_openIndique que le modèle a démarré une nouvelle région, telle que content ou reasoning_content.field (str) : le nom du champ.
region_chunkUn fragment de texte pour la région actuelle.field (str) : le nom du champ. text (str) : le nouveau fragment. dirty (bool) : True si le fragment est du texte brut nécessitant une analyse.
region_closeIndique qu'une région a terminé et que la clé est désormais finalisée.field (str) : le nom du champ. value (any) : la valeur entièrement analysée pour la région

Les événements region_chunk sont émis pour chaque région à mesure que les octets arrivent, ce qui permet à une interface en streaming d’afficher la progression, y compris pour les régions structurées. Pour les régions de type texte (text, int, float, bool), les fragments sont marqués dirty=False : chaque fragment fait déjà partie de la valeur finale (à l’exception des espaces de fin supprimés lors de la fermeture). Pour les régions structurées, comme les appels d’outils au format JSON, les fragments sont marqués dirty=True. Cela signifie que le texte est le corps brut, non analysé ; il est sûr de l’afficher de manière incrémentale, mais la valeur parsed (un dictionnaire, une liste, etc.) n’arrive que dans l’événement region_close correspondant. Dans tous les cas, la valeur finalisée d’une région est toujours transmise par region_close, de sorte que les consommateurs qui ne se soucient pas du rendu intermédiaire peuvent simplement ignorer les événements region_chunk.

Si le prefix de la conversation a écrit quelque chose dans le message (par exemple, si le modèle a ouvert un bloc de réflexion, ou si un préremplissage de l'assistant a démarré une réponse avant de passer la main au modèle), l'analyseur expose ces événements sous la forme de parser.initial_events, une liste que vous pouvez rejouer dans votre rendu avant d'alimenter la sortie du modèle. Les régions qui ont été ouvertes et fermées dans le préfixe produisent une séquence complète region_open / region_chunk / region_close et leur valeur analysée est insérée dans le dictionnaire de sortie, exactement comme si le modèle lui-même les avait écrites.

Un flux d'événements typique pourrait ressembler à ceci :

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

Notez que reasoning_content est émis avec dirty=False, car des champs comme reasoning_content et content sont généralement du texte brut. Cela signifie que vous pouvez traiter les fragments comme une « sortie partielle » valide. En revanche, tool_calls est marqué comme dirty car le texte brut nécessite un nettoyage important : les appels d’outils doivent souvent être analysés au format JSON ou un autre format, puis restructurés pour générer le dictionnaire final d’appel d’outil. En conséquence, la sortie finale de ces régions diffère souvent considérablement du texte brut. Cette analyse finale n’aura lieu que lorsque region_close sera atteint. Il vous appartient de décider quoi faire des fragments dirty d’ici là : vous pouvez les afficher tels quels pour montrer à l’utilisateur la sortie « brute », ou simplement attendre d’avoir quelque chose de propre à afficher.

Ceci résume l’essentiel de ce qu’il faut savoir pour utiliser les modèles de réponse. Le reste de ce document se concentre sur le fonctionnement interne du système d’analyse et sur la manière d’écrire des modèles de réponse. Cela concerne principalement les développeurs et les créateurs de modèles. La plupart des personnes peuvent s’arrêter en toute sécurité ici !

Avancé : Écrire un modèle de réponse

La meilleure façon de comprendre comment écrire un modèle de réponse est de choisir un exemple concret. Voici à quoi pourrait ressembler une réponse brute de SmolLM :

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

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

Lorsque nous analysons cette sortie au format de dictionnaire de message standard, elle devrait ressembler à ceci :

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

Et voici le modèle qui le parse. Ne vous laissez pas intimider - une grande partie est assez auto-explicative !

{
    "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 substance, le modèle définit des champs et des délimiteurs. Chaque champ correspond à une clé du dictionnaire de sortie. Les champs contiennent également des informations pour l’analyse du texte situé entre leurs délimiteurs. Il y a une subtilité : le champ content n’a pas de open, car dans SmolLM (et plusieurs autres modèles), il n’est pas marqué par un jeton spécial. Au lieu de cela, content est stocké dans l’espace situé après les autres régions, mais avant la fin de la séquence. Dans notre modèle, nous représentons cela par un champ implicite / résiduel qui récupère tout texte non réclamé par une autre région.

Les noms de champs doivent correspondre aux clés que le modèle de chat template lit, afin qu’un message analysé puisse être ajouté à la conversation et rendu à nouveau. En particulier, nommez le champ de raisonnement reasoning_content. C’est la clé standard pour le raisonnement dans les dictionnaires de messages, et celle que la quasi-totalité des chat templates lisent pour le raisonnement.

En plus des fields, le modèle prend en charge deux clés de premier niveau :

  • defaults (facultatif) — Un dictionnaire de valeurs pré-remplies dans la sortie (par ex. {"role": "assistant"}). Les clés présentes ici sont toujours conservées dans la sortie analysée, même si aucun champ n’a écrit dessus ; les autres clés sont supprimées lorsque leur champ n’a capturé aucune valeur.
  • start_anchor (str) / start_anchor_pattern (str regex) — Indique où commence le message actuel de l’assistant à l’intérieur d’un prompt de chat. Lorsque vous passez prefix= à parse_response ou get_response_parser, l’analyseur tronque le préfixe à droite au-delà de la dernière occurrence de cet ancre avant de le traiter, afin que les tours précédents d’une conversation multi-tours ne polluent pas l’état du message actuel. L’ancre n’est appliquée qu’au prefix, jamais à la réponse/génération que vous analysez — certains formats la réémettent légitimement en cours de message (la sortie harmony de gpt-oss ouvre chaque canal avec <|start|>assistant), de sorte que supprimer la réponse au-delà de l’ancre ferait perdre le raisonnement et les appels d’outils du modèle. C’est pourquoi la génération seule ne suffit jamais à se protéger contre la contamination par l’historique : passez le prompt en tant que prefix=. Pour les modèles de type ChatML, l’ancre est généralement "<|im_start|>assistant\n". Exactement un des paramètres start_anchor ou start_anchor_pattern doit être défini.

Par exemple, étant donné ce préfixe multi-tours (notez les deux tours de l’assistant) :

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

l’analyseur tronque tout jusqu’au dernier <|im_start|>assistant\n, en écartant le tour antérieur "Hello!". La règle est que tout sauf le dernier tour de l’assistant est toujours supprimé.

Comme pour les modèles de chat, les modèles de réponse sont stockés en tant qu’attributs du tokenizer et sauvegardés avec celui-ci. Contrairement aux modèles de chat, nous les enregistrons dans tokenizer_config.json et non dans un fichier séparé, car leur format s’adapte naturellement au JSON, contrairement à un script Jinja de modèle de chat.

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

Avancé : Référence de l'API des champs

Chaque champ prend en charge plusieurs clés. Nous pouvons les diviser en deux types. D'abord, il y a les clés qui définissent la manière dont le champ doit être capturé :

CléTypeObjectif
openstr ou list[str]Chaîne littérale qui ouvre cette région. Une liste de chaînes signifie « correspond à l'une de ces chaînes ».
open_patternstr (regex)Alternative par expression régulière à open. Les groupes nommés deviennent des variables de capture disponibles pour transform.
closestr ou list[str]Chaîne littérale (ou liste de chaînes) qui ferme cette région. Omettez pour exécuter jusqu'à la fin du flux.
close_patternstr (regex)Alternative par expression régulière à close. Les groupes nommés deviennent des variables de capture disponibles pour transform.
repeatsboolSi vrai, le champ est une liste et chaque correspondance est ajoutée. Par défaut false.
joinstrAvec repeats, concaténez les correspondances en une seule chaîne avec ce séparateur au lieu d'une liste.
optionalboolIf false and the region never matches, we raise an error. Default true.

Un champ doit avoir soit open soit open_pattern, mais pas les deux, et il en va de même pour close et close_pattern.

Un champ peut omettre entièrement close/close_pattern, auquel cas la région reste ouverte jusqu'à la fin du texte généré. Cela est utile pour un champ final qui s'étend jusqu'à la fin du message.

Un champ sans ni open ni open_pattern est le champ implicite : il est actif chaque fois qu’aucune région explicite n’est ouverte, ce qui lui permet de capturer le texte restant. Au plus un champ peut être implicite. C’est le cas le plus fréquent lorsque content ne comporte pas de balises de jetons spéciaux, le texte étant simplement écrit en clair après les autres champs.

En plus des délimiteurs d’ouverture et de fermeture, vous pouvez également spécifier repeats, ce qui indique que le champ est une liste et que les délimiteurs peuvent correspondre plusieurs fois. C’est le cas le plus courant pour l’appel d’outils parallèle, lorsqu’un modèle émet plusieurs appels d’outils simultanément :

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

Tous les champs répétés ne doivent pas pour autant devenir des listes. Certains modèles émettent plusieurs blocs reasoning_content ou content dans un seul message, et la sortie naturelle pour ceux-ci est une chaîne concaténée. Définir join (une chaîne de séparateur, souvent simplement "") fait passer un champ repeats de la collecte d’une liste à la concaténation de ses correspondances :

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

Chaque correspondance d’un champ join doit être analysée en chaîne de caractères. Lors du streaming, region_close conserve toujours la valeur propre à chaque bloc ; le séparateur n’apparaît que dans le dictionnaire du message final.

Enfin, vous pouvez spécifier optional: false pour les champs qui doivent être présents. Si un tel champ est manquant, nous levons une erreur au lieu de simplement renvoyer un dictionnaire de message sans ce champ.

La fin de la génération fermera et finalisera toutes les régions ouvertes, même si leur délimiteur de fermeture n'a pas été rencontré.

Analyse du contenu d'un champ

Une fois que nous avons défini comment capturer un champ, nous devons également spécifier comment analyser le texte brut à l'intérieur de cette capture. Il existe quatre clés qui contrôlent cela :

CléTypeObjectif
contentstrLe type de contenu à l'intérieur de cette région. La valeur par défaut est "text". Chaque type possède son propre analyseur.
content_argsdictArguments à passer à l'analyseur de contenu pour cette région.
transformdict/listModèle optionnel post-analyse qui redonne une forme au corps analysé (voir Transform).
transform_eachboolSi vrai, le contenu analysé doit être une liste et transform est appliqué élément par élément.

La première (et la plus importante) clé est content. Elle indique le type de contenu du champ, ce qui détermine l’analyseur utilisé pour convertir le texte brut capturé dans le champ en sortie finale. content_args sert à configurer l’analyseur et nous permet de prendre en charge diverses particularités de format sans avoir besoin de code personnalisé. Nous examinerons chaque type d’analyseur et ses arguments à tour de rôle.

Types de base

text, int, float et bool sont les types de base. Ces types de contenu suppriment simplement les espaces insécables, puis effectuent une conversion de type simple si nécessaire. Ils ne possèdent aucun content_args, à l’exception de text qui prend en charge l’argument strip, qui supprime les espaces insécables au début et à la fin du texte capturé, et dont la valeur par défaut est true.

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

json

L’analyseur json interprète le texte capturé comme du JSON. C’est l’outil de référence pour les arguments d’appel d’outils et tout autre contenu à structure imbriquée. Il accepte un ensemble d’arguments optionnels content_args pour gérer les différentes façons dont les modèles déforment le JSON dans la pratique :

  • unquoted_keys (bool, par défaut false) : à activer lorsque les noms de clés sont bruts plutôt que cités (par ex. {name: "foo"}). Utile pour les modèles qui émettent des littéraux d'objets de type Javascript plutôt que du JSON strict.
  • string_delims (liste de paires [open, close], facultatif) : pour les modèles qui encadrent les valeurs de chaîne avec des délimiteurs personnalisés au lieu de "...". Chaque paire fournit un marqueur d'ouverture et un marqueur de fermeture.
  • allow_non_json (bool, par défaut false) : en cas d’échec de l’analyse, renvoie le texte brut nettoyé au lieu de lever une exception. Utile comme solution de repli pour les champs où le modèle émet généralement du JSON mais bascule occasionnellement en texte brut.

unquoted_keys et string_delims existent tous deux pour gérer les modèles qui émettent une sortie non standard, presque mais pas tout à fait au format JSON, vous ne devriez donc en avoir besoin que pour une poignée de modèles.

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

xml-inline

Le parseur xml-inline est destiné aux régions composées d'une séquence plate de balises de type XML, où chaque balise devient une entrée dans un dictionnaire. Il est le plus souvent utilisé à l'intérieur d'un champ tool_calls pour les modèles qui émettent chaque argument sous forme de sa propre balise plutôt que sous forme de bloc JSON :

  • tag_pattern (str, obligatoire) : expression régulière correspondant à une balise unique. Doit contenir les groupes nommés key (la clé du dictionnaire résultant) et value (le texte brut qui devient la valeur du dictionnaire).
  • value_parser (dict, optionnel) : analyseur de contenu imbriqué appliqué à chaque value capturé. Un dict contenant name (l’analyseur, par ex. "json", "int") et args optionnel (ses content_args). S’il est omis, les valeurs restent des chaînes brutes.
  • merge_duplicates (bool, par défaut false) : lorsqu'une même clé apparaît plusieurs fois, les valeurs sont regroupées dans une liste au lieu de laisser les correspondances ultérieures écraser les précédentes.

Par exemple, Qwen3 émet chaque argument d’appel d’outil sous forme de sa propre balise <parameter>, et nous l’interprétons ainsi :

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

Notez le value_parser imbriqué : chaque valeur de paramètre est elle-même passée par le parseur json (avec allow_non_json pour que les chaînes simples passent quand même). En alimentant le champ tool_calls ci-dessus avec cette entrée :

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

L’analyseur kv-lines gère les paires key: value séparées par des lignes (pensez aux métadonnées de type YAML ou aux fichiers .env). Chaque ligne devient une entrée du dictionnaire résultant. Tous les arguments sont facultatifs :

  • line_sep (str, par défaut "\n") : séparateur entre les paires.
  • kv_sep (str, défaut ":") : séparateur entre une clé et sa valeur au sein d’une seule ligne. Seule la première occurrence est utilisée comme point de séparation, de sorte que les valeurs peuvent elles-mêmes contenir le séparateur.
  • strip (bool, par défaut true) : supprime les espaces en début et en fin de chaque clé et valeur.
  • value_parser (dict, facultatif) : analyseur de contenu imbriqué appliqué à chaque valeur, dans le même format {"name": ..., "args": ...} que pour xml-inline. S'il est omis, les valeurs restent sous forme de chaînes brutes.

Les lignes vides ou ne contenant pas kv_sep sont ignorées silencieusement, de sorte que les lignes vides parasites dans la région capturée sont tolérées.

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

Note age conserve "30" en tant que chaîne de caractères ; ajoutez un value_parser de {"name": "int"} pour le convertir en 30.

Typage des arguments d'appel d'outil

Parfois, l’analyseur de réponse peut interpréter les sorties du modèle avec le mauvais type. Par exemple, il peut interpréter le flottant 1.5 comme « 1.50 ». Cela peut poser problème lors de l’appel d’outils, si les outils s’attendent à un argument d’un type donné mais en reçoivent un d’un autre type. Pour éviter cela, vous pouvez passer les tools de la requête à parse_response() ou ResponseParser afin de les convertir en utilisant les parameters du schéma JSON de chaque outil. Les outils sont acceptés dans le même format que apply_chat_template() : schémas JSON, ou fonctions Python avec des annotations de types et des docstrings, converties automatiquement en schémas.

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

Transformer

Pour la plupart des champs, la clé transform est inutile. Elle est utilisée lorsque le corps analysé doit être restructuré pour produire la sortie finale, ou lorsque des informations issues des délimiteurs doivent être fusionnées dans le résultat. Elle apparaît le plus souvent dans les champs tool_calls, car ceux-ci ont souvent une structure complexe.

transform est un modèle : un dictionnaire (ou une liste) qui décrit la forme de la sortie, où toute chaîne de la forme "{name}" est remplacée par la valeur correspondante. Les valeurs peuvent être accédées depuis content (le corps analysé de cette région) et depuis les groupes nommés capturés par open_pattern / close_pattern. Un cas d’usage très courant consiste à envelopper un dictionnaire d’appel d’outil dans un dictionnaire externe avec une clé function, car ceux-ci font partie de notre format standard d’appel d’outil :

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

Ainsi, cette sortie brute :

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

devient (remarquez que repeats: True transforme tool_calls en liste) :

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

Un emplacement réservé couvrant toute la chaîne, comme "{content}", renvoie la valeur recherchée en préservant son type — ainsi, dans l’exemple ci-dessus, le dictionnaire JSON analysé est inséré directement comme valeur de function. L’emplacement réservé doit constituer la chaîne entière : mélanger du texte et des emplacements réservés ("abc {name} def") n’est pas autorisé. Ce ne sont pas des f-strings !

Les espaces réservés peuvent également prendre un chemin pointé comme "{content.args}", qui recherche content puis descend dans la clé args du dictionnaire analysé. C'est ainsi que vous redonnez la forme aux corps d'appels d'outils dont les noms de clés ne correspondent pas à notre format standard. Par exemple, un modèle qui émet {"name": ..., "args": ...} nécessite de renommer args en arguments :

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

Un chemin qui ne se résout pas (ici, un appel d’outil dont le corps ne contient pas args) lève une erreur plutôt que d’émettre un appel d’outil mal formé.

transform est assez polyvalent, ce qui devient nécessaire lorsque la sortie du modèle a un format très différent de notre API standard. GPT-OSS en est un bon exemple : il intègre le nom de la fonction dans l’en-tête du canal plutôt que dans le corps JSON, il faut donc le capturer avec un groupe nommé dans open_pattern et le fusionner avec content à l’intérieur du transform. Tous les groupes nommés de open_pattern et close_pattern deviennent disponibles en tant que variables aux côtés de 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}"}},
},

Le nom de la fonction se trouve dans l'en-tête du canal, et non dans le corps JSON, donc ceci :

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

devient :

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

Parfois, le contenu analysé d’un champ est lui-même une liste d’enregistrements et vous souhaitez redimensionner chacun d’entre eux. Le modèle Cohere en est un bon exemple : il émet tous les appels d’outils dans un seul tableau JSON, nous définissons donc transform_each: True pour appliquer la transformation à chaque élément. Les clés de chaque élément du tableau sont dépaquetées dans le périmètre du modèle, de sorte que "{tool_name}" recherche tool_name dans l’élément actuel :

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

Cela convertira une sortie de ce type :

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

Vers une sortie de ce type, qui correspond à notre API standard :

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

Le drapeau transform_each n'est nécessaire que lorsque content est déjà une liste ; dans le cas plus courant où chaque correspondance contribue à un seul élément (et repeats: True les accumule), la transformation s'applique par défaut à chaque élément.

Développeurs de frameworks : Portabilité des expressions régulières

open_pattern, close_pattern et start_anchor_pattern sont des chaînes regex. Pour la plupart des utilisateurs, et même pour la plupart des auteurs de modèles, cela ne devrait pas poser de problème, mais si vous êtes un développeur écrivant une implémentation de l’analyse de réponse dans un autre langage, vous devez être conscient de nos détails d’implémentation. Cette section est dédiée à tous ceux qui ont dû implémenter un parseur Jinja complet pour faire fonctionner le chat templating non-Python – nous espérons que si vous suivez les directives simples ci-dessous, les modèles de réponse seront beaucoup moins pénibles :

  • Nous utilisons le module regex de Python pour toutes les expressions régulières utilisées dans l’analyse des conversations. Comme toutes les chaînes Python3 sont en unicode, toutes nos correspondances d’expressions régulières prennent en compte l’unicode. Cela affecte en particulier les caractères courants comme \w. Assurez-vous de définir les drapeaux unicode appropriés dans votre moteur.
  • Nous compilons toutes les expressions régulières avec re.DOTALL activé et re.MULTILINE désactivé, de sorte que . correspond à \n mais que ^ et $ ne correspondent qu'au début/à la fin de l'entrée complète, et non aux retours à la ligne.
  • Nous utilisons la syntaxe (?P<name>...) pour les groupes nommés. D'autres implémentations d'expressions régulières ont une syntaxe très différente pour les groupes de capture nommés, il est donc possible que vous deviez rechercher ce motif dans les expressions régulières et le réécrire pour qu'il corresponde à votre implémentation locale.
  • Nous utilisons l’appariement partiel d’expressions régulières pour déterminer quand émettre des données. Cela est nécessaire car la fin d’une région peut être constituée de plusieurs jetons, et nous ne voulons pas émettre ce délimiteur de fin de région. Nous retenons donc les jetons jusqu’à ce que nous soyons certains qu’ils sont à l’intérieur de la région et ne font pas partie de la frontière. Cela signifie que chaque fois qu’une expression régulière de fin de région présente un appariement partiel, nous retenons les données jusqu’à ce que l’expression régulière soit appariée ou échoue. Si votre moteur d’expressions régulières ne prend pas en charge l’appariement partiel, vous pouvez tout de même implémenter des modèles de réponse, mais vous devrez peut-être trouver une autre solution à ce problème. Une approche simple consiste à retenir ces régions / à les émettre comme dirty jusqu’à ce que vous observiez définitivement le délimiteur de fin.
  • Votre moteur d’expressions régulières peut (rarement) ne pas prendre en charge les assertions de contexte comme (?!...). Bien que celles-ci ne soient pas couramment utilisées dans les modèles de réponse, elles peuvent apparaître et nous les prenons en charge ! Vous devrez peut-être soit lever une erreur dans ces cas, soit extraire manuellement les assertions de contexte et les imposer dans votre code lorsque le moteur d’expressions régulières trouve une correspondance possible.
  • D’autres fonctionnalités avancées, telles que les références arrière, les groupes atomiques, les quantificateurs possessifs, la récursivité, etc., ne sont généralement pas utilisées dans les modèles de réponse. Nous essaierons de dissuader les auteurs de modèles de les utiliser, vous pourrez donc les ignorer en toute sécurité.
Update on GitHub