- BrainTools - https://www.braintools.ru -

Всем привет! На связи София из команды применения больших языковых моделей ecom.tech [1]. Сегодня хочу поделиться одной малоизвестной библиотекой, которую мы волей судьбы откопали на просторах github, попробовали использовать для поиска по нашей кодовой базе, и, о чудо! Это ощутимо помогло нам. Казалось бы, такой маленький шаг для человечества, но такой полезный для нашего проекта.
Здесь нужно понимать специфику и детали нашей задачи. Мы автоматизируем процесс reverse engineering – восстановление документации REST API и Kafka по коду. Простыми словами нам надо искать релевантные куски кода в одном или нескольких репозиториях и обрабатывать их, после чего генерить документацию с помощью LLM. Про часть, связанную с LLM, мы тоже обязательно расскажем в наших следующих статьях, сегодня поговорим только про поиск.
Говоря про код, немаловажным фактом является то, что нам нужно покрывать репозитории на различных языках – Golang, Kotlin, Ruby, Elixir и PHP. В нашей компании сотни микросервисов, написанных на разных языках программирования, с использованием разных фреймворков. Несмотря на это обилие технологий, архитектурные стандарты предписывают размещать описание REST‑эндпоинтов в строго определённых местах.
Например, в Golang‑проектах это хендлеры, привязанные к маршрутам через http.HandleFunc или вызовы методов GET/POST фреймворка Gin/Echo. В Kotlin — контроллеры Spring с аннотациями @RestController, @RequestMapping, либо декларативные маршруты в Ktor (routing { get(“/”) { … } }). В Ruby — контроллеры Rails и файл routes.rb, где resources или get явно объявляют эндпоинты. В Elixir — модули с использованием Phoenix.Router, макросы get “/”, PageController, :index. В PHP — маршруты в routes/api.php Laravel (callbacks или массивы [Controller::class, ‘method’]) либо аннотации в контроллерах Symfony.
Аналогичная картина с Kafka: продюсеры в Golang вызывают producer.SendMessage (библиотека sarama), в Kotlin — инжект KafkaTemplate и метод send, в Ruby — вызов producer.produce у ruby-kafka, в Elixir — KafkaEx.produce, в PHP — методы KafkaProducer::send поверх php-rdkafka. Консьюмеры аннотируются @KafkaListener (Kotlin/Spring), подписываются на топики через consumer.Subscribe (Go, Sarama), используют consumer.subscribe в Ruby, объявляются как GenConsumer в Elixir, или через консьюмер‑группы в PHP.
Иными словами, разработчики следуют единым корпоративным стандартам, поэтому паттерны поиска точек входа известны заранее и одинаковы для всех проектов внутри каждого языка. Казалось бы, бери любой инструмент, ищи эти аннотации/вызовы — и документация готова.
Сначала мы пробовали применить библиотеку Tree-sitter [2] напрямую, но она не показала хорошего качества на нашей кодовой базе. В нашей задаче важен не столько синтаксис, сколько семантика фреймворков. В коде активно используются обёртки над HTTP/Kafka-библиотеками, базовые контроллеры, DSL и метапрограммирование (особенно в Ruby и Elixir), поэтому вызовы фреймворка в дереве выглядят как обычные пользовательские функции. В результате tree-sitter либо пропускал реальные эндпоинты, либо давал много ложных совпадений. Кроме того, для каждого языка и фреймворка приходилось поддерживать отдельный набор правил, чувствительный к стилю кода и версиям библиотек, поэтому точность сильно плавала от проекта к проекту, а поддержка стала занимать уйму времени.
Потом мы построили классический RAG пайплайн с эмбеддером gte-multilingual-base, затем преобразовали его в гибридный RAG с последующим ре-ранжированием через BM-25. Но поиск занимал довольно много времени и всё равно находились не все нужные нам файлы. Средняя точность по всем ЯП была 85%. Тогда мы стали смотреть в сторону обычного grep из linux, дешево и сердито.
При использовании grep нам приходилось буквально «выкручивать» регулярные выражения, чтобы отсечь ложные срабатывания: комментарии на русском и английском, строковые литералы, в которых встречаются похожие сигнатуры, импорты и объявления типов. Например, в Java аннотация @GetMapping может быть частью Javadoc, а в Golang комментарий // GetUser не имеет ничего общего с хендлером. Без понимания синтаксической структуры кода невозможно надёжно отличить объявление эндпоинта от простого упоминания тех же слов в другом контексте. Мы пытались уточнять паттерны, добавлять просмотр назад/вперёд, ограничивать расстояние от начала строки — это помогало, но хрупкость таких правил давала о себе знать при малейшем изменении стиля форматирования (например, аннотация перенесена на новую строку, или перед ней стоит другой декоратор). Проектов много, код обновляется ежедневно, и поддерживать этот набор костыльных регулярных выражений становилось всё больнее. И это я ещё молчу про легаси код, не поддающийся актуальным правилам архитектуры.
Учитывая всё это, решение было неплохим, но со своими нюансами, связанными с трудностью подбора точных паттернов и непониманием структуры кода и проекта. Тогда мы стали искать его модернизированные версии. Так мы и наткнулись на grep-ast [3]. Искали медь, а нашли золото.

