Головна / Статті

Чотири типи пам’яті, необхідні вашому кодуючому агенту (і лише один з них реалізований).

Графи коду надають агентам структуру, проте інженерам також потрібні причини, історія та процедури. Розгляд чотирьох типів пам’яті, рівнів впевненості та способів їх поєднання.

Видавець: 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, підтримуваним справжнім перевірювачем типів. Він починає працювати повільно, потребує готової структури проекту та має працювати поза маршрутом запитів, оскільки неробочий мовний сервер значно збільшує затримки. Його слід запускати як фоновий модуль для доповнення інформації.
  • Bytecode відображає код, який існує лише після виконання процесу збірки: запити, створені на основі 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. Кластерування за принципом Лейдена не потребує моделі. Воно ділить граф на вкладені спільноти за критерієм модулярності, групуючи елементи, у яких кількість внутрішніх зв’язків значно перевищує кількість зовнішніх.
    5. Звіти про спільноти — це узагальнення від ШІ для кожної спільноти, створені знизу догори, причому вищі рівні формуються на основі звітів нижчих рівнів.
    6. Ембеддинги створюють вектори, які слугують точками входу для запитів.

    Два терміни заслуговують більш уважного розгляду. «Край» у цьому контексті — це не вказівник, введений користувачем. Він містить опис речення від моделі разом із вагою та посиланнями на джерела, тож кожен такий «край» фіксує інтерпретацію речення моделлю. Спільнота також є чимось більшим, ніж просто довільний підграф, адже випадковий набір вузлів може зовсім не мати спільних «країв», залишаючи ізольовані точки, які неможливо узагальнити. Leiden обирає кластери, у яких індуковані «краї» є щільними, що дозволяє створити послідовний звіт.

    Перевагою є можливість глобального пошуку. Замість отримання фрагментів, найближчих до запиту, система ставить запит до кожного окремого звіту спільноти та поєднує часткові відповіді. Таким чином стає можливим отримання інформації про всю корпусну базу даних: запит про постійні причини збоїв у процесі вирішення проблем має відповідь, яка не міститься в жодному окремому звіті про аналіз причин. Звичайний векторний RAG не може цього зробити, як би точно не налаштовувалися фрагменти.

    Обновлення відбуваються щомісяця, а їх виконання є дорогим: три з шести кроків передбачають виклики LLM, тому витрати на індексацію зростають з розміром корпусу, кількістю проходжень для збору даних та вартістю моделі. Це є прийнятним, оскільки корпус даних майже не змінюється.

    3. Епізодична пам’ять: що відбувалося під час попередніх спроб

    Цей елемент фіксує власну історію агента в цьому конкретному репозиторії. Останнє зміна SettlementService призвела до провалу трьох тестів на розподіл. Зміна цього інтерфейсу без повторної генерації маперів порушує процес збірки. Дві найновіші PR-запити, які проігнорували шар валідації, були відхилені.

    У ньому також є властивість, яку жоден семантичний індекс не може стверджувати: його базова істина є детермінованою. У звіті про аналіз після виникнення проблеми міститься людське твердження, отримане за допомогою моделі. Фраза „Агент змінив X, і CI став червоним“ є оцінкою результату, визначеною системою збірки. Щодо запитань, які вона вирішує, це робить епізодичну пам’ять найнадійнішим джерелом у цій конфігурації.

    Є три аспекти дизайну, у яких легко припуститися помилки:

    • Пропустіть GraphRAG тут. Епізоди розташовані в часовій послідовності та мають певні результати. Групування за щільністю зв’язків — це неправильна структура; потрібно враховувати актуальність, частоту та успішність чи невдачу.
    • Необхідне видалення даних. Семантичні індекси можуть існувати без цього, але цей — ні. Уроки, отримані під час рефакторингу двох архітектур раніше, не лише застарілі, а й ведуть агента неправильним шляхом.
    • Помилки накопичуються. Припустимо, агент прийшов до хибного висновку та записав результат. Цей результат стає запам’ятованим фактом, і кожна наступна сесія успадковує його. Зберігайте результат (перевірка провалена, PR відхилено) з високою достовірністю, а власний висновок агента — з значно нижчою; потім дозвольте висновку зникнути, тоді як результат залишиться.

    Відтік відбувається за кожною сесією.

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

    Процедурна пам’ять охоплює способи виконання завдань: команди створення, перелік перевірок, порядок міграції, стиль роботи. Першим імпульсом людей є зробити це знайденим, але цьому імпульсу слід протистояти.

    Усе, що завжди актуальне, погано піддається пошуку. Це має бути в системному запиті або у файлі AGENTS.md, який завантажується з кожною сесією без пошуку. Розміщення цього матеріалу після етапу пошуку призводить до проблеми, коли агент не знає, де шукати, а сподівання на те, що агент щось знайде, не є ефективним механізмом.

    Усе, що має значення лише в певних ситуаціях, має бути інструментом із спеціальним тригером. Наприклад, запуск набору інтеграцій має бути чимось, що агент може викликати, а не документом, який йому потрібно самостійно шукати.

    Слід уважно стежити за процесом роботи зі знаннями. Припустимо, агент виявляє, що в цьому репозиторії мапери потрібно генерувати заново перед запуском тестів, і ця послідовність продовжує працювати. Це процедура, отримана на основі попередніх прикладів. Збережіть її як кандидата на правило з підрахунком кількості успішних виконань, після того як вона доведе свою ефективність, піднесіть її до категорії завжди завантажуваних інструкцій, а також збережіть початкові приклади, щоб можна було перевірити процес піднесення.

    Отже, процедурна пам’ять — це не четвертий індекс. Це те, що залишається після третього, що вилучається з процесу пошуку та записується у вигляді інструкцій.

    Об’єднання: один простір імен символів

    Три відновлені спогади збігаються за ідентичністю з певним символом, і ніхто ще не розгадав цю частину. Структурні вузли використовують повністю кваліфіковані назви. Епізодичні записи вказують на символ, яким фактично керував агент, тому вони з’єднуються точно. Ентитети в індексі знань — це рядки, які модель виокремила з прози, наприклад "SettlementService", "the settlement service", "the settlement reconciliation path" або "the counterparty SLA constraint", а їхнє відповідність реальним символам — це завдання нечіткого зіставлення.

    Вдача, ніби все інакше, — це саме та проблема, яку мають запобігти ці рівні, тому для з’єднання використовується власна ієрархія:

    • exact: назва зустрічається дослівно та вказує саме на один символ.
    • strong: нормалізований вираз чітко вказує на один клас.
  • weak: поняття, яке охоплює кілька класів із нечіткими межами. Повертається набір символів, і агент їх перевіряє.
  • unresolved: справжня обмеженість, для якої немає відповідних символів ніде. „SLA контрагента“ є справжньою архітектурною обмеженістю, яку код не відображає.
  • Щодо останнього рівня, залиште його, поверніть його та не змушуйте до збігу. Читабельна обмеженість у статусі „unresolved“ краща, ніж неправильне прив’язування символів, а примус до збігу — це спосіб, яким агент впевнено редагує неправильний клас.

    Що насправді запитує агент

    Поверхня запитів може виглядати так:

    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 та комітів із написом „fix bug“ не залишають індексу знань нічого для роботи. Перегляньте, що у вас є, перш ніж починати роботу.
    • Резолюція ентитетів ніколи не стає ідеальною. Лadder допомагає з цим жити, але не усуває проблему. Рівень weak, ймовірно, буде найбільш переповненим, тому плануйте перевірки, а не покладання на надійність.
    • Почніть з обмеженого формату. Розпочніть із tree-sitter та LSP. Додавайте байткод, коли генерований код почне давати неправильні відповіді, та трекери — коли запити передаються між сервісами. Додавайте індекс знань лише тоді, коли корпус вартий індексації.
  • Ніхто не створив усе це повністю — це може бути як можливість, так і попередження. Ці елементи знаходяться в різних спільнотах: фахівці з аналізу коду не читають статті про GraphRAG, фахівці GraphRAG не замислюються над перевірками типів, а ті, хто працює з архітектурою пам’яті, переважно займаються чат-асистентами, а не кодовими базами.
  • Подумайте про інтеграцію нової людини

    Припустимо, у понеділок приєднується старший інженер та отримує доступ до репозиторію, але нічого більше. Він може читати кожен рядок, відстежувати кожен виклик та запускати тести. До п’ятниці він все ще працює повільніше, ніж колега, якого найняли рік тому, і це не пов’язано з його здібностями.

    У колеги з минулого року є три речі, яких ніколи не було в репозиторії, а у новачка їх немає.

    Одна з причин — це причини дивних виборів. Повторна спроба здійснюється тричі через інцидент у березні, а тимчасове рішення для узгодження існує тому, що API партнера погано працює у п’ятницю. Жоден коментар не зазначає цього. Це описано в ADR, у звіті про аналіз після інциденту та у розмовах.

    Інша причина — це історія. Хтось намагався спростити цей інтерфейс у другому кварталі, і тести з розподілом зламалися на тиждень, тому люди залишили все як є.

    Третя причина — це місцевий спосіб роботи. Перед запуском тестів необхідно генерувати мапери заново. Запити PR, які ігнорують перевірку, відхиляються. Ніхто не звертається до документа з цього приводу — так просто працює команда.

    Співробітник, у якого є лише карта коду, — це новачок, найнятий у понеділок: він не має повного уявлення про репозиторій, не розуміє, чому все є саме так, не пам’ятає попередніх спроб та не знає місцевих особливостей. Кожна сесія починається як новий понеділок.

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

    Однорядкова версія

    Коротко: компілятор керує структурою, оскільки структура є детермінованою. Модель відображає намір, оскільки намір — це неструктурований текст. Система збірки ґрунтується на досвіді. Процедура формується на основі досвіду та завантажується заздалегідь, її більше не отримують.

    Жоден інструмент у цій категорії не помиляється щодо жодної з пам’ятей. Кожен з них створює одну пам’ять та зупиняється, і ніщо не поєднує їх між собою. Кращий граф — це не рішення. Рішення полягає у чотирьох пам’ятях, кожна з яких чесно вказує на свою надійність, відображених у єдиному спільному просторі імен символів.

    Терміни та етапи процесу GraphRAG відповідають реалізації від Microsoft Research. Витрати сильно залежать від розміру корпусу даних, налаштувань збору інформації та вибору моделі, тому обов’язково проведіть тестування на власних даних перед прийняттям рішення.

    П’ятнадцятихвилинна перевірка

    Виберіть у своєму кодбазі інтерфейс, який має п’ять або більше відомих реалізаторів, наприклад PaymentProcessor або NotificationChannel; один з них має бути підключений через @Autowired, а не створений безпосередньо. Дізнайтеся у своїй діаграмі коду, хто його викликає, та підрахуйте кількість таких викликів.

    Опубліковані показники для цієї категорії вимірюють кількість токенів, кількість викликів інструментів та час відгуку. Наскільки відомо, жоден з них не перевіряє, чи є повернена діаграма викликів правильною. У кодбазі з оголошеними інтерфейсами та відомими реалізаторами цю перевірку можна провести за п’ять хвилин, порівнюючи з вже наявними даними.