Сравнение Codex, Claude Code и Kimi. Кто лучше на практических задачах

Сравнение Codex, Claude Code и Kimi. Кто лучше на практических задачах

Меня зовут Илья, я разработчик и пишу про разработку приложений. Сегодня расскажу про эксперимент, который я решил провести после выхода очередной модели, которая превышала по бенчмаркам другие. Показатели каждой новой версии моделей стабильно превышают предыдущие, но при этом они не отвечают на вопрос: какой cli агент лучше использовать при разработке. Я решил попробовать сравнить три агента Codex, Claude Code и Kimi на одной реальной задаче в одинаковых стартовых условиях, чтобы посмотреть чем реально отличаются результаты их работы.

Что на входе

Экспериментальный проект — рабочий лендинг агентства. Стек: Next.js 16.2.10, React 19, TypeScript, Tailwind v4, next-intl. Сайт собирается в два отдельных билда под два домена ru/en, для тестов используются vitest и Cypress, настроен CI/CD.

Одна важная деталь: серверного кода в проекте не было вообще. Все страницы пререндерены заранее в SSR, а все переменные окружения имеют префикс NEXT_PUBLIC_ — это значит, что они попадают в собранный бандл и видны любому, кто откроет исходники страницы. Форма заполнения заявки уже была отрисована, но данные никуда не отправляла.

Это то, что необходимо было доделать — отправку заявки в CRM систему. Задачу я скопировал в каждую сессию дословно, вместе с опечатками:

Разработка CRM-хука для формы отправки данных

Сейчас на всех кнопках отправки заявкина проект ничего не происходит. Нужно чтобы заполненные поля валидировались и отправлялись в CRM систему. Это будет Система X — отдельная очередь Лиды.

Сервис должен дергаться на бекенде сайта, не отсвечивая конечный адрес в исходниках фронта, отдающихся на клиент.

Нужно рассмотерть два варианта создания задач: либо напрямую дергается апи crm, либо создать отдельную форму в яндекс формах, автоматически заполнять ее, а потом уже интегрировать форму и crm.

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

Кто участвует в сравнении:

CLI

версия

модель

контекст

codex

codex-cli 0.145.0

gpt-5.6-sol

258 400

claude code

Claude Code 2.1.220

claude-opus-5

1M

kimi

kimi 1.49.0

kimi-k3 (Moonshot API)

1M

Прогонов на самом деле было четыре. Первый codex я запустил на дефолтных настройках и только потом, уже разбирая цифры, заметил, что дефолтный уровень рассуждений у codex-cli — low, а у Claude Code — xhigh. То есть при сравнении уровень effort’a был нерелевантен, поэтому прогон в codex пришлось повторить увеличив уровень до extra high. Проход на Low уровне я из сравнения убрал, но для информации могу отметить, что переключение уровня добавило к реализации на codex вчетверо больше тестов и в 4.6 раза дороже.

Первый шаг: Анализ кодовой базы

Перед стартом задачи я прогнал в каждой копии команду /init. Она просит агента изучить проект и написать документацию для будущих сессий — что-то вроде шпаргалки, которую студент готовит себе перед экзаменом.

В результате получились файлы в 37 строк у codex, 94 у claude и 126 у kimi. В целом во всех трех файлах не было чего-то надуманного или некорректного, но вот что каждый счёл нужным включить в файл, сильно отличалось.

Codex написал документ, больше описывающий процесс разработки. Он взял шаблон «Repository Guidelines» за основу и из него включил в описание: структуру модулей, команды, стиль кода, а также правила коммитов и пулл-реквестов. Двухдоменную сборку он упомянул одним абзацем, зато единственный расписал, что должно быть в описании PR.

Claude написал документ больше про архитектуру проекта с обоснованиями. Первый раздел у него называется «Two single-locale builds, one codebase» и в нем описывается: почему в маршрутах нет языкового сегмента, почему переключатель языка это обычная ссылка на соседний домен и почему публичные переменные приходится передавать в Docker аргументами сборки.

Kimi написал скорее полноценную документацию reference. Самый длинный из трёх: версии всего стека, дерево репозитория с комментарием к каждому каталогу, модель хранения контента, тестирование, сборка и деплой вплоть до перечня секретов в CI. Он единственный вынес критерий приёмки отдельной именованной строкой — «verification gate for a change».

И у него же в конспекте оказался раздел «Security considerations», а в нём вот такое:

No server-side secrets exist. Every env var is NEXT_PUBLIC_* and therefore public in the client bundle — never introduce a private secret with that prefix.

The contact form submits nowhere (no backend). If a backend is added, treat validation, consent checkbox, and rate limiting as server responsibilities.

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

Второй шаг: выполнение задачи

