Code RAG: индексируем код с помощью Tree-sitter и AST

В большом репозитории разработчики часами ищут нужный фрагмент, а обычный поиск по тексту выдаёт не то. Мы настраиваем индексацию кодовой базы под ключ: разбор синтаксиса, правила доступа, регламенты обновления. Команда находит нужный код в разы быстрее, а новички выходят на результат за дни, а не недели.
Часто задаваемые вопросы

Индексация кодовой базы: почему поиск по коду не работает

Наивный поиск по коду опирается на разбиение файлов на части фиксированного размера — по числу строк или символов, как это делают универсальные системы семантического поиска. В монорепозитории такая схема ломается сразу: функция на сорок строк распадается на три куска, контекст теряется, а в выдачу попадает середина класса без сигнатуры и без места вызова.

Релевантность выдачи падает и на уровне векторных представлений. Вектор фрагмента не знает, что перед ним метод сервиса, а не конфиг или тест, что имя переменной здесь — тип домена, а не строка. Поиск уверенно возвращает похожие по набору слов куски, но не те, что связаны со смыслом задачи. Разработчик получает десяток файлов и вручную выясняет, какой вообще относится к делу.

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

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

Что даёт разбор синтаксиса: измеримый эффект

При индексации без разбора синтаксиса файл режется на окна фиксированного размера, и в выдачу попадают обрывки: половина функции, чужая сигнатура, кусок условия. Модель по такому фрагменту отвечает неточно или достраивает недостающее — отсюда ложные совпадения и ссылки на код, которого в проекте нет. Tree-sitter строит дерево разбора, и единицей индекса становится целая функция, класс или метод.

Точность выдачи растёт потому, что совпадение привязано к осмысленному узлу, а не к строке. Поиск по имени метода в разных компонентах даёт разные узлы с разным окружением, и контекст вызова отсекает ложные совпадения до того, как они дойдут до модели. В верхней части выдачи почти не остаётся мусора из разорванных блоков.

Скорость поиска нужного фрагмента — это навигация по дереву: сразу переходим к определению и его вызовам вместо ручного листания файлов. Число обращений к первоисточнику падает, потому что инженеру реже нужно открывать репозиторий и проверять, что имела в виду модель.

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

Способы разбора кода: сравнение подходов

Выбор способа разбора определяет, какие вопросы вообще сможет закрыть поиск по репозиторию. Индексация кодовой базы — это всегда компромисс между точностью структуры и устойчивостью к разнородному коду: строгий разбор даёт связи между сущностями, но ломается на диалектах и сгенерированных файлах, а размытый работает везде, но возвращает шум.

Отдельно решается гранулярность разбиения: часть файла, функция, класс или модуль целиком. Чем крупнее единица, тем больше контекста попадает в ответ и тем выше шанс, что нужная деталь в нём утонет.

Вариант Когда подходит Ограничения
Плоский поиск по тексту файлов Точные имена, пути, редкие строки конфигураций Не различает смысл, ловит совпадения в комментариях, синонимы не находит
Векторный поиск по частям файла Смысловые вопросы, разнородный код, много языков программирования Гранулярность разбиения задана формально: функции рвутся, в выдаче много близких по теме, но нерелевантных фрагментов
Разбор в синтаксическое дерево Однородная кодовая база, важны связи «определение — вызов — зависимость» Нужна поддержка разбора для каждого языка; на диалектах и шаблонном коде разбор частично теряется
Гибридная схема Большие репозитории, вопросы «где это используется и что сломается» Сложнее во внедрении, требует пересчёта индекса при изменениях

Гибридная схема обычно и даёт рабочий результат: кандидаты набираются лексическим и векторным поиском, а границы блоков уточняются по синтаксическому дереву. Качество проверяется долей ответов с корректной ссылкой на файл и строку — это и есть критерий, по которому подходы стоит сравнивать на вашем коде.

Этапы внедрения: от аудита репозиториев до передачи

Индексация кодовой базы — не разовый скрипт, а регламент, который живёт вместе с репозиториями. Качество ответов ассистента определяется тем, как код нарезан на фрагменты и насколько свежа выдача, поэтому работу ведём этапами и на каждом фиксируем проверяемый результат.

