Назад в блог

Модели и сравненияИнструкция

Честное сравнение двух LLM на своей задаче: swap и лог затрат

Сравните две модели на своих тикетах: одинаковые настройки, каждая пара оценивается в обоих порядках, а лог calls.jsonl даёт стоимость прогона.

Автор: CheapTokens TeamОпубликовано 7 мин чтения

Обложка: честное сравнение двух моделей, три шага — один промпт, обмен сторон, лог каждого вызова
Содержание
  1. Что делает публичный турнир и что можно взять себе
  2. Задача: черновики ответов на 30 тикетов поддержки
  3. Меняем стороны: каждую пару судим дважды
  4. Логируем каждый вызов и считаем стоимость прогона
  5. Перед полным прогоном
  6. Как читать результат и не обмануть себя

Вы прогнали три промпта через две модели, и выиграла вторая. Прежде чем верить, проверьте, какая стояла второй. Честному сравнению нужны три вещи: одинаковые настройки у обеих моделей, оценка каждой пары в обоих порядках и лог токенов по каждому вызову — чтобы знать, сколько стоил весь прогон. Ниже небольшой скрипт на Python, который делает всё это на вашей задаче.

Что делает публичный турнир и что можно взять себе#

Clocktower Bench Дваркеша Пателя — неофициальный исследовательский проект, где языковые модели играют друг против друга в «Кровь на часовой башне». Сама игра тут не важна, важна его обвязка. По странице About (проверено 8 ноября 2026) проект делает то, что стоит делать в любом сравнении:

  • одна модель играет за всех «добрых», другая за всех «злых», а каждая раздача проигрывается дважды с обменом сторон, так что удачное место не решает исход;
  • настройки у всех моделей одинаковые: то же усилие рассуждения, потолок 8 000 токенов на ход и лимит 150 слов на реплику;
  • ведущий игры — скрипт, ни одна модель не судит партию;
  • каждый вызов API пишется в calls.jsonl с токенами и стоимостью. По словам страницы, одна партия обходится примерно от $2 до $50 в зависимости от моделей (на API OpenAI и Anthropic).

Обвязка рассчитана на модели OpenAI и Anthropic, а ваша задача вряд ли настольная игра. Но три привычки переносятся: равные настройки, обмен местами и лог затрат. Дальше они применены к обычному делу.

Каждый тикет уходит обеим моделям; судья видит пару дважды, в противоположных порядках
Цикл с обменом сторон для одного тикета

Задача: черновики ответов на 30 тикетов поддержки#

Допустим, у вас небольшая поддержка, и вы хотите понять, кто пишет лучшие первые черновики — DeepSeek-V4-Flash или GLM-5.3-Flash. Есть файл tickets.jsonl с 30 настоящими тикетами, по одному JSON в строке: {"id": 1, "text": "..."}.

API совместим с OpenAI, поэтому официальный пакет openai работает с CheapTokens по адресу https://api.cheaptokens.dev/v1. Берите полные id моделей из документации: deepseek-ai/DeepSeek-V4-Flash-0731 и zai-org/GLM-5.3-Flash. Судьёй будет третья модель, MiniMaxAI/MiniMax-M2.7, чтобы ни один участник не оценивал сам себя.

Начнём с того, что делает лог возможным: одна функция, через которую проходит каждый вызов.

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)

Поля в calls.jsonl выбирает этот скрипт; цифры токенов берутся из объекта usage в ответе API. Обе модели получают один системный промпт, одинаковые temperature и max_tokens. Поменяйте что-то одно для одной модели — и вы сравниваете уже настройки, а не модели.

Меняем стороны: каждую пару судим дважды#

Теперь судья. Судья-модель может отдавать предпочтение ответу на определённом месте, и один вердикт смешивает качество с позицией. Приём из Clocktower здесь подходит как нельзя лучше: показать каждую пару в обеих ролях. Ответ выигрывает тикет, только если судья выбрал его в обоих порядках. Если вердикт следует за местом, а не за ответом, тикет — ничья.

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)

Две детали легко пропустить. Судья работает при temperature=0, чтобы вердикты были стабильнее; не каждая модель учитывает этот параметр, поэтому перезапустите несколько пар и сверьте вердикты, прежде чем им верить. Вердикт читается только из последней строки Verdict:, а лимиты с запасом (800 для черновиков, 1000 для судьи), чтобы модель, которая рассуждает перед ответом, не оборвалась на полуслове; оценка ниже считает типичные размеры, а не лимиты. Непонятный ответ даёт None, который попадает в ничью, а не роняет весь прогон.

Попробуйте самиПопробуйте: по тикету 7 судья в обоих порядках сказал «A». Кто выиграл тикет?Показать ответ

