диагностика

Решение проблем

Чаще всего «бот молчит» — это один из четырёх разрывов в цепочке GitLab → вебхук → бот → LLM. Запустите скрипт диагностики — он найдёт, какой именно, — или пройдите проверки ниже руками. Строки, которые бот пишет в MR/PR, приведены здесь в русском варианте: он включается ключом language: ru в .reviewgate/config.yml, по умолчанию бот отвечает по-английски.

Скрипт диагностики

diagnose.sh запускается на хосте рядом с docker-compose и .env бота и за пару секунд проверяет всю цепочку: переменные окружения, здоровье бота, токен GitLab, приём вебхука, Redis, ключ LLM и прокси. Заодно печатает версию вашего бота — её просят приложить к обращению в поддержку.

скачать и запустить
curl -O https://reviewgate.dev/diagnose.ru.sh
less diagnose.ru.sh     # read it — it is short and holds no surprises
bash diagnose.ru.sh
что он делаетТолько читает .env (построчно, не выполняя его) и шлёт безвредные запросы: GET к вашему GitLab и боту, «пинг» вебхука, который бот игнорирует (ревью не запускается), и GET /v1/models к LLM — это не тратит токены. Дифф и код никуда не уходят.

Параметры под нестандартный контур:

терминал
# the bot is behind a reverse proxy, or the port is not on localhost:
bash diagnose.ru.sh --bot-url https://bot.acme.ru
# and check that the webhook is configured in the project:
bash diagnose.ru.sh --project 42

Ревью не появилось в MR/PR

Пять проверок по порядку цепочки — или просто запустите diagnose.sh:

  1. Бот жив.curl http://localhost:3000/api/health должен вернуть status:ok. Если нет — см. раздел «Бот не отвечает» ниже.
  2. Вебхук доставляется.GitLab → проект → Settings → Webhooks → ваш хук → Recent events. Видно код ответа последней доставки: 202 — бот принял; 401 — не совпал секрет; ошибка соединения — GitLab не достучался.
  3. GitLab достучится до бота.Для бота во внутренней сети нужно разрешение — см. «Локальная сеть» ниже.
  4. Секрет совпадает.GITLAB_WEBHOOK_SECRET ↔ поле Secret token в вебхуке — см. «Вебхук отклонён» ниже.
  5. LLM отвечает.Ключ валиден и, если вашей сети нужен прокси, он работает — см. «Ревью пустое» ниже.
202 — не всегда «ревью пошло»Бот отвечает 202 и когда событие принято, и когда оно штатно пропущено: правка описания или лейбла, аппрув, мерж, системная заметка — новых коммитов там нет. Причина всегда пишется в лог строкой ⏭ Webhook skipped, а в теле ответа возвращается queued: false. Если же GitLab показывает в Recent events код 500 — ищите в логе записи уровня error: такие доставки GitLab не повторяет, и событие теряется.
Повторное событие на ту же вершину — тоже пропускЕсли этот коммит уже отревьюен, бот не проверяет его заново: находки были бы теми же, а прогон оплачивается вашим ключом. В логе — строка ⏭ Skipped: head … is already reviewed. Чтобы получить новое ревью, нужен новый коммит или смена целевой ветки. Прогон, который не дошёл до конца, вершину отревьюенной не помечает — повтор после сбоя проходит как обычно.

GitLab не достучится до бота (локальная сеть)

Причина №1, и её легко пропустить: self-hosted GitLab по умолчанию запрещает вебхуки во внутреннюю сеть. Доставки просто не происходит — в Recent events ошибка соединения или пусто.

разрешите исходящие в локальную сетьAdmin → Settings → Network → Outbound requests → «Allow requests to the local network from webhooks». Подробнее о настройке вебхука — в разделе «Подключение к GitLab».

Бот не отвечает или не стартует

Проверьте здоровье и логи:

терминал
curl http://localhost:3000/api/health
# {"status":"ok","ts":...,"version":"0.1.x"}
терминал
docker compose logs --tail=50 app

Если /api/health молчит — контейнер не поднялся либо порт не проброшен наружу. Когда бот стоит за reverse-proxy, у diagnose.sh укажите --bot-url с его адресом.

