CONTENTS

Comparaison des formats de données structurées pour les LLM

  ·   8 min read

Alors que nous commençons à entraîner des LLM en tant qu’Agents, nous devons réfléchir à la meilleure façon de transmettre les informations depuis et vers l’environnement réel. S’il appelle une fonction externe, comment les arguments doivent-ils être passés ? Comment les données de l’environnement doivent-elles être fournies au modèle ? La solution la plus simple (et la plus générale) utilise des formats de données structurées, comme le JSON. Ces formats peuvent encoder des structures de données arbitrairement imbriquées et de types hétérogènes.

Mais le JSON est-il le bon choix ? Nous avons de nombreuses options, comme TOML, YAML, XML, etc. Dans cet article, nous examinons et mesurons des métriques qui nous aideront à faire le bon choix.

Efficacité des tokens

Une limite fondamentale des LLM actuels est le contexte fini. On ne peut pas simplement injecter en continu l’intégralité des données du monde dans le modèle au fil du temps. Cela signifie que nous voulons communiquer les informations les plus utiles possibles avec le moins de tokens.

Testons donc quels formats de données structurées sont tokenisés de la manière la plus efficace, sans télémétrie réelle. Pour ce faire, nous générons aléatoirement des données structurées (dicts et lists imbriqués) avec des clés construites à partir d’un dictionnaire système. Par exemple, en JSON :

[
  {
    "leptomeninx_xylitone": [
      147.396,
      { "asellus_spinelike_triliterality": null }
    ]
  },
  {
    "costively_zoetic": [["decipherably_wheat"]],
    "neurectome_sorcery_tangleproof": null
  }
]

Le processus d’échantillonnage consiste à choisir une taille d’arbre , puis à sélectionner récursivement de manière aléatoire les types de conteneurs et les valeurs terminales. Vous remarquerez peut-être que le nombre de tokens structurels utilisés dépend du type de données traité. Si l’entrée de votre agent ne traite pas de données arbitrairement imbriquées, une spécification plus simple pourrait suffire. Pour en tenir compte, nous définissons un ensemble de formes :

  • nested (imbriqué) : combinaisons profondes de dict/list se terminant par des valeurs scalaires.
    Exemple
    [
      {
        "comforter": {
          "dosadh_disruption_prosodiac": {
            "unsnatch_moslem": 837
          },
          "tone_redefine": {
            "cribrose_aoul": [
              [
                "christianization-casuariiformes-overbravery-chevronel"
              ]
            ]
          }
        }
      },
      {
        "bovarysm": [
          "oropharynx_consentant_fibronuclear",
          "bajardo-liquidy-calibered-belucki"
        ],
        "materialistic": {
          "paleostylic": -27.23,
          "praediality_juvenilify_benempt": 104,
          "roquelaure": -407
        }
      },
      {
        "filicites": [
          "unpalatableness-allocaffeine",
          126.204,
          {
            "manesheet": "emery_tricyclene"
          }
        ],
        "imposing_elchee_mentation": 3,
        "inadvisability": -12.726
      }
    ]
    
  • sparse (épars) : principalement des valeurs nulles avec de rares scalaires numériques ou textuels dans des structures imbriquées.
    Exemple
    [
      {
        "areole_auramine_kojiki": {
          "hyperabsorption_uraniscorrhaphy": -776
        },
        "maplebush_piete": [
          {
            "shadowgraphist": null,
            "stakeholder_busybodyness_crebrity": 644
          }
        ],
        "preadamite": null
      },
      {
        "bellmaking_brachydont": {
          "jalapin_chandelier_accelerando": null,
          "mandative": -79,
          "totora_peristaphylitis_graphy": null
        },
        "subferryman_dephlegmator": [
          {
            "manuka_uncriminally_archdeceiver": null
          }
        ]
      },
      {
        "daytime": [
          {
            "overfeminine_catholicist": -242.239,
            "sulfophthalein_irreciprocal": null
          }
        ],
        "gata": null,
        "macaranga_circuitman": null,
        "ostraciidae_subsidiariness": "throneward"
      }
    ]
    
  • tabular (tabulaire) : tables basées sur des colonnes avec des lignes de valeurs scalaires et un schéma partagé.
    Exemple
    {
      "columns": [
        "viragoish_isogonality_swarming",
        "supralocally_nuncioship",
        "zoomorph",
        "cavitary_visie",
        "permutableness_impunity_bipack",
        "forby_archly",
        "rivinian",
        "unheal_annelidian_samurai"
      ],
      "rows": [
        [
          true,
          false,
          "cincinnatia-cyanhidrosis-auto",
          false,
          true,
          null,
          "acetosoluble nonexclamatory homogangliate croupal",
          -219
        ],
        [
          null,
          836,
          -904,
          "metasomatic-mundanism-hotchpotchly-secantly",
          null,
          309.642,
          "floodgate-baluchitherium-unimaginary-sheepkeeper",
          -396
        ],
        [
          "postcritical-tug",
          true,
          -948,
          0.135,
          399.166,
          -123,
          "palaeoniscus",
          true
        ]
      ]
    }
    

