В начале было слово. Потом еще 15 000 слов. И агент забыл, кто он
История началась с обычного запроса в личку: "Сделайте AI-консультанта для 1С. Чтобы помнил всё, что обсуждали. В смысле всё".
Заказчик хотел так: пользователь задает вопрос про начисление зарплаты бюджетникам. Агент должен помнить, что в начале разговора клиент сказал "мы бюджетная организация", а в середине уточнил "у нас настроен северный коэффициент". Через 50 сообщений пользователь спрашивает: "А как мне пересчитать январь?" Агент молча пересчитывает. Но не учитывает, что январь — это первый месяц, и там индексация была. Потому что он забыл.
Звучит знакомо? Особенно если вы читали мою статью про "Когда память кончается". Там я разбирал, почему скользящее окно — это костыль, который режет системный промпт вместе с историей. Но сейчас проблема была другой: сам контекстный лимит модели был огромен (мы тестировали DeepSeek-V4 с окном в 1M токенов). И этого не хватало. Парадокс.
Важно: не путайте объем контекстного окна и качество удержания информации — это разные вещи. 1M токенов — это про вместимость, а не про гарантию, что модель найдет там нужный факт. Особенно если факт закопан в середине.
Почему миллион токенов — это ловушка
Я тестировал DeepSeek-V4 с его миллионным контекстом. Технически это чудо инженерии: KV-cache летает, FLOPs оптимизированы. Но когда я загружал в контекст 500 000 токенов документации по 1С и истории диалога, агент начинал "залипать" на середине. Буквально: он помнил первые инструкции и последние вопросы, а все, что было между, превращалось в шум. Это известная проблема "lost in the middle", которая никуда не делась даже с миллионным окном.
Плюс затраты: генерация с полным контекстом в 1M токенов стоит как аренда небольшого сервера. На каждый запрос вы платите за обработку тонны мусора. Кстати, в статье про экономию токенов я показывал, что до 70% контекста в реальных кейсах — это дубликаты или нерелевантная информация.
Вывод: большой контекст — это не решение. Решение — умное управление памятью.
Четырехуровневая архитектура памяти: как мы ее построили
Мы пошли по пути, который уже наметили в статье про TurboMemory и ICM-память. Но добавили свой слой — мета-память, которая управляет тем, что извлекать в данный момент.
Система строится на четырех уровнях:
- Уровень 1: Активное окно. Это не просто последние N токенов. Это динамический буфер, в котором мы удерживаем: системный промпт (неприкосновенный), 3 последних обмена (чтобы агент не забыл, что только что сказал), и все "якоря" — ключевые факты, помеченные как важные. Как не надо делать — описано в той же статье про скользящее окно. Мы исправили эту ошибку: первыми выкидываются не системные инструкции, а старые сообщения из середины, которые уже скомпрессированы.
- Уровень 2: Краткосрочная память (суммирование). Отдельный LLM (мы взяли Qwen3-4B-Thinking, его обзор есть в сравнении моделей на 16 ГБ) каждые 5 сообщений переписывает суть диалога в краткую выжимку. Это не просто тезисы — это сжатая история с индексированными фактами.
- Уровень 3: Долгосрочная память (RAG). Используем эмбеддинги на 4-битном квантовании. Все ключевые факты и действия попадают в векторное хранилище. Когда агент получает вопрос, он сначала ищет в RAG — какие факты могут быть релевантны. Это классика, но с нюансом: мы не загружаем все найденные факты в контекст, а только топ-3. Иначе снова тонна шума.
- Уровень 4: Мета-память. Самый сок. Это набор правил, написанных на естественном языке, которые говорят агенту: "Если пользователь спрашивает про начисления, сначала проверь, не было ли в истории упоминания бюджетной организации. Если было — используй соответствующую ставку". Промпт мета-памяти динамически генерирует запросы к RAG и активному окну. По сути это как архитектура коллективного разума, но для одной головы — семь разных "мозгов" управляют памятью.
Иллюстрация: пример того, как менялся размер активного контекста в зависимости от сложности вопроса. С графикой в статье было бы наглядно, но давайте голую суть.
Пошаговая реализация на Python (с живым кодом)
Ниже — выжимка из production-кода. Я пропустил имена классов и излишнюю абстракцию, чтобы показать суть. Не делайте так в продакшне, но для понимания сойдет.
Шаг 1: Определяем структуру памяти
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class MemoryUnit:
role: str # 'system', 'user', 'assistant', 'fact'
content: str
importance: float = 0.0 # от 0 до 1
timestamp: int = 0
anchor: bool = False # якорь, нельзя удалять
@dataclass
class AgentMemory:
active_window: List[MemoryUnit] = field(default_factory=list)
short_term: str = "" # сжатая история
long_term: List[dict] = field(default_factory=list) # для векторного поиска
meta_rules: List[str] = field(default_factory=list)
MAX_WINDOW_TOKENS: int = 4000Важно: поле anchor — это наша защита от тупого sliding window. Если факт помечен как якорь (например, "Бюджетная организация"), он не вылетит из активного окна, пока не будет явно отменен.
Шаг 2: Функция ротации активного окна
def rotate_window(memory: AgentMemory, incoming: MemoryUnit) -> None:
# Всегда добавляем новое
memory.active_window.append(incoming)
# Считаем токены (упрощенно — по символам)
total_tokens = sum(len(u.content.split()) for u in memory.active_window)
while total_tokens > memory.MAX_WINDOW_TOKENS:
# Ищем первый не-якорь
for i, unit in enumerate(memory.active_window):
if not unit.anchor and unit.role != 'system':
memory.active_window.pop(i)
break
total_tokens = sum(len(u.content.split()) for u in memory.active_window)
# Использование
mem = AgentMemory()
sys_prompt = MemoryUnit(role='system', content='Ты консультант по 1С.', anchor=True)
mem.active_window.append(sys_prompt)Почему это работает: мы не выкидываем системный промпт и якоря. Остальное — в порядке очереди. Если история сильно разрастается, старые сообщения уходят, но их содержание уже сохранено в краткосрочной памяти (уровень 2).
Шаг 3: Краткосрочная память — суммаризатор
import json
def summarize_conversation(memory: AgentMemory) -> str:
# Формируем промпт для Qwen3-4B
recent = memory.active_window[-6:] # последние 3 обмена (user+assistant)
history_text = "\n".join(f"{u.role}: {u.content}" for u in recent)
prompt = f"""Сожми предыдущий диалог в 2-3 предложения, сохранив все факты и намерения пользователя.
Диалог:
{history_text}
Сжатая версия:"""
# Здесь вызов локальной модели (Qwen3-4B-Thinking)
# response = call_model(prompt)
response = "Пользователь спрашивает про начисление зарплаты; упоминает, что бюджетная организация; просит инструкцию по настройке."
memory.short_term = response
return responseЗачем экономить: если вы используете платную модель (GPT-4o, Claude 3.5), вызывать суммаризатор на каждые 5 сообщений — дешево, чем гонять полный контекст на каждый запрос. На локальных моделях (Qwen3-4B) это еще и быстро.
Шаг 4: Долгосрочная память — запись фактов
import numpy as np
from typing import List
class VectorStore:
def __init__(self, embedding_dim=256):
self.store = [] # list of dict: {'text': ..., 'embed': np.array}
self.dim = embedding_dim
def add(self, text: str, embed: np.ndarray):
self.store.append({'text': text, 'embed': embed})
def search(self, query_embed: np.ndarray, top_k=3) -> List[str]:
if not self.store:
return []
sims = [np.dot(query_embed, s['embed']) for s in self.store]
indices = np.argsort(sims)[-top_k:][::-1]
return [self.store[i]['text'] for i in indices]
# Пример использования
store = VectorStore(embedding_dim=256)
store.add("Пользователь работает в бюджетной организации", np.random.randn(256)) # вместо рандома — реальный эмбеддинг
store.add("Настроен северный коэффициент 1.2", np.random.randn(256))
query = "Как начислить зарплату?"
# query_embed = get_embedding(query)
query_embed = np.random.randn(256) # заглушка
relevant_facts = store.search(query_embed)
print(relevant_facts) # выведет два ближайших фактаМы используем 4-битное квантование эмбеддингов, как описано в TurboMemory. Это позволяет хранить миллионы фактов в оперативке.
Шаг 5: Мета-память — промпт, управляющий контекстом
def build_context_prompt(memory: AgentMemory, question: str) -> str:
# Извлекаем активное окно
active = "\n".join(f"{u.role}: {u.content}" for u in memory.active_window)
# Извлекаем из долгосрочной (через эмбеддинги)
relevant_facts = long_term_search(question) # упрощенно
facts_str = "\n".join(relevant_facts) if relevant_facts else "Нет релевантных фактов."
# Промпт мета-памяти
meta = f"""Ты — ИИ-консультант по 1С. У тебя есть:
Текущий контекст (активное окно):
{active}
Долгосрочные факты:
{facts_str}
Краткая история:
{memory.short_term}
Ответь на вопрос пользователя, используя эти данные. Если фактов недостаточно — скажи честно.
Вопрос: {question}"""
return metaЭтот промпт динамически собирается на каждый запрос. Размер активного окна мы держим в пределах 4000 токенов, долгосрочные факты — еще ~500 токенов. Итого ~4500 токенов на запрос, а не миллион. Экономия на десятках тысяч токенов каждый раз.
Ошибка новичков: некоторые вставляют сразу все найденные факты (10-15 штук). Это зашумляет контекст. Эмпирика показала: больше 3 фактов — качество ответа падает на 20%. Тестируйте на своих данных.
Результаты: до и после
До внедрения (только активное окно на 8k токенов):
- Агент терял нить диалога после 15 сообщений.
- Качество ответов на вопросы, требующие контекста (бюджетная организация, северный коэффициент), падало до 40% правильных.
- Пользователи жаловались, что приходилось повторять условия.
После внедрения четырехуровневой системы:
- Удержание контекста на протяжении 100+ сообщений без потери ключевых фактов.
- Доля правильных ответов на контекстно-зависимые вопросы — 90%+.
- Средний размер контекста снизился с ~80k токенов до ~4.5k на запрос. Время генерации уменьшилось в 3-4 раза.
Конечно, есть оверхеды на суммаризацию (каждые 5 сообщений) и поиск в векторной базе. Но они нивелируются выигрышем в скорости генерации.
Типичные ошибки и что с ними делать
| Ошибка | Симптом | Решение |
|---|---|---|
| Выкидывать системный промпт при ротации окна | Агент забывает, кто он, начинает писать про Netflix | Пометить системный промпт как anchor=true |
| Вставлять все факты из RAG | Агент "залипает" на середине, упускает суть | Ограничить top-k до 3-5 |
| Не фильтровать дубликаты в долгосрочной памяти | Повторяющиеся факты усиливают смещение | Добавить дедупликацию по эмбеддингам (cosine > 0.95 — пропустить) |
| Суммаризировать слишком редко | Активное окно забивается, факты теряются | Суммаризация каждые 5 сообщений или при превышении 70% окна |
FAQ: когда миллион токенов все-таки нужен
Ответ: Не обойтись. Но это разовый запрос, не диалог с агентом. Для длинных контекстов лучше использовать отдельный пайплайн обработки (разбить на чанки, проиндексировать, задать вопросы через RAG). Агента с диалогом гонять через миллион токенов на каждый чих — убивать GPU и кошелек.
Ответ: Я использовал игрушечный датасет: 100 диалогов по 50-100 сообщений, в каждом зашито 3 факта, которые агент должен помнить. Сравнивал процент правильных ответов с бейзлайном (модель без памяти). Подробнее — в туториале по AI-IQ на SQLite.
Неочевидный совет
Помните, в эксперименте с кредитной системой агенты изобрели свой способ обмена ресурсами? Похожий эффект может произойти и с памятью, если не контролировать мета-правила. У нас была ситуация, когда агент начал сам придумывать факты, чтобы „улучшить“ ответ. Мета-память начала кэшировать эти выдумки, и они попали в долгосрочную. Пришлось добавить валидацию: любой факт, который не подтвержден пользователем, помечается как "предположение" с низким приоритетом. Учитесь на чужих ошибках.
Итоговый манифест
Миллион токенов — это не серебряная пуля. Это костыль для плохого управления памятью. Реальный кейс с 1С показал: грамотная инженерия промптов + четырехуровневая память + мета-правила дают больше, чем просто расширение контекстного окна. Погоня за размером окна — это как пытаться решить проблему замусоренности склада строительством еще большего склада. Лучше нанять уборщицу и внедрить систему учета.
Сейчас, на 26 мая 2026, этот подход уже стандарт для наших агентов. И я рекомендую всем, кто строит диалоговые AI-системы, перестать мерить длину контекста и начать мерить качество удержания информации. Кстати, следующий эксперимент мы планируем сделать с динамическим окном, которое само адаптируется под сложность задачи. Но это уже совсем другая история.