Перейти к содержанию

Урок 4: рабочая память код-агента

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

Концепция: чем агент отличается от чат-бота

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

Рабочая память агента должна отвечать не на вопрос «когда это было?», а на вопрос «насколько это важно для текущей задачи?». Именно это делает WorkingMemory из protoprompt.agent.

Концепция: горячая и холодная зоны

Память делится на две зоны:

  • Горячая — то, что агент использует сейчас. Живёт в оперативке.
  • Холодная — то, что вытеснили за ненадобностью. Живёт в store, про него помнит manifest.

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

Практика: читаем проект

Откройте examples/tutorials/04_coder_agent/main.py.

Шаг 1. Создаём память

mem = WorkingMemory(
    store=InMemStore(),          # холодная зона
    llm=llm,
    counter=RegexTokenCounter(),
    max_tokens=600,              # маленький бюджет — чтобы показать вытеснение
)
  • store — куда выселяются элементы. InMemStore — опять же для урока; в бою берите SqliteStore("agent.db").
  • llm — нужен для эмбеддингов: цель и смысл элементов.
  • counter — считает токены элементов.
  • max_tokens — бюджет горячей зоны. 600 токенов — мало, поэтому вытеснение будет видно сразу.

Шаг 2. Ставим цель

await mem.set_goal("добавить функцию count_attempts в tenacity")

Цель превращается в вектор. Теперь каждый элемент получает бонус за смысловую близость к ней — память «держит курс» на задачу.

Шаг 3. Кладём разные виды элементов

await mem.add("file", "# retry.py\n- Retrying", summary="файл retry.py")
await mem.add("edit", "def count_attempts(...)", summary="новая функция")
await mem.note("count_attempts возвращает 0, если атрибута нет")
for i in range(30):
    await mem.add("log", f"debug шаг {i}...", summary=f"лог {i}")

У каждого вида — свой базовый вес:

kind Вес Что это
edit 3.0 Правка агента
note 2.5 Заметка агента
file 1.5 Прочитанный файл
test_result 1.0 Результат тестов
tool_output 0.8 Вывод инструмента
log 0.5 Сырые логи

Поэтому правка и заметка переживают десяток логов.

Шаг 4. Смотрим на вытеснение

Элементов в горячей зоне: 21
Выселено в холодную зону: 12

Пока вы добавляли 30 логов, бюджет кончился, и слабые элементы поехали в холод. При этом edit, note и file остались — они важнее. Заметки note() к тому же закреплены (pinned), их не выселить без спроса.

Шаг 5. Собираем контекст

ctx = await mem.assemble()
print(ctx.render()[:400])

assemble сортирует горячую зону по важности и набивает бюджет max_tokens. В начало контекста попадает то, что важнее всего — правка, заметка, файл. А не свежие логи.

Шаг 6. Возвращаем из холода

restored = await mem.recall("что делает count_attempts?")

recall ищет вытесненное по двум каналам: по символам (идентификаторы в запросе) и по смыслу (векторный поиск). Найденное возвращается в горячую зону как новый элемент.

Скоринг: из чего складывается важность

Балл элемента — сумма пяти слагаемых:

балл = вес_вида
     + вес_ссылок (как часто на элемент ссылались позже)
     + смысл (близость к цели)
     + свежесть (чем старше, тем меньше)
     − размер (чем больше текст, тем больше штраф)

Скоринг не зовёт LLM — всё считается на лету. Особенно интересны ссылки: если агент в новом элементе упомянул имя, определённое в старом (например, count_attempts), старый элемент получает +1. «На что ссылаются — то важно» — сборка мусора по счётчику ссылок, без единого вызова модели.

Задание

  1. Уберите цикл с 30 логами — вытеснений не будет, посмотрите на количество элементов.
  2. Добавьте recall_cooldown_steps=10 в конструктор и сразу после добавления вызовите recall. Вытесненное не вернётся — оно в карантине первые 10 шагов.
  3. Смените цель на что-то далёкое от кода («написать стихотворение») и посмотрите, изменился ли порядок в контексте. Смысловой бонус перестанет помогать правке.
  4. Поменяйте InMemStore() на SqliteStore("agent.db") и перезапустите — холодная зона переживёт перезапуск, горячую можно сохранить через export_state() / import_state().

Итог курса

Поздравляем — вы прошли путь от «где лежит факт» до «что важно для текущей задачи»:

Урок Вы научились
1 Хранить факты и искать по смыслу (store)
2 Сжимать длинный диалог (session)
3 Не платить за повторные эмбеддинги (cache)
4 Различать сигнал и шум в памяти агента (agent)
5 Помнить человека между сессиями (profile)

Теперь вы понимаете, из чего собирается память LLM-приложения. Следующий шаг — общий обзор памяти, где все части показаны в одной картине, и API-справочник, если захочется заглянуть в детали. А урок 5 добавит к картине профиль пользователя.