Коли ми починали merged, перша версія "оцінки PR-а" виглядала так: беремо diff, кидаємо в Claude, просимо поставити оцінку від 1 до 10 з поясненням. Працювало. Поки ми не перевірили.
Узгодженість з людським ревʼюером — 41%. Модель ставила різні оцінки одному й тому ж PR-у в сусідніх запитах. Найгірше: вона впевнено пояснювала свої оцінки навіть тоді, коли вони були випадковими.
Сьогодні наш LLM-суддя дає 87% узгодженості з сеньйором. Без перенавчання моделі. Усе — в промпті і рубриці.
Рубрика — це схема, а не чек-лист
Перша помилка — думати про рубрику як про список критеріїв. Це спокусливо, бо так воно виглядає на папері. Але для моделі критично, як саме декомпозовані критерії і які дані вона може повернути.
Ось як виглядає один критерій у нашій рубриці (фрагмент):
id: test_coverage_behavior
weight: 0.15
question: |
Чи тести перевіряють поведінку, а не реалізацію?
Сигнали "так": перевірка зовнішнього контракту, edge-кейси,
regression-тест на баг. Сигнали "ні": перевірка приватних
методів, mock-верифікація замість асерту на результат.
output_schema:
score: { type: int, min: 0, max: 4 }
evidence: { type: string, min_words: 15 }
counter_evidence: { type: string, required: true, min_words: 10 }
confidence: { type: enum, values: [low, medium, high] }
Критичне — counter_evidence. Ми вимагаємо від моделі написати, що саме свідчить проти її ж оцінки. Цей єдиний field дав нам +14 процентних пунктів узгодженості.
Чому contrastive reasoning працює
Коли ми просимо LLM "поясни оцінку 4", вона пояснює оцінку 4. Коли ми просимо "поясни, чому оцінка не 5, але й не 3" — вона змушена зважувати. Модель не може одночасно бути впевненою і писати contrastive evidence у протилежних напрямках — це викликає внутрішній dissonance на етапі генерації.
Це не magic prompting. Це робить структура задачі для моделі іншою: вона перетворюється з задачі "оціни" на задачу "класифікуй у межах шкали з обґрунтуванням границь".
Архітектура LLM-судді
Наш pipeline виглядає так:
┌──────────────┐
PR diff → │ normalizer │ → canonical form
└──────────────┘
│
┌──────▼───────┐
│ per-criterion│ × N criteria (parallel)
│ LLM call │
└──────┬───────┘
│
┌──────▼───────┐
│ aggregator │ → final score + detailed report
└──────────────┘
Normalizer
Перший виклик переводить diff у канонічну форму: нормалізує імпорти, прибирає trailing whitespace, згортає дуже довгі рядки, витягує метадані комітів. Це прибирає близько 30% варіативності відповідей моделі на однаковому семантичному контенті.
Per-criterion calls
Замість одного виклику "оціни PR" ми робимо N паралельних викликів — по одному на критерій. Кожен виклик бачить тільки релевантний контекст для свого критерію. Оцінка commit_message_quality не бачить коду — тільки git log. Оцінка test_coverage не бачить commit-повідомлень — тільки тести і код, який вони покривають.
Це різко знижує inter-criteria leakage: коли модель ставить 4/5 за коміти і потім "докручує" оцінку тестів, бо "ну він же старався".
Aggregator
Окремий виклик бачить усі per-criterion результати і не має права змінювати оцінки — тільки агрегує по вагах і пише підсумковий наратив. Це архітектурне обмеження.
Де ми системно помиляємось
За 14 місяців продакшну зібрали каталог регресій:
- Culture/style bias. Модель вище оцінює код у стилі, на якому вона більше навчалась. Функціональний TS з lodash — так. Імперативний Go з явними for-циклами — нижче, хоча це часто ідіоматичніше.
- Verbose commits advantage. Коміти з повними реченнями отримують вищий
commit_message_quality, навіть колиfix: handle UA VAT edge case— точніше за абзац маркетингового тексту. - Refactor-blindness. Pure refactor PR-и (переіменування, витягання функцій) отримують нижчу оцінку за "фокус диффу", бо таких змін багато за обʼємом. Ми додали
refactor_intentяк окрему гілку рубрики.
Для кожного з цих багів — калібрувальний сет на ~200 PR-ів, розмічений двома сеньйорами, і периодичний regression-прогін.
Що саме комерційно нетривіальне
Це не RAG. Це не fine-tune. Це не agent framework з 14 tools. Це одна ключова ідея: якщо ти хочеш від LLM надійне судження — роби його схемою, а не текстом. Дай моделі структурований output. Вимагай contrastive reasoning. Сегментуй контекст. І ніколи не вір першій цифрі, яку вона поверне.
У наступній статті — як ми калібруємо задачі під різні рівні (middle / senior / staff) і чому задача для сеньйора не повинна бути складнішою, а має бути іншою за природою.