Последовательность работ

  1. Аудит репозиториев. Считаем число репозиториев, языков и объём кода, отделяем генерируемые и вендорные каталоги, помечаем заброшенные ветки: мусор в индексе даёт ложные срабатывания.
  2. Проектирование схемы разбора. Определяем единицу смысла — функция, класс, модуль. Нарезка идёт по синтаксическому дереву с сохранением связей между вызовами, а не по фиксированному числу строк.
  3. Сборка индекса. Первичная сборка по всей кодовой базе с привязкой к коммиту: любую выдачу можно сопоставить с конкретной версией кода.
  4. Настройка обновления. Индекс перестраивается по событию в системе контроля версий — точечно, по изменённым файлам, а не целиком.
  5. Замеры качества. Готовим эталонный набор вопросов к кодовой базе, считаем долю попаданий в нужный модуль и полноту контекста в ответе.
  6. Разграничение доступа. Права наследуются от репозиториев и веток: поиск не отдаёт код, который сотруднику не положено видеть.
  7. Передача документации. Отдаём регламенты обновления, схему разбора и метрики, по которым команда сама контролирует качество индекса.

После передачи индекс остаётся управляемым: понятно, что чинить при падении качества и как подключать новые репозитории без пересборки с нуля.

Артефакты внедрения: схемы, правила доступа, регламенты

Внедрение заканчивается не демонстрацией поиска на подготовленных вопросах, а моментом, когда команда заказчика сама подключает новый репозиторий, меняет правила доступа и обновляет индекс без нас. Поэтому мы передаём не «работающий сервис», а комплект артефактов, где каждый закрывает свой участок эксплуатации.

  • Схема индекса. Какие сущности вычленяются из кода (модуль, класс, функция), как связываются по вызовам и наследованию, где лежат метаданные и векторные представления фрагментов. По ней видно, почему поиск находит определение функции по косвенному описанию.
  • Карта разбора. Перечень языков и грамматик, которые разбираются в синтаксическое дерево, и что попадает в индекс как обычный текст. Отдельно отмечены участки неполного разбора — чтобы от поиска не ждали невозможного.
  • Правила доступа. Какие репозитории, ветки и каталоги видны каждой команде, на каком слое проверяется право и как ограничение отражается в выдаче. Права наследуются из существующих групп, а не заводятся вторым списком.
  • Регламент обновления. Что запускает переиндексацию (слияние в основную ветку, расписание, ручной запуск), сколько она длится, что происходит с индексом при удалении кода и переименовании файлов. Плюс порядок полной пересборки, если схема разбора изменилась.
  • Журнал замеров качества. Эталонный набор вопросов к кодовой базе с ожидаемыми ответами и отчёт по доле попаданий до и после правок. Изменения весов и разбора проверяются на нём, а не на ощущениях.
  • Инструкции для разработчиков. Как формулировать запрос, чтобы поиск вернул нужный фрагмент, как добавить новый репозиторий, куда смотреть в логах при неверном результате.
  • Эксплуатационные материалы. Конфигурации, схема развёртывания, точки мониторинга и порядок отката на предыдущую версию индекса — этого хватает, чтобы дежурная смена подняла систему после сбоя без нас.

Кейс: поиск по монорепозиторию до и после

Монорепозиторий на несколько тысяч пакетов и десятки миллионов строк: ответ на вопрос «где это определено и кто это вызывает» занимал у инженера до семи минут — с переключением между каталогами, чтением похожих по имени сущностей и ручной проверкой. Полнотекстовый поиск по строкам цеплял совпадения в комментариях, фикстурах и сгенерированном коде, поэтому в топ-5 релевантных попаданий оказывалось меньше трети.

Что изменили

Добавили разбор структуры через Tree-sitter: обход файлов, построение дерева разбора и индекса сущностей — модулей, классов, методов, вызовов и импортов с привязкой к диапазонам строк. Запрос идёт не по тексту, а по графу связей: «кто вызывает этот метод», «где реализован интерфейс», «что затронет смена сигнатуры». Переиндексация инкрементальная: после коммита обновляется только затронутая ветвь дерева, полный проход не требуется.

Метрики до и после

Метрика До После
Время найти нужный участок около 7 минут около 40 секунд
Релевантных совпадений в топ-5 31% 82%
Ошибок «ссылка на несуществующий метод» около 35 в неделю единичные

Разграничение доступа наследуется из системы контроля версий: индекс хранит те же права на файл, что и сам репозиторий, поэтому закрытые модули не всплывают в выдаче у тех, кому доступ к ним не выдан. Роли и группы не дублируются в отдельном списке — при переносе сотрудника между командами права на поиск меняются вместе с правами на код.

Чем инкрементальная индексация отличается от полной пересборки?

