Powrót do bloga

Modele i porównaniaPrzewodnik

Porównaj dwa LLM uczciwie na swoim zadaniu: zamiana stron i log kosztów

Przeprowadź porównanie jeden na jeden dwóch modeli na własnych ticketach: równe ustawienia, każda para oceniana w obu kolejnościach i log calls.jsonl, który podaje koszt przebiegu.

Autor: CheapTokens TeamOpublikowano 7 min czytania

Okładka: Porównaj dwa modele uczciwie, z trzyetapowym przepływem: ten sam prompt, zamiana stron, logowanie każdego wywołania
Na tej stronie
  1. Co robi publiczny turniej, co możesz zapożyczyć
  2. Zadanie: redagowanie odpowiedzi na 30 ticketów wsparcia
  3. Zamień strony: oceniaj każdą parę dwukrotnie
  4. Loguj każde wywołanie, potem wyceń przebieg
  5. Zanim uruchomisz pełny przebieg
  6. Czytanie wyniku bez oszukiwania siebie

Przepuściłeś trzy prompty przez dwa modele i drugi wygrał. Zanim w to uwierzysz, sprawdź, który był wymieniony jako drugi. Uczciwe porównanie wymaga trzech rzeczy: identycznych ustawień dla obu modeli, oceny każdej pary w obu kolejnościach oraz logu tokenów na wywołanie, żebyś wiedział, ile kosztował cały przebieg. Poniżej znajduje się mały skrypt w Pythonie, który robi wszystkie trzy rzeczy na Twoim własnym zadaniu.

Co robi publiczny turniej, co możesz zapożyczyć#

Clocktower Bench Dwarkesha Patela to nieoficjalny projekt badawczy, w którym modele językowe grają przeciwko sobie w grę dedukcji społecznej Blood on the Clocktower. Gra nie jest tu sednem; sednem jest harness. Według strony About projektu (sprawdzonej 8 listopada 2026) robi on kilka rzeczy, które powinno robić każde porównanie jeden na jeden:

  • jeden model zajmuje wszystkie dobre miejsca, a drugi wszystkie złe, i każde rozdanie jest rozgrywane dwukrotnie ze zamienionymi stronami, więc szczęśliwe miejsce nie decyduje o wyniku;
  • wszystkie modele dostają te same ustawienia: ten sam wysiłek rozumowania, limit 8000 tokenów na turę i limit 150 słów wypowiedzi;
  • Storyteller jest skryptowany, więc żaden model nie sędziuje gry;
  • każde wywołanie API jest zapisywane do calls.jsonl wraz z tokenami i kosztem. Strona podaje, że jedna gra kosztuje od około 2 do 50 dolarów, w zależności od modeli (na API OpenAI i Anthropic).

Harness jest zbudowany dla modeli OpenAI i Anthropic, a Twoje zadanie prawdopodobnie nie jest grą planszową. Ale trzy nawyki się przenoszą: równe ustawienia, zamienione strony i log kosztów. Reszta tego przewodnika stosuje je do czegoś zwyczajnego.

Każdy ticket trafia do obu modeli; sędzia widzi parę dwukrotnie, w przeciwnych kolejnościach
Pętla zamiany stron dla jednego ticketu

Zadanie: redagowanie odpowiedzi na 30 ticketów wsparcia#

Powiedzmy, że prowadzisz małe biuro wsparcia i chcesz wiedzieć, czy DeepSeek-V4-Flash czy GLM-5.3-Flash pisze lepsze pierwsze szkice. Masz plik tickets.jsonl z 30 prawdziwymi ticketami, po jednym obiekcie JSON na linię: {"id": 1, "text": "..."}.

API jest zgodne z OpenAI, więc oficjalny pakiet openai działa z CheapTokens pod https://api.cheaptokens.dev/v1. Użyj pełnych identyfikatorów modeli z dokumentacji: deepseek-ai/DeepSeek-V4-Flash-0731 i zai-org/GLM-5.3-Flash. Oba modele dostają też trzeci model jako sędziego, MiniMaxAI/MiniMax-M2.7, żeby żaden z uczestników nie oceniał własnej pracy.

