Volver al blog

Modelos y comparativasGuía

Compara dos LLM de forma justa en tu tarea: intercambio de bandos y registro de costes

Haz un enfrentamiento directo de dos modelos con tus propios tickets: ajustes iguales, cada par evaluado en ambos órdenes y un registro calls.jsonl que da el coste de la ejecución.

Por CheapTokens TeamPublicado el 8 min de lectura

Portada: Compara dos modelos de forma justa, con un flujo de tres pasos: mismo prompt, intercambia bandos, registra cada llamada
En esta página
  1. Lo que hace un torneo público que puedes tomar prestado
  2. La tarea: redactar respuestas a 30 tickets de soporte
  3. Intercambia los bandos: evalúa cada par dos veces
  4. Registra cada llamada y luego calcula el precio de la ejecución
  5. Antes de lanzar la ejecución completa
  6. Leer el resultado sin engañarte

Ejecutaste tres prompts con dos modelos y ganó el segundo. Antes de confiar en eso, comprueba cuál se listó en segundo lugar. Una comparación justa necesita tres cosas: ajustes idénticos para ambos modelos, cada par evaluado en ambos órdenes y un registro de tokens por llamada, para que sepas cuánto costó toda la ejecución. Abajo tienes un pequeño script de Python que hace las tres cosas con tu propia tarea.

Lo que hace un torneo público que puedes tomar prestado#

Clocktower Bench de Dwarkesh Patel es un proyecto de investigación no oficial en el que modelos de lenguaje juegan al juego de deducción social Blood on the Clocktower entre sí. El juego no es lo importante aquí; el arnés sí. Según la página About del proyecto (consultada el 8 de noviembre de 2026), hace algunas cosas que cualquier enfrentamiento directo debería hacer:

  • un modelo ocupa todos los asientos buenos y otro todos los malos, y cada reparto se juega dos veces con los bandos intercambiados, para que un asiento afortunado no decida el resultado;
  • todos los modelos reciben los mismos ajustes: el mismo esfuerzo de razonamiento, un límite de 8.000 tokens por turno y un límite de 150 palabras en el habla;
  • el Narrador está guionizado, así que ningún modelo arbitra la partida;
  • cada llamada a la API se escribe en calls.jsonl con sus tokens y coste. La página dice que una sola partida cuesta entre unos 2 $ y 50 $, según los modelos (en las API de OpenAI y Anthropic).

El arnés está hecho para modelos de OpenAI y Anthropic, y tu tarea probablemente no sea un juego de mesa. Pero los tres hábitos se trasladan: ajustes iguales, bandos intercambiados y un registro de costes. El resto de esta guía los aplica a algo corriente.

Cada ticket va a ambos modelos; el juez ve el par dos veces, en órdenes opuestos
El bucle de intercambio de bandos para un ticket

La tarea: redactar respuestas a 30 tickets de soporte#

Supón que llevas un pequeño servicio de soporte y quieres saber si DeepSeek-V4-Flash o GLM-5.3-Flash redacta mejores primeros borradores. Tienes un archivo tickets.jsonl con 30 tickets reales, un objeto JSON por línea: {"id": 1, "text": "..."}.

La API es compatible con OpenAI, así que el paquete oficial openai funciona con CheapTokens en https://api.cheaptokens.dev/v1. Usa los ids de modelo completos de la documentación: deepseek-ai/DeepSeek-V4-Flash-0731 y zai-org/GLM-5.3-Flash. Ambos modelos reciben además un tercer modelo como juez, MiniMaxAI/MiniMax-M2.7, para que ningún concursante califique su propio trabajo.

Empieza por la parte que hace posible el registro: una función por la que pasen todas las llamadas.

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)

Los campos de calls.jsonl son los que elige este script; las cifras de uso vienen del objeto usage de la respuesta de la API. Ambos modelos reciben el mismo system prompt, el mismo temperature y el mismo max_tokens. Cambia uno de ellos para un modelo y estarás comparando ajustes, no modelos.

Intercambia los bandos: evalúa cada par dos veces#

Ahora el juez. Un modelo juez puede inclinarse hacia la respuesta que ocupa una posición concreta, así que un único veredicto mezcla calidad con posición. El truco de Clocktower encaja perfectamente: muestra cada par en ambos roles. Una respuesta gana un ticket solo si el juez la elige en ambos órdenes. Si el veredicto sigue a la posición en lugar de a la respuesta, el ticket es un empate.

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)

Dos detalles son fáciles de pasar por alto. El juez se ejecuta con temperature=0 para mantener sus veredictos más estables; no todos los modelos respetan ese ajuste, así que vuelve a ejecutar algunos pares y compara los veredictos antes de confiar en ellos. El veredicto se lee solo de una línea final Verdict:, y los límites son generosos (800 para los borradores, 1000 para el juez) para que un modelo que razona antes de responder no se corte a mitad de pensamiento; la estimación de abajo usa tamaños típicos, no los límites. Una respuesta no analizable da None, que cae en un empate en lugar de romper la ejecución.

Pruébalo tú mismoPruébalo: el juez dijo "A" en ambos órdenes para el ticket 7. ¿Quién gana ese ticket?Mostrar respuesta