Никто. В первом порядке A был ответом DeepSeek, во втором — ответом GLM, то есть судья выбрал место, а не модель. Код считает это ничьей. Куча таких ничьих — тоже результат: модели на вашей задаче близки, либо промпт судьи слишком расплывчатый.

Порядок — лишь один из способов исказить тест. Перекосить его могут и настройки, и промпты, и отсутствие лога, поэтому проверяйте всю схему, а не только то, кто идёт первым. Вот тот же опыт до и после.

Сравнение

Что меняется, когда сравнение перестаёт быть на глаз

На глаз

Один промпт на модель, настройки по умолчанию, пара показана в одном порядке, а счёт смотрят в панели в конце месяца. Победить может та модель, что оказалась в списке первой.

Честно

Один общий системный промпт и одинаковые temperature и max_tokens, каждая пара судится в обоих порядках, на каждый вызов — строка в calls.jsonl. Побеждает та, что выигрывает с обоих мест.

Логируем каждый вызов и считаем стоимость прогона#

Из лога сравнение превращается в бюджет. После прогона просуммируйте токены по моделям:

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

На выходе по строке на модель: число вызовов, входные и выходные токены. Умножьте на две цены модели (вход и выход считаются отдельно) или проще — подставьте суммы в живую метку стоимости.

Оценить можно и до запуска. Допустим, тикет вместе с системным промптом — около 300 входных токенов, черновик — около 250 выходных, а судья видит примерно 800 входных (тикет, оба ответа и вопрос) и отвечает около 200 токенами (строка вердикта и немного места на рассуждение):

  • черновики: 30 тикетов × 2 модели = 60 вызовов, на каждого участника 30 × 300 = 9000 входных и 30 × 250 = 7500 выходных токенов;
  • вердикты: 30 тикетов × 2 порядка = 60 вызовов, у судьи 60 × 800 = 48000 входных и 60 × 200 = 12000 выходных токенов.

По текущим ценам 30 черновиков одного участника стоят $0.00089 на DeepSeek-V4-Flash и $0.00048 на GLM-5.3-Flash. 60 вердиктов на MiniMax-M2.7 — $0.0042. Сложите три числа — и цена всего сравнения известна до первого запроса. Та же нагрузка черновиков на всех трёх моделях, с входом и выходом вместе:

Модель Стоимость 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 входных и 7 500 выходных токенов всего. Цены за 1M токенов актуальны и меняются вместе с нашими тарифами; столбец OpenRouter показывает справочные цены для той же нагрузки.

В таблице есть и справочная цена OpenRouter, так что видно, как выглядит итог. Допущения по токенам меняйте под себя: если тикеты длинные, быстрее всего растёт вход судьи — в нём оба ответа, причём дважды. Если судья рассуждает вслух перед ответом, растёт и его выход, и ваш лог это покажет. О привычке считать токены до запроса — в статье как посчитать токены перед запросом.

Стоит перенять и ограничитель из Clocktower: там потолок 8 000 токенов на ход, а у вас есть max_tokens. Положите его в SETTINGS, и слишком длинный ответ не раздует смету незаметно.

Перед полным прогоном#

Чек-лист

Предполётная проверка честного сравнения

Последний пункт важен. Прогоните три тикета, откройте calls.jsonl и прочитайте. Проверьте, что в finish стоит stop, а не length: если черновик обрезан по max_tokens, одна модель может проигрывать лишь потому, что её ответ оборвался. То же с вызовами судьи: length там превращается в молчаливую ничью, поэтому считайте и вердикты None. А если у вашей задачи один правильный ответ, судья может и не понадобиться: проверьте промпт на 20 примерах с точным совпадением.

Как читать результат и не обмануть себя#

Посмотрите на три числа в wins. Тридцать тикетов — маленькая выборка: разброс вроде 12 к 9 при 9 ничьих говорит, что модели близки, а не что одна лучше. Здесь помогает лог. Если одна модель выигрывает чуть-чуть, но пишет заметно длиннее, вопрос уже в цене задачи, а не в числе побед; как это сравнивать, разобрано в статье цена за 1M токенов и стоимость задачи.

Если результат близкий, берите для задачи более дешёвую модель или добавьте тикетов и повторите. Попробовать оба варианта недорого: вся механика уже цикл. Когда захотите повторить упражнение с другой моделью, все три модели доступны через один API и один ключ: настройка в документации одинакова для каждого, а на странице цен видно, сколько каждая стоит сейчас. Вывод Дваркеша Пателя про модели OpenAI и Anthropic (обнаружение против обмана) — результат его игры; ваш результат даст ваша выборка тикетов.