Zacznij od części, która umożliwia logowanie: jednej funkcji, przez którą przechodzi każde wywołanie.

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)

Pola w calls.jsonl to te, które wybiera ten skrypt; liczby użycia pochodzą z obiektu usage odpowiedzi API. Oba modele dostają ten sam prompt systemowy, ten sam temperature i ten sam max_tokens. Zmień jeden z nich dla jednego modelu, a porównujesz ustawienia, nie modele.

Zamień strony: oceniaj każdą parę dwukrotnie#

Teraz sędzia. Model sędziowski może faworyzować odpowiedź w konkretnym slocie, więc pojedynczy werdykt miesza jakość z pozycją. Sztuczka z Clocktower pasuje idealnie: pokaż każdą parę w obu rolach. Odpowiedź wygrywa ticket tylko wtedy, gdy sędzia wybierze ją w obu kolejnościach. Jeśli werdykt podąża za slotem, a nie za odpowiedzią, ticket jest remisem.

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)

Dwa szczegóły łatwo przeoczyć. Sędzia działa z temperature=0, aby jego werdykty były stabilniejsze; nie każdy model respektuje to ustawienie, więc uruchom ponownie kilka par i porównaj werdykty, zanim im zaufasz. Werdykt jest odczytywany tylko z końcowej linii Verdict:, a limity są hojne (800 dla szkiców, 1000 dla sędziego), żeby model, który rozumuje przed odpowiedzią, nie został ucięty w połowie myśli; poniższe oszacowanie używa typowych rozmiarów, a nie limitów. Nieparsowalna odpowiedź daje None, co wpada w remis zamiast wysadzać przebieg.

Spróbuj samSpróbuj: sędzia powiedział "A" w obu kolejnościach dla ticketu 7. Kto wygrywa ten ticket?Pokaż odpowiedź

Nikt. W pierwszej kolejności A było odpowiedzią DeepSeek, w drugiej A było odpowiedzią GLM, więc sędzia preferował slot, a nie model. Kod liczy to jako remis. Stos takich remisów sam w sobie jest wynikiem: modele są blisko siebie na Twoim zadaniu albo Twój prompt sędziego jest zbyt niejasny.

Kolejność to tylko jeden ze sposobów przekrzywienia testu. Ustawienia, prompty i logowanie też mogą go przekrzywić, więc sprawdź całą konfigurację, nie tylko to, kto idzie pierwszy. Oto ten sam eksperyment przed i po.

Porównaj

Co się zmienia, gdy przestajesz porównywać niedbale

Niedbale

Jeden prompt na model, domyślne ustawienia pozostawione, para pokazana w jednej kolejności, a rachunek odczytany z dashboardu pod koniec miesiąca. Zwycięzcą może być model, który przypadkiem był wymieniony pierwszy.

Uczciwie

Jeden wspólny prompt systemowy i te same temperature i max_tokens, każda para oceniana w obu kolejnościach, i linia w calls.jsonl dla każdego wywołania. Zwycięzcą jest ten, kto wygrywa z obu miejsc.

Loguj każde wywołanie, potem wyceń przebieg#

Log to, co zamienia porównanie w budżet. Po przebiegu zsumuj tokeny na model:

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

Otrzymasz jedną linię na model z liczbą wywołań, tokenami wejściowymi i wyjściowymi. Pomnóż przez dwie ceny modelu (wejście i wyjście są rozliczane osobno) lub, łatwiej, wklej sumy do znacznika kosztów na żywo.