grep-ast [3] — это Python-библиотека для поиска и анализа исходного кода на основе его абстрактного синтаксического дерева (AST). В отличие от обычного grep она выполняет не только простой текстовый поиск, но и может искать структурные паттерны в коде, понимает его синтаксическую структуру (что нам критически важно!). Grep-ast показывает совпадения не как строки, а как элементы AST с контекстом: функции, классы, циклы и вложенные блоки.
С grep-ast можно смотреть соответствующие строки с полезным контекстом, показывающим, как они вписываются в код. Можно увидеть циклы, функции, методы, классы и т.д., которые содержат все необходимые строки, узнать о том, что находится внутри соответствующего определения класса или функции. Мы видим нужный код на каждом уровне абстрактного синтаксического дерева, над и под совпадениями. Grep-AST базируется на основе tree-sitter [2] и tree-sitter-languages [4], поэтому поддерживает много языков (русский, английский, китайский, ладно, шутка :) По сути это убойное комбо grep и tree-sitter.
По дефолту grep-AST выполняет рекурсивный поиск по текущему каталогу во всех файлах. А также он поддерживает работу с .gitignore. Интерфейс похож на grep: базовый вызов выглядит как grep-ast [pattern] [filenames…], есть флаг игнорирования регистра, управление цветом вывода, явная настройка кодировки, а также режим печати таблицы парсеров (–languages).

AST (Abstract Syntax Tree) – это абстрактное синтаксическое дерево, структура данных, которая представляет исходный код программы в виде дерева, отражая его логическую и синтаксическую структуру, а не конкретный текст. AST – это способ, которым компилятор или интерпретатор «понимает» код.
Он разбирает текст программы и превращает его в дерево из узлов: операций, переменных, выражений и т.д. В отличие от Parse Tree, который содержит все синтаксические элементы (скобки, запятые, точки с запятой), точно следуя грамматике, AST — упрощенная, семантическая версия, которая содержит только структурные элементы кода, необходимые для компиляции или интерпретации, исключая лишние символы.

Сравним grep и grep-ast на примерах. Чтобы разница была наглядной, возьмём небольшой синтетический проект из двух файлов.
1.Поиск метода
Найдём метод check_requirement внутри helpers.py
Обычный grep
“grep -n "def check_requirement" mini_project/helpers.py [5]“

Мы получили только номер строки. Теперь нужно открыть файл, прокрутить, понять где начинается метод и где он заканчивается. Попробуем получить контекст:
“grep -n -B 3 -A 10 "def check_requirement" mini_project/helpers.py [5]“

Кому-то может показаться, что grep -B/-A решает проблему контекста. Формально — да, мы можем видеть несколько строк до и после совпадения. Но по факту этот контекст случаен, он не знает, где начинается и заканчивается метод, он легко «захватывает» код из соседних функций, он полностью теряет информацию о том, в каком классе или логическом блоке мы находимся.
grep-ast, в отличие от этого, всегда работает в терминах AST. Метод показан ровно в своих границах, внутри нужного класса, без ручного подбора количества строк и без риска сломать структуру
grep-ast
“grep-ast "def check_requirement" mini_project/helpers.py [5]“

Мы получаем метод целиком, docstring, его тело и главное, что он принадлежит классу JudgeAgent. По сути, вместо «точки входа» мы сразу получаем осмысленный фрагмент кода, с которым уже можно работать дальше – как человеку, так и AI-агенту.
2. Поиск по всем файлам проекта
Теперь найдём JudgeAgent во всём проекте.
Обычный grep ```grep -r "JudgeAgent" mini_project/ --include="*.py"
“`

В результате перемешаны объявления, импорты и использования, нет различия между JudgeAgent и NotJudgeAgent, нет понимания связей, нужно открывать каждый файл вручную. Это просто список совпадений.
grep-ast ```grep-ast "JudgeAgent" mini_project/
“`

