Почему ваша кодовая база — это граф: парсинг 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 токенов. Звучит разумно, но это один из самых быстрых способов получить посредственного ассистента. Лучше семантическое разбиение: по функциям, классам, символам и логическим границам, чтобы поиск шёл так же, как думает о коде опытный инженер. В итоге получается индекс репозитория из осмысленных единиц, готовый к эмбеддингам, векторному поиску и более умному извлечению.