Możesz też oszacować przed uruchomieniem. Załóż, że każdy ticket plus prompt systemowy to około 300 tokenów wejściowych, szkic to około 250 tokenów wyjściowych, a sędzia widzi około 800 tokenów wejściowych (ticket plus obie odpowiedzi plus pytanie) i odpowiada około 200 tokenami (linia werdyktu plus trochę miejsca na myślenie):

  • szkice: 30 ticketów × 2 modele = 60 wywołań, 30 × 300 = 9000 tokenów wejściowych i 30 × 250 = 7500 tokenów wyjściowych na uczestnika;
  • werdykty: 30 ticketów × 2 kolejności = 60 wywołań, 60 × 800 = 48000 tokenów wejściowych i 60 × 200 = 12000 tokenów wyjściowych u sędziego.

Przy obecnych cenach 30 szkiców jednego uczestnika kosztuje $0.00089 na DeepSeek-V4-Flash i $0.00048 na GLM-5.3-Flash. 60 werdyktów na MiniMax-M2.7 kosztuje $0.0042. Dodaj trzy i masz cenę całego porównania, zanim wyślesz choćby jedno żądanie. To samo obciążenie redakcyjne dla wszystkich trzech modeli, z wejściem i wyjściem liczonym razem:

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

9000 tokenów wejściowych i 7500 tokenów wyjściowych łącznie. Ceny za 1 mln tokenów są aktualne i zmieniają się wraz z naszymi taryfami; kolumna OpenRouter pokazuje ceny referencyjne dla tego samego obciążenia.

Tabela pokazuje też odniesienie OpenRouter, żebyś zobaczył, jak wypada suma. Założenia dotyczące tokenów są do zmiany: jeśli Twoje tickety są długie, wejście sędziego rośnie najszybciej, bo niesie obie odpowiedzi dwukrotnie. Jeśli Twój sędzia myśli na głos przed odpowiedzią, jego liczba wyjściowa też rośnie, a Twój log to pokaże. O nawyku liczenia tokenów przed żądaniem przeczytaj count tokens before you send a request.

Ten sam limit, którego używa Clocktower, warto ukraść. Ustawia 8000 tokenów na turę, a Ty już masz max_tokens. Wstaw go do SETTINGS, a wymykająca się odpowiedź nie może po cichu pomnożyć Twojego oszacowania.

Zanim uruchomisz pełny przebieg#

Lista kontrolna

Lista kontrolna przed uczciwym porównaniem

Ten ostatni punkt ma znaczenie. Uruchom trzy tickety, otwórz calls.jsonl i przeczytaj. Sprawdź, czy finish mówi stop, a nie length; jeśli szkic został ucięty przez max_tokens, jeden model może przegrywać tylko dlatego, że jego odpowiedź została obcięta. To samo dotyczy wywołań sędziego: length tam zamienia się w cichy remis, więc policz też werdykty None. Jeśli Twoje zadanie ma jedną poprawną odpowiedź, możesz w ogóle nie potrzebować sędziego: test your prompt on 20 cases with exact-match grading.

Czytanie wyniku bez oszukiwania siebie#

Spójrz na trzy liczby w wins. Trzydzieści ticketów to mała próba: podział jak 12 do 9 z 9 remisami mówi, że modele są blisko, a nie że jeden jest lepszy. Log tu pomaga. Jeśli jeden model wygrywa nieznacznie, ale zużywa znacznie więcej wyjścia, prawdziwym pytaniem jest cena zadania, a nie liczba zwycięstw; price per 1M tokens versus cost per task omawia to porównanie.

Jeśli wynik jest bliski, wybierz tańszy model do zadania albo dodaj więcej ticketów i uruchom ponownie. Oba są tanie do wypróbowania, bo cała maszyneria jest już pętlą. Kiedy chcesz powtórzyć ćwiczenie na innym modelu, trzy powyższe modele stoją za jednym API i jednym kluczem: konfiguracja w dokumentacji jest taka sama dla każdego, a strona cen wymienia, ile każdy kosztuje teraz. Własne odkrycie Dwarkesha Patela dotyczące modeli OpenAI versus Anthropic (detekcja versus oszustwo) jest jego wynikiem dla jego gry; Twój będzie pochodzić z Twoich ticketów.