Урок 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. Ставим цель¶
Цель превращается в вектор. Теперь каждый элемент получает бонус за смысловую близость к ней — память «держит курс» на задачу.
Шаг 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. Смотрим на вытеснение¶
Пока вы добавляли 30 логов, бюджет кончился, и слабые элементы поехали в
холод. При этом edit, note и file остались — они важнее. Заметки
note() к тому же закреплены (pinned), их не выселить без спроса.
Шаг 5. Собираем контекст¶
assemble сортирует горячую зону по важности и набивает бюджет
max_tokens. В начало контекста попадает то, что важнее всего — правка,
заметка, файл. А не свежие логи.
Шаг 6. Возвращаем из холода¶
recall ищет вытесненное по двум каналам: по символам (идентификаторы
в запросе) и по смыслу (векторный поиск). Найденное возвращается в
горячую зону как новый элемент.
Скоринг: из чего складывается важность¶
Балл элемента — сумма пяти слагаемых:
балл = вес_вида
+ вес_ссылок (как часто на элемент ссылались позже)
+ смысл (близость к цели)
+ свежесть (чем старше, тем меньше)
− размер (чем больше текст, тем больше штраф)
Скоринг не зовёт LLM — всё считается на лету. Особенно интересны
ссылки: если агент в новом элементе упомянул имя, определённое в старом
(например, count_attempts), старый элемент получает +1. «На что
ссылаются — то важно» — сборка мусора по счётчику ссылок, без единого
вызова модели.
Задание¶
- Уберите цикл с 30 логами — вытеснений не будет, посмотрите на количество элементов.
- Добавьте
recall_cooldown_steps=10в конструктор и сразу после добавления вызовитеrecall. Вытесненное не вернётся — оно в карантине первые 10 шагов. - Смените цель на что-то далёкое от кода («написать стихотворение») и посмотрите, изменился ли порядок в контексте. Смысловой бонус перестанет помогать правке.
- Поменяйте
InMemStore()наSqliteStore("agent.db")и перезапустите — холодная зона переживёт перезапуск, горячую можно сохранить черезexport_state()/import_state().
Итог курса¶
Поздравляем — вы прошли путь от «где лежит факт» до «что важно для текущей задачи»:
| Урок | Вы научились |
|---|---|
| 1 | Хранить факты и искать по смыслу (store) |
| 2 | Сжимать длинный диалог (session) |
| 3 | Не платить за повторные эмбеддинги (cache) |
| 4 | Различать сигнал и шум в памяти агента (agent) |
| 5 | Помнить человека между сессиями (profile) |
Теперь вы понимаете, из чего собирается память LLM-приложения. Следующий шаг — общий обзор памяти, где все части показаны в одной картине, и API-справочник, если захочется заглянуть в детали. А урок 5 добавит к картине профиль пользователя.