«Некорректная конфигурация окружения»

Бот проверяет переменные окружения на старте и падает сразу с указанием, чего не хватает. Безусловно обязателен только REDIS_HOST. Дальше нужен хотя бы один настроенный вендор: GitLab — GITLAB_BASE_URL + GITLAB_TOKEN + GITLAB_WEBHOOK_SECRET, GitHub — GITHUB_WEBHOOK_SECRET + (GITHUB_APP_ID и GITHUB_APP_PRIVATE_KEY_B64 либо GITHUB_TOKEN); у обоих секретов вебхука минимум 8 символов. Группа вендора становится обязательной целиком, как только задана хотя бы одна её переменная: одинокий GITLAB_TIMEOUT_MS из шаблона потребует и остальных трёх GitLab-переменных. Полный справочник — «Установка и запуск».

Образ не скачивается

Канонический адрес образа — registry.reviewgate.dev/reviewgate-bot (наш реестр, pull анонимный). Если он недоступен — тот же образ бит-в-бит лежит на зеркале Docker Hub: замените в docker-compose.yml строку image: на novohudonossor/reviewgate-bot:<тот же тег>.

Если недоступен и Docker Hub — он уже закрывал доступ целым регионам, и гарантий, что это не повторится, нет, — подключите pull-through-зеркало. Оно проксирует только образы Docker Hub, поэтому работает в паре с предыдущим шагом: в image: должно стоять имя с Docker Hub (novohudonossor/…), его менять уже не нужно:

pull-through-зеркало Docker Hub
# /etc/docker/daemon.json (create it if the file is missing;
# in an existing one, ADD the key without wiping the other settings)
{
  "registry-mirrors": ["https://dh-mirror.gitverse.ru"]
}
# then restart Docker (it restarts every container on the host —
# pick a quiet moment):
sudo systemctl restart docker

GitLab недоступен или токен не принят

diagnose.sh шаг 3 бьёт GET /api/v4/user. Ошибка соединения — неверный GITLAB_BASE_URL (он должен быть без /api/v4 и хвостового слэша) или сеть. Код 401/403 — проблема с токеном.

Права токена

GITLAB_TOKEN — Project или Personal Access Token со scope api, роль не ниже Developer (чтобы оставлять discussion-комментарии). Протухший или урезанный токен даёт 401/403.

Вебхук отклонён: 401

В Recent events у доставки код 401 — бот получил вебхук, но секрет не совпал. Сверьте поле Secret token в настройке вебхука со значением GITLAB_WEBHOOK_SECRET в .env.

частая причина.env поправили, а контейнер не перезапустили — бот держит старый секрет. Перезапустите: docker compose up -d app.

Вебхук отклонён: 413 или 400

В Recent events у доставки код 413 — тело запроса больше лимита бота, чаще всего это событие комментария, куда вставили большой отчёт или лог. Код 400 в той же графе значит, что тело не удалось прочитать: битый JSON или оборванное соединение.

В обоих случаях событие потеряно — вендор такие доставки сам не повторяет, и по ним не будет ни ревью, ни ответа в треде. В логе бота отказ виден отдельной строкой Тело запроса отклонено с типом события и заявленным размером — по ней вы найдёте доставку в списке недавних и при необходимости отправите её заново: у GitLab это кнопка Resend, у GitHub — Redeliver.

какой лимит у бота32 МБ на тело запроса. Вебхуки GitLab столько не весят даже с очень длинными комментариями, поэтому 413 обычно означает старую версию образа — обновитесь до актуальной.

GitHub: бот молчит, а доставки вебхука успешны

В App: Advanced → Recent Deliveries видно 202, но в PR ничего не появляется — почти всегда дело в наборе событий, а не в боте.

  • Нужного события нет в подписке. Вопрос под сводкой приходит событием Issue comment, и в списке событий App оно появляется только после выдачи права Issues (read). Ответы в тредах замечаний — событие Pull request review comment.
  • Права выданы, но не приняты инсталляцией. Галочка меняет права только у самого App; каждая инсталляция принимает их отдельно (Configure → «Accept new permissions»). До принятия GitHub шлёт старый набор событий, и это выглядит как молчание без ошибок.
  • Ответы вообще выключены. Reply mode по умолчанию выключен — включается reply.enabled в .reviewgate/config.yml (или REPLY_ENABLED в окружении бота). В логе бота это видно прямой строкой «Ответ пропущен: reply mode выключен».
  • Под сводкой бот отвечает только на упоминание своей учётки: лента PR у GitHub не тредирована, и без этого правила бот вклинивался бы в разговоры людей.