Дальше каждый агент получил одинаковые инструкции с исходной задачей и начался процесс ее выполнения.

Я ожидал разного поведения на развилке из двух вариантов, но прямой вызов API CRM выбрали все три, связку через Яндекс Формы не стал делать никто. Все агенты делали полноценный ресерч: Claude сделал 10 запросов на чтение страниц и два поиска. Codex — 4 серверных поиска с прицельными запросами вида site:yandex.ru/dev/. Kimi запустил для ресерча отдельного субагента, тот сходил по докам 8 раз.

Контракт для работы с API в итоге получился у всех один и тот же:

POST https://api.crm.com/v1/leads/
Authorization: OAuth <token>  |  Bearer <IAM-token>
X-Org-ID  |  X-Cloud-Org-ID

Каркас реализованных решений тоже совпал у всех агентов, вплоть до раскладки по файлам: роутер, модуль валидации, отдельный серверный клиент CRM.

Назначение

codex

claude code

kimi

API-маршрут

app/api/contact/route.ts

app/api/lead/route.ts

app/api/lead/route.ts

Валидация, контракты

lib/contact.ts

lib/lead.ts

lib/lead.ts

Клиент CRM

lib/crm.ts

lib/crm.ts

lib/crm.ts

Throttling

Внутри роутера

lib/rate-limit.ts

Тестовая инфраструктура

test/server-only.ts

Тесты

app/api/contact/route.test.tslib/contact.test.tslib/crm.test.ts

app/api/lead/route.test.tslib/lead.test.tslib/rate-limit.test.tslib/crm.test.ts

app/api/lead/route.test.tslib/lead.test.ts

Правки в компонентах формы

ContactForm.tsx

ContactForm.tsxContactFormWithToast.tsxContactModal.tsxContactBlock.tsxContactSection.tsx

ContactForm.tsx

Итого в src/

7 новых, 2 изменённых

8 новых, 6 изменённых

5 новых, 2 изменённых

В этот момент мне показалось, что сравнивать особо и нечего, все агенты справились с задачей.

Сравнение результатов

Форма заявки на сайте используется в трёх местах: в модальном окне, в блоке на странице контактов и в секции на лендинге. В каждом месте она подключается чуть по-своему. Claude Code поправил каждое из этих мест. Два других агента поправили сам компонент формы и на этом закончили.

Далее кратко какие функции добавили агенты сами на основе своих знаний и своего харнеса:

codex

claude code

kimi

Валидация данных на сервере

+

+

+

Honeypot от ботов

+

+

Throttling

+

+

Защита от повторной отправки

+

Ретраи при ошибке сервера

+

+

Проверка источника запроса

+

XSS защита

+

Если считать по перечисленным функциям, то вперёд вырывается codex: 6 пунктов из 7 против 5 у claude и 1 у kimi. Причём два из них: проверка источника запроса и XSS защита больше никто не сделал. Но это не самый правильный подход к оценке, потому что codex проявил фантазию и вообще очень много сделал сверх того о чем его просили.

Единственное, чего codex не сделал — защиту от повторной отправки, в отличие от Claude Code. Claude передаёт в CRM поле unique и если запрос оборвётся уже после того, как заявка создалась, повторная попытка вернет код 409, который понимается как успешный и не требующий ретрая. Без этого пользователь, дважды нажавший кнопку, получит две одинаковые заявки в очереди.

И по другому направлению claude code уходит вперёд заметно: 39 написанных тест-кейсов против 16 у codex. Claude code берёт не количеством дополнительных функций, а покрытием тестами того, что сделано.

Kimi throttling не сделал, но написал замечание в результатах своей работы: «rate limiting / anti-spam is NOT implemented yet, add it if the endpoint gets abused». Т.е. он остался в рамках исходной задачи, отложив на будущее решение о том, делать ли это или нет, что тоже неплохо.

Вот как выглядит покрытие тестами:

Функционал

codex

claude code

kimi

Валидация

5

12

15

API-роут

5

8

8

Клиент CRM

4

7

Throttling

4

Форма заявки

+2

+8

+4

Итого кейсов

16

39

27

Claude code заметно опережает других агентов. И еще важно отметить, что клиент CRM — единственный модуль, который использует внешний API, то есть самая важная часть всей задачи. И у kimi он вообще не покрыт автотестами.

Как выглядит код

Отдельно хочу показать как выглядит код разных агентов на двух одинаковых по назначению блоках. Файлы клиента CRM получились объемом в 181, 179 и 112 строк.

Чтение конфигурации CRM

Та самая развилка с двумя схемами авторизации решена тремя разными способами.

