Retour au blog

Modèles & comparaisonsGuide

Comparez deux LLM équitablement sur votre tâche : échange de camps et journal des coûts

Lancez un duel entre deux modèles sur vos propres tickets : réglages égaux, chaque paire jugée dans les deux ordres, et un journal calls.jsonl qui donne le coût de l'exécution.

Par CheapTokens TeamPublié le 9 min de lecture

Couverture : Comparez deux modèles équitablement, avec un flux en trois étapes : même prompt, échange de camps, journalisation de chaque appel
Sur cette page
  1. Ce qu'un tournoi public fait que vous pouvez emprunter
  2. La tâche : rédiger des réponses à 30 tickets de support
  3. Échangez les camps : jugez chaque paire deux fois
  4. Journalisez chaque appel, puis chiffrez l'exécution
  5. Avant de lancer l'exécution complète
  6. Lire le résultat sans se faire d'illusions

Vous avez fait passer trois prompts dans deux modèles, et le second a gagné. Avant de faire confiance à ce résultat, vérifiez lequel était listé en second. Une comparaison équitable nécessite trois choses : des réglages identiques pour les deux modèles, chaque paire jugée dans les deux ordres, et un journal des tokens par appel, pour savoir ce que l'ensemble de l'exécution a coûté. Ci-dessous, un petit script Python qui fait ces trois choses sur votre propre tâche.

Ce qu'un tournoi public fait que vous pouvez emprunter#

Le Clocktower Bench de Dwarkesh Patel est un projet de recherche non officiel où des modèles de langage jouent au jeu de déduction sociale Blood on the Clocktower les uns contre les autres. Le jeu n'est pas le sujet ici ; le harnais l'est. D'après la page À propos du projet (consultée le 8 novembre 2026), il fait quelques choses que tout duel devrait faire :

  • un modèle occupe tous les sièges du camp du bien et un autre tous les sièges du camp du mal, et chaque distribution est jouée deux fois avec les camps échangés, pour qu'un siège chanceux ne décide pas du résultat ;
  • tous les modèles reçoivent les mêmes réglages : le même effort de raisonnement, un plafond de 8 000 tokens par tour et une limite de 150 mots pour la parole ;
  • le Conteur est scripté, donc aucun modèle n'arbitre la partie ;
  • chaque appel API est écrit dans calls.jsonl avec ses tokens et son coût. La page indique qu'une seule partie coûte entre environ 2 $ et 50 $, selon les modèles (sur les API OpenAI et Anthropic).

Le harnais est conçu pour les modèles OpenAI et Anthropic, et votre tâche n'est probablement pas un jeu de société. Mais les trois habitudes se transposent : réglages égaux, camps échangés et journal des coûts. Le reste de ce guide les applique à quelque chose d'ordinaire.

Chaque ticket passe par les deux modèles ; le juge voit la paire deux fois, dans des ordres opposés
La boucle d'échange de camps pour un ticket

La tâche : rédiger des réponses à 30 tickets de support#

Supposez que vous gérez un petit service de support et que vous voulez savoir si DeepSeek-V4-Flash ou GLM-5.3-Flash rédige de meilleurs premiers jets. Vous avez un fichier tickets.jsonl avec 30 tickets réels, un objet JSON par ligne : {"id": 1, "text": "..."}.

L'API est compatible OpenAI, donc le package officiel openai fonctionne avec CheapTokens à https://api.cheaptokens.dev/v1. Utilisez les identifiants de modèle complets de la documentation : deepseek-ai/DeepSeek-V4-Flash-0731 et zai-org/GLM-5.3-Flash. Les deux modèles reçoivent aussi un troisième modèle comme juge, MiniMaxAI/MiniMax-M2.7, pour qu'aucun concurrent ne note son propre travail.

Commencez par la partie qui rend le journal possible : une fonction par laquelle passe chaque appel.

python
import json, os, re
from openai import OpenAI

client = OpenAI(base_url="https://api.cheaptokens.dev/v1",
                api_key=os.environ["CHEAPTOKENS_API_KEY"])

CONTESTANTS = {"deepseek": "deepseek-ai/DeepSeek-V4-Flash-0731",
               "glm": "zai-org/GLM-5.3-Flash"}
JUDGE = "MiniMaxAI/MiniMax-M2.7"
SETTINGS = {"temperature": 0.2, "max_tokens": 800}   # same for both

def call(model, messages, role, ticket_id, **settings):
    r = client.chat.completions.create(model=model, messages=messages, **settings)
    with open("calls.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps({
            "role": role, "ticket": ticket_id, "model": model,
            "input_tokens": r.usage.prompt_tokens,
            "output_tokens": r.usage.completion_tokens,
            "finish": r.choices[0].finish_reason}) + "\n")
    return r.choices[0].message.content or ""

