Главная / Статьи

Четыре вида памяти, необходимые вашему кодирующему агенту (и только один из них реализован).

Графы кода предоставляют агентам структуру, однако инженерам также необходимы причины, история и процедуры. Рассмотрим четыре типа памяти, уровни уверенности и способы их объединения.

Издатель: REACTAPP.TOP11 мин чтения2627 слов

Источники: Your Coding Agent Is Always the New Guy on Monday.

В последнее время появилась группа инструментов, цель которых — помешать ИИ-агентам для кодирования сканировать репозитории. Они анализируют код с помощью Tree-sitter, хранят символы и связи в SQLite и предоставляют их через MCP, либо запускают серверы языка, обеспечивая агенту настоящую семантику IDE. Полученные результаты впечатляющи, и идея сама по себе разумна.

Все они отвечают на один и тот же тип вопросов: кто вызывает этот элемент, что это за тип, что может сломаться в результате изменений.

Теперь посмотрите, как опытный инженер подходит к изменениям в незнакомой области. На поиск вызывающих элементов уходит примерно тридцать секунд. Затем следует час вопросов, которые не касаются ничего из вышеупомянутой категории: почему всё построено именно так? Что произошло в прошлый раз, когда кто-то пытался это сделать? Каков обычный порядок действий? Нужно ли сгенерировать код заново перед запуском тестов?

Четыре вопроса, четыре разных вида памяти. Почти каждая современная конфигурация агента включает один из них и называет его контекстом.

Тест, который их разделяет

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

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

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

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