Nous considérons les formats suivants :

  • json : JSON entièrement minifié avec clés triées et séparateurs compacts.
    Exemple
    [{"backlotter_overboast":"calligraphist_megabar_uninstructively","landspout_souper":[null],"liquefier_unconvicting":-151.898,"unbegot":[961],"unreformedness":-189.15},{"detriment_muckender":[469.486,{"aspergillum_sharebroker_akebia":337},-302.978],"heeder_aerophyte_unbase":499.655,"metamer_powsoddy":null},{"fascicled_fibrous_bajardo":{"octaeterid_pharmacolite_tentativeness":{"underfellow":83.76},"plethysmography_unchangeably_positioned":432.985,"transvestitism":82},"mirror":{"uninfallibility_benny":null}}]
    
  • yaml : Sérialisation YAML en style bloc avec un ordre de clés déterministe.
    Exemple
    - backlotter_overboast: calligraphist_megabar_uninstructively
      landspout_souper:
      - null
      liquefier_unconvicting: -151.898
      unbegot:
      - 961
      unreformedness: -189.15
    - detriment_muckender:
      - 469.486
      - aspergillum_sharebroker_akebia: 337
      - -302.978
      heeder_aerophyte_unbase: 499.655
      metamer_powsoddy: null
    - fascicled_fibrous_bajardo:
        octaeterid_pharmacolite_tentativeness:
          underfellow: 83.76
        plethysmography_unchangeably_positioned: 432.985
        transvestitism: 82
      mirror:
        uninfallibility_benny: null
    
  • toml : Document TOML enveloppant les enregistrements sous un tableau records, avec les valeurs nulles converties en chaînes.
    Exemple
    [[records]]
    landspout_souper = [
        "null",
    ]
    backlotter_overboast = "calligraphist_megabar_uninstructively"
    liquefier_unconvicting = -151.898
    unreformedness = -189.15
    unbegot = [
        961,
    ]
    
    [[records]]
    detriment_muckender = [
        469.486,
        { aspergillum_sharebroker_akebia = 337 },
        -302.978,
    ]
    heeder_aerophyte_unbase = 499.655
    metamer_powsoddy = "null"
    
    [[records]]
    
    [records.fascicled_fibrous_bajardo]
    transvestitism = 82
    plethysmography_unchangeably_positioned = 432.985
    
    [records.fascicled_fibrous_bajardo.octaeterid_pharmacolite_tentativeness]
    underfellow = 83.76
    
    [records.mirror]
    uninfallibility_benny = "null"
    
  • xml : Arbre XML verbeux utilisant des balises sémantiques et des noms de types explicites.
    Exemple
    <records>
      <object name="record" index="0">
        <array name="landspout_souper">
          <null name="0" />
        </array>
        <string name="backlotter_overboast">calligraphist_megabar_uninstructively</string>
        <number name="liquefier_unconvicting">-151.898</number>
        <number name="unreformedness">-189.15</number>
        <array name="unbegot">
          <number name="0">961</number>
        </array>
      </object>
      <object name="record" index="1">
        <array name="detriment_muckender">
          <number name="0">469.486</number>
          <object name="1">
            <number name="aspergillum_sharebroker_akebia">337</number>
          </object>
          <number name="2">-302.978</number>
        </array>
        <number name="heeder_aerophyte_unbase">499.655</number>
        <null name="metamer_powsoddy" />
      </object>
      <object name="record" index="2">
        <object name="fascicled_fibrous_bajardo">
          <number name="transvestitism">82</number>
          <object name="octaeterid_pharmacolite_tentativeness">
            <number name="underfellow">83.76</number>
          </object>
          <number name="plethysmography_unchangeably_positioned">432.985</number>
        </object>
        <object name="mirror">
          <null name="uninfallibility_benny" />
        </object>
      </object>
    </records>
    
  • csv : Lignes séparées par des virgules avec en-tête, générées à partir d’enregistrements tabulaires.
    Exemple
    bicellular_russification_unsinister,crude_paynim,isoetales,postembryonic_encrisp
    braza apology catalufa tofu,,rampager,triformous
    ,True,481.226,
    421.281,868,photodysphoria,escortage
    

Maintenant, pour chaque format, puis pour chaque forme, nous pouvons tracer une carte thermique du nombre moyen de tokens par nœud. Les comptes de tokens proviennent de la moyenne entre les tokenizers de Qwen 3, Llama 3.2 et gpt-oss.

