Чому ваша кодова база — це граф: парсинг Tree-sitter для ШІ-асистента з коду
Чому ШІ-асистенти не читають увесь репозиторій, як графи й синтаксичні дерева передають структуру і як розібрати код через Tree-sitter, щоб побудувати індекс символів.
Код — це граф, а не купа тексту
Цей момент знайомий кожному, хто приходив на новий проєкт. Клонуєш репозиторій і думаєш, де ж живе логіка повторних спроб платежу. Рука сама тягнеться до одного з варіантів:
Ctrl + Shift + F
або
grep -r "retry" .
Іноді цього досить, але часто ні. Код повторів може ховатися за інтерфейсом, в іншому модулі, всередині декоратора, у базовому класі чи за патерном «стратегія». Щоб його знайти, замало збігу рядків: усе залежить від того, як частини пов'язані між собою.
А тепер спитайте Cursor «Як працюють повторні спроби платежу?», і за кілька секунд він підсвітить кілька файлів, пояснить хід виконання й навіть запропонує покращення. Схоже на магію. Дивно тут те, що він не читає весь ваш репозиторій — і не зміг би.
Поширена хибна модель
Більшість розробників спершу уявляють роботу інструмента так:
Repository
│
▼
Large Language Model
│
▼
Answer
Звучить розумно, але працювати не може. Візьмімо кодову базу середнього розміру:
12,000 Python files
≈ 2 million lines of cod
Навіть моделі з дуже великим контекстним вікном не можуть ефективно обробляти весь репозиторій на кожен запит, а якби й могли, це коштувало б дорого. Надсилання кожного вихідника, README, конфігу, тесту й згенерованого файлу означало б жахливу затримку й величезний рахунок. Гірше того, майже всі ці токени були б не до справи. Якщо питання про повтори платежів, моделі нема чого читати код обробки зображень.
Ви ж теж читаєте не все
Подивіться, як шукає досвідчений інженер. Якщо його спитати, де реалізовано автентифікацію, ніхто не відкриває main.py і не читає файли по черзі. У голові одразу складається мапа, приблизно така:
Request
│
▼
API Router
│
▼
Authentication Middleware
│
▼
JWT Validator
│
▼
User Service
Ви йдете за зв'язками, тобто обходите граф. Різниця величезна. Досвідчені інженери не запам'ятовують мільйони рядків. Вони пам'ятають зв'язки.
Чим насправді є репозиторій
Репозиторій — не мішок із файлами. Це мережа пов'язаних символів. Візьмімо невеликий проєкт:
src/
├── api.py
├── payment.py
├── retry.py
├── queue.py
└── logger.py
На перший погляд це п'ять не пов'язаних файлів. Та якщо намалювати зв'язки, картина інша:
api.py
│
▼
payment.py
│ │
▼ ▼
retry.py logger.py
│
▼
queue.py
Ця схема пояснює більше, ніж будь-яке дерево каталогів. Кожен виклик функції, імпорт, успадкування, реалізація інтерфейсу й залежність — це ребро графа. З такого погляду ШІ-асистенти стають зрозумілішими: вони пересуваються зв'язками, а не просто шукають по файлах.
Де текстовий пошук ламається
Візьмімо маленький приклад:
class PaymentProcessor:
def charge(self, order):
return self.gateway.execute(order)
Де насправді відбувається платіж? Незрозуміло. gateway може виявитися Stripe, Razorpay, PayPal, моком чи тестовим дублером, і пошук за «charge» цього не прояснить. Додайте впровадження залежностей, і стане ще гірше:
processor = container.resolve(PaymentProcessor)
Тепер реалізацію обирають під час виконання, і текстовий пошук майже марний. Сучасна розробка сильно спирається на абстракції, і, за іронією, саме абстракції б'ють по наївному пошуку.
Код має структуру
Мови програмування — не простий текст. Вони структуровані. Візьмімо таку функцію:
def calculate_total(price, tax):
return price + tax
Ви бачите символи, а компілятор бачить структуру, приблизно таку:
FunctionDefinition
├── Name
│ └── calculate_total
├── Parameters
│ ├── price
│ └── tax
└── Return
└── BinaryExpression(+)
Таке подання називають абстрактним синтаксичним деревом, або AST. Воно зберігає зміст і відкидає форматування. Ці два варіанти запису:
return price + tax
і
return (
price
+
tax
)
дають по суті однакове AST. Саме тому сучасні асистенти з коду починають із розбору вихідників, а не працюють із ними як із сирим текстом.
Tree-sitter
На Tree-sitter тримається багато інструментів розробника. Замість того щоб сканувати файл рядок за рядком, він будує синтаксичне дерево, яке розуміє мову. Візьмімо такий код:
class PaymentService:
def retry_payment(self):
pass
Tree-sitter бачить не два рядки тексту, а щось ближче до цього:
Module
│
├── Class
│ └── PaymentService
│
└── Function
└── retry_payment
Ця структура окупається згодом: з нею можна відповісти, якому класу належить метод, які функції публічні, які методи викликають певну функцію і які символи експортуються. Текстовий пошук ніколи не відповість на це надійно.
Розбір репозиторію
Час зібрати першу частину асистента. Встановлюємо залежності:
pip install tree-sitter tree-sitter-python
Потім розбираємо файл:
from tree_sitter import Language, Parser
import tree_sitter_python as tspython
parser = Parser()
PY_LANGUAGE = Language(tspython.language())
parser.language = PY_LANGUAGE
code = """
class PaymentService:
def retry_payment(self):
print("Retrying...")
"""
tree = parser.parse(bytes(code, "utf8"))
print(tree.root_node)
Виведення виглядає так:
(module
(class_definition
(identifier)
(block
(function_definition))))
Не гарно, зате корисно. Видно, де починається кожен клас, де закінчується кожна функція, де стоять імпорти й де оголошено декоратори, і нікому не довелося писати парсер вручну.
Дістаємо всі функції
Далі обходимо синтаксичне дерево й збираємо всі функції:
from tree_sitter import Parser, Language
import tree_sitter_python as tspython
parser = Parser()
parser.language = Language(tspython.language())
def extract_functions(node, source):
if node.type == "function_definition":
name = node.child_by_field_name("name")
print(source[name.start_byte:name.end_byte].decode())
for child in node.children:
extract_functions(child, source)
source = open("payment.py", "rb").read()
tree = parser.parse(source)
extract_functions(tree.root_node, source)
Виведення:
retry_payment
refund
calculate_total
Зверніть увагу, що сталося. Код не шукав ключове слово def. Він попросив у синтаксичного дерева кожне визначення функції, і це суттєва різниця.
Будуємо індекс
Замість того щоб зберігати файли цілком, створіть структурований індекс із такими записами:
{
"symbol": "retry_payment",
"type": "function",
"file": "payment.py",
"class": "PaymentService",
"start_line": 42,
"end_line": 67
}
Зробіть це для кожної функції, класу, інтерфейсу, переліку й константи, і неструктурований репозиторій перетвориться на знання, якими можна шукати.
Що вже маємо
Конвеєр, зібраний до цього моменту, виглядає так:
Git Repository
│
▼
Repository Scanner
│
▼
Tree-sitter Parser
│
▼
Abstract Syntax Tree
│
┌────────────┴────────────┐
▼ ▼
Function Index Class Index
│ │
└────────────┬────────────┘
▼
Repository Graph
На ШІ-систему він поки не схожий, бо нею не є. Це конвеєр видобування знань, і з нього починається будь-який сучасний ШІ-асистент з коду.
Чому починати саме звідси
Багато туторіалів одразу стрибають до ембедингів і векторних баз. Це як будувати Google, поки не існує жодної вебсторінки. Те, наскільки добре асистент розуміє репозиторій ще до будь-якого машинного навчання, значною мірою і визначає його якість. Якщо індекс поганий, жоден промпт-інжиніринг його не врятує. Сміття на вході — сміття на виході.
Що далі
На цьому етапі асистент знає, де лежить кожна функція й кожен клас, але не розуміє змісту. Наступне завдання — розбиття на чанки. Більшість розробників ріжуть репозиторії на довільні блоки по 500 чи 1000 токенів. Звучить розумно, але це один із найшвидших способів отримати посереднього асистента. Краще семантичне розбиття: за функціями, класами, символами й логічними межами, щоб пошук ішов так, як про код думає досвідчений інженер. У результаті маємо індекс репозиторію з осмислених одиниць, готовий до ембедингів, векторного пошуку й розумнішого видобування.