Быстрая сверка фактического состояния: GET /app (что у App) против GET /repos/{owner}/{repo}/installation (что реально действует на инсталляции) — расхождение permissions/events и есть ответ.

Redis недоступен

Очередь обязательна: вебхук только кладёт задачу, всю работу делает воркер. Зелёный /api/health ничего не говорит о Redis — ответ собирается, не заглядывая в очередь, а клиент Redis подключается лениво, поэтому бот поднимается и отвечает status:ok при лежащем Redis. Проверяйте сам Redis: docker compose ps redis — и лог бота, куда выписывается ошибка соединения с REDIS_HOST/REDIS_PORT.

В логе: «Джоба … признана зависшей»

Очередь считает задачу зависшей, когда до Redis перестаёт доходить продление блокировки — обычно из-за просадки самого Redis или перегруженного воркера, а не из-за длительности ревью (её продление переживает). Задача выдаётся повторно, но второй платный прогон почти всегда не случается: дубль ждёт, пока закончится первый (до 10 минут), и затем видит, что вершина уже отревьюена — в логе строка ⏭ Пропуск, в метриках статус repeat_head. Задвоиться прогон может, если первый идёт дольше отсрочек: тогда рядом в логе будет запись об исчерпанных отсрочках. Единичные записи можно игнорировать; регулярные — повод посмотреть доступность Redis и REVIEW_CONCURRENCY.

Ревью пустое или комментарий про ошибку ключа

Бот публикует находки, но если LLM недоступна — в сводку попадёт уведомление о причине (BYOK: бот не угадывает за вас). diagnose.sh шаг 6 проверяет ключ через GET /v1/models, не тратя токены: 401/403 — ключ неверный или нет кредитов; ошибка соединения — см. «Прокси к LLM» ниже.

Прокси к LLM

Если ваша сеть не достаёт до провайдера напрямую, нужен LLM_PROXY (слепой CONNECT-туннель; TLS до провайдера остаётся end-to-end, прокси видит только хост, не код). Если ключ задан, а соединения нет — проверьте, что прокси жив и креды верны. Локальной LLM (Ollama/vLLM) прокси не нужен — как и провайдеру, до которого сеть и так достаёт; подробнее в разделе «LLM и свой ключ». Для OpenAI-совместимых провайдеров (DeepSeek, OpenRouter, Ollama, vLLM — не Anthropic: там соединение повторяет сам SDK) стрим, который провайдер оборвал на середине ответа (в логе — stream interrupted … retrying 1/1), повторяется один раз сам, прежде чем считаться сетевой ошибкой; повтор виден и в диагностике прогона, и в usage.calls[].retries отчёта — расход оборванной попытки в токенах не учтён.

Ревью не выполнено: контекст не влез в окно модели

Бот пишет в MR/PR, что контекст не помещается в окно модели, а в логах — prompt is too long. Обычно причина в том, что в изменения попал очень большой или сгенерированный код: у него объём в разы больше рукописного, а ревьюить его незачем. Чаще всего это всплывает при включённом llm.full_file_context — бот берёт изменённые файлы целиком.