D’un coup d’œil, on voit que csv est clairement le meilleur pour les données tabulaires, et json obtient les meilleures performances en moyenne. Pour avoir une image plus claire, nous pouvons prendre la moyenne sur chaque forme pour voir le nombre moyen de tokens par format.

Cela montre qu’en termes d’efficacité des tokens uniquement, le classement est json > yaml > toml > xml. Cependant, ce n’est pas parce qu’un format est dense qu’il est bon. Mais comment pouvons-nous quantifier cela ? Qu’est-ce qui rend un format bon pour les LLM ? Je propose une métrique simple — qui se trouve également être un benchmark de contexte long/précision — qui encapsule cela.

Intuitivité du format

Un format intuitif est facile pour les modèles de langage à analyser et générer. Pour mesurer l’intuitivité, nous proposons le benchmark suivant. Toutes les exécutions utilisent DeepSeek V3 (2025-09) en mode chat brut sans utilisation d’outils, donc le modèle doit exécuter “mentalement” l’extrait de code Python.

  • Étant donné un format , une taille d’arbre d’entrée et une taille d’arbre de sortie .
  • Générer un arbre de données d’entrée avec nœuds
  • Générer un programme Python qui définit une variable target, qui évalue à un arbre de données imbriqué de taille , qui interroge l’arbre de données d’entrée
  • Demander au modèle de générer target sérialisé dans notre format
Exemple de Prompt pour JSON

Format : json_min Nœuds d’entrée observés : 8 Nœuds de sortie cible : 9

Instructions :

  1. Analysez le jeu de données dans une variable Python nommée data.
  2. Exécutez l’extrait de code Python ci-dessous pour peupler une variable nommée target.
  3. Sérielez target en utilisant le format d’origine (json_min) et placez le résultat dans un bloc de code délimité étiqueté json.
  4. Le bloc de code ne doit contenir que les données sérialisées.
  5. Soyez très attentif à ce que le format et la structure correspondent exactement.

Exemples : Exemple 1 : Jeu de données :

{"results":{"lo_unaddicted":[{"fleeting_geneserine_desmodynia":[-163.354]},{"subcrepitation_maddeningly":{"homoanisic":-3}},"helminth_vengeable"],"touchiness":[{"cataphyllum_educand":"remilitarize","unhumiliated_poorwill_oryctognostically":"resound","herrnhuter":false},["uptrace",["subastringent"],"scruff","theurgically_tritonymph",[-123]]],"ichthyornithes_revisionary":{"alcogel_freckle":{"inquisition":"lehi"},"oniomaniac_flamineous_ledgerdom":{"tylotoxeate":-141,"hemeralopia":272.837},"unremember":[false,[-30],true]},"amphiumidae":{"unenterprised_meltage":[149],"psilanthropist_garrulinae":{"averrable_deporter":399.228,"riotproof_terebratuloid_monophyodontism":-22},"coed":{"indigoid_pulicid":"airbrush_oenothera","paillasse":"rutelinae"},"inhume_photoprinting_pasturability":["chiselly_backfilling"],"route_anisopogonous":[{"kotal_schematization_zestfulness":-91}]},"unexcised_seamless_intwist":{"cordaitean":-108,"unrising":"monarchist"}}}

Extrait Python :

target = [
    data["amphiumidae"]["route_anisopogonous"][0],
    data["amphiumidae"]["inhume_photoprinting_pasturability"],
    data["touchiness"][1],
]

Réponse :

{"results":[{"kotal_schematization_zestfulness":-91},["chiselly_backfilling"],["uptrace",["subastringent"],"scruff","theurgically_tritonymph",[-123]]]}

Exemple 2 : Jeu de données :

{"results":[[["selachostomous",88.259,"altair_assiniboin",{"samphire_symbolology":{"scarfed_wambutti":-28}},"bocca_ponerid"],[["gibberosity","footway_antecardium",[true],["myxosporous"],"repopulate"]],{"prairied":-13,"amara_huccatoon_massivity":34,"alehouse_uncumber":154}],{"tartary_loculose":[[{"counterwind":"endophasic"}],[{"subhyaline_asiatical_tobikhar":"angolar_cheeriness","scutelliform_riverweed_putback":-7,"thirdsman_phlogistical_tropacocaine":"bawdry"}]],"hydrophore":[{"insubvertible":119,"overwomanize":{"cobble_orography_caprice":-127},"queriman_episcopally_railway":{"unadoration":["weedage"]},"stactometer_toggle_cleavability":[453.262]},{"forejudge_tacnode":{"undersupport":105},"floorward":-170,"dormer_abysmal_occasional":-484.491,"wheatgrower":346.849,"phobism_intendingly":91.698}]},{"conirostres":[{"monorhymed_kioway":"taxlessly","ungloriousness_urosternite":true},["pendanting_allegation",-30],["hemiobol","monont_paradoxial"]],"sistrum":[{"untaintable_polladz":true},[-162,true],{"preclassic_standoffishness_pagina":true}]},[{"earlock_unmantled":{"philoradical_micranthropos":-10,"derout":["unfrock",90.415]},"hepatologist_unrushed":-270.882},[[["argyrol_art"]],["daftness"],[-12,149.452]],[[{"loatuko":"floriken_tecali"},[-153.065],-51,153.874,"pile"]],{"hexacanth":[[-3,-19]]}]]}

