model в теле запроса — остальной код остаётся тем же.temperature=0, одинаковый max_tokens, замер задержки и доли обрезанных ответов (finish_reason: "length").
curl).python3 --version) — примеры ниже используют только стандартную библиотеку, pip install не нужен.curl (curl --version) — для быстрой проверки одним запросом.mkdir -p ~/ds-ab-test && cd ~/ds-ab-test — в нём будут лежать скрипт и лог прогона.Пустые ячейки — не ошибка: это поля, которые вы заполняете из документации провайдера, а не из чужих обзоров.
| Модель | Отличие (по карточке модели) | Цена за 1M входных / выходных токенов | Где взять цифру |
|---|---|---|---|
| DeepSeek V4.1-Flash-1017 | в исходных материалах не указано — сверьтесь с документацией | не указано — сверьтесь с документацией | раздел Pricing + карточка модели |
| DeepSeek V3 | в исходных материалах не указано — сверьтесь с документацией | не указано — сверьтесь с документацией | раздел Pricing + карточка модели |
Пример: если в прайсе указано «$X за 1M входных токенов», то стоимость одного прогона из 30 промптов по 500 входных и 300 выходных токенов считается как 30 × (500/1_000_000 × X_вх + 300/1_000_000 × X_вых). Подставьте X из прайса — арифметика занимает одну строку в таблице.
Если вам нужен более широкий контекст по тому, как вообще сравнивать модели между поколениями, начните с материала GPT-6.1 Sol или GPT-4o: сравнение моделей и что выбрать — логика замера там та же.
Что сделать: создайте файл prompts.txt — по одному промпту на строку, 10–30 штук из вашей реальной задачи (не «напиши стих», а то, что приложение реально спрашивает у модели).
Зачем: без фиксированного набора вы будете сравнивать случайные ответы и получите случайный вывод. Набор — это ваш эталон, его можно переиспользовать при следующем обновлении модели.
Пример: prompts.txt для сервиса поддержки:
Верни JSON с полями id и title для обращения «оплата не проходит третий день».
Классифицируй обращение одним словом: billing, delivery, account: «не приходит SMS с кодом».
Сократи ответ до 200 символов: <вставьте свой реальный абзац>.
Как проверить: wc -l prompts.txt должен показать число от 10 до 30, а head -n 3 prompts.txt — ваши первые три строки.

