Ревью пул-реквестов давно стало узким местом. Сеньоры часами листают диффы, где 90% составляет бойлерплейт, пропускают нужные 10% и всё равно мерджат тонкие баги. В 2026 году вручную это делать незачем. Claude Sonnet 4.6 остаётся сильнейшей из публично доступных моделей для кода, а в паре с GitHub API она превращается в штатного ревьюера: он работает на каждом PR, стоит копейки и не устаёт.
Ниже собираем бота для ревью PR на Claude Sonnet 4.6 через codexkey (https://api.codexkey.ru/v1). Текущие клиентские цены и доступность канала смотрите в каталоге. Бот должен переживать лимиты провайдера и временные ошибки.
Почему для код-ревью выбираем Claude
Причин три, расположены по важности:
- Умение работать с кодом. Sonnet 4.6 держится в верхней части SWE-bench Verified и читает диффы как сеньор: следит за типами между файлами, замечает пропущенную обработку ошибок, ловит off-by-one, мимо которых проходит линтер. GPT-5 и Gemini 3 недалеко, но в ревью, где ценятся осторожность и обоснование, у Claude лучше соотношение сигнала и шума.
- Предсказуемый структурированный вывод. В
tool_useClaude следует JSON-схемам почти на 100%. Для бота это критично: ответ должен разбираться без сбоев каждый раз. Один битый JSON на тысячу запусков, и вас будят среди ночи. - Длинный контекст и кэширование промптов. В 200K токенов помещаются стайл-гайд репозитория, архитектурные заметки и свежие коммиты, и всё это уходит в системный промпт. После первого вызова кэш делает такой контекст почти бесплатным. Экономику кэша мы разобрали в обзоре цен Claude API.
С хостед-сервисами вроде CodeRabbit и Greptile расклад такой. Они неплохие, но вы отдаёте им исходный код, платите $30-$100 за разработчика в месяц и принимаете чужой промпт. Свой бот обходится в $0.05-$0.30 за PR, работает на вашем промпте, а код не покидает ваш CI.
Архитектура
Весь пайплайн состоит из четырёх блоков:
PR открыт/обновлён в GitHub
↓
Срабатывает GitHub Action
↓
Тянем дифф через GitHub API
↓
Шлём в Claude Sonnet 4.6 (через codexkey)
↓
Парсим JSON-ответ
↓
Постим комменты ревью через GitHub API
И всё. Ни векторной базы, ни эмбеддингов, ни агентного цикла. Дифф небольшой (обычно <50K токенов), Claude читает его один раз и возвращает структурированные комментарии. Любое усложнение остаётся оверинжинирингом, пока данные не докажут обратное.
Промпт
Промпт составляет 80% работы. Ниже шаблон, который у нас сработал на кодовых базах на Go, TypeScript и Python:
Ты — senior staff engineer, ревьюишь pull request. Твоя задача — ловить
баги, проблемы безопасности и ошибки дизайна, а НЕ следить за стилем
(для этого есть линтеры) и НЕ хвалить хороший код.
SCOPE:
- Комментируй только строки, изменённые в этом диффе.
- Не комментируй неизменённые контекстные строки.
- Не требуй изменений, которые не можешь обосновать конкретным сценарием
отказа.
СЕРЬЁЗНОСТЬ:
- blocker: приведёт к продакшн-инциденту, потере данных, бреши в
безопасности или очевидному багу корректности. Чинить до мерджа.
- warning: реальная проблема, но не блокер. Гонки под маловероятной
нагрузкой, отсутствующие error-пути, плохое именование, которое
выстрелит позже.
- nit: мелкая придирка. Используй экономно — максимум 2 на PR.
ВЫВОД:
Вызови инструмент `post_review` со списком комментариев в JSON. Каждый
комментарий должен содержать:
- file: путь относительно корня репо
- line: номер строки в новом файле (после изменения)
- severity: "blocker" | "warning" | "nit"
- comment: 1–3 предложения. Сначала проблема, затем как чинить.
- justification: почему это важно. Если не можешь это сформулировать —
выкини коммент.
Если PR выглядит нормально — верни пустой список. Не выдумывай проблемы.
КОНТЕКСТ РЕПО:
{кэшированный системный блок: стайл-гайд, заметки по архитектуре,
недавние решения}
ДИФФ:
{собственно дифф}
Работает он благодаря двум неочевидным вещам.
Поле justification защищает от ложных срабатываний. Дайте волю, и Claude (как любая другая модель) начнёт выдумывать проблемы. Обязательное письменное обоснование снижает шум примерно на 60% в нашем внутреннем эвале: модель сама отбрасывает слабые комментарии, не успев их выдать.
tool_use вместо просьбы «ответь JSON-ом». Если описать post_review как инструмент со строгой схемой входа, кривой вывод отбраковывает сам API. Функцию починки JSON писать не придётся.
Описание инструмента
const postReviewTool = {
name: "post_review",
description: "Опубликовать комменты ревью на PR. Передай пустой массив, если проблем нет.",
input_schema: {
type: "object",
properties: {
comments: {
type: "array",
items: {
type: "object",
properties: {
file: { type: "string" },
line: { type: "integer" },
severity: {
type: "string",
enum: ["blocker", "warning", "nit"]
},
comment: { type: "string" },
justification: { type: "string" }
},
required: ["file", "line", "severity", "comment", "justification"]
}
}
},
required: ["comments"]
}
};
Чтобы модель обязательно вызвала инструмент, задайте tool_choice: { type: "tool", name: "post_review" }. Текстового ответа не будет, лишнего тоже.
Бот на TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { Octokit } from "@octokit/rest";
const claude = new Anthropic({
apiKey: process.env.CODEXKEY_API_KEY,
baseURL: "https://api.codexkey.ru/v1",
});
const gh = new Octokit({ auth: process.env.GITHUB_TOKEN });
async function reviewPR(owner: string, repo: string, pull_number: number) {
// 1. Тянем дифф
const { data: files } = await gh.pulls.listFiles({
owner, repo, pull_number, per_page: 100,
});
// 2. Бьём по файлам — параллелим для больших PR
const reviews = await Promise.all(
files
.filter(f => f.patch && f.status !== "removed")
.map(file => reviewFile(file))
);
// 3. Сплющиваем + дедуп по (file, line, severity)
const all = reviews.flat();
const seen = new Set<string>();
const unique = all.filter(c => {
const k = `${c.file}:${c.line}:${c.severity}`;
if (seen.has(k)) return false;
seen.add(k);
return true;
});
// 4. Постим одним ревью
await gh.pulls.createReview({
owner, repo, pull_number,
event: unique.some(c => c.severity === "blocker")
? "REQUEST_CHANGES"
: "COMMENT",
comments: unique.map(c => ({
path: c.file,
line: c.line,
body: `**[${c.severity}]** ${c.comment}\n\n_${c.justification}_`,
})),
});
}
async function reviewFile(file: any) {
const response = await claude.messages.create({
model: "claude-sonnet-5",
max_tokens: 4096,
system: [
{
type: "text",
text: REPO_CONTEXT, // стайл-гайд, архитектура
cache_control: { type: "ephemeral" },
},
{
type: "text",
text: REVIEW_INSTRUCTIONS, // промпт выше
},
],
tools: [postReviewTool],
tool_choice: { type: "tool", name: "post_review" },
messages: [
{
role: "user",
content: `Файл: ${file.filename}\n\nДифф:\n\`\`\`diff\n${file.patch}\n\`\`\``,
},
],
});
const toolUse = response.content.find(b => b.type === "tool_use");
return toolUse ? (toolUse.input as any).comments : [];
}
Обратите внимание на cache_control у блока с контекстом репозитория. Это и есть кэширование промптов: первый PR оплачивает эти токены полностью, а каждый следующий в пределах 5 минут платит 10%. На активном репозитории стоимость падает на 70% и больше.
Большие диффы
PR на 5000 строк разнесёт токенный бюджет, если отправить его одним куском. Стратегия выше, один вызов Claude на файл, снимает проблему сама собой:
- Вызовы по файлам идут параллельно. Время остаётся около 3-5 секунд даже для PR из 30 файлов.
- У каждого вызова свой узкий контекст. Claude смотрит
auth.ts, иpackage-lock.jsonему не мешает. - Дубли убираются между вызовами. Иногда Claude отмечает один и тот же паттерн в двух файлах, а ключ
(file, line, severity)их склеивает.
Если в PR больше 100 файлов, добавьте префильтр, который отбрасывает lock-файлы, сгенерированный код и vendor/. После него большинство таких PR сжимается до 10 файлов, которые стоит ревьюить.
Настройка GitHub Actions
name: Claude Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npx tsx scripts/review.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CODEXKEY_API_KEY: ${{ secrets.CODEXKEY_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
Положите ключ codexkey в секреты репозитория под именем CODEXKEY_API_KEY, как в YAML, закоммитьте файл, и бот заработает.
Сколько стоит один PR
Реальные цифры с нашего деплоя на 200 PR:
| Размер PR | Токены на входе | Токены на выходе | Стоимость (с кэшем) |
|---|---|---|---|
| Маленький (<10 файлов) | ~8K | ~500 | $0.04 |
| Средний (10-30) | ~25K | ~1.5K | $0.12 |
| Большой (30-80) | ~70K | ~3K | $0.28 |
Цифры получены на ценах Claude Sonnet 4.6 в codexkey при включённом кэшировании. Команда из 50 разработчиков при 100 PR в день тратит примерно $300 в месяц. Это меньше одной лицензии CodeRabbit Pro.
Свой бот или готовый сервис вроде CodeRabbit
| Параметр | Свой бот (Claude + codexkey) | CodeRabbit / Greptile |
|---|---|---|
| Стоимость | $0.05-$0.30 за PR | $30-$100 за разработчика в месяц |
| Контроль над промптом | Полный | Никакого |
| Где хранятся данные | В вашем CI | На их серверах |
| Кастомизация | Всё, что удаётся описать промптом | Набор их функций |
| Время на запуск | Один день | Один час |
| Поддержка | Ваша | Их |
Размен настоящий. SaaS быстрее подключить, и поддерживать его не нужно. Свой бот даёт контроль, на масштабе выходит дешевле в пересчёте на PR и позволяет вложить в ревью вкус вашей команды.
Итог
Бота можно выкатить за день. Все составляющие (Claude Sonnet 4.6 через codexkey, GitHub Actions, Octokit SDK) стабильны, хорошо описаны и недороги. Сложная часть одна, промпт, и шаблон выше закрывает 90% того, что нужно.
Начните с простого: один промпт, по файлу за раз, метки blocker, warning и nit. Прогоните бота по последним 20 замерженным PR и прочитайте результат. Промпт придётся поправить пару раз, и к концу недели у вас будет инструмент лучше большинства сервисов за $50 в месяц.