Nadie. En el primer orden A era la respuesta de DeepSeek, en el segundo orden A era la respuesta de GLM, así que el juez prefirió la posición, no un modelo. El código lo cuenta como empate. Un montón de empates así es en sí mismo un resultado: los dos modelos están igualados en tu tarea, o tu prompt de juez es demasiado vago.

El orden es solo una forma de sesgar una prueba. Los ajustes, los prompts y el registro también pueden sesgarla, así que revisa toda la configuración, no solo quién va primero. Aquí está el mismo experimento antes y después.

Comparar

Qué cambia cuando dejas de comparar a la ligera

A la ligera

Un prompt por modelo, los valores por defecto sin tocar, el par mostrado en un solo orden y la factura leída del panel al final del mes. El ganador puede ser el modelo que resultó estar listado primero.

Justa

Un system prompt compartido y el mismo temperature y max_tokens, cada par evaluado en ambos órdenes y una línea en calls.jsonl por cada llamada. El ganador es quien gana desde ambos asientos.

Registra cada llamada y luego calcula el precio de la ejecución#

El registro es lo que convierte la comparación en un presupuesto. Después de la ejecución, suma los tokens por modelo:

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

Obtendrás una línea por modelo con el número de llamadas, los tokens de entrada y los tokens de salida. Multiplica por los dos precios del modelo (la entrada y la salida se facturan por separado) o, más fácil, pega los totales en un marcador de coste en vivo.

También puedes estimar antes de ejecutar. Supón que cada ticket más el system prompt son unos 300 tokens de entrada, un borrador son unos 250 tokens de salida, y el juez ve aproximadamente 800 tokens de entrada (ticket más ambas respuestas más la pregunta) y responde con unos 200 tokens (la línea del veredicto más un poco de margen para pensar):

  • borradores: 30 tickets × 2 modelos = 60 llamadas, 30 × 300 = 9000 de entrada y 30 × 250 = 7500 tokens de salida por concursante;
  • veredictos: 30 tickets × 2 órdenes = 60 llamadas, 60 × 800 = 48000 de entrada y 60 × 200 = 12000 tokens de salida en el juez.

Con los precios actuales, los 30 borradores de un concursante cuestan $0.00089 en DeepSeek-V4-Flash y $0.00048 en GLM-5.3-Flash. Los 60 veredictos en MiniMax-M2.7 cuestan $0.0042. Suma los tres y tienes el precio de toda la comparación antes de enviar una sola petición. La misma carga de redacción en los tres modelos, con entrada y salida contadas juntas:

Modelo Costo OpenRouter
GLM-5.3-Flash $0.00048 $0.00092
DeepSeek-V4-Flash $0.00089 $0.0017
MiniMax-M2.7 $0.0016 $0.0189

9000 tokens de entrada y 7500 tokens de salida en total. Los precios por 1M de tokens son actuales y cambian junto con nuestras tarifas; la columna de OpenRouter muestra precios de referencia para la misma carga de trabajo.

La tabla también muestra la referencia de OpenRouter, para que veas cómo se compara el total. Las suposiciones de tokens son tuyas para cambiarlas: si tus tickets son largos, la entrada del juez crece más rápido, porque lleva ambas respuestas dos veces. Si tu juez piensa en voz alta antes de responder, su cifra de salida también crece, y tu registro lo mostrará. Para el hábito de contar tokens antes de una petición, consulta contar tokens antes de enviar una petición.

Vale la pena robar el mismo límite que usa Clocktower. Fija 8.000 tokens por turno, y ya tienes max_tokens. Ponlo en SETTINGS y una respuesta desbocada no podrá multiplicar tu estimación en silencio.

Antes de lanzar la ejecución completa#

Lista de verificación

Comprobación previa para una comparación justa

Ese último punto importa. Ejecuta tres tickets, abre calls.jsonl y léelo. Comprueba que finish diga stop en lugar de length; si un borrador se cortó por max_tokens, puede que un modelo esté perdiendo solo porque su respuesta se truncó. Lo mismo vale para las llamadas del juez: un length ahí se convierte en un empate silencioso, así que cuenta también los veredictos None. Si tu tarea tiene una única respuesta correcta, puede que no necesites juez en absoluto: prueba tu prompt en 20 casos con calificación de coincidencia exacta.

Leer el resultado sin engañarte#

Mira los tres números en wins. Treinta tickets es una muestra pequeña: un reparto como 12 a 9 con 9 empates te dice que los modelos están igualados, no que uno sea mejor. El registro ayuda aquí. Si un modelo gana por poco pero usa mucho más output, la verdadera pregunta es el precio de la tarea, no el número de victorias; precio por 1M de tokens frente a coste por tarea recorre esa comparación.

Si el resultado está reñido, elige el modelo más barato para la tarea, o añade más tickets y vuelve a ejecutar. Ambos son baratos de probar porque toda la maquinaria ya es un bucle. Cuando quieras repetir el ejercicio con otro modelo, los tres modelos de arriba están detrás de una API y una clave: la configuración en la documentación es la misma para cada uno, y la página de precios lista lo que cuesta cada uno ahora mismo. El propio hallazgo de Dwarkesh Patel sobre modelos de OpenAI frente a Anthropic (detección frente a engaño) es su resultado para su juego; el tuyo saldrá de tus tickets.