Что сделать: сохраните ключ и идентификаторы моделей в переменные окружения, затем запустите скрипт ab.py, который делает одинаковые запросы к обеим моделям и пишет задержку и usage.
Зачем: один и тот же код с разным значением model — единственный способ получить сравнимые числа. Идентификаторы моделей в API обычно записываются строчными буквами и отличаются от человекочитаемых названий, поэтому точные id сверьте с документацией и списком моделей.
export DEEPSEEK_API_KEY="<ваш-ключ>"
export DEEPSEEK_BASE_URL="<базовый-URL-из-документации>"
export MODEL_A_ID="<id модели DeepSeek V4.1-Flash-1017 из документации>"
export MODEL_B_ID="<id модели DeepSeek V3 из документации>"
Быстрая проверка, что ключ и URL рабочие (до всякого сравнения):
curl -s "$DEEPSEEK_BASE_URL/models" -H "Authorization: Bearer $DEEPSEEK_API_KEY" | head -c 400
Ожидаемый вывод: JSON со списком объектов, у каждого есть поле id. Если вместо этого пришёл {"error":...} — ключ или URL неверны, дальше идти нет смысла.
Сам прогон:
import os, json, time, statistics, urllib.request
BASE = os.environ["DEEPSEEK_BASE_URL"].rstrip("/")
KEY = os.environ["DEEPSEEK_API_KEY"]
MODELS = {"A": os.environ["MODEL_A_ID"], "B": os.environ["MODEL_B_ID"]}
PROMPTS = [l.strip() for l in open("prompts.txt", encoding="utf-8") if l.strip()]
def call(model, prompt):
body = json.dumps({
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
"max_tokens": 1024,
}).encode()
req = urllib.request.Request(
f"{BASE}/chat/completions", data=body,
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
t0 = time.perf_counter()
with urllib.request.urlopen(req, timeout=90) as r:
data = json.load(r)
dt = time.perf_counter() - t0
ch = data["choices"][0]
return dt, ch.get("finish_reason"), data.get("usage", {}), ch["message"]["content"]
for key, model in MODELS.items():
lat, truncated = [], 0
for p in PROMPTS:
dt, finish, usage, text = call(model, p)
lat.append(dt)
truncated += (finish == "length")
print(key, model, f"{dt:.2f}s", finish, usage.get("completion_tokens"), repr(text[:60]))
time.sleep(1) # пауза против 429
print(f"{key}: медиана {statistics.median(lat):.2f}s, обрезано {truncated} из {len(PROMPTS)}")
Почему это работает: temperature=0 убирает случайность формулировок, одинаковый max_tokens не даёт одной модели «обрезаться» раньше другой, а time.perf_counter() измеряет полное время запроса, включая сеть.
Как проверить: в консоли после каждого ответа печатается строка вида A <id> 1.84s stop 128 'Отлично, вот JSON...', а в конце — итог A: медиана ...s, обрезано 0 из 20. Если итоговой строки нет — скрипт упал на исключении, смотрите раздел проблем ниже.
Что сделать: сведите результаты прогона в чек-лист и посчитайте стоимость по прайсу.
Зачем: выбор «на глаз» по паре ответов почти всегда ошибочен: модели отличаются не качеством в целом, а поведением на вашем типе задач.
Пример: чек-лист, который заполняется один раз на каждую модель:
prompts.txtfinish_reason: "length" равна нулю для обеих моделейcompletion_tokens из usage по каждому прогонутокены × цена за 1M / 1_000_000 по прайсуdecision.md с датой прогонаКак проверить: откройте decision.md — там должно быть две строки с числами (задержка, стоимость, доля брака) и одна строка с решением: «для прод-нагрузки берём модель A, для офлайн-разбора — модель B». Если хотя бы одно поле пустое, возвращайтесь к Шагу 2.
Проблема. На запрос к одной из моделей API отвечает 404 вместо ответа модели.
{"error":{"message":"Model Not Exist","type":"invalid_request_error"}}model передано человекочитаемое название («DeepSeek V4.1-Flash-1017») вместо идентификатора из API, либо у ключа нет доступа к этой модели.curl -s "$DEEPSEEK_BASE_URL/models" -H "Authorization: Bearer $DEEPSEEK_API_KEY" и скопируйте id из ответа в MODEL_A_ID. Если нужного id в списке нет — сверьтесь с документацией: возможно, доступ выдаётся отдельно.Проблема. Прогон падает на середине, в логе 429, часть ответов пустая.
{"error":{"message":"Rate limit reached for requests","type":"rate_limit_error"}}time.sleep(1) между вызовами и оберните urlopen в повтор с нарастающей паузой по коду 429:except urllib.error.HTTPError as e:
if e.code == 429:
time.sleep(10); continue # повтор после паузы
raise
Проблема. Сравнение нечестное: у одной модели ответы заметно короче.
"finish_reason": "length", у другой — "stop".max_tokens мало для полного ответа, модель обрывается на середине, и вы сравниваете обрезанный текст с целым.max_tokens до одинакового значения (например, 2048) для обеих моделей, перезапустите прогон и исключите из подсчёта все ответы с "length".
MODEL_A_ID.Общий обзор того, как разные ассистенты и модели ведут себя на типовых задачах, есть в гайде ChatGPT, Claude, Gemini, DeepSeek: сравнение ИИ-ассистентов.
Вы получили воспроизводимый стенд: одинаковые промпты, одинаковые параметры запроса и три измеримых числа по каждой модели. Следующий шаг — перенести выбранный идентификатор модели в конфиг приложения и повторить прогон через месяц, чтобы поймать изменения на стороне провайдера.
Можно ли переключаться между DeepSeek V4.1-Flash-1017 и DeepSeek V3 без переписывания кода?
Да, если API OpenAI-совместим: меняется только значение поля model в теле запроса, остальные поля и структура ответа остаются прежними. Точные идентификаторы сверьте с документацией.
Где взять цены за токены, если в обзорах их нет?
В разделе Pricing документации провайдера; далее считайте по формуле токены × цена за 1M / 1_000_000, используя usage из реального прогона, а не оценку «на глаз».
Почему скрипт вернул 429, хотя я сделал всего 20 запросов?
Сработал лимит запросов в минуту (rate limit). Добавьте паузу между вызовами и повтор с задержкой при коде 429 — пример есть в разделе проблем.
Нужно ли менять промпты при переходе на другую модель?
Да, прогоните свой набор из prompts.txt заново: формулировки, которые работали на одной модели, могут давать другой формат ответа на другой. Именно для этого набор и сохраняется в файл.