Память проекта: что агенту знать заранее и как это хранить
Модель ничего не помнит между сессиями. Всё, что агент должен знать о проекте, надо где-то хранить и вовремя ему подавать. Разберём, что хранить, как раскладывать и как доставать без лишних токенов.
После урока вы сможете
- объяснить, зачем агенту внешняя память и чем она отличается от окна контекста;
- разложить знания проекта на заметки: правила, решения, справку, грабли;
- выбрать способ выдачи знаний: всё сразу, оглавление или поиск;
- написать простой поиск по заметкам с бюджетом токенов;
- не дать устаревшей заметке сломать работу агента.
Библиотекарь с каталогом
Новому сотруднику не читают вслух всю вики компании. Ему дают оглавление и говорят: «Нужно что-то — найди по каталогу». На столе лежат только книги для текущей задачи.
Как библиотекарь находит ответ
схемаГлавная мысль: хорошая память — это не «запомнить всё». Это быстро найти нужное и не тащить лишнее. Каждая лишняя заметка на столе стоит денег и мешает модели заметить главное.
От простого к сложному
Внешняя память — это папка с текстом и правило, как её читать
Из урока 1 вы знаете: модель не хранит состояние. Закрыли сессию — всё забыто. Окно контекста ограничено. Значит, знания о проекте надо держать снаружи, в обычных файлах. Это и называют Second Brain (второй мозг).
Схема простая. Перед задачей harness читает оглавление, выбирает подходящие заметки и кладёт их в контекст. После задачи агент дописывает новое знание.
// Перед задачей
const index = read("memory/INDEX.md"); // 40 строк: имя + описание
const picked = choose(index, task); // 2–3 заметки по теме задачи
context.push(...picked.map(name => read(`memory/${name}.md`)));
// После задачи
if (learnedSomethingNew) write("memory/new-fact.md", note);
Что хранить и как раскладывать
Пять видов знаний, которые окупаются почти в любом проекте:
- Правила проекта. «Деньги храним в копейках, целым числом». «Миграции только через Doctrine».
- Архитектурные решения. Формат ADR: контекст, решение, последствия. Объясняет «почему так», чтобы агент не «улучшил» обратно.
- Справка. Как запустить проект, тесты, деплой. Команды, которые агент выполнит сам.
- Продуктовый контекст. Кто пользователи, что для бизнеса важно, какие слова нельзя писать клиентам.
- Грабли. Инциденты и их причины: «перезапуск воркера руками обрывает очередь».
Как раскладывать. Одна мысль — один маленький файл. В начале файла — frontmatter: имя, однострочное описание, тип. Отдельный файл-оглавление собирает по строке на каждую заметку. Заметки ссылаются друг на друга через [[имя]].
--- name: money-in-kopecks description: Деньги храним целым числом в копейках, никогда не float type: rule updated: 2026-09-14 --- Все суммы в БД и коде — integer в копейках (1 BYN = 100). Float теряет точность: 0.1 + 0.2 != 0.3. Связано: [[adr-003-money-type]], [[incident-refund-rounding]]
Три способа выдачи знаний агенту:
- Всё сразу. Склеить все заметки в системный промпт. Просто, но дорого. При росте базы важное тонет в шуме.
- Оглавление и подгрузка. Агент видит только индекс и сам открывает нужные файлы. Дёшево, точно, но требует хороших описаний.
- Поиск. Программа находит заметки по тексту задачи и подкладывает их. Работает на больших базах. Может промахнуться.
Пример структуры каталога памяти — как это выглядит на диске
memory/
├── INDEX.md ← оглавление: по строке на заметку
├── rules/
│ ├── money-in-kopecks.md
│ └── migrations-only-doctrine.md
├── decisions/
│ ├── adr-001-symfony-messenger.md
│ └── adr-003-money-type.md
├── howto/
│ ├── run-tests.md
│ └── deploy-backend.md
├── product/
│ └── never-mention-fees.md
└── incidents/
└── 2026-08-worker-restart.md
# INDEX.md - [money-in-kopecks](rules/money-in-kopecks.md) — деньги целым числом в копейках - [deploy-backend](howto/deploy-backend.md) — выкатка только через make up - [adr-001-symfony-messenger](decisions/adr-001-symfony-messenger.md) — очереди на Messenger, не на cron - [2026-08-worker-restart](incidents/2026-08-worker-restart.md) — ручной рестарт воркера обрывает очередь
Пример ADR — контекст, решение, последствия
# ADR-003. Тип для денежных сумм Статус: принято, 2026-03-02 ## Контекст Суммы хранились во float. При возвратах накопились расхождения в 1 копейку. ## Решение Храним суммы целым числом в копейках (BIGINT). В коде — value object Money. ## Последствия + Нет ошибок округления. - При выводе нужно делить на 100. Все старые колонки переводим миграцией. - Внешние API, которые отдают float, переводим в копейки на входе.
AGENTS.md, который отсылает к памяти — точка входа для агента
Файл правил (урок 2) остаётся коротким. Он не пересказывает всё, а говорит, где искать.
# Правила для агента - Перед задачей прочитай memory/INDEX.md и открой заметки по теме. - Заметка — подсказка, а не правда. Перед действием сверь её с кодом. - Если заметка разошлась с кодом — прав код. Исправь заметку и обнови дату. - Новое знание (решение, грабли) — отдельной заметкой и строкой в INDEX.md. - Как запускать тесты и деплой — docs/howto/*.md, не выдумывай команды.
Поиск по смыслу, устаревание и иерархия правды
Поиск по словам находит заметки, где есть слова из запроса. Простой подсчёт совпадений работает плохо: частые слова вроде «код» есть везде. Алгоритм BM25 даёт больший вес редким словам и не любит слишком длинные документы.
Поиск по смыслу решает проблему синонимов. «Деплой» и «выкатка» — разные слова, но смысл один. Текст превращают в эмбеддинг: координаты смысла. Представьте карту, где похожие по смыслу фразы лежат рядом. Поиск — это «найди точки рядом с вопросом». Схему «найти, подложить в контекст, ответить» называют RAG.
Устаревание — главная беда любой базы знаний. Код меняют каждый день, а заметки — редко. Правила гигиены:
- в заметке есть дата обновления;
- перед действием агент сверяет заметку с кодом;
- нашёл расхождение — исправил заметку сразу, а не завёл вторую рядом;
- противоречивые заметки — это баг, его чинят.
BM25 и косинусная близость без страха — для любопытных
BM25 для каждого слова запроса считает два множителя и складывает результат по всем словам:
- IDF (редкость слова): чем в меньшем числе заметок встречается слово, тем оно ценнее. «Doctrine» ценнее, чем «проект».
- TF с насыщением: второе упоминание слова в заметке добавляет меньше, чем первое, десятое — почти ничего. Длинные заметки слегка штрафуются.
score(док, запрос) = Σ IDF(слово) · tf·(k+1) / (tf + k·(1 − b + b·длина/средняя_длина)) обычно k ≈ 1.2, b ≈ 0.75 близость(a, b) = (a · b) / (|a| · |b|) ← косинус угла между векторами смысла, от −1 до 1
Эмбеддинги считает отдельная модель. Векторы хранят в базе с поиском ближайших соседей (например, расширение pgvector для Postgres). На старте проекта часто хватает обычного поиска по словам и хорошего оглавления.
Простой поиск по заметкам в коде — JS, TS, PHP, Python
Подсчёт совпавших слов без стоп-слов. Не BM25, но для базы из сотни заметок уже полезно.
import { readdirSync, readFileSync } from "node:fs";
const STOP = new Set(["и", "в", "на", "как", "что", "по", "с", "для", "не"]);
const words = s => s.toLowerCase().match(/[a-zа-яё0-9]+/g)?.filter(w => !STOP.has(w)) ?? [];
function search(dir, query, limit = 3) {
const q = new Set(words(query));
return readdirSync(dir)
.filter(f => f.endsWith(".md"))
.map(f => {
const text = readFileSync(`${dir}/${f}`, "utf8");
const score = new Set(words(text).filter(w => q.has(w))).size;
return { file: f, score };
})
.filter(r => r.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
console.log(search("memory", "как выкатить бэкенд на прод"));
import { readdirSync, readFileSync } from "node:fs";
interface Hit { file: string; score: number }
const STOP = new Set(["и", "в", "на", "как", "что", "по", "с", "для", "не"]);
const words = (s: string): string[] =>
(s.toLowerCase().match(/[a-zа-яё0-9]+/g) ?? []).filter(w => !STOP.has(w));
export function search(dir: string, query: string, limit = 3): Hit[] {
const q = new Set(words(query));
return readdirSync(dir)
.filter(f => f.endsWith(".md"))
.map((file): Hit => {
const text = readFileSync(`${dir}/${file}`, "utf8");
return { file, score: new Set(words(text).filter(w => q.has(w))).size };
})
.filter(h => h.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
<?php
const STOP = ['и', 'в', 'на', 'как', 'что', 'по', 'с', 'для', 'не'];
function words(string $s): array
{
preg_match_all('/[a-zа-яё0-9]+/u', mb_strtolower($s), $m);
return array_values(array_diff($m[0], STOP));
}
function search(string $dir, string $query, int $limit = 3): array
{
$q = array_unique(words($query));
$hits = [];
foreach (glob($dir . '/*.md') as $file) {
$score = count(array_intersect($q, array_unique(words(file_get_contents($file)))));
if ($score > 0) {
$hits[] = ['file' => basename($file), 'score' => $score];
}
}
usort($hits, fn ($a, $b) => $b['score'] <=> $a['score']);
return array_slice($hits, 0, $limit);
}
print_r(search(__DIR__ . '/memory', 'как выкатить бэкенд на прод'));
import re
from pathlib import Path
STOP = {"и", "в", "на", "как", "что", "по", "с", "для", "не"}
def words(s: str) -> list[str]:
return [w for w in re.findall(r"[a-zа-яё0-9]+", s.lower()) if w not in STOP]
def search(folder: str, query: str, limit: int = 3) -> list[tuple[str, int]]:
q = set(words(query))
hits = []
for path in Path(folder).glob("*.md"):
score = len(q & set(words(path.read_text(encoding="utf-8"))))
if score:
hits.append((path.name, score))
return sorted(hits, key=lambda h: -h[1])[:limit]
print(search("memory", "как выкатить бэкенд на прод"))
Память вымышленного проекта Shopik
Shopik — бэкенд небольшого интернет-магазина на Symfony. У него есть база из 16 заметок. На ней и поработаем.
Карта знаний
интерактив 1 из 4Нажмите на заметку. Увидите её текст, на кого она ссылается и кто ссылается на неё. Так агент переходит от одной заметки к соседним.
Поиск и бюджет контекста
интерактив 2 из 4Введите вопрос. Поиск упрощённый, похож на BM25: редкие слова весят больше. Окончания грубо обрезаются до 5 букв. Ползунок задаёт, сколько токенов мы готовы отдать памяти. Что не влезло — агент не увидит.
Почему «деплой» ничего не нашёл — откройте после пробы
В заметках написано «выкатка» и «make up», слова «деплой» нет. Поиск по словам не знает, что это синонимы. Варианты лечения: добавить синонимы в описание заметки, держать хорошее оглавление, чтобы агент выбирал сам, или включить поиск по смыслу (эмбеддинги).
Заметка говорит одно, код — другое
интерактив 3 из 4Задача агенту: «Добавь поле deliveryDate в ответ GET /api/orders/{id}». Заметка написана полгода назад. Переключите режим и пройдите по шагам.
Память: orders-serialization.md
Код проекта
Всё сразу, оглавление или поиск
интерактив 4 из 4Пять вопросов агенту по базе Shopik на 60 заметок. Выберите стратегию и посмотрите, сколько токенов она съела и сколько ответов верны. Цифры учебные, но соотношение типичное.
| Вопрос | Результат | Почему |
|---|
Песочницы с автопроверкой
Код выполняется прямо в браузере. Нажмите «Запустить» или Ctrl+Enter. Под выводом появятся проверки, как в юнит-тестах.
Песочница 1. Разберите заметку
JSНапишите parseNote(md). Если файл начинается со строки ---, до следующей --- идут строки ключ: значение. Значение — всё после первого двоеточия, без пробелов по краям. Остальное — тело. Из тела достаньте ссылки [[имя]]: без повторов, в порядке появления. Нет frontmatter — meta пустой объект, тело — весь текст.
Песочница 2. Поиск с бюджетом токенов
JSНапишите search(notes, query, budget). Заметки и стоп-слова уже загружены: переменные NOTES и STOP, функция tokensOf(note) оценивает размер заметки. Правила:
- Слова: только буквы и цифры, в нижнем регистре, без стоп-слов.
- Очки заметки — сколько разных слов запроса встречается в её
name,descriptionилиbody. - Заметки с нулём очков не берём. Сортировка по очкам убыванием, при равенстве — по имени.
- Идём по списку и берём заметку, если она влезает в оставшийся бюджет. Не влезла — пропускаем и пробуем следующую.
- Вернуть массив имён.
Что посмотреть в заметках — данные песочницы
В NOTES восемь заметок: deploy-backend, worker-restart, run-tests, money-in-kopecks, adr-001-messenger, docker-local, never-mention-fees, refund-rounding. Напечатайте console.log(NOTES), чтобы увидеть всё.
Песочница 3. Проверка ADR перед сохранением
JSАгент пишет новый ADR. Прежде чем сохранить, harness проверяет формат. Напишите checkAdr(md): вернуть список недостающих частей. Нужны строка Статус: и разделы ## Контекст, ## Решение, ## Последствия. Раздел считается пустым, если под заголовком нет текста до следующего заголовка — тогда ошибка "пусто: Решение".
Проверьте себя
Зачем агенту внешняя память, если у модели огромное окно контекста?
Окно живёт один запрос. Новая сессия начинается с нуля. Память в файлах переживает сессии, а выдача по задаче экономит токены.
Заметка говорит «сериализатор — JMS», а в коде Symfony Serializer. Что должен сделать агент?
Код — источник правды выше памяти. Расхождение — баг заметки. Его чинят сразу, а не копят вторую заметку рядом.
Почему плохо просто склеить все 300 заметок в системный промпт?
Помните урок 1: история отправляется на каждом шаге, и эффект «потерянной середины». Оглавление или поиск дают те же знания за долю токенов.
Что такое эмбеддинг простыми словами?
Поэтому поиск по эмбеддингам находит «выкатку» по запросу «деплой»: слова разные, смысл рядом.
Задание на своём проекте — 30 минут
- Создайте в репозитории папку
memory/(илиdocs/agent/). - Напишите пять заметок с frontmatter: как запустить проект, как запустить тесты, одно правило, одно архитектурное решение в формате ADR, одни грабли из вашей практики.
- Соберите
INDEX.md: по строке на заметку, с хорошим описанием. Проверьте: по одной строке понятно, когда открывать заметку? - Добавьте в файл правил агента три строки: «прочитай INDEX.md», «сверяй заметку с кодом», «нашёл расхождение — исправь заметку».
- Дайте агенту задачу, где пригодится одна из заметок. Посмотрите, открыл ли он её.
Отметьте, что усвоили
- Понимаю, зачем агенту внешняя память и почему окна контекста недостаточно.
- Знаю пять видов знаний: правила, решения (ADR), справка, продуктовый контекст, грабли.
- Умею писать заметку: одна мысль, frontmatter, ссылки на соседей.
- Могу сравнить три способа выдачи: всё сразу, оглавление, поиск.
- Понимаю разницу между поиском по словам и по смыслу (эмбеддинги).
- Знаю иерархию правды: человек, код и тесты, спецификации, документация, память.
- Решил песочницы: разбор заметки и поиск с бюджетом.
- Знаю, что делать с устаревшей заметкой: исправить сразу и обновить дату.
Что почитать и где попробовать
Материалы на русском отмечены зелёной меткой. Все ссылки проверены 18 октября 2026 года.
Почитать
- Как Claude запоминает ваш проектофициально: CLAUDE.md, правила и автопамять, что и когда грузится. рус., офиц.
- Немного о подходе Architecture Decision Recordsзачем ADR (записи архитектурных решений) и из чего состоит запись. рус.
- Architectural Decision Records (ADRs)шаблоны ADR и инструменты для ведения журнала решений. англ.
- Что такое RAG-система? Полный разбор от теории до продакшенаRAG по частям: чанкинг, эмбеддинги, векторный поиск, генерация. рус.
- У Claude Code уже есть такая память. Тогда зачем я держу свою?память агента на markdown-файлах вместо RAG: что стоит помнить и почему. рус.
Песочницы по теме
- ChunkVizвставить текст и посмотреть, как разные способы нарезки делят его на чанки для RAG.
- Embedding Projectorувидеть, как слова с похожим смыслом оказываются рядом в векторном пространстве.
- Mermaid Live Editorнарисовать схему для документации текстом, который агент тоже может читать и править.
Запустить код из урока
- TypeScript PlaygroundJS и TS из примеров урока: вставить и запустить кнопкой Run.
- 3v4l.orgPHP-примеры урока на любой версии PHP.
- JupyterLitePython-примеры урока прямо в браузере, без установки.