Три месяца назад я наблюдал, как мой агент на Llama 3.1 8B в третий раз спрашивает, как меня зовут.Я представился в первом сообщении. Двести сообщений назад...АТри месяца назад я наблюдал, как мой агент на Llama 3.1 8B в третий раз спрашивает, как меня зовут.Я представился в первом сообщении. Двести сообщений назад...А

Мой локальный агент помнит проект лучше меня. Контекст — 32K токенов. Расскажу, как

2026/02/09 22:31
9м. чтение

Три месяца назад я наблюдал, как мой агент на 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 думают, или просто притворяются.

Источник

Возможности рынка
Логотип Ucan fix life in1day
Ucan fix life in1day Курс (1)
$0.0006238
$0.0006238$0.0006238
+33.20%
USD
График цены Ucan fix life in1day (1) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу service@support.mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.