Три месяца назад я наблюдал, как мой агент на Llama 3.1 8B в третий раз спрашивает, как меня зовут.
Я представился в первом сообщении. Двести сообщений назад...
Агент забыл. Не потому что тупой. Потому что контекст переполнился и начало разговора уехало в никуда.
Это был момент, когда я понял: мы неправильно думаем о памяти.
Когда вышел Claude с контекстом на миллион токенов, казалось — проблема решена. Запихиваем всё в контекст, модель помнит всё. Красота.
Потом пришёл счёт за API.
Потом я заметил, что модель с миллионным контекстом всё равно теряет информацию из середины. Есть исследования на эту тему — "Lost in the Middle" называется. Модели хорошо помнят начало и конец, а середина превращается в кашу.
Потом я попробовал запустить такое локально и понял, что моя видеокарта на это не рассчитана.
Локальные модели — это 32K токенов. Иногда 128K, если повезло с квантизацией и памятью. Но даже 128K — это один длинный рабочий день. К вечеру агент забудет, что было утром.
Стандартное решение — обрезать старые сообщения. Или суммаризировать их: сжать историю в пару абзацев и положить в начало.
Я попробовал оба варианта. Оба работают плохо.
Обрезка теряет важное. Суммаризация теряет детали. После трёх циклов сжатия агент помнит, что «работает над проектом», но не помнит над каким.
А потом до меня дошло.
Вспомните, как вы ведёте сложный проект.
Вы не держите все детали в голове. Вы записываете. В Notion, в Obsidian, в текстовый файл, на бумажке. Где-то лежит описание архитектуры. Где-то — список решений и почему их приняли. Где-то — заметки с созвона.
Когда нужно что-то вспомнить — вы ищете. Не в голове. В заметках.
Мозг — это процессор, не жёсткий диск. Хранение мы выносим наружу.
У Борхеса есть рассказ про Фунеса — человека с абсолютной памятью. Он помнил каждую секунду жизни, каждый лист на каждом дереве. Фунес не мог думать. Потому что думать — значит обобщать. Забывать детали, видеть паттерны. Фунес тонул в деталях.
LLM с бесконечным контекстом — это Фунес. Помнит всё подряд, не умеет выбирать важное.
Нам нужна не бесконечная память. Нам нужна правильная память.
Я разделил память агента на три хранилища. Каждое — для своего типа информации.
Первое — быстрые факты. Имя пользователя, название проекта, текущая задача, ключевые решения. То, что нужно часто и быстро. Для этого идеален Redis: хранит данные в оперативной памяти, отвечает за миллисекунды.
Второе — семантический поиск. Когда нужно найти «тот разговор про производительность», но не помнишь, когда он был и как назывался. Текст превращается в вектор — набор чисел, отражающих смысл. Похожие по смыслу тексты дают похожие векторы. Можно искать по близости.
Третье — документы. Архитектурные решения, чеклисты, большие заметки. То, что слишком велико для Redis и слишком структурировано для векторов. Обычные markdown-файлы в папках.
Агент умеет писать во все три хранилища и читать из них. Контекст остаётся маленьким — только последние сообщения. Но память большая.
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 думают, или просто притворяются.
Источник


