Контекстный хаос: когда ваши агенты начинают жить своей жизнью
Помните ту статью про агентов, которые тупят и забывают инструкции? Молитесь, чтобы она осталась для вас просто интересным чтением. Потому что если ваши LLM-агенты в продакшне начали вести себя странно — поздравляю, вы попали в контекстный ад.
Разработчики приходят ко мне с одинаковыми историями. Вчера агент прекрасно отвечал на вопросы клиентов. Сегодня он начал советовать покупать конкурентов. На тестовых данных всё работает идеально. В продакшне — непредсказуемый хаос. Промпты те же, модели те же, код не менялся. Что пошло не так?
Контекст съел вашу систему. И я сейчас не про token limit.
Если вы думаете, что проблема в длине контекста — вы лечите симптомы, а не болезнь. Настоящая проблема в управлении контекстом.
Почему контекст ломает всё
У LLM нет памяти. Нет состояния. Нет понимания «сессии» в человеческом смысле. Каждый запрос — это новая вселенная, где модель видит только то, что вы ей скормили в этом конкретном промпте.
А теперь представьте агентскую систему. Десять агентов общаются между собой. Каждый добавляет в контекст свою информацию. Через пять итераций контекст превращается в информационный суп, где плавают инструкции, результаты выполнения, промежуточные выводы и случайный шум.
Модель пытается найти паттерны в этом супе. И находит. Только не те, которые вы хотели.
1 Диагностика: ваш контекст болен?
Проверьте эти симптомы:
- Агент «забывает» инструкции, данные в начале диалога
- Качество ответов ухудшается с каждым следующим запросом
- Агенты начинают повторять одни и те же фразы
- Появляются галлюцинации, которых не было в изолированных тестах
- Система становится чувствительной к порядку информации в контексте
Если хотя бы два пункта совпали — у вас проблемы с контекстным инжинирингом. Не с промптами. Не с моделью. С контекстом.
Архитектура контекста: как не утонуть в токенах
Первый и самый очевидный совет — «используйте RAG». Второй и самый бесполезный. Потому что RAG не решает проблему управления контекстом. Он её усугубляет.
Добавляя в контекст результаты поиска по векторной БД, вы увеличиваете энтропию. Теперь в супе плавают не только инструкции агентов, но и случайные куски документации, похожие по embedding'у, но нерелевантные по смыслу.
2 Стратификация контекста: разделяй и властвуй
Нельзя сваливать всё в одну кучу. Контекст нужно структурировать. Я использую четыре слоя:
| Слой | Что содержит | Приоритет |
|---|---|---|
| Системный | Роль агента, основные инструкции, ограничения | Всегда первый, никогда не удаляется |
| Сессионный | Цель текущей сессии, состояние диалога | Обновляется, но не удаляется полностью |
| Оперативный | Результаты последних действий, текущий контекст | Циклический буфер, старые данные удаляются |
| Референсный | Данные из RAG, справочная информация | Фильтруется и сжимается перед добавлением |
Давайте посмотрим, как это работает в коде. Сначала — как НЕ надо делать:
# ПЛОХО: всё в одну кучу
context = [
system_prompt,
*history,
rag_results,
current_question,
"Пожалуйста, ответь на вопрос выше"
]
response = llm.generate(context)
Теперь правильный подход:
# ХОРОШО: стратифицированный контекст
class StratifiedContext:
def __init__(self, llm):
self.llm = llm
self.system_layer = []
self.session_layer = []
self.operational_layer = []
self.reference_layer = []
def add_to_system(self, instruction):
# Системный слой — только самое важное
if len(self.system_layer) > 5:
# Сжимаем старые инструкции
self.compress_system_layer()
self.system_layer.append(instruction)
def add_to_reference(self, rag_chunks):
# Не просто добавляем, а фильтруем и сжимаем
compressed = self.compress_rag_chunks(rag_chunks)
# Оставляем только самые релевантные
self.reference_layer = compressed[:3] # максимум 3 чанка
def build_context(self):
# Собираем слои в правильном порядке
return [
"# СИСТЕМНЫЕ ИНСТРУКЦИИ",
*self.system_layer,
"\n# ТЕКУЩАЯ СЕССИЯ",
*self.session_layer,
"\n# ОПЕРАТИВНЫЙ КОНТЕКСТ",
*self.operational_layer[-5:], # только последние 5
"\n# СПРАВОЧНАЯ ИНФОРМАЦИЯ",
*self.reference_layer
]
Контекстная амнезия и как с ней бороться
Самая большая проблема в агентских системах — потеря контекста между агентами. Агент А что-то узнал, передал результат агенту Б, а тот начинает с чистого листа.
Вы думаете: "Ну так передавайте весь контекст!" Нельзя. Потому что тогда у вас будет interpretation drift на стероидах.
Решение — контекстные снимки (snapshots). Каждый агент создаёт сжатое резюме того, что он узнал. Не сырые данные. Не полный контекст. Резюме.
class Agent:
def create_context_snapshot(self):
"""Создаёт сжатый снимок контекста для передачи другому агенту"""
prompt = f"""
Ты — агент {self.role}.
Ты узнал следующую информацию в ходе текущей задачи:
{self.get_operational_context()}
Создай краткое резюме (максимум 3 предложения) самой важной информации,
которая нужна следующему агенту для продолжения работы.
Игнорируй технические детали, оставь только суть.
"""
summary = self.llm.generate(prompt)
return {
"agent": self.role,
"summary": summary,
"timestamp": time.time()
}
Это работает лучше, чем передача сырого контекста. Почему? Потому что LLM лучше справляются с обработкой структурированных резюме, чем с анализом сырых диалогов.
Токены дорогие: как экономить без потери качества
Каждый токен стоит денег. Каждый лишний токен в контексте увеличивает энтропию. Но просто обрезать контекст — самоубийство.
Вот что работает на практике:
3 Динамическое сжатие контекста
Вместо того чтобы хранить всю историю диалога, используйте LLM для её сжатия. Но не просто "сожми этот текст". Это не работает.
def compress_conversation_history(history, current_focus):
"""
Сжимает историю диалога с учётом текущего фокуса задачи.
:param history: список сообщений [{"role": "user/assistant", "content": "..."}]
:param current_focus: на чём сейчас сфокусирована задача
:return: сжатая история
"""
compression_prompt = f"""
Ты — компрессор контекста. Твоя задача — сократить историю диалога,
сохранив только информацию, релевантную текущему фокусу: {current_focus}
История диалога:
{history}
Инструкции по сжатию:
1. Удали все приветствия, прощания и формальности
2. Сохрани факты, решения и действия
3. Удали повторяющуюся информацию
4. Объедини похожие идеи
5. Максимальная длина результата — 200 слов
Верни только сжатую версию, без дополнительных комментариев.
"""
return llm.generate(compression_prompt)
Этот подход даёт сжатие в 3-5 раз без потери ключевой информации. Но есть нюанс: нужно делать это асинхронно, параллельно с основным выполнением задачи.
Контекстные ловушки: что убивает вашу систему
Есть три смертных греха контекстного инжиниринга. Избегайте их как огня.
Грех 1: Смешивание уровней абстракции. Нельзя класть в один контекст технические инструкции по API и философские размышления о цели проекта. LLM запутается.
Грех 2: Контекстное загрязнение. Когда каждый агент добавляет в контекст свои внутренние мысли, отладочную информацию, промежуточные вычисления. Через 10 итераций контекст превращается в свалку.
Грех 3: Слепая конкатенация. Просто склеивать всё подряд — самый быстрый путь к катастрофе. Если вы делаете context += new_info без разбора — готовьтесь к сюрпризам.
Продакшн-паттерны: что работает в реальных системах
Теория — это хорошо. Но что работает на практике? Вот паттерны, которые я использую в продакшн-системах:
Паттерн 1: Контекстные шлюзы
Перед тем как добавить что-то в контекст, это что-то должно пройти через шлюз. Шлюз решает: добавлять, сжать или отклонить.
class ContextGateway:
def __init__(self, max_tokens_per_layer):
self.max_tokens = max_tokens_per_layer
def evaluate_addition(self, new_content, layer_type):
"""Оценивает, стоит ли добавлять контент в контекст"""
evaluation_prompt = f"""
Оцени релевантность следующего контента для слоя {layer_type}:
Контент: {new_content}
Верни JSON:
{
"should_add": true/false,
"priority": 1-10,
"suggested_compression": "как сжать, если нужно",
"reason": "почему такое решение"
}
"""
decision = llm.generate(evaluation_prompt, parse_json=True)
return decision
Паттерн 2: Контекстные версии
Контекст меняется со временем. То, что было важным час назад, может быть нерелевантным сейчас. Храните версии контекста и умейте откатываться.
class VersionedContext:
def __init__(self):
self.versions = []
self.current_version = 0
def save_version(self, context, metadata):
"""Сохраняет версию контекста"""
snapshot = {
"version": self.current_version,
"context": copy.deepcopy(context),
"metadata": metadata,
"timestamp": time.time(),
"hash": self._calculate_hash(context)
}
self.versions.append(snapshot)
self.current_version += 1
# Храним только последние 10 версий
if len(self.versions) > 10:
self.versions.pop(0)
def rollback(self, version):
"""Откатывается к предыдущей версии контекста"""
for v in self.versions:
if v["version"] == version:
return v["context"]
return None
Паттерн 3: Контекстные теги
Помечайте каждый кусок контекста тегами: "факт", "инструкция", "предположение", "результат". Это помогает фильтровать и реорганизовывать контекст на лету.
def tag_context_chunk(chunk):
"""Автоматически тегирует кусок контекста"""
tagging_prompt = f"""
Проанализируй следующий текст и определи его тип:
Текст: {chunk}
Возможные типы:
- FACT: объективный факт, данные
- INSTRUCTION: инструкция, что делать
- ASSUMPTION: предположение, гипотеза
- RESULT: результат выполнения
- METADATA: мета-информация, не влияющая на задачу
- NOISE: шум, можно удалить
Верни только тип тега.
"""
tag = llm.generate(tagging_prompt).strip()
return {"content": chunk, "tag": tag, "timestamp": time.time()}
Мониторинг и отладка: как понять, что контекст сломался
Вы не можете ждать, пока пользователи пожалуются. Нужно мониторить здоровье контекста в реальном времени.
Метрики, которые я отслеживаю:
- Контекстная энтропия: как быстро растёт беспорядок в контексте
- Коэффициент сжатия: насколько эффективно сжимается история
- Тэг-распределение: какой процент контекста — полезная информация vs шум
- Контекстный дрейф: как меняется семантика контекста со временем
class ContextMonitor:
def calculate_entropy(self, context):
"""Вычисляет энтропию контекста (грубая оценка)"""
# Простая эвристика: чем больше уникальных слов на токен, тем выше энтропия
words = re.findall(r'\w+', str(context))
unique_words = set(words)
if len(words) == 0:
return 0
return len(unique_words) / len(words)
def check_health(self, context):
"""Проверяет здоровье контекста"""
entropy = self.calculate_entropy(context)
metrics = {
"entropy": entropy,
"token_count": self.count_tokens(context),
"tag_distribution": self.get_tag_distribution(context),
"health_score": 0
}
# Вычисляем оценку здоровья
if entropy > 0.8:
metrics["health_score"] -= 30
if metrics["token_count"] > 8000:
metrics["health_score"] -= 20
metrics["health_score"] = max(0, 100 + metrics["health_score"])
return metrics
Локальные модели vs облачные: есть ли разница для контекста?
Да. Огромная. Локальные модели через Ollama часто имеют меньший эффективный контекст, чем заявлено. То, что работает с GPT-4 с контекстом 128K, сломается с локальной моделью на 8K.
Но есть и плюс: с локальными моделями вы можете делать то, что с облачными слишком дорого. Например, постоянная перекомпрессия контекста или множественные проходы для его очистки.
Что делать сейчас: чек-лист на сегодня
- Проверьте, нет ли в вашем контексте смешения уровней абстракции
- Внедрите хотя бы базовую стратификацию (системный слой отдельно от оперативного)
- Начните тегировать контекст — это самый быстрый способ понять, что в нём происходит
- Установите лимиты на каждый слой контекста и соблюдайте их
- Добавьте мониторинг энтропии контекста в ваши дашборды
Контекстный инжиниринг — это не про написание идеальных промптов. Это про управление информационным потоком в системе, которая по своей природе не имеет памяти. Это про создание искусственной памяти там, где её нет. Про поддержание порядка в хаосе токенов.
Самая большая ошибка — думать, что если вы решите проблему контекста один раз, она останется решённой навсегда. Контекстный инжиниринг — это не задача, это процесс. Как уборка в доме, где живут десять хаотичных нейросетей.
Если через месяц ваши агенты снова начнут странно себя вести — не удивляйтесь. Просто вернитесь к этому руководству и проверьте, не нарушили ли вы какие-то принципы. Контекст имеет свойство загрязняться. Даже у самых дисциплинированных LLM.