Что помогает, по порядку:

  • добавить генерируемое в ignore в config.yml — например **/*.g.dart, **/*.freezed.dart (Flutter), **/*.generated.ts, снапшоты, бандлы, вендорные каталоги;
  • выключить llm.full_file_context — тогда бот смотрит только дифф;
  • разбить MR/PR на части поменьше;
  • на модели с небольшим окном (обычно локальной LLM) — уменьшить LLM_DIFF_CHAR_BUDGET и LLM_MAX_TOKENS: в лимит окна входит и запрошенный размер ответа.

Ретраев на этой ошибке нет: результат не изменился бы — нужна правка конфига. Если включён блок 🔬 диагностики, то в следующем успешном ревью строка «Контекст модели» покажет, сколько файлов не влезло в бюджет.

Ревью не выполнено: ответ не уместился в лимит выхода

Зеркальная ситуация: в окно модели всё влезло, но ответ модели упёрся в потолок LLM_MAX_TOKENS (в логах — truncated by max_tokens или finish_reason=length). Обычно так бывает на очень больших MR и PR: у reasoning-моделей внутренние рассуждения расходуют тот же лимит, что и сам ответ, плюс объём находок растёт вместе с диффом.

Бот сначала лечит это сам: на моделях Anthropic повторяет запрос с сокращёнными рассуждениями (на OpenAI-совместимых повтор не делается — при temperature: 0 он обрезался бы точно так же). Если не помогло — пишет уведомление в MR/PR и не тратит деньги на слепые ретраи: каждый повтор оплачивал бы весь вход заново. Что помогает:

  • поднять LLM_MAX_TOKENS в окружении бота — в пределах потолка выхода вашей модели: он сильно различается между провайдерами (от единиц до десятков тысяч токенов), смотрите документацию модели. Если поставить больше потолка — провайдер начнёт отклонять запросы, и бот напишет об этом отдельным уведомлением;
  • разбить MR/PR на части поменьше — большие диффы и ревьюируются хуже;
  • исключить сгенерированные файлы через ignore в config.yml — меньше находок, короче ответ.

Ревью не выполнено: провайдер перегружен или лимитирует

В логах — overloaded_error (у Anthropic это HTTP 529), ответ 503/529 с «overloaded» у OpenAI-совместимых либо затяжной 429 (rate limit — исчерпан лимит запросов или токенов вашего тарифа у вендора). Это состояние на стороне провайдера: ключ, сеть и конфигурация обычно ни при чём.

Бот лечит это сам: повторяет ревью с нарастающими паузами (несколько попыток, окно около 7 минут) — обычно перегрузка проходит за это время и ревью публикуется как обычно. Если не прошла, бот пишет уведомление в MR/PR и запустится заново на следующем пуше.

Когда это повторяется часто на потоке — посмотрите статус-страницу провайдера и лимиты тарифа; помогает выбрать менее загруженную модель того же вендора (первая роль llm.generators в config.yml) либо сменить вендора основного ревью — backend роли или LLM_PROVIDER/LLM_MODEL в окружении бота. Дополнительные роли (вторые generators, llm.judges, llm.arbiter) здесь не спасут: они добавляют вызовы поверх основного генератора, а не подменяют его, — как и именованные бэкенды LLM_BACKEND_<ИМЯ>_*, которые работают только для этих дополнительных ролей.

Ревью не выполнено: endpoint или модель не найдены

В MR/PR — уведомление «LLM-провайдер ответил, но по указанному адресу ничего нет», в логах — 404 либо model_not_found. Сеть тут исправна: запрос дошёл, ответа по этому адресу просто не существует. Две обычные причины — LLM_BASE_URL без /v1 у OpenAI-совместимого сервера (адрес должен указывать на API, а не на корень сайта) и опечатка в LLM_MODEL либо имя модели, которого у вендора нет.

Повторов здесь не будет намеренно: с той же настройкой ответ не изменится, и пять попыток только сожгли бы время. Поправьте адрес или имя модели — следующий пуш запустит ревью. Проверить настройку до коммита можно командой reviewgate doctor: она опрашивает endpoint живьём, не тратя токенов.

Ревью остановлено по бюджету (🛑 в сводке)

Задан потолок трат (LLM_BUDGET_TOKENS / LLM_BACKEND_<ИМЯ>_BUDGET_TOKENS), и расход какого-то бэкенда пробил тройной лимит — прогон остановлен на ближайшем чекпойнте, найденное к этому моменту опубликовано (находки могут быть не подтверждены судьёй — это сказано в сводке). Барьеры фиксированные и не настраиваются: 80% — нотис, 100% — громкий нотис (ревью выполняется целиком), 300% — стоп. Такое превышение — почти всегда не «дорогое ревью», а аномалия: гигантский сгенерированный дифф, зацикленный агент, карусель ретраев. Что делать: посмотрите блок 🔬 диагностики (строка «Бюджет прогона (токены)»), проверьте состав диффа и раскладку ролей; если расход ожидаем — поднимите лимит в env инсталляции. Для автоматизации срез машиночитаем: в JSON-отчёте CLI и ответе MCP — поле budget (барьер, расход и лимит по бэкендам), в метриках инсталляции — budget_barrier и tokens_by_backend для любой поверхности, включая прогоны CLI.

Ключ или токен скопирован из мессенджера

Симптом бывает разный — бот не стартует, reviewgate doctor ругается на сервер ревью, прогон падает, — а причина одна: в значении есть символ вне ASCII. Кириллические с, е, р и «умные» кавычки визуально неотличимы от латиницы, а в HTTP-заголовок они не проходят.

Мы называем такое прямо. Для ключей LLM в сообщении указаны позиция символа, сам символ и его код (эти сообщения печатаются по-английски), для токенов вендоров — позиция символа. Лечение — выпустить ключ или токен заново и скопировать целиком, без выделения мышью в переписке. Проверка стоит до первого платного вызова, поэтому дефект ловится стартом бота и reviewgate doctor, а не в середине ревью.

Родственный случай — токен, разорванный при копировании на две строки. Запрос с таким значением уходит нормально и получает от GitLab обычное «доступа нет», из-за чего люди идут к администратору за правами, хотя дело в лишнем пробеле. Такое мы тоже называем словами. Лишние пробелы и перевод строки по краям значения снимаются молча — это шум копирования, а не часть токена.

Логи

Смотреть логи бота — docker compose logs app; каждая строка ревью помечена тегом джобы. Централизованный сбор (Graylog / ELK / Loki), структурный LOG_FORMAT=json и рецепт GELF-драйвера вынесены на отдельную страницу «Логи и мониторинг».

Сводка: симптом → причина → что делать

симптомпричинарешение
Бот не реагирует на MR/PR, в Recent events пустовебхук не настроен или не доставляетсяпройдите проверки выше; diagnose.sh --project N
GitHub: доставки 202, но бот молчитсобытие не подписано либо права App не приняты инсталляциейраздел «GitHub: бот молчит…» выше
Recent events: ошибка соединения / timeoutGitLab не достучится во внутреннюю сетьвключите Outbound requests (раздел «Локальная сеть»)
В MR/PR: «endpoint или модель не найдены»LLM_BASE_URL без /v1 либо неверное имя моделипоправьте адрес/модель; проверка — reviewgate doctor
Recent events: 401секрет вебхука не совпалсверьте Secret token ↔ .env, перезапустите бот
Recent events: 413 или 400тело события не прочитано — оно потерянообновите образ; повторите доставку (Resend в GitLab, Redeliver в GitHub)
docker compose pull падает или виситреестр недоступензеркало Docker Hub (раздел «Образ не скачивается»)
Бот не стартует, в логах «Некорректная конфигурация»нет обязательной переменнойзаполните .env (раздел «Конфигурация»)
/api/health не отвечаетконтейнер не поднялся или порт не проброшенсмотрите логи; задайте --bot-url
Ревью пустое или коммент про ошибку ключаключ LLM невалиден или нет кредитовпроверьте ключ LLM и баланс
В логах таймаут к провайдерунет рабочего проксизадайте LLM_PROXY
«Контекст не помещается», в логах prompt is too longв изменения попал большой или сгенерированный кодignore для генерируемого; см. раздел выше
«Ответ не уместился в лимит выхода», в логах truncated by max_tokensрассуждения модели + объём находок больше LLM_MAX_TOKENSподнять LLM_MAX_TOKENS; см. раздел выше
«Провайдер перегружен», в логах overloaded_errorвременная нагрузка на стороне LLM-провайдераобычно проходит само; см. раздел выше
В сводке «Community (ключ привязан к другой инсталляции)»ключ выдан на другую инсталляцию — привязка не совпала ни с одним настроенным вендоромпроверить лицензию