bots-garden/mini-mepublic Fork
⑂ v0.4.0
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. ffb1541Unverified · on v0.4.0 · k33g · 14d ago
structured-output.md · 40 lines · 5.1 KBmarkdown
Blame HistoryOpen raw

Sortie structurée — explication

De quoi s'agit-il ?

Certaines réponses sont destinées à un programme plutôt qu'à une personne : une liste de pays pour un script, un tri de rapports de bugs pour un agent coordinateur. Le texte libre s'y prête mal — le programme doit deviner où commencent les données, et un modèle s'écarte du format demandé. Les serveurs qui parlent l'API OpenAI peuvent au contraire contraindre la génération : avec un JSON Schema dans response_format, llama.cpp le compile en grammaire, et le modèle ne peut produire aucun token hors de celle-ci. structuredOutput (ticket 80) est la façon dont un agent mm l'active.

Pourquoi des déclarations Go

Le JSON Schema est le format sur le fil, mais il est verbeux et facile à rater à la main. La section s'écrit donc en déclarations de types Go : compactes, lisibles, familières à ceux qui écrivent les configurations de mm — et aux modèles entraînés sur du code.

Aucun type Go n'est créé à l'exécution : Go ne sait pas compiler du code pendant qu'il tourne. mm lit le texte comme du code source. go/parser, de la bibliothèque standard, construit son arbre syntaxique ; internal/structured le parcourt et traduit chaque déclaration — une struct en objet fermé, une slice en tableau, un type déclaré en $ref — puis remet le schéma à Genkit avec ai.WithOutputSchema. Les constructions non prises en charge sont refusées au chargement de la configuration, avec leur ligne dans le bloc YAML, plutôt qu'en réponse ratée plus tard.

Alternatives écartées :

  • reflect.StructOf fabrique des types struct à l'exécution, mais ne sait exprimer ni les types nommés ni les types récursifs, ignore les commentaires, et ne ferait que revérifier ce que dit déjà le schéma.
  • Un interpréteur Go embarqué (yaegi) : une grosse dépendance pour exécuter du code dont on n'utilise que les déclarations.
  • Une notation de forme en YAML (countries: [{name: string}]) : plus courte, mais un mini-langage à inventer et à documenter, sans place pour les descriptions.
  • Un JSON Schema brut dans la config : possible, mais c'est précisément ce que la forme Go évite d'écrire.

Pourquoi les déclarations vont aussi dans le prompt système

Le schéma contraint la syntaxe, pas le sens. Quand la contrainte est native, Genkit ne met rien du schéma dans le prompt, et llama.cpp n'en garde que la grammaire : le modèle voit les noms des clés, jamais les descriptions. Mesuré sur Qwen3.5-4B : à qui l'on demandait des capitales, il écrivait chaque fois le nom du pays dans capital. mm ajoute donc les déclarations elles-mêmes, commentaires compris, au prompt système. C'est pourquoi les commentaires comptent : ce sont les consignes que lit le modèle.

Pourquoi les clés gardent leur ordre de déclaration — et pourquoi cela peut ne rien changer

Les properties partent dans l'ordre de déclaration, car un modèle remplit un champ à partir de ce qu'il a déjà écrit : déclarer Name en premier, c'est dire « décide d'abord ceci ». Une simple map Go les aurait triées. Mais llama.cpp (build b11429, mesuré à travers llmman) réordonne les clés par ordre alphabétique en construisant sa grammaire : sur ce serveur, capital est toujours écrit avant name. Les serveurs qui respectent l'ordre en profitent ; sur llama.cpp, le remède est un commentaire autosuffisant sur chaque champ.

Pourquoi un agent qui a des outils ne peut pas en avoir

Un serveur qui contraint chaque réponse au schéma ne laisse aucune place à un appel d'outil, et llama.cpp refuse les contraintes de grammaire en même temps que des outils. La règle de Genkit dit la même chose (ConstrainedSupportNoTools). Plutôt qu'un tour caché en deux temps (travailler avec les outils, puis reformater), mm sépare les rôles : un agent agit, ou répond dans une forme fixe. Les deux se rejoignent par ask_agent — un coordinateur qui a des outils interroge un agent JSON et reçoit le JSON comme résultat de l'outil. L'erreur de configuration liste les outils à désactiver.

Ce qui garantit la réponse

Trois couches : la grammaire du serveur (quand il la prend en charge), la validation de la réponse par Genkit contre le schéma (une réponse non conforme fait échouer le tour), et — avec -p — un code de sortie qu'un script peut tester, avec rien d'autre que le JSON sur stdout. L'échec habituel n'est pas une syntaxe fautive mais une réponse coupée : un petit modèle qui répète les éléments d'une liste jusqu'à max_tokens. mm nomme ce cas explicitement, car le remède est un réglage ou une liste bornée, pas la syntaxe du modèle.

Liens avec le reste

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Sortie structurée — explication

## De quoi s'agit-il ?

