Команда бекенду в SaaS-компанії має 180 000 рядків коду, 40 runbook-ів у Confluence та автоматично згенеровану з Swagger документацію API. Новий програміст перший тиждень проводить переважно на питаннях на кшталт: «де обробляється ця помилка?», «як працює авторизація для ендпоінту X?» та «що робить цей прапорець?». Старші колеги знають. Проблема в тому, що ці знання ніде не записані у формі, з якої можна здійснити пошук.
Ми в Cashcrown досліджуємо, як RAG над репозиторіями та технічною документацією працює в практичних впровадженнях. Нижче стаття описує відмінності від RAG над прозою, підхід до символічного чанкінгу, роль гібридного пошуку та межі того, що вимагає рішення програміста.
Чому код і технічна документація — це інший виклик, ніж проза
Документи HR, процедури чи FAQ мають лінійну структуру. Сплітер на основі символів або токенів дає осмислені фрагменти, бо речення, вирване з контексту, все ще залишається реченням.
Код має символічну структуру. Функція calculate_discount(order_id, user_tier) є семантичною одиницею. Перервеш її визначення посередині — отримаєш фрагмент без сигнатури або фрагмент без тіла, кожен з яких окремо некорисний. Те саме стосується класів, методів, блоків try/except і декораторів.
Технічна документація є гібридом: runbook містить список кроків, що посилаються на конкретні команди та змінні середовища, вікі API описує ендпоінти з точними типами параметрів. Пошук за ключовими словами працює тут значно краще, ніж зі вільною прозою. До того ж додається мінливість: RAG, що індексує раз на місяць, цитує сигнатури, які перестали існувати два тижні тому.
Чанкінг за символами, а не за абзацами
Хороша стратегія чанкінгу коду базується на межах символів, а не токенів. Парсер AST (Abstract Syntax Tree) виокремлює функції, класи та методи як замкнені одиниці. Бібліотеки на кшталт tree-sitter підтримують понад 40 мов і повертають межі кожного символу як номери рядків.
Практичний шаблон: chunk = один символ з docstring, сигнатурою та тілом. Обов’язкові метадані: file_path, symbol_name, start_line, end_line, last_commit_sha. Для технічної документації правило простіше: ділити за заголовками секцій (H2/H3), а не за кількістю токенів. Runbook, поділений за кроками, дає фрагменти, самодостатні процедурно.
Нижче таблиця порівнює підхід до чанкінгу для різних типів ресурсів:
| Тип ресурсу | Стратегія чанкінгу | Обов’язкові метадані | Орієнтовний розмір |
|---|---|---|---|
| Вихідний код (Python, TS, Go) | AST за межами символу | file_path, symbol_name, start_line, end_line, last_commit_sha | 50–300 рядків на символ |
| Довідка API (OpenAPI/Swagger) | Один ендпоінт = один chunk | method, path, operationId, version | 200–600 токенів |
| Runbook / операційна процедура | Recursive за заголовками H2/H3 | назва секції, service_name, last_modified | 300–800 токенів |
| Внутрішня вікі (технічна проза) | Recursive з overlap 10–15% | page_title, page_url, author, last_modified | 512–1024 токени |
| Changelog / повідомлення комітів | Fixed-size або весь запис | commit_sha, author, date, branch | 128–256 токенів |
Більше про стратегії чанкінгу загалом описує стаття чанкінг документів для RAG.
Чому гібридний пошук тут необхідний
Для документів у прозовій формі векторний пошук справляється добре, бо запити є семантичними: «як обробити скаргу?» потрапляє до відповідного абзацу навіть без буквального збігу слів.
У коді запити часто містять точні ідентифікатори: UserRepository.findByEmail, POST /v1/orders/{id}/cancel, CASHCROWN_API_JWT_SECRET. Семантичний пошук (embedding-базований) не справляється з рідкісними токенами. Модель бачить UserRepository як послідовність символів без широкого семантичного значення і потрапляє на загальні фрагменти замість визначення класу.
BM25 — це інвертований індекс на основі частоти слів. Для ідентифікаторів у вихідному коді працює як повнотекстовий пошук: знайде UserRepository.findByEmail саме там, де цей метод визначений або викликаний. Поєднання BM25 з векторним пошуком через гібридний пошук (RRF або лінійне зважування результату) підвищує recall для запитів з точними назвами без погіршення результатів для семантичних запитів.
У внутрішніх тестах Cashcrown на наборі з 12 000 символів з комерційного проекту recall@5 при чистому векторному пошуку становив 61%. Після додавання BM25 у гібридному режимі зріс до 79%. Різниця була помітною переважно при запитах, що містили сигнатури методів і шляхи файлів. Це орієнтовні цифри з одного проекту, не гарантія повторюваності на іншому коді.
Після retrieval варто додати шар reranking, який повторно сортує top-k фрагментів за релевантністю до конкретного запиту. Для коду це важливо, бо BM25 може просунути фрагменти з назвою, ідентичною лексично, але з іншого модуля або простору імен. Reranker оцінює повний контекст, а не лише наявність токена. Деталі порівняння векторних баз, що підтримують hybrid search нативно, описує стаття як обрати векторну базу.
Свіжість індексу: проблема, яку не можна ігнорувати
Код має інший життєвий цикл, ніж процедура HR. Процедура відпустки змінюється раз на рік. Сигнатура методу в активному проекті змінюється кожен спринт.
RAG над кодом, який не оновлює індекс, генерує відповіді з deprecated API. Програміст, який отримає приклад використання функції, видаленої два комити тому, витрачає час на дебаг помилки, згенерованої асистентом.
Три рівні оновлення, які ми застосовуємо залежно від швидкості змін проекту:
Інкрементальна реіндексація після комиту. Webhook з GitLab/GitHub (або хук post-receive) запускає реіндексацію лише файлів, змінених у коміті. git diff --name-only HEAD~1 HEAD повертає список файлів. Кожен проходить через парсер AST заново. Символи, межі яких не змінилися, залишаються в індексі; змінені замінюють попередні версії.
Версіонування чанків за commit SHA. Кожен chunk зберігає last_commit_sha у метаданих. При запиті можна фільтрувати за гілкою (branch: main), що дозволяє відповідати як про актуальний код, так і про конкретну історичну версію.
TTL для документації. Runbook-и та вікі не мають webhook-а. Встановлюємо TTL, після якого документ автоматично реіндексується: 24-48 годин для активно змінюваної документації, 7-14 днів для стабільних специфікацій.
Асистент має показувати дату останньої індексації документа в цитаті. «Джерело: src/auth/user_service.py, рядок 142, індексовано 2 години тому» дає програмісту сигнал, чи можна довіряти відповіді. Пропуск цієї інформації — прихований ризик.
Цитування джерела як вимога, а не опція
У випадку документів у прозовій формі цитування назви файлу та номера сторінки — це хороша практика. Для коду це вимога безпеки.
Програміст, який отримує відповідь «функція calculate_discount знаходиться в модулі pricing», не може її застосувати без верифікації. Може існувати кілька модулів pricing у monorepo. Метод у останньому коміті міг змінити сигнатуру. Асистент міг переплутати схожу за назвою функцію з іншого модуля.
Корисна технічно відповідь виглядає так: «calculate_discount(order_id: int, user_tier: str) -> Decimal (файл: src/billing/pricing.py, рядок 87, коміт a3f9c12, індексовано 40 хвилин тому)». Програміст переходить за посиланням на файл, бачить актуальний код, перевіряє сигнатуру і лише тоді застосовує.
Guardrails для RAG над кодом мають блокувати два класи відповідей:
Відсутність покриття в індексі. Якщо retrieval не повернув жодного фрагмента з результатом подібності вище порогу (орієнтовно 0,65–0,75 для моделі семантичного пошуку), асистент відповідає: «не знайшов цієї сигнатури в індексованій версії коду». Це краще, ніж спроба реконструкції з пам’яті моделі, яка створить неіснуюче API.
Старий індекс. Якщо метадані чанка вказують, що файл не реіндексувався більше ніж X годин (поріг залежить від темпу змін проекту), відповідь містить попередження про потенційну неактуальність.
Більше про те, як RAG над базою знань обробляє цитування та guardrails, у статті корпоративний GPT на базі знань.
Випадки використання: де асистент допомагає найбільше
Три випадки, які ми зустрічаємо найчастіше в проектах, і яка роль людини в кожному з них:
Онбординг програміста. Новий член команди питає: «де обробляється авторизація OAuth?». Асистент повертає фрагмент src/auth/oauth_handler.py з початковим рядком, docstring та ім’ям автора комиту. Програміст читає код. Асистент скорочує час до першого осмисленого питання до старшого колеги, але не замінює код-рев’ю.
Питання «де реалізовано X?». На етапі code review або дебагу: «де реалізовано механізм retry для помилок 503?». Асистент повертає список місць у коді, які підходять семантично та лексично. Програміст вирішує, яке з них є правильним місцем для даного контексту. Асистент не знає нічого про архітектурний намір, який є в голові архітектора.
Відповіді на питання саппорту з документації. Клієнт саппорту питає про параметр API. Асистент шукає в індексованій довідці OpenAPI і повертає опис з номером версії специфікації. Саппорт перевіряє в Swagger UI перед відповіддю. Асистент скорочує час пошуку, але не усуває верифікацію.
У всіх трьох випадках роль людини є незамінною: архітектурні рішення, застосування коду у продакшені та відповіді, де помилка має реальні наслідки, завжди вимагають людського судження. Асистент RAG над кодом — це навігаційний інструмент, а не авторитет.
Ширший контекст гібридного пошуку для технічних баз знань описує стаття гібридний пошук BM25 і вектори.
FAQ
#Чи може RAG над кодом пропонувати готові фрагменти для вставки?
#Може повернути існуючий фрагмент коду з репозиторію як цитату. Це відрізняється від генерації нового коду: асистент показує, як даний патерн вже реалізований у проекті, з зазначенням файлу та рядка. Програміст оцінює, чи підходить фрагмент для повторного використання в новому контексті. Асистент не повинен генерувати код, якого немає в базі, бо ризик вигадування неіснуючої сигнатури високий.
Як обробити питання про код, який ще не індексовано?
Коли retrieval повертає відсутність результатів або результати нижче порогу подібності, guardrail має відповісти прямо: «не знайшов покриття для цього запиту в індексованій версії репозиторію». Асистент може запропонувати альтернативний запит або вказати, що даний файл міг ще не бути індексований. Вгадування сигнатури функції на основі загальних знань моделі — найчастіша причина помилкових відповідей у таких системах.
Як часто слід реіндексувати технічну документацію в Confluence?
#Залежить від темпу змін. Runbook-и для активно розроблюваних сервісів варто реіндексувати кожні 24 години. Стабільна архітектурна специфікація може мати TTL 7-14 днів. Ключово — інформувати асистента про дату останньої індексації в кожній відповіді, щоб користувач знав, чи актуальні цитовані матеріали. Старий індекс без попередження гірший за відсутність індексу.
Чому чисто векторний пошук недостатній для коду?
Модель ембедінгу представляє семантичне значення, але назви функцій, класів і змінних — це рідкісні токени без широкого контексту значення в загальному корпусі. Чисто векторний пошук буде просувати загальні фрагменти на тему аутентифікації замість конкретного класу AuthService. BM25 трактує ідентифікатори як ключові слова і знаходить їх точно. Поєднання обох через гібридний пошук обробляє як семантичні, так і лексичні запити.
Що робити, коли асистент цитує сигнатуру, яка вже не існує?
Це сигнал, що індекс неактуальний. Реакція має бути двоетапною: з боку користувача — верифікація вихідного файлу перед застосуванням, бо це завжди обов’язково. З боку системи — скорочення TTL реіндексації для змінюваних файлів та додавання попередження у відповідь, якщо метадані вказують на старий commit SHA. Ніколи не застосовуй код, повернутий асистентом, без перевірки актуальної версії в репозиторії.