Extrait Python :

target = [
    data[1]["tartary_loculose"][0][0],
    data[1]["hydrophore"][1]["wheatgrower"],
    data[1]["tartary_loculose"][1],
    data[1]["hydrophore"][0]["queriman_episcopally_railway"]["unadoration"],
    data[0][1][0][1],
    data[0][2],
    data[2]["conirostres"][0]["monorhymed_kioway"],
    data[3][2],
]

Réponse :

{"results":[{"counterwind":"endophasic"},346.849,[{"subhyaline_asiatical_tobikhar":"angolar_cheeriness","scutelliform_riverweed_putback":-7,"thirdsman_phlogistical_tropacocaine":"bawdry"}],["weedage"],"footway_antecardium",{"prairied":-13,"amara_huccatoon_massivity":34,"alehouse_uncumber":154},"taxlessly",[[{"loatuko":"floriken_tecali"},[-153.065],-51,153.874,"pile"]]]}

Jeu de données :

{"results":["relict",{"intolerant_ignify":"cragginess_reapprobation","detriment_wholesalely_spillway":-49},true,"stewardess",-94]}

Extrait Python :

target = [
    data[1]["intolerant_ignify"],
    data[4],
    data[1]["detriment_wholesalely_spillway"],
    data[2],
    data[3],
    data[1],
]

Nous allons omettre XML à cause de son extrême verbosité. Pour chaque taille d’entrée et de sortie, nous générons 5 arbres de données et demandons au LLM. En traçant la proportion de réponses correctes, nous obtenons

Matrice de précision minimale pour JSON
Matrice de précision minimale pour JSON
JSON
Matrice de précision minimale pour le style bloc YAML
Matrice de précision minimale pour le style bloc YAML
YAML
Matrice de précision minimale pour TOML
Matrice de précision minimale pour TOML
TOML
JSON et TOML semblent avoir des performances similaires, TOML étant plus facile à lire. YAML est difficile à générer pour Deepseek.

Les graphiques peuvent être interprétés comme suit : si nous voyons du vert le long de l’axe Y, cela signifie que le score s’adapte bien aux grandes entrées, et le format est lisible. Si nous voyons du vert loin le long de l’axe X, cela signifie que le score s’adapte aux grands arbres de sortie, et le format est facile à générer. YAML est étonnamment médiocre, contrairement à mon intuition qu’il s’agirait d’un format plus ergonomique. Le modèle semble préférer TOML et JSON de manière similaire.

Cependant, utiliser la correspondance exacte comme métrique pourrait être trop strict. Nous pouvons plutôt attribuer plus de crédit aux tentatives qui partagent plus de structure avec la référence. Nous faisons cela en calculant l’indice de Jaccard, ou l’intersection sur l’union entre la réponse soumise et la référence. En traçant cela, en utilisant les mêmes données que le graphique précédent, nous obtenons

Matrice de Jaccard minimale pour JSON
Matrice de Jaccard minimale pour JSON
JSON
Matrice de Jaccard minimale pour le style bloc YAML
Matrice de Jaccard minimale pour le style bloc YAML
YAML
Matrice de Jaccard minimale pour TOML
Matrice de Jaccard minimale pour TOML
TOML
L’indice de Jaccard nous donne une représentation plus lisse de la précision. Nous voyons que TOML performe remarquablement bien, avec JSON juste derrière.

Nous voyons une différence plus marquée entre les performances de JSON et TOML. Le modèle est capable d’avoir un chevauchement beaucoup plus élevé avec la réponse correcte avec TOML par rapport à JSON. YAML continue de performer médiocrement.

Conclusion

Le principal enseignement des données est de ne pas utiliser YAML. J’ai vu beaucoup de gens en ligne dire qu’il est meilleur que le JSON pour les LLMs, mais ce n’est presque certainement pas vrai. Il utilise en moyenne ~19 % de tokens en plus, et est beaucoup moins lisible. Les performances de lecture/écriture du TOML semblent mieux évoluer par rapport au JSON, mais il utilise ~44 % de tokens en plus pour encoder les mêmes données. Pour la plupart des usages, le JSON est le meilleur choix.

Reproduisez les résultats avec le code sur GitHub.

Transparence IA

Codex CLI a été utilisé pour écrire le code de traçage et d'expérimentation, qui a été relu et testé par moi. Toute la rédaction est la mienne.