1. Структурная память: влияние изменений

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

  • Tree-sitter обеспечивает широту охвата: перечень узлов и интервалов, поддержку более 40 языков, толерантность к ошибкам и измерение времени обработки в миллисекундах на файл. Он не разрешает типы. Он может указать, что на 3-й строке содержится вызов функции с именем execute, но не указать, конкретно какой execute. Поиск совпадений имён не является анализом уровня компилятора, и этот недостаток особенно заметен в полиморфном, многоуровневом коде с внедрением зависимостей.
  • LSP обеспечивает корректность благодаря элементам textDocument/definition, references и implementation, подкреплённым настоящим проверщиком типов. Он запускается медленно, требует готовой сборки и должен работать в фоновом режиме, поскольку работа сервера языка без предварительной подготовки сильно увеличивает задержки. Его следует запускать в качестве фонового инструмента для обогащения данных.
  • Байткод показывает код, который существует только после выполнения процесса сборки: запросы, созданные с использованием Spring Data, прокси CGLIB, мапперы MapStruct, а также всё, что было написано обработчиком аннотаций. В исходном коде этого нет; всё это отображается в папке target/classes.
  • Результаты AOT или содержимое папки /actuator/beans показывают, как осуществляется внедрение зависимостей, другими словами, какая конкретная класс реализует ту или иную интерфейс в том или ином профиле. Изучение исходного кода не поможет ответить на этот вопрос, поскольку ответа там нет.
  • Распределенные трейсы отслеживают взаимодействия между сервисами и на границах асинхронных операций: от контроллера через HTTP к другому сервису, от производителя данных к теме сообщений к потребителю. Каждый сервер языка останавливается на границах модулей, и распределенные системы в основном состоят именно из таких элементов. Если вы используете OpenTelemetry, вы уже собираете граф вызовов между сервисами, но затем удаляете его.
  • Самое важное решение по дизайну заключается в том, что каждой ребру присваивается уровень:

    • resolved: проверка типов подтвердила это, поэтому можно действовать с уверенностью.
    • observed: это было получено во время реальной работы, что является основой для обрабатываемых путей.
    • inferred: совпадение определено только по имени. Вероятно, правильно, но не проверено.
    • ambiguous: существует несколько вариантов. Возвращаются все из них, и ни один не выбирается.
    • dynamic: рассчитывается во время выполнения и является нерешаемым, поэтому он записывается как нерешаемый и не удаляется.

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

    Одно из последствий, о котором редко прямо говорят в реализациях: в многоуровневой кодовой базе на Spring или Django анализ влияния показывает лишь нижнюю границу, но никогда не дает полного представления. Утверждение «17 вызывающих компонентов, 3 нерешенных вызова интерфейсов, 1 сгенерированный метод» является точным. Утверждение лишь «17 вызывающих компонентов» вводит в заблуждение.

    Процесс пересборки происходит ежечасно, и сама пересборка занимает секунды.

    2. Память знаний: причины, лежащие в основе кода

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

    В файле конфигурации укажите количество попыток повторной обработки как 3. Обоснование этого показателя приводится в документе ADR 2023 года, в анализе случаев дублирования инструкций по расчетам, а также в треде, где кто-то описывал SLA контрагента. Связь между ними — политика, возникшая в результате инцидента и ограничивающая интеграцию — упоминается в трех документах, причем каждый из них формулирует ее по-своему.

    Здесь хорошо подойдет инструмент GraphRAG, и полезно знать, чем он служит. Существует шесть шагов:

    1. Разбиение на фрагменты — детерминированное разделение документов на текстовые единицы.
    2. Извлечение информации — задача ИИ: модель читает каждый фрагмент и выделяет сущности и связи между ними.
    3. Краткое описание — также задача ИИ, заключающаяся в объединении одной и той же сущности, описанной пятьюдесятью способами, в одно описание.
    4. Кластеризация Leiden не требует использования модели. Она разделяет граф на вложенные сообщества на основе модулярности, группируя узлы таким образом, чтобы количество внутренних рёбер значительно превышало количество рёбер, ведущих к другим узлам.
    5. Отчёты о сообществах — это краткие сводки, сгенерированные большими языковыми моделями для каждого сообщества; они составляются снизу вверх, причём более высокие уровни формируются на основе отчётов нижестоящих уровней.
    6. Эмбеддинги генерируют векторы, которые служат точками входа для запросов.

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

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

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

    3. Эпизодическая память: что происходило при предыдущих попытках

    Здесь записывается собственная история агента в этом конкретном хранилище. Последняя попытка изменения SettlementService нарушила три теста на распределение ресурсов. Изменение этого интерфейса без перегенерации мапперов приводит к сбою сборки. Два самых недавних PR-запроса, в которых пропустили слой валидации, были отклонены.

    У него также есть свойство, которое ни один семантический индекс не может претендовать на наличие: его истинные данные являются детерминированными. В отчете о причинах сбоя приводится утверждение человека, сделанное на основе анализа модели. Фраза «Агент изменил X, и CI показал красный цвет» — это оценочный результат, определяемый системой сборки. Для рассматриваемых вопросов это делает эпизодическую память наиболее надежным источником в данной схеме.

    Есть три аспекта дизайна, в которых легко допустить ошибку:

    • Пропустите раздел GraphRAG. Эпизоды следуют по временной шкале и имеют результаты. Группировка по плотности связей — неверная структура; необходимо взвешивание по актуальности, частоте и успеху или неудаче.
    • Требуется механизм удаления данных. Семантические индексы могут обойтись без него, но этот — нет. Уроки, полученные при рефакторинге двух архитектур два года назад, не только устарели, но и ведут агента по неверному пути.
    • Ошибки накапливаются. Предположим, агент сделал неверные выводы и зафиксировал результат. Этот результат превращается в запомненный факт, который наследуется во всех последующих сессиях. Храните результаты (отказ CI, отклонение PR) с высокой степенью уверенности, а собственные выводы агента — с гораздо более низкой; затем позвольте выводам устаревать, в то время как результаты останутся.

    Уход пользователей происходит в каждой сессии.

    4. Процедурная память: совсем не индекс

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

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

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

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

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

    Слияние: один пространство имен символов

    Три восстановленных воспоминания совпадают по идентификатору символа, но никто ещё не решил эту проблему. Структурные узлы используют полностью квалифицированные имена. Эпизодические записи указывают на тот символ, с которым фактически работал агент, поэтому они точно совпадают. Энтитеты в индексе знаний — это строки, выделенные моделью из прозы, такие как «SettlementService», «the settlement service», «the settlement reconciliation path» или «the counterparty SLA constraint», и их соотнесение с реальными символами представляет собой задачу нечёткого совпадения.

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

    • exact: имя встречается дословно и указывает ровно на один символ.
    • strong: нормализованная фраза чётко указывает на один класс.
  • weak: понятие, охватывающее несколько классов с нечеткими границами. Возвращается набор символов, который агент проверяет.
  • unresolved: реальное ограничение, для которого нет соответствующих символов нигде. «SLA контрагента» является настоящим архитектурным ограничением, которое не отражается в коде.
  • Что касается этого последнего уровня, оставьте его как есть, верните его и не заставляйте производить сопоставление. Читаемое нерешенное ограничение лучше, чем неверное привязывание символов, и именно попытки принудительного сопоставления позволяют агенту уверенно исправлять неверный класс.

    Что на самом деле спрашивает агент

    Интерфейс запросов может выглядеть так:

    what_breaks(symbol)        → structural: resolved callers, tiered
    why_is_this(symbol)        → knowledge: decisions, incidents, constraints
    what_happened_before(sym)  → episodic: past attempts and their outcomes
    related_by_intent(concept) → resolve a phrase to a symbol set, tiered
    

    Затем возникает вопрос, на который никто из троих не может ответить в одиночку: «Я собираюсь изменить политику повторных попыток при расчетах. Что сломается, почему их три и пробовал ли кто-нибудь это раньше?»

    Карты знаний связывают «политику повторных попыток» с SettlementService.retry на уровне strong и возвращают информацию об ADR и анализе после сбоя. Механизм Structural берет этот символ и возвращает его вызывающие компоненты, включая два сервиса, видимые только в трейсах. Механизм Episodic указывает, что аналогичные изменения в прошлом квартале нарушили тесты на распределение ресурсов.

    Это один из ответов, сформированных на основе трех видов доказательств; каждое доказательство генерируется соответствующим механизмом, и каждое сопровождается указанием степени доверия к нему.

    Какова стоимость

    • Большинство коллекций документов слишком малы. Девять ADR и коммитов с указанием «исправить баг» не дают индексу знаний ничего для работы. Посмотрите, что у вас есть, прежде чем что-то создавать.
    • Разрешение сущностей никогда не становится идеальным. Инструменты вроде лестницы помогают с этим справляться, но не решают проблему. Уровень weak, скорее всего, будет самым загруженным, поэтому планируйте проверки, а не полное доверие к данным.
    • Начинайте с ограниченного подхода. Начните с tree-sitter плюс LSP. Добавляйте байткод, когда генерируемый код начнет давать неверные ответы, и трейсы — когда запросы будут переходить между сервисами. Индекс знаний добавляйте только тогда, когда корпус данных действительно стоит индексации.
  • Никто не создал всё это целиком — это может быть как призыв к действию, так и предупреждение. Эти компоненты находятся в разных сообществах: специалисты по анализу кода не читают статьи о GraphRAG, разработчики GraphRAG не занимаются проверкой типов, а специалисты по архитектуре памяти в основном работают над чат-ассистентами, а не над кодовыми базами.
  • Подумайте о процессе адаптации сотрудника

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

    У коллеги прошлого года есть три вещи, которых никогда не было в репозитории, и у новичка их нет.

    Первая причина — необычные решения, принятые командой. Попытки выполнения операции происходят три раза из-за инцидента в марте, а временное решение проблемы существует потому, что API партнера работает некорректно по пятницам. Об этом нигде не указано; информация содержится в документе ADR, отчете о последствиях инцидента и записях переговоров.

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

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

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

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

    Однострочная версия

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

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

    Термины GraphRAG и этапы обработки следуют реализации от Microsoft Research. Расходы сильно варьируются в зависимости от размера корпуса данных, настроек сбора информации и выбранной модели, поэтому перед принятием решения проведите тестирование на собственных данных.

    Проверка за пятнадцать минут

    Выберите в своем кодовом базисе интерфейс, имеющий пять или более известных реализаторов, например PaymentProcessor или NotificationChannel; один из них должен быть подключен с помощью @Autowired, а не создан напрямую. Узнайте в графе кода, кто его вызывает, и подсчитайте количество таких вызовов.

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