Осторожно, много кода
// codex — явные типы источника токена, валидация формата очереди регуляркой,
// собственный класс ошибки. И только IAM: ветки OAuth нет вовсе.
function getTrackerConfig(): TrackerConfig {
  const iamSource = process.env.CRM_IAM_SOURCE?.trim() || "env";
  const iamToken = process.env.CRM_IAM_TOKEN?.trim();
  const cloudOrganizationId = process.env.CRM_CLOUD_ORG_ID?.trim();
  const queue = process.env.CRM_QUEUE_KEY?.trim() || DEFAULT_QUEUE;

  if (iamSource !== "env" && iamSource !== "metadata") {
    throw new TrackerConfigurationError("CRM_IAM_SOURCE must be env or metadata");
  }
  if (!cloudOrganizationId) {
    throw new TrackerConfigurationError("CRM_CLOUD_ORG_ID is not configured");
  }
  if (!/^[A-Z][A-Z0-9_-]{1,49}$/.test(queue)) {
    throw new TrackerConfigurationError("CRM_QUEUE_KEY has an invalid format");
  }

  return {
    iamSource,
    iamToken,
    cloudOrganizationId,
    queue,
  };
}
// claude — отдельный рубильник AUTH_SCHEME, независимый от типа организации,
// плюс переопределяемый базовый URL. Нет конфига — возвращает null, не бросает.
function readConfig(): TrackerConfig | null {
  const token = process.env.CRM_TOKEN?.trim();
  const orgId = process.env.CRM_ORG_ID?.trim();
  const cloudOrgId = process.env.CRM_CLOUD_ORG_ID?.trim();
  if (!token || !(orgId || cloudOrgId)) return null;

  return {
    apiUrl: process.env.CRM_API_URL?.trim() || "https://api.crm.com/",
    token,
    // OAuth token by default; "Bearer" when a IAM token is used.
    authScheme: process.env.CRM_AUTH_SCHEME?.trim() || "OAuth",
    orgHeader: cloudOrgId ? "X-Cloud-Org-ID" : "X-Org-ID",
    orgId: (cloudOrgId || orgId) as string,
    queue: process.env.CRM_QUEUE?.trim() || "LEAD",
  };
}
// kimi — самодокументируемые типы и зафиксированное ограничение API:
// IAM бывает только у облачных организаций, значит заголовок форсится.
export function trackerConfig(): TrackerConfig | null {
  const token = process.env.CRM_TOKEN;
  const orgId = process.env.CRM_ORG_ID;
  if (!token || !orgId) return null;
  const iam = process.env.CRM_TOKEN_TYPE === "iam";
  return {
    authorization: iam ? `Bearer ${token}` : `OAuth ${token}`,
    orgId,
    // IAM auth exists only for Cloud organizations
    orgHeader:
      iam || process.env.CRM_ORG_TYPE === "cloud" ? "X-Cloud-Org-ID" : "X-Org-ID",
    queue: process.env.CRM_QUEUE || "LEAD",
  };
}

Результат работы агентов показывает три разных подхода к организации кода. Codex вводит явные переменные-перечисления и проверяет формат ключа очереди регулярным выражением. Claude Code даёт максимум управляемых параметров, включая подмену базового адреса, что удобно для тестов. Kimi хардкодит ограничение самого API прямо в логике.

Отправка запроса в CRM

Один и тот же POST запрос, но три разных подхода к тому, что делать с ошибкой.