Полная пересборка обходит всю кодовую базу целиком: каждый файл заново разбирается, режется на фрагменты и векторизуется. Время такой операции зависит от размера репозитория, а не от объёма правки — в монорепозитории это десятки минут, и всё это время поиск отдаёт вчерашний срез кода.

Инкрементальное обновление стартует от изменений в коммите или слиянии ветки. Разбор через Tree-sitter даёт дерево разбора, и по нему видно, какие функции, классы и модули действительно затронуты: пересчитываются только изменённые узлы, а остальные фрагменты и их векторы переиспользуются как есть.

У инкремента есть границы применимости, и они важнее самой оптимизации. Обновлять по изменениям нельзя при смене модели векторизации, схемы нарезки на фрагменты или добавлении нового языка: старые и новые векторы окажутся в несогласованных пространствах, и близость перестанет что-либо значить. Удалённые файлы отслеживаются записями-надгробиями — иначе выдача возвращает код, которого в репозитории уже нет.

По нагрузке разница тоже ощутима: инкремент дописывает новые сегменты вместо перестройки хранилища целиком, поэтому пиковая запись идёт фоном и не конкурирует с поиском. Периодическое сжатие по расписанию убирает мусор от удалённых и переписанных файлов.

На практике мы держим гибрид: инкремент после каждого слияния в основную ветку, полная пересборка — по расписанию и обязательно при смене модели или схемы разбора. Свежесть выдачи тогда измеряется минутами, а не часами.

Технические сомнения перед стартом проекта

Перед стартом сомнения обычно одни и те же: не превратится ли общий индекс в дыру в правах, что будет после переписывания истории, потянем ли объём и языки, придётся ли ломать уже работающее. Ниже — как эти вопросы закрываются на практике.

Индекс общий — не получится ли, что код утечёт между командами?

Индекс собирается отдельным слоем, а права берутся из системы контроля версий и наследуются по путям. Фильтрация по доступу выполняется до выдачи фрагментов, а не после: в ответ модели не попадают куски из репозиториев, закрытых для пользователя. Состав групп пересчитывается при каждом изменении, поэтому выдача всегда соответствует текущим правам.

Что происходит при переписывании истории и принудительной перезаписи ветки?

Индексирование привязано к коммитам, а не к «текущему состоянию файлов». При переписывании истории пересобираются только затронутые пути, остальное не трогаем. Устаревшие версии фрагментов помечаются неактуальными и удаляются по правилу хранения — поиск не выдаёт код, которого в основной ветке уже нет.

У нас несколько языков и большой монорепозиторий — потянет ли?

Разбор идёт по синтаксическому дереву для каждого языка отдельно, поэтому связи между функциями и классами не размываются на смешанном коде. Файлы без разборщика обрабатываются резервным способом — по разделам и отступам. Монорепозиторий индексируется частями: приоритет у активных веток, остальное догоняет в очереди.

У нас уже развёрнут свой поиск по коду — всё придётся переделывать?

Нет. Мы сверяем покрытие существующего решения, подключаем его как источник и дополняем там, где не хватает структурного разбора. Полная замена нужна только если текущий поиск не отдаёт фрагменты с привязкой к файлу и строке.

Обсудим задачу: вернёмся с оценкой и планом

Постановка задачи у нас начинается с вводных, а не с коммерческого предложения: чем точнее исходные данные, тем меньше допущений в оценке объёма работ и тем реалистичнее порядок этапов. Для первичного разбора достаточно такого набора:

  • доступ к репозиторию или выгрузку и список языков, которые нужно покрыть индексацией кодовой базы;
  • структуру каталогов и правила сборки — по ним видно, как код делится на модули и сервисы;
  • примеры вопросов, на которые решение должно отвечать: поиск по функции, трассировка вызовов между модулями, объяснение незнакомого участка;
  • ограничения по доступам: какие каталоги, ветки и файлы не должны попадать в индекс;
  • чем не устраивает текущий поиск по коду — скорость, полнота выдачи, отсутствие контекста;
  • желаемые точки, где нужно ваше согласование этапов.

На созвоне разберём границы индексации, частоту переиндексации при изменениях в ветках, требования к качеству выдачи и способ подключения к рабочим инструментам команды.

По итогам заказчик получает схему индексации с разбором кода на синтаксическом дереве, порядок работ по этапам и план внедрения — с точками согласования и измеримым результатом каждого этапа.

Опишите задачу в двух-трёх абзацах — вернёмся с оценкой объёма работ и планом внедрения.

Направления AI-разработки

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    815
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139