def draft(model, ticket):
    msgs = [{"role": "system", "content": "You answer support tickets. "
             "Reply in under 80 words, politely, with no promises about refunds."},
            {"role": "user", "content": ticket["text"]}]
    return call(model, msgs, "draft", ticket["id"], **SETTINGS)

Les champs de calls.jsonl sont ceux que ce script choisit ; les chiffres d'utilisation proviennent de l'objet usage de la réponse de l'API. Les deux modèles reçoivent le même prompt système, le même temperature et le même max_tokens. Modifiez l'un d'eux pour un modèle et vous comparez des réglages, pas des modèles.

Échangez les camps : jugez chaque paire deux fois#

Passons au juge. Un modèle juge peut pencher vers la réponse d'un emplacement particulier, donc un verdict unique mélange qualité et position. L'astuce de Clocktower s'applique parfaitement : montrez chaque paire dans les deux rôles. Une réponse ne gagne un ticket que si le juge la choisit dans les deux ordres. Si le verdict suit l'emplacement plutôt que la réponse, le ticket est une égalité.

python
def judge(ticket, first, second):
    prompt = (f"Ticket:\n{ticket['text']}\n\nReply A:\n{first}\n\nReply B:\n{second}\n\n"
              "Which reply is better for the customer? Finish with a last line: Verdict: A or Verdict: B.")
    out = call(JUDGE, [{"role": "user", "content": prompt}], "judge",
               ticket["id"], temperature=0, max_tokens=1000)
    letters = re.findall(r"Verdict:\s*([AB])\b", out)
    return letters[-1] if letters else None

tickets = [json.loads(l) for l in open("tickets.jsonl", encoding="utf-8")]
wins = {"deepseek": 0, "glm": 0, "tie": 0}

for t in tickets:
    a = draft(CONTESTANTS["deepseek"], t)
    b = draft(CONTESTANTS["glm"], t)
    v1 = judge(t, a, b)   # DeepSeek's reply shown as A
    v2 = judge(t, b, a)   # GLM's reply shown as A
    pick1 = {"A": "deepseek", "B": "glm"}.get(v1)
    pick2 = {"A": "glm", "B": "deepseek"}.get(v2)
    wins[pick1 if pick1 and pick1 == pick2 else "tie"] += 1

print(wins)

Deux détails sont faciles à manquer. Le juge tourne à temperature=0 pour garder ses verdicts plus stables ; tous les modèles ne respectent pas ce réglage, donc relancez quelques paires et comparez les verdicts avant de leur faire confiance. Le verdict n'est lu que sur une dernière ligne Verdict:, et les plafonds sont généreux (800 pour les jets, 1000 pour le juge) pour qu'un modèle qui raisonne avant de répondre ne soit pas coupé en pleine réflexion ; l'estimation ci-dessous utilise des tailles typiques, pas les plafonds. Une réponse non analysable donne None, ce qui tombe dans une égalité au lieu de faire planter l'exécution.

Essayez vous-mêmeEssayez : le juge a dit « A » dans les deux ordres pour le ticket 7. Qui gagne ce ticket ?Afficher la réponse

Personne. Dans le premier ordre, A était la réponse de DeepSeek ; dans le second ordre, A était la réponse de GLM, donc le juge a préféré l'emplacement, pas un modèle. Le code le compte comme une égalité. Une pile de telles égalités est en soi un résultat : les deux modèles sont proches sur votre tâche, ou votre prompt de juge est trop vague.

L'ordre n'est qu'une façon de biaiser un test. Les réglages, les prompts et la journalisation peuvent aussi le biaiser, donc vérifiez toute la configuration, pas seulement qui passe en premier. Voici la même expérience avant et après.

Comparer

Ce qui change quand vous arrêtez de comparer à la légère

À la légère

Un prompt par modèle, les valeurs par défaut laissées en place, la paire montrée dans un seul ordre, et la facture lue sur le tableau de bord à la fin du mois. Le gagnant peut être le modèle qui se trouvait être listé en premier.

Équitable

Un prompt système partagé et les mêmes temperature et max_tokens, chaque paire jugée dans les deux ordres, et une ligne dans calls.jsonl pour chaque appel. Le gagnant est celui qui gagne depuis les deux sièges.

Journalisez chaque appel, puis chiffrez l'exécution#

Le journal est ce qui transforme la comparaison en budget. Après l'exécution, additionnez les tokens par modèle :

python
from collections import defaultdict

totals = defaultdict(lambda: [0, 0, 0])
for line in open("calls.jsonl", encoding="utf-8"):
    c = json.loads(line)
    s = totals[c["model"]]
    s[0] += c["input_tokens"]; s[1] += c["output_tokens"]; s[2] += 1