При поиске по всему проекту, кажется, разница видна невооруженным взглядом. grep возвращает набор разрозненных строк из разных файлов — импорты, объявления классов, вызовы без малейшего понимания связей между ними. grep-ast группирует результаты по файлам и показывает их в структурном контексте: где объявлен класс, где он используется, в каком именно месте и в рамках какого логического блока. Для человека это экономит десятки переходов по файлам, а для AI-агента – превращает поиск из «поиска строк» в поиск смысловых сущностей кода.
В итоге ключевая разница между grep и grep-ast не в том, находит ли он строку, а в том, что именно он возвращает в качестве результата. Grep отвечает на вопрос «где встречается этот текст», а grep-ast – «какая часть программы за этим стоит».
Для задач анализа кода, навигации по проекту и особенно для AI-инструментов второе оказывается значительно важнее первого.
По запросу grep-ast в гугле вы также найдете библиотеку с перевернутым названием – AST-GREP [6]. Она похожа в большей мере только названием и общим концептом, оба инструмента в своей основе используют AST, построены на Tree-sitter и полиглоты.

Различаются они тем, как используют полученное AST. Grep-ast: показывает контекст вокруг найденных строк, ast-grep: выполняет структурный поиск и переписывание. Есть еще парочка нюансов: AST-GREP быстрее, так как написан на Rust, сложнее, потому что нужны сложные с��руктурные запросы по AST, а не по строкам, и многофункциональнее или, попросту говоря, перегруженнее, есть возможности переписывания кода, линтинга с кастомными правилами, композиции правил (операторы, сложные условия). Мы пришли к выводу, что для нашего AI-агента это слишком сложный инструмент с избыточным функционалом, и в итоге остались верны grep-ast.
|
Критерий |
grep-ast |
ast-grep |
|
Язык реализации |
Python |
Rust |
|
Производительность |
Умеренная |
Очень высокая |
|
Основная задача |
Поиск с AST-контекстом |
Поиск+замена+линтинг |
|
Паттерны |
Regex по тексту |
Структурные паттерны AST |
|
Рефакторинг |
Нет |
Да (интерактивный codemod) |
|
Кастомные правила |
Нет |
Да (YAML конфиги) |
|
Интерфейсы |
CLI |
CLI+Node.js API + LSP |
В какой-то момент мы посмотрели на всё это чуть под другим углом. Если grep-ast уже умеет находить в коде структурные сущности – классы, методы, точки входа, значит это не просто CLI-утилита для разработчика. Это готовая операция навигации по коду, которую можно дать агенту как инструмент.
Фактически можно превратить grep-ast в инструмент для tool calling. После этого LLM перестаёт работать вслепую. Вместо того, чтобы угадывать по кускам контекста или полагаться на эмбеддинги, она начинает действовать как разработчик: находит релевантный класс, смотрит методы, понимает зависимости, уточняет следующий запрос. В таком случае, как только мы перестаем давать модели готовые куски кода и позволяем ей самой обходить репозиторий, поиск перестает быть разовым retrieval-запросом. Он превращается в итеративное исследование.
Кстати, именно благодаря обёртке в tool calling мы можем получить ещё один неожиданный, но крайне ценный бонус – полную прослеживаемость. Каждый вызов grep-ast от лица LLM автоматически логируется: с каким паттерном пошла, в каком файле что нашла, куда решила нырнуть дальше. Теперь не нужно гадать, почему агент принял то или иное решение. Мы буквально можем видеть его цепочку рассуждений: «сначала ищет контроллеры, потом проваливается в DTO, затем уточняет Kafka-продюсеров». Чёрный ящик перестает быть чёрным.
В нашем случае большая часть ценности была не в семантическом поиске по всему проекту, а в точном и воспроизводимом нахождении структурных элементов кода – классов, методов, точек входа. И здесь AST-подход оказался куда эффективнее вездесущих эмбеддингов.
grep-ast стал для нас недостающим звеном между «тупым» grep и более сложными инструментами. Он не пытается быть всем сразу, но отлично решает свою узкую задачу и именно поэтому хорошо ложится в архитектуру наших AI-агентов. Возможно, grep-ast не станет вашим основным инструментом, но как минимум заслуживает места в тулбоксе.
Автор: sofiierm
Источник [7]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/26516
URLs in this post:
[1] ecom.tech: http://ecom.tech
[2] Tree-sitter: https://tree-sitter.github.io/tree-sitter/
[3] grep-ast: https://github.com/Aider-AI/grep-ast
[4] tree-sitter-languages: https://github.com/grantjenks/py-tree-sitter-languages
[5] helpers.py: http://helpers.py
[6] AST-GREP: https://ast-grep.github.io/
[7] Источник: https://habr.com/ru/companies/ecom_tech/articles/1005610/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1005610
Нажмите здесь для печати.