Certaines réponses sont destinées à un programme plutôt qu'à une personne : une liste de pays pour un script, un tri de rapports de bugs pour un agent coordinateur. Le texte libre s'y prête mal — le programme doit deviner où commencent les données, et un modèle s'écarte du format demandé. Les serveurs qui parlent l'API OpenAI peuvent au contraire **contraindre** la génération : avec un JSON Schema dans `response_format`, llama.cpp le compile en grammaire, et le modèle ne peut produire aucun token hors de celle-ci. `structuredOutput` (ticket 80) est la façon dont un agent mm l'active.

## Pourquoi des déclarations Go

Le JSON Schema est le format sur le fil, mais il est verbeux et facile à rater à la main. La section s'écrit donc en déclarations de types Go : compactes, lisibles, familières à ceux qui écrivent les configurations de mm — et aux modèles entraînés sur du code.

Aucun type Go n'est créé à l'exécution : Go ne sait pas compiler du code pendant qu'il tourne. mm lit le texte comme du **code source**. `go/parser`, de la bibliothèque standard, construit son arbre syntaxique ; `internal/structured` le parcourt et traduit chaque déclaration — une struct en objet fermé, une slice en tableau, un type déclaré en `$ref` — puis remet le schéma à Genkit avec `ai.WithOutputSchema`. Les constructions non prises en charge sont refusées au chargement de la configuration, avec leur ligne dans le bloc YAML, plutôt qu'en réponse ratée plus tard.

Alternatives écartées :

- **`reflect.StructOf`** fabrique des types struct à l'exécution, mais ne sait exprimer ni les types nommés ni les types récursifs, ignore les commentaires, et ne ferait que revérifier ce que dit déjà le schéma.
- **Un interpréteur Go embarqué** (yaegi) : une grosse dépendance pour exécuter du code dont on n'utilise que les déclarations.
- **Une notation de forme en YAML** (`countries: [{name: string}]`) : plus courte, mais un mini-langage à inventer et à documenter, sans place pour les descriptions.
- **Un JSON Schema brut** dans la config : possible, mais c'est précisément ce que la forme Go évite d'écrire.

## Pourquoi les déclarations vont aussi dans le prompt système

Le schéma contraint la **syntaxe**, pas le sens. Quand la contrainte est native, Genkit ne met rien du schéma dans le prompt, et llama.cpp n'en garde que la grammaire : le modèle voit les noms des clés, jamais les descriptions. Mesuré sur Qwen3.5-4B : à qui l'on demandait des capitales, il écrivait chaque fois le nom du pays dans `capital`. mm ajoute donc les déclarations elles-mêmes, commentaires compris, au prompt système. C'est pourquoi les commentaires comptent : ce sont les consignes que lit le modèle.

## Pourquoi les clés gardent leur ordre de déclaration — et pourquoi cela peut ne rien changer

Les `properties` partent dans l'ordre de déclaration, car un modèle remplit un champ à partir de ce qu'il a déjà écrit : déclarer `Name` en premier, c'est dire « décide d'abord ceci ». Une simple map Go les aurait triées. Mais llama.cpp (build b11429, mesuré à travers llmman) réordonne les clés par ordre alphabétique en construisant sa grammaire : sur ce serveur, `capital` est toujours écrit avant `name`. Les serveurs qui respectent l'ordre en profitent ; sur llama.cpp, le remède est un commentaire autosuffisant sur chaque champ.

## Pourquoi un agent qui a des outils ne peut pas en avoir

Un serveur qui contraint chaque réponse au schéma ne laisse aucune place à un appel d'outil, et llama.cpp refuse les contraintes de grammaire en même temps que des outils. La règle de Genkit dit la même chose (`ConstrainedSupportNoTools`). Plutôt qu'un tour caché en deux temps (travailler avec les outils, puis reformater), mm sépare les rôles : un agent agit, ou répond dans une forme fixe. Les deux se rejoignent par `ask_agent` — un coordinateur qui a des outils interroge un agent JSON et reçoit le JSON comme résultat de l'outil. L'erreur de configuration liste les outils à désactiver.

## Ce qui garantit la réponse

Trois couches : la grammaire du serveur (quand il la prend en charge), la validation de la réponse par Genkit contre le schéma (une réponse non conforme fait échouer le tour), et — avec `-p` — un code de sortie qu'un script peut tester, avec rien d'autre que le JSON sur stdout. L'échec habituel n'est pas une syntaxe fautive mais une réponse coupée : un petit modèle qui répète les éléments d'une liste jusqu'à `max_tokens`. mm nomme ce cas explicitement, car le remède est un réglage ou une liste bornée, pas la syntaxe du modèle.

## Liens avec le reste

- [Interroger un autre agent](asking-another-agent.md) — comment le JSON parvient à un coordinateur.
- [Modèles qui raisonnent](thinking-models.md) — un agent de consultation a rarement besoin de raisonner ; un coordinateur, souvent (sur Qwen3.5-4B, sans raisonnement, le coordinateur répondait de mémoire en affirmant avoir demandé).
- [Comment obtenir des réponses en JSON](../how-to/get-json-answers.md) et la [référence de `structuredOutput`](../reference/configuration.md#structuredoutput).