for model, (tin, tout, n) in totals.items():
    print(model, n, "calls", tin, "input", tout, "output")

Vous obtiendrez une ligne par modèle avec le nombre d'appels, les tokens d'entrée et les tokens de sortie. Multipliez par les deux prix du modèle (l'entrée et la sortie sont facturées séparément) ou, plus simple, collez les totaux dans un marqueur de coût en direct.

Vous pouvez aussi estimer avant de lancer. Supposez que chaque ticket plus le prompt système représente environ 300 tokens d'entrée, qu'un jet représente environ 250 tokens de sortie, et que le juge voit environ 800 tokens d'entrée (ticket plus les deux réponses plus la question) et répond avec environ 200 tokens (la ligne de verdict plus un peu de marge pour réfléchir) :

  • jets : 30 tickets × 2 modèles = 60 appels, 30 × 300 = 9000 tokens d'entrée et 30 × 250 = 7500 tokens de sortie par concurrent ;
  • verdicts : 30 tickets × 2 ordres = 60 appels, 60 × 800 = 48000 tokens d'entrée et 60 × 200 = 12000 tokens de sortie sur le juge.

Aux prix actuels, les 30 jets d'un concurrent coûtent $0.00089 sur DeepSeek-V4-Flash et $0.00048 sur GLM-5.3-Flash. Les 60 verdicts sur MiniMax-M2.7 coûtent $0.0042. Additionnez les trois et vous avez le prix de toute la comparaison avant d'envoyer la moindre requête. La même charge de rédaction sur les trois modèles, entrée et sortie comptées ensemble :

Modèle Coût OpenRouter
GLM-5.3-Flash $0.00048 $0.00092
DeepSeek-V4-Flash $0.00089 $0.0017
MiniMax-M2.7 $0.0016 $0.0189

9 000 tokens d'entrée et 7 500 tokens de sortie au total. Les prix par million de tokens sont actuels et évoluent avec nos tarifs ; la colonne OpenRouter indique des prix de référence pour la même charge de travail.

Le tableau montre aussi la référence OpenRouter, pour que vous puissiez voir comment le total se compare. Les hypothèses de tokens sont à vous de modifier : si vos tickets sont longs, l'entrée du juge croît le plus vite, car elle porte les deux réponses deux fois. Si votre juge réfléchit à voix haute avant de répondre, son nombre de sortie croît aussi, et votre journal le montrera. Pour l'habitude de compter les tokens avant une requête, voir compter les tokens avant d'envoyer une requête.

Le même plafond que Clocktower utilise vaut la peine d'être repris. Il fixe 8 000 tokens par tour, et vous avez déjà max_tokens. Mettez-le dans SETTINGS et une réponse qui s'emballe ne peut pas multiplier discrètement votre estimation.

Avant de lancer l'exécution complète#

Liste de contrôle

Vérifications avant vol pour une comparaison équitable

Ce dernier point compte. Lancez trois tickets, ouvrez calls.jsonl et lisez-le. Vérifiez que finish indique stop plutôt que length ; si un jet a été coupé par max_tokens, un modèle peut perdre uniquement parce que sa réponse a été tronquée. Il en va de même pour les appels du juge : un length à cet endroit se transforme en égalité silencieuse, donc comptez aussi les verdicts None. Si votre tâche a une seule bonne réponse, vous n'avez peut-être pas besoin de juge du tout : testez votre prompt sur 20 cas avec une notation par correspondance exacte.

Lire le résultat sans se faire d'illusions#

Regardez les trois nombres dans wins. Trente tickets, c'est un petit échantillon : une répartition comme 12 à 9 avec 9 égalités vous dit que les modèles sont proches, pas que l'un est meilleur. Le journal aide ici. Si un modèle gagne légèrement mais utilise beaucoup plus de sortie, la vraie question est le prix de la tâche, pas le nombre de victoires ; prix par 1M de tokens versus coût par tâche détaille cette comparaison.

Si le résultat est serré, choisissez le modèle le moins cher pour la tâche, ou ajoutez plus de tickets et relancez. Les deux sont peu coûteux à essayer car toute la machinerie est déjà une boucle. Quand vous voulez répéter l'exercice sur un autre modèle, les trois modèles ci-dessus se trouvent derrière une seule API et une seule clé : la configuration dans la documentation est la même pour chacun, et la page de tarifs liste ce que chacun coûte en ce moment. La propre conclusion de Dwarkesh Patel sur les modèles OpenAI versus Anthropic (détection versus tromperie) est son résultat pour son jeu ; le vôtre viendra de vos tickets.