Осторожно, много кода
// codex — один вызов, ошибки заворачиваются в типизированные классы
try {
  response = await fetch(CRM_ENDPOINT, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${iamToken}`,
      "Content-Type": "application/json",
      "X-Cloud-Org-ID": config.cloudOrganizationId,
    },
    body: JSON.stringify({
      summary: `Лид с сайта — ${inline(submission.name).slice(0, 100)}`,
      queue: config.queue,
      description: buildDescription(submission),
    }),
    cache: "no-store",
    signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
  });
} catch (error) {
  throw new TrackerRequestError(null, error);
}
if (!response.ok) {
  throw new TrackerRequestError(response.status);
}
// claude — три попытки с бэкоффом и разной трактовкой кодов ответа
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
  const response = await fetch(`${config.apiUrl}/issues/`, { /* … */ });

  if (response.ok) {
    const issue = (await response.json().catch(() => null)) as { key?: string } | null;
    return { ok: true, key: issue?.key ?? null, duplicate: false };
  }

  // Same `unique` already in the queue — the lead is registered.
  if (response.status === 409) {
    return { ok: true, key: null, duplicate: true };
  }

  // 4xx is our fault (bad token, missing queue, malformed payload) and
  // will not fix itself on retry.
  if (response.status < 500 && response.status !== 429) {
    return { ok: false, reason: "rejected" };
  }

  if (attempt < MAX_ATTEMPTS) await sleep(BACKOFF_MS[attempt - 1]);
}
return { ok: false, reason: "unavailable" };
// kimi — один вызов, но с тегами и разбором тела ошибки
const res = await fetch(TRACKER_API_URL, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: config.authorization,
    [config.orgHeader]: config.orgId,
  },
  body: JSON.stringify({
    queue: config.queue,
    summary: `Заявка с сайта: ${lead.name} — ${lead.interest}`,
    description: buildDescription(lead),
    markupType: "md",
    tags: ["site-lead", lead.locale],
  }),
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!res.ok) {
  // Safe to log: the CRM error body never contains our token.
  const body = await res.text().catch(() => "");
  throw new Error(`CRM API responded ${res.status}: ${body.slice(0, 500)}`);
}

Здесь есть один важный момент — Codex единственный защитил свой модуль от импорта на клиент программно: первой строкой файла стоит import "server-only" и любая попытка подтянуть этот код в браузерный компонент не позволит выполнить сборку. У claude code и kimi на том же месте только комментарий в шапке — «SERVER ONLY, never import from client components».

Что мне не понравилось

Пока всё выглядит так, что будто «сделал больше» означает «сделал лучше», но это не так. Codex своей инициативностью надобавлял лишнего.

Он добавил получение облачного IAM-токена, о котором никто не просил. Мало того, что он сделал его единственным способом авторизации, токен для него берется из метадата-сервиса Yandex Cloud, который доступен только изнутри виртуальной машины, который отдаёт временные учётные данные. Если проект деплоится докер-контейнером на обычный сервер, где никакого метадата-сервиса нет, то это решение просто не заработает.

Он также влез в глобальный конфиг тестов, чтобы сделать тесты на модуль, помеченный как server-only, и прописал в vitest.config.ts дополнительную настройку: теперь server-only во всех тестах разрешается в файл-заглушку. Это конечно сработает, но эта подмена действует на весь тестовый прогон целиком. Получается, что программная защита, которую агент сам же и поставил, он же и снял внутри всей тестовой среды. Claude обошёлся без правки конфига именно потому, что у него на этом месте комментарий.

Третий факт уже про харнес codex. Решение по двумя вариантами реализации claude и kimi представили мне на подтверждение «какой вариант интеграции выберем», обосновав рекомендуемые варианты. Codex ничего не спрашивал, просто сам обосновал, принял решение и начал делать. В данном конкретном случае он сделал правильно, но практика показывает, что не всегда рекомендуемые агентами решения являются верными.

Результаты в цифрах

Статистика сессий по результатам выполнения задачи показала следующие цифры:

codex

claude code

kimi

Уровень рассуждений

xhigh

xhigh

всегда включён

Время работы

24 мин

24 мин

63 мин

Ходов модели

12

48

70

Вызовов инструментов

87

67

190

Запусков тестов и линтера

16

8

25

Выходных токенов

39 289

54 491

77 041

Прочитано из кэша

13.2M

7.06M

4.94M

Стоимость

$8.76

$5.43

$3.33

Kimi сгенерировал вдвое больше токенов (это связано с тем, что у него нет как таковой настройки уровня рассуждений, то возможность включить или отключить ризонинг), чем codex, но при этом обошёлся в два с половиной раза дешевле из-за цены токенов.

Строка про вызовы инструментов тоже требует отдельного пояснения. У kimi 94 вызова из 190 — это вызовы внутри субагентов: он создает отдельные экземпляры для ресерча, и работают они в своих контекстах. Т.е. это не говорит о том, что он задействует больше инструментов, а просто о другой архитектуре, заложенной в харнес cli агента.

Что хотелось бы еще отметить: был запущет только один прогон. Одна задача, один стек и один запрос агенту, без каких-либо уточнений. Значение параметра «Ходов модели» нельзя просто сравнивать без контекста. В журналах трёх агентов это структурно разные сущности, и числа 12, 48 и 70 говорят лишь о подробности логов, а не о поведении.

Итоги

Все три агента решили реальную задачу в живом проекте с первого раза. Еще год-два назад это было не так. Я раньше много экспериментировал с qwen code, но в итоге отказался от него. Руками писать код получалось быстрее, чем объяснять агенту что я от него хочу. Теперь фокус смещается с вопроса «справится ли?» на «что именно напишет и сколько это будет стоить?».

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

Я в итоге продолжаю использовать claude code и не потому что он победил, а потому что его подход ближе к моему: покрытие тестами, согласование подходов к реализации, стиль кода.

А главный вывод, который из этого следует: уже достаточно много сильных агентов, которые умеют писать код хорошо. И все начинает упираться в качественную постановку задачи, а также наличие инструкций (скиллов) для агентов, которые говорят как им нужно писать код в данном конкретном проекте.

Автор: master1985

Источник

Оставить комментарий