Три месяца назад я наблюдал, как мой агент на Llama 3.1 8B в третий раз спрашивает, как меня зовут.
Я представился в первом сообщении. Двести сообщений назад...
Агент забыл. Не потому что тупой. Потому что контекст переполнился и начало разговора уехало в никуда.
Это был момент, когда я понял: мы неправильно думаем о памяти.
Почему большие контексты — это ловушка
Когда вышел Claude с контекстом на миллион токенов, казалось — проблема решена. Запихиваем всё в контекст, модель помнит всё. Красота.
Потом пришёл счёт за API.
Потом я заметил, что модель с миллионным контекстом всё равно теряет информацию из середины. Есть исследования на эту тему — "Lost in the Middle" называется. Модели хорошо помнят начало и конец, а середина превращается в кашу.
Потом я попробовал запустить такое локально и понял, что моя видеокарта на это не рассчитана.
Локальные модели — это 32K токенов. Иногда 128K, если повезло с квантизацией и памятью. Но даже 128K — это один длинный рабочий день. К вечеру агент забудет, что было утром.
Стандартное решение — обрезать старые сообщения. Или суммаризировать их: сжать историю в пару абзацев и положить в начало.
Я попробовал оба варианта. Оба работают плохо.
Обрезка теряет важное. Суммаризация теряет детали. После трёх циклов сжатия агент помнит, что «работает над проектом», но не помнит над каким.
А потом до меня дошло.
Мы сами так не работаем
Вспомните, как вы ведёте сложный проект.
Вы не держите все детали в голове. Вы записываете. В Notion, в Obsidian, в текстовый файл, на бумажке. Где-то лежит описание архитектуры. Где-то — список решений и почему их приняли. Где-то — заметки с созвона.
Когда нужно что-то вспомнить — вы ищете. Не в голове. В заметках.
Мозг — это процессор, не жёсткий диск. Хранение мы выносим наружу.
У Борхеса есть рассказ про Фунеса — человека с абсолютной памятью. Он помнил каждую секунду жизни, каждый лист на каждом дереве. Фунес не мог думать. Потому что думать — значит обобщать. Забывать детали, видеть паттерны. Фунес тонул в деталях.
LLM с бесконечным контекстом — это Фунес. Помнит всё подряд, не умеет выбирать важное.
Нам нужна не бесконечная память. Нам нужна правильная память.
Три типа памяти
Я разделил память агента на три хранилища. Каждое — для своего типа информации.
Первое — быстрые факты. Имя пользователя, название проекта, текущая задача, ключевые решения. То, что нужно часто и быстро. Для этого идеален Redis: хранит данные в оперативной памяти, отвечает за миллисекунды.
Второе — семантический поиск. Когда нужно найти «тот разговор про производительность», но не помнишь, когда он был и как назывался. Текст превращается в вектор — набор чисел, отражающих смысл. Похожие по смыслу тексты дают похожие векторы. Можно искать по близости.
Третье — документы. Архитектурные решения, чеклисты, большие заметки. То, что слишком велико для Redis и слишком структурировано для векторов. Обычные markdown-файлы в папках.
Агент умеет писать во все три хранилища и читать из них. Контекст остаётся маленьким — только последние сообщения. Но память большая.
Реализация: факты в Redis
Redis — стандартная штука. Если не работали с ним раньше — это база данных «ключ-значение» в оперативной памяти. Запустить можно через Docker одной командой, или установить локально.
import redis import json from datetime import datetime class FactMemory: def __init__(self): self.redis = redis.Redis( host='localhost', port=6379, decode_responses=True ) def remember(self, key: str, value: str): """Сохранить факт.""" data = { "value": value, "updated_at": datetime.now().isoformat() } self.redis.hset("agent:facts", key, json.dumps(data)) def recall(self, key: str) -> str | None: """Вспомнить факт.""" raw = self.redis.hget("agent:facts", key) if raw: return json.loads(raw)["value"] return None def all_facts(self) -> dict: """Все факты для отладки.""" raw = self.redis.hgetall("agent:facts") return {k: json.loads(v)["value"] for k, v in raw.items()}
Использование тривиальное:
memory = FactMemory() memory.remember("user_name", "Алексей") memory.remember("project", "backend-api") memory.remember("db", "PostgreSQL") # После перезапуска, через неделю: name = memory.recall("user_name") # "Алексей"
Данные переживают перезапуск агента. Переживают перезагрузку сервера, если включить persistence в Redis.
Реализация: семантический поиск
Для векторного поиска использую ChromaDB. Можно FAISS, можно Qdrant, можно Milvus — принцип одинаковый. ChromaDB выбрал за простоту: работает локально, не требует настройки, сохраняет на диск.
Для превращения текста в векторы — sentence-transformers. Модель intfloat/multilingual-e5-base понимает русский и занимает ~400MB.
import chromadb from sentence_transformers import SentenceTransformer import hashlib import time class SemanticMemory: def __init__(self, path: str = "./chroma_db"): self.client = chromadb.PersistentClient(path=path) self.collection = self.client.get_or_create_collection("memories") self.encoder = SentenceTransformer('intfloat/multilingual-e5-base') def store(self, text: str, metadata: dict = None): """Сохранить текст с возможностью поиска по смыслу.""" embedding = self.encoder.encode(text).tolist() doc_id = hashlib.md5(text.encode()).hexdigest()[:16] self.collection.add( ids=[doc_id], embeddings=[embedding], documents=[text], metadatas=[metadata or {"timestamp": time.time()}] ) def search(self, query: str, n_results: int = 3) -> list[str]: """Найти похожие по смыслу записи.""" query_embedding = self.encoder.encode(query).tolist() results = self.collection.query( query_embeddings=[query_embedding], n_results=n_results ) return results['documents'][0] if results['documents'] else []
Пример:
semantic = SemanticMemory() # Сохраняем обсуждения semantic.store("Выбрали PostgreSQL вместо MongoDB, потому что нужны транзакции") semantic.store("Проблема с производительностью на эндпоинте /users — добавили индекс") semantic.store("Пользователь просит использовать TypeScript везде") # Ищем по смыслу results = semantic.search("почему не взяли монгу?") # Находит: "Выбрали PostgreSQL вместо MongoDB, потому что нужны транзакции"
Обратите внимание: запрос «почему не взяли монгу» находит текст про «PostgreSQL вместо MongoDB». Это не поиск по ключевым словам. Это поиск по смыслу.
Реализация: файлы
Для больших документов — обычная файловая система. Markdown-файлы в папках.
from pathlib import Path class FileMemory: def __init__(self, base_path: str = "./agent_notes"): self.base = Path(base_path) self.base.mkdir(exist_ok=True) def write(self, folder: str, name: str, content: str): """Записать документ.""" path = self.base / folder path.mkdir(exist_ok=True) (path / f"{name}.md").write_text(content) def read(self, folder: str, name: str) -> str | None: """Прочитать документ.""" path = self.base / folder / f"{name}.md" return path.read_text() if path.exists() else None def list_docs(self, folder: str) -> list[str]: """Список документов в папке.""" path = self.base / folder return [f.stem for f in path.glob("*.md")] if path.exists() else []
Структура получается человекочитаемой:
agent_notes/ ├── architecture/ │ ├── database.md │ └── api.md ├── decisions/ │ └── typescript.md └── context/ └── project.md
Можно открыть любой файл руками и посмотреть, что агент думает о проекте. Это удобно для отладки.
Собираем агента
Теперь главное — научить агента пользоваться этой памятью.
Я добавляю в системный промпт инструкции и специальные команды. Агент пишет команды в ответе, я их парсю и выполняю.
SYSTEM_PROMPT = """Ты — ассистент с внешней памятью. У тебя есть три хранилища: 1. Факты — быстрый доступ по ключу 2. Семантика — поиск по смыслу 3. Документы — структурированные заметки Команды (пиши прямо в ответе): [SAVE_FACT key="..." value="..."] — запомнить факт [GET_FACT key="..."] — вспомнить факт [SEARCH_MEMORY query="..."] — поиск по смыслу [SAVE_DOC folder="..." name="..." content="..."] — записать документ [READ_DOC folder="..." name="..."] — прочитать документ Когда запоминать: - Имя пользователя и его предпочтения - Решения и их причины - Технические детали проекта Когда искать: - Пользователь ссылается на прошлое ("как мы решили", "тот баг") - Ты не уверен в чём-то, что обсуждали раньше ВАЖНО: Не выдумывай. Если не помнишь — поищи или спроси. """
Парсер команд:
import re def execute_commands(response: str, facts: FactMemory, semantic: SemanticMemory, files: FileMemory) -> str: """Выполнить команды памяти и вернуть очищенный ответ.""" # [SAVE_FACT key="..." value="..."] for match in re.finditer(r'\[SAVE_FACT key="([^"]+)" value="([^"]+)"\]', response): facts.remember(match.group(1), match.group(2)) # [SEARCH_MEMORY query="..."] for match in re.finditer(r'\[SEARCH_MEMORY query="([^"]+)"\]', response): results = semantic.search(match.group(1)) # Результаты можно добавить в следующий промпт # [SAVE_DOC folder="..." name="..." content="..."] pattern = r'\[SAVE_DOC folder="([^"]+)" name="([^"]+)" content="([^"]+)"\]' for match in re.finditer(pattern, response): files.write(match.group(1), match.group(2), match.group(3)) # Убираем команды из ответа пользователю clean = re.sub(r'\[(?:SAVE_FACT|GET_FACT|SEARCH_MEMORY|SAVE_DOC|READ_DOC)[^\]]+\]', '', response) return clean.strip()
Перед каждым запросом к модели я собираю контекст из памяти:
def build_context(user_message: str, facts: FactMemory, semantic: SemanticMemory) -> str: """Собрать контекст из памяти для текущего запроса.""" context_parts = [] # Базовые факты — нужны почти всегда known_facts = facts.all_facts() if known_facts: facts_str = "\n".join(f"- {k}: {v}" for k, v in known_facts.items()) context_parts.append(f"Известные факты:\n{facts_str}") # Семантически релевантные воспоминания relevant = semantic.search(user_message, n_results=3) if relevant: memories_str = "\n".join(f"- {m[:200]}" for m in relevant) context_parts.append(f"Релевантные воспоминания:\n{memories_str}") return "\n\n".join(context_parts)
Что это даёт на практике
Сценарий: вы работаете с агентом над проектом неделю. Каждый день — десятки сообщений.
Без внешней памяти: к третьему дню агент забывает имя, к пятому — забывает проект. На вопрос «почему мы выбрали PostgreSQL?» начинает выдумывать.
С внешней памятью: неделю спустя агент помнит имя, проект, ключевые решения. На вопрос про PostgreSQL достаёт из семантической памяти запись первого дня и цитирует реальные причины.
Бонус: агент работает быстрее. Контекст маленький — 20-30 последних сообщений вместо пятисот. Модели легче, инференс быстрее.
Ещё бонус: можно посмотреть, что агент «помнит». Файлы читаемые, Redis можно залезть посмотреть. Это сильно помогает в отладке.
Грабли, на которые я наступил
Агент не всегда использует память. Иногда игнорирует инструкции и отвечает сразу. Особенно на простых вопросах.
Частично помогает снижение temperature до 0.3-0.5. Частично — более строгие инструкции. Полностью не решается.
Мусор накапливается. Через месяц в памяти сотни записей, половина устарела. Нужно периодически чистить.
Я удаляю записи старше 30 дней, к которым не обращались. Грубо, но работает. Хорошего решения пока нет.
Конфликты. Если в фактах написано «db: PostgreSQL», а в семантике нашлось «решили переходить на MongoDB» — что делать?
Пока никак. Последнее побеждает. Нужна версионность, но я её не сделал.
Encoding-модель занимает память. sentence-transformers держит модель в GPU. Если у вас и так мало VRAM — это проблема.
Можно использовать CPU для кодирования (медленнее, но работает). Можно взять модель поменьше. Можно вынести в отдельный сервис.
Сколько это стоит по ресурсам
На моём сервере (RTX 4090, 64GB RAM):
Redis: ~50MB RAM, latency <2ms
ChromaDB + модель для эмбеддингов: ~2GB RAM, ~1GB VRAM, latency ~100ms на поиск
Файловая система: зависит от размера, latency ~5ms
На фоне инференса 8B-модели (2-5 секунд на запрос) — незаметно.
Если VRAM мало — эмбеддинги можно считать на CPU. Будет ~300-500ms вместо 100ms, всё ещё терпимо.
Философское отступление
Мы привыкли думать, что память — это хранилище. Положил, достал. Но человеческая память работает иначе.
Каждое воспоминание — реконструкция. Мы не проигрываем запись, мы создаём её заново каждый раз. Поэтому воспоминания меняются. Поэтому свидетели одного события помнят его по-разному.
LLM с гигантским контекстом — это магнитофон. Точная запись, но лента конечна.
LLM с внешней памятью — ближе к человеку. Неточно, избирательно, с интерпретацией при извлечении. Зато масштабируется.
Может, это и есть правильный путь. Не делать идеальный магнитофон, а делать систему, которая умеет забывать неважное и вспоминать важное.
Что дальше
Это базовая версия. Дальше хочу попробовать:
Автоматическое решение, что запоминать. Сейчас агент сам решает. Иногда решает плохо. Возможно, нужен отдельный классификатор важности.
Коллективную память. Несколько агентов пишут в общую базу. Учатся на опыте друг друга. Там должны быть интересные эмерджентные эффекты.
Умное забывание. Не по времени, а по важности и частоте использования. Spaced repetition наоборот: что не используешь — забывай.
Если тема интересна — пишите в комментариях, какие аспекты разобрать подробнее. И расскажите, как вы решаете проблему памяти в своих агентах. Наверняка есть подходы, о которых я не знаю.
Если хотите ещё про внутренности агентов, то пишу про такое в токены на ветер — иногда о том, как LLM думают, или просто притворяются.
Комментарии (71)

Oldweedkeeper
09.02.2026 17:57Зачем это нужно, когда тебе буквально иногда нужно поменять чат и очистить контекст, чтобы агент начал лучше работать

ScriptShaper Автор
09.02.2026 17:57Очистить контекст — это как перезагрузить компьютер, когда тормозит. Помогает, только ты теряешь всё. Через неделю работы над проектом «поменять чат» — это потерять неделю контекста. Внешняя память, как раз, чтобы этого не делать.

SurdLen
09.02.2026 17:57@Oldweedkeeper написал про другую ситуацию, которая требует очистки контекста и нового чата. Если длительное время общаться с моделью, и при этом у модели возникла проблема с пониманием, она начинает галлюцинировать и фантазировать вместо нормальных ответов. Это периодически происходит по разным причинам, например, модель была недостаточно обучена на нужных в проекте примерах кода, или код при обучении был для другой версии, или код был другого стиля, или когда в вашем коде больше двух косвенно связанных ошибок, или контекст модели переполнили противоречивой информацией. Вы можете наблюдать это, когда агент модели начинает совершать совсем глупые логические ошибки, или начинает забивать код огромными кусками копипасты, или начинает странные правки и их отмены, или начинает вырезать без спроса важные непонятные ему куски кода, или начинает править в куче разных мест, хотя явно просили этого не делать, или начинает выдавать очень длинные рассуждения, где спорит сам с собой о приоритетах в немного противоречивых вещах, в которых запустался.
P.S.: Это не только про ту ситуацию, когда вы принудитлеьно очищаете старую память старше месяца. Таких ситуаций намного больше.

janvarev
09.02.2026 17:57Добрый день!
А почему сделали свое решение с командами, а не воспользовались стандартным механизмом tools для LLM?
Из современных его поддерживающих локально могу выделить например Ministral 3B (да и вроде вся серия, но 3B руками пробовал)
Bobos
Агент - это система, которая самостоятельно ставит цели, планирует, действует с помощью инструментов и оценивает результаты. Странно, что агент был такой глупенький, что забыл название проекта. Может это не агент, а чат с расширенной rag-системой? Это же база для построения агентов, и этого всё и начинается.
ScriptShaper Автор
Формально — да, это не агент в академическом смысле. Это чат с памятью. Но давайте честно: 90% того, что сейчас называют «агентами» — это чат с инструментами. Я использую термин так, как его использует индустрия, а не учебник. А суть статьи — не в терминологии, а в архитектуре памяти. Она работает одинаково, назови хоть агентом, хоть RAG-ом, хоть Василием
Bobos
Большинство всё-таки называют агентами Василича, который берет задачу, пишет код, запускает его, фиксит ошибки и коммитит. Если бы это была разровая статья - ок, но вы вроде как решили глубоко нырнуть в тему, хотелось бы в тележке получать годноту ;)
Motin
Я рекомендую вам сесть и написать агента руками - окажется что это всё тот-же чат (LLM) с инструментами и не более того
Bobos
Спасибо за рекомендацию, садился, писал. Правда начал с памяти, потому что сразу понимал ограничения контекстного окна. Запуск кода, чтение ошибок и коммит - это интсрументы агента, все они бессмысленны без качественно составленного промпта. А промт - это и есть грамотное использование "памяти", с которой всё и должно начинаться.
Чат - это способ взаимодействия с ллм, когда вы скидываете несколько сообщений, в промежутках получая ответ, что формирует контекст. для следующих ответов. Не имеет смысла писать агентов на основе чатов, под капотом всё равно обработка одного запроса с заранее сформированым контекстом.
Motin
Тогда не понимаю ваш комментарий к посту, раз вы в курсе что для LLM всегда есть один запрос - один ответ и нет никакой истории, есть только аккуратно составленный запрос (контекст)
Bobos
Посмотрите, как пост начинается
По мне это признаки не агента, который на каждое действие получает четкую инструкцию, а чата с ллм.