DepMesh — делаем зависимости между файлами частью архитектуры проекта ru en
Перед внесением изменений, например в код, агенты часто должны ответить на прикладные вопросы:
- Какие тесты следует прочитать перед изменением этого файла?
- Какие спецификации регулируют этот модуль?
- Какие модули импортирует этот код?
- Какие артефакты затронуты этим изменением спецификации?
- etc.
Иными словами, агент должен обнаружить все зависимости между файлами и добавить их в контекст, чтобы корректно выполнить задачу.
Получение ответов на подобные вопросы требует от агента подумать, сформировать план действий, выполнить его и проанализировать результаты. Всё это съедает токены, контекст и время, без гарантии полноты и корректности результата.
Например, агент должен решить каким способом искать каждый конкретный тип зависимостей. Иногда, как в случае с цепочками импортов, агент должен прочитать и распарсить исходник, чтобы понять какие модули он импортирует, а затем сгенерировать пути к этим модулям и прочитать их — трудоёмко и неэффективно.
Результат такого «агентного поиска» не гарантирован, агент может забыть погрепать по имени функции и потерять важную зависимость, или пропустить нишевую спеку, потому что решил, что именно в данном случае она не нужна.
Более того, для поиска агент задействует кучу инструментов, каждый из которых самим своим использованием увеличивает потребление токенов и отъедает контекст. Для одной и той же задачи агент может раз от раза выбирать разные инструменты или вызывать их с разными параметрами, что убивает предсказуемость и воспроизводимость результата.
Общепринятые способы улучшить ситуацию, это интеграция LSP или чего-то подобного в качестве инструмента агента или разворачивание одной из миллиона RAG-систем. Это помогает делу, но не снимает все проблемы. Например, не гарантирует полноту, минимальность и детерминированность результата.
В то же время, с древних времён у нас есть огромный пул утилит и библиотек для поиска файлов и анализа исходного кода, которые умеют делать это быстро и эффективно, без всяких там LLM. Каждая из них в своей конкретной области работает намного лучше вероятностной модели.
Было бы здорово, подумал я, иметь одну абстракцию над всеми этими инструментами, которая предоставляет агенту универсальный интерфейс для извлечения любых зависимостей между файлами (какие настроите) и не требует от него думать что и каким образом искать.
Чтобы её можно было использовать, например, вот так:
> depmesh dependencies -r governed_by -r tested_by ./depmesh/cli/application.py
## governed_by
Specifications that apply to the artifact.
- @/specs/architecture/entities.md
- @/specs/architecture/errors.md
- @/specs/architecture/modules_layout.md
- @/specs/architecture/naming.md
- @/specs/architecture/static_analysis.md
- @/specs/architecture/tests.md
- @/specs/behavior/cli.md
- @/specs/behavior/file_paths.md
## tested_by
Tests that verify the artifact.
- @/depmesh/cli/tests/test_application.py
Так и появился DepMesh.
Как работает DepMesh
DepMesh — это CLI утилита с конфигурационным файлом (depmesh.toml) в корне вашего проекта.
В конфиге вы указываете две вещи:
- Типы зависимостей между файлами.
- Правила поиска связанных файлов.
Простейший конфиг может выглядеть так:
version = 1
[[relations]]
id = "tested_by"
description = "Tests that verify the artifact."
[[rules]]
relation = "tested_by"
input = { type = "glob", pattern = "./src/{**package_path}/{*module}.py" }
output = { type = "files", pattern = "./src/{package_path}/tests/test_{module}.py" }
В нём мы определяем один тип зависимости tested_by и правило «Каждый исходный файл .../module.py связан отношением tested_by с файлом .../tests/test_module.py».
Каждое правило определяется через три параметра:
relation— тип зависимости, который мы определяем.input— условие, которое определяет можем ли мы применить правило к переданному файлу.output— генератор путей файлов, которые связаны с входным файлом через указанную зависимость.
Кроме glob-паттернов из примера выше, DepMesh поддерживает вызов сторонних утилит, поэтому вы можете анализировать файлы как душе угодно: использовать линтеры, LSP-серверы, парсить исходники регулярными выражениями, ходить в базу данных, вызывать HTTP API и так далее. И всё это без необходимости тратить токены и загрязнять контекст агента.
Синтаксис правил довольно мощный и гибкий, например, в одном правиле можно задавать несколько шаблонов файлов и объединять несколько генераторов.
Подробную документацию по работе с утилитой и её настройке можно найти в depmesh/README.md.
DepMesh не осуществляет вывод обратных связей
Для зависимости вида file --tested_by--> test_file DepMesh не сможет автоматически найти обратную зависимость test_file --tests--> file.
Это осознанное решение, так как из-за асимметрии в том, как мы организуем информацию, поиск прямой и обратной зависимости может занимать на порядок разное время и ресурсы. Например, прямая зависимость imports может быть обработана очень быстро (надо лишь прочитать один файл и проанализировать его импорты), а обратная зависимость imported_by может потребовать обхода всего проекта.
Поэтому решение о поддерживаемых отношениях полностью отдаётся пользователю.
Со временем я улучшу этот аспект, но пока будем следовать принципу «явное лучше неявного» и определять все зависимости явно в конфиге.
Как я использую DepMesh
Примеры реальных конфигов можно найти в репозитории самой утилиты depmesh/depmesh.toml, или в репозитории моей читалки новостей feeds.fun/depmesh.toml.
Типы зависимостей
Мои типичные отношения между файлами:
tested_by— тесты, которые проверяют исходник.tests— исходники, которые проверяются тестами.imports/uses— исходники, которые импортируются/используются данным исходником.imported_by/used_by— исходники, которые импортируют/используют данный исходник.governed_by— спецификации, которые регулируют данный артефакт.governs— артефакты, которые регулируются данной спецификацией.terms_defined_by— словари, которые содержат определения терминов, используемых в файле.defines_terms_for— файлы, для которых словарь содержит определения терминов.indexed_by— индексные файлы (индекс — отдельный тип спецификации), которые содержат ссылки на данный файл.indexes— файлы, которые находятся в данном индексе.
Зависимости можно определять между любыми файлами
DepMesh «слеп» к типу и содержимому файлов, их семантика остаётся на вашей совести.
Для примера, у меня есть спецификации, которые govern другие спецификации: meta/general.md и часть конфигов для неё:
# Every specification document is governed by the general specification rules.
# Note: general.md governs itself too.
[[rules]]
relation = "governed_by"
input = { type = "glob", pattern = "./specs/**/*.md" }
output = { type = "list", artifacts = ["./specs/meta/general.md"] }
# The general specification rules govern every specification document.
# Note: general.md governs itself too.
[[rules]]
relation = "governs"
input = { type = "one_of", artifacts = ["./specs/meta/general.md"] }
output = { type = "files", pattern = "./specs/**/*.md" }
Определение зависимостей
Я стараюсь определять зависимости на уровне файловых путей — стандартные имена, стандартные пути, как в случае с тестами: ./src/module.py всегда имеет парный ./src/tests/test_module.py. Начал это делать задолго до взлёта ИИ — это удобно для человека; кроме того, glob-паттерны работают очень быстро. Например, вот агентская спека по лэйауту модулей бэкенда Feeds Fun ./specs/backend_architecture/modules_layout.md.
Но с импортами, конечно, такая фишка не прокатывает.
Для Python я использую tach — линтер зависимостей между модулями. Кроме того, что он может их возвращать в удобном для скриптов виде, он, собственно, линтер. Вы можете описать правила импортов в проекте и он будет проверять их соблюдение — очень удобно.
Для Rust я использую навайбкодженную обёртку вокруг cargo modules dependencies. К сожалению, устоявшихся утилит для анализа зависимостей в Rust нет (что странно). Если хотите сделать вклад в сообщество — это отличная возможность — концептуальной сложности нет, нужно просто потратить время.
Профит
- Агент не думает где не должен. Значит тратит меньше токенов, не забивает контекст и работает быстрее.
- Агент не пропускает зависимости, значит меньше ошибается и ведёт себя предсказуемее.
- Вы можете использовать DepMesh как базовый компонент более сложной автоматизации, так как универсальный интерфейс к зависимостям нужен не только агентам.
В контексте последнего пункта, я сейчас экспериментирую с системой, которая автоматически контролирует синхронизированность всех файлов проекта друг с другом, используя DepMesh для поиска зависимостей и Donna для исправления рассинхронизации.
Читать далее
- Donna — предсказуемость и управляемость для ваших агентов
- Donna готова к использованию
- Сходил в гости в подкаст «Две Столицы — Уютный IT Подкаст»
- Заметки про coding agents
- Пара слов о GitHub Сopilot
- Мои GPT-шки и prompt engineering
- Feeds Fun — читалка новостей с тегами и ChatGPT
- LLM агенты всё ещё не подходят для реальных задач
- Prompt engineering: строим промпты от бизнес кейсов
- Мигрируем с GPT-3.5-turbo на GPT-4o-mini