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

Alors que nous commençons à entraîner des LLM en tant qu’Agents, nous devons réfléchir à la meilleure façon de transmettre des informations depuis et vers le modèle dans l’environnement réel. Par exemple, 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) consiste à utiliser des formats de données structurées, comme le JSON. Ces formats peuvent encoder des structures de données arbitrairement imbriquées, avec différents types.

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 certaines métriques qui nous aideront à faire le bon choix.

Efficacité des jetons

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

Ainsi, testons quels formats de données structurées sont tokenisés de la manière la plus efficace, sans télémétrie du monde réel. Pour ce faire, nous générons des données structurées (des dict et list imbriqués) avec des clés aléatoires construites en utilisant le 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 implique de sélectionner une taille d’arbre , et de 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 jetons structurels utilisés dépend du type de données que nous traitons. Si l’entrée de votre agent ne traite pas de données arbitrairement imbriquées, une spécification plus simple pourrait suffire. Nous définissons donc un ensemble de formes, qui est exactement ce que cela semble être :

  • 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
      }
    ]
    
  • creux : 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"
      }
    ]
    
  • 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 d’enregistrements, 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 : Arborescence XML verbeuse utilisant des balises sémantiques et des noms de type 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 de chaleur du nombre moyen de jetons par nœud. Les décomptes de jetons proviennent de la moyenne entre les tokenizers Qwen 3, Llama 3.2 et gpt-oss.

D’un coup d’œil, nous pouvons voir que csv est un gagnant clair pour les données tabulaires, et json obtient les meilleures performances en moyenne. Pour avoir une image plus claire, nous pouvons faire la moyenne pour chaque forme afin de voir le nombre moyen de jetons par format.

Cela montre qu’en matière d’efficacité des jetons seule, 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 fait qu’un format est bon pour les LLM ? Je propose une métrique simple, qui fait également office de benchmark de contexte long/précision, et qui résume cela.

Intuitivité du format

Un format intuitif est facile à analyser et à générer pour les modèles de langage. 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 s’évalue en 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éralisez 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 de bloc YAML
Matrice de précision minimale pour le style de 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 selon laquelle c’est un format plus ergonomique. Le modèle semble préférer TOML et JSON de manière similaire.

Cependant, utiliser une 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 de bloc YAML
Matrice de Jaccard minimale pour le style de 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, JSON suivant de près.

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

Pour moi, la principale conclusion des données est n’utilisez pas YAML. J’ai vu beaucoup de gens en ligne dire que c’est mieux que JSON pour les LLMs, mais ce n’est définitivement pas vrai. Il utilise ~19 % de tokens en plus en moyenne, et est moins lisible et facile à écrire. Les performances de lecture/écriture de TOML semblent mieux évoluer comparé à JSON, mais il utilise ~44 % de tokens en plus pour encoder les mêmes données. Pour la plupart des usages, JSON semble être le meilleur choix.

Reproduisez les résultats avec le code : https://github.com/nathom/token-efficiency.