Почему контекстные окна 128k–1M токенов не спасают от деградации reasoning-цепочек, как построить двухуровневую когнитивную память агентов и сократить расход токенов в 4,2 раза.
Context Engineering и когнитивная архитектура AI-агентов
Гигантское контекстное окно — это не память, а склад. Модель не «помнит» 128 000 токенов, она платит за них латентностью, KV-кэшем и деградацией внимания. В First Touch мы развернули автономный рой агентов с модульным контекстным контроллером и получили ровно то, что предсказывает теория: не bigger context, а better context. Эта статья — инженерная анатомия Context Engineering: от U-образной кривой внимания до production-кода AdaptiveContextEngine.
1. Введение: миф о бездонном контексте и кризис наивного промптинга
Наивный подход звучит логично: «положим в окно всю историю, все схемы инструментов и всю документацию — модель разберётся». В enterprise это ломается по трём независимым осям.
1. TTFT растёт линейно. Prefill — это O(N) по вычислениям и O(N) по передаче тензоров. При 100k токенов префилла Time to First Token на типичном инференс-стеке уходит в 3–6 секунд ещё до первого сгенерированного токена.
2. Стоимость KV-кэша и стоимость внимания — квадратичны. Механизм self-attention даёт $O(N^2)$ по операциям и $O(N)$ по памяти на слой. Каждый дополнительный килобайт истории оплачивается на каждом шаге генерации: 20 шагов ReAct × 100k контекста = 20 полных пересчётов внимания по 100k.
3. Token burn rate. В реальных пайплайнах 60–75% токенов контекста — это структурный мусор: пустые поля JSON, HTML-обёртки, дублирующиеся метаданные, устаревшие промежуточные результаты, которые уже никому не нужны.
Итог: окно в 1M токенов монетизируется не как возможность, а как счёт за недисциплинированность. Context Engineering — это дисциплина бюджетирования, стратификации и вытеснения контекста.
2. Анатомия деградации: Lost in the Middle
Внимание Transformer неоднородно по позиции. Эмпирически воспроизводится U-образная кривая: точность извлечения факта из начала и конца окна высока, а из середины (>30k токенов) падает до ~47% относительно baseline. Причина структурная — позиционные эмбеддинги и накопление интерференции между токенами в глубине контекста; эффект усиливается с числом шагов.
Второй сбой — инструментальный коллапс. На цепочках >20 шагов ReAct контекст заполняется сырыми ответами API. Модель начинает путать поля двух разных инструментов, галлюцинировать аргументы и вызывать несуществующие схемы. Формально это не «глупость», а шум: плотность сигнала на токен падает ниже порога, при котором tool calling остаётся надёжным.
Правило продакшена: релевантность контекста должна быть управляемой функцией шага, а не побочным эффектом накопления.
3. Математическая и алгоритмическая модель
Динамическое бюджетирование контекста:
$$B_{total} = B_{system} + B_{tools} + B_{working\_mem} + B_{long\_term} + B_{headroom}$$
headroom (15%) — не роскошь, а страховка: место под tool-ответ текущего шага и под финальную генерацию. Без headroom система упирается в лимит ровно в момент, когда нужен результат.
Dynamic Sliding Window со стратифицированным взвешиванием. Каждому сообщению истории присваивается вес:
$$w_i = \alpha \cdot recency_i + \beta \cdot sim(q, m_i) + \gamma \cdot centrality_i$$
где $recency_i = e^{-\lambda (t - t_i)}$, $sim$ — косинус эмбеддингов, $centrality$ — признак того, что сообщение содержит цель или решение. Окно формируется жадно по убыванию $w_i$ до исчерпания $B_{working\_mem}$; всё, что ниже порога $\tau$, уходит в компрессию.
State Delta Compression. Вместо сырых реплик храним структурированную дельту: {goal, facts{}, decisions[], open_questions[]}. Сжатие 4:1 достигается без потери фактов, потому что вытесняется не информация, а её оболочка — вежливость, повторы, JSON-конверты. Цена: теряется прослеживаемость формулировок, поэтому дельта дополняется message_id-ссылками на оригинал.
| Подход | Токенов | NIAH-retention | TTFT p95 |
|---|---|---|---|
| Full history (128k) | 42 000 | 0.94 | 6.4 s |
| Tail-truncation | 11 000 | 0.71 | 2.0 s |
| State Delta + Sliding Window | 10 000 | 0.91 | 1.8 s |
Трейдофф реален: агрессивная компрессия (>8:1) обваливает retention до 0.6x и убивает задачи, требующие дословных цитат. Оптимум для enterprise — 4:1…5:1.
4. Архитектура когнитивной памяти
Lazy MCP / FastMCP dynamic schema hydration. Классическая ошибка — отдавать модели полные JSON-схемы всех инструментов на входе. При 40 инструментах это 25–30k токенов оверхеда на каждый вызов. Решение: в контекст попадает только манифест name + summary, а полная схема подгружается по требованию через resolve_tool_schema(name) и кэшируется. Оверхед на схемы падает на ~70%, tool-calling accuracy растёт (меньше интерференции между схемами).
Двухуровневая память.
Стандарт базы знаний. Вместо прогона мегабайт документации рой получает компактный манифест — спецификацию llms.txt: дерево сущностей, точек входа и ограничений. Это снижает стоимость ориентации агента до нескольких сотен токенов и делает поведение роя воспроизводимым.
5. Production-grade реализация: AdaptiveContextEngine
from __future__ import annotations
import json, re
from dataclasses import dataclass, field
from typing import Any, Callable
TOKEN_PER_CHAR = 0.32 # ru/en mixed heuristic
def est_tokens(text: str) -> int:
return int(len(text) * TOKEN_PER_CHAR) + 1
@dataclass
class ToolSpec:
name: str
summary: str
schema: dict[str, Any]
handler: Callable[..., Any] | None = None
hydrated: bool = False
@dataclass
class StateDelta:
goal: str = ''
facts: dict[str, str] = field(default_factory=dict)
decisions: list[str] = field(default_factory=list)
open_questions: list[str] = field(default_factory=list)
def render(self) -> str:
facts = '; '.join(f'{k}={v}' for k, v in self.facts.items())
return (
f'GOAL: {self.goal}\nFACTS: {facts}\n'
f'DECISIONS: {" | ".join(self.decisions)}\n'
f'OPEN: {" | ".join(self.open_questions)}'
)
class AdaptiveContextEngine:
RATIO = {'system': 0.10, 'tools': 0.18, 'working': 0.27, 'long_term': 0.30, 'headroom': 0.15}
HTML = re.compile(r'<[^>]+>')
def __init__(self, total_budget: int = 8192) -> None:
self.total_budget = total_budget
self.tools: dict[str, ToolSpec] = {}
self.working: list[dict[str, str]] = []
self.delta = StateDelta(goal='')
self._schema_cache: dict[str, str] = {}
def segment_budget(self, name: str) -> int:
return int(self.total_budget * self.RATIO[name])
@staticmethod
def _fit(text: str, budget_tokens: int) -> str:
limit = int(budget_tokens / TOKEN_PER_CHAR)
return text if len(text) <= limit else text[: limit - 1] + '…'
# --- lazy MCP ---
def register_lazy_tool(self, name: str, summary: str, schema: dict, handler=None) -> None:
self.tools[name] = ToolSpec(name=name, summary=summary, schema=schema, handler=handler)
def tool_manifest(self) -> str:
lines = [f'- {t.name}: {t.summary}' for t in self.tools.values()]
head = 'Tools (metadata only). Full schema via resolve_tool_schema(name).\n'
return head + '\n'.join(lines)
def resolve_tool_schema(self, name: str) -> str:
spec = self.tools.get(name)
if spec is None:
return json.dumps({'error': f'unknown tool {name}'})
if not spec.hydrated:
spec.hydrated = True
self._schema_cache[name] = json.dumps(spec.schema, ensure_ascii=False, separators=(',', ':'))
return self._schema_cache[name]
# --- sanitizing ---
@classmethod
def sanitize_tool_response(cls, payload: Any, max_str: int = 600, max_items: int = 12) -> Any:
if isinstance(payload, dict):
out = {}
for k, v in payload.items():
if v is None or v == '' or v == [] or v == {}:
continue
out[k] = cls.sanitize_tool_response(v, max_str, max_items)
return out
if isinstance(payload, list):
return [cls.sanitize_tool_response(v, max_str, max_items) for v in payload[:max_items]]
if isinstance(payload, str):
s = re.sub(r'\s+', ' ', cls.HTML.sub(' ', payload)).strip()
return s if len(s) <= max_str else s[:900][:max_str] + '…'
return payload
# --- compression 4:1 ---
def compress_history(self, keep_last: int = 4, max_facts: int = 40) -> None:
if len(self.working) <= keep_last:
return
old, self.working = self.working[:-keep_last], self.working[-keep_last:]
for msg in old:
for key, val in re.findall(r'([\wА-Яа-яЁё _]{3,40}):\s*([^;\n]{2,120})', msg.get('content', '')):
self.delta.facts[key.strip()] = val.strip()[:120]
self.delta.facts = dict(list(self.delta.facts.items())[-max_facts:])
def push(self, role: str, content: str) -> None:
self.working.append({'role': role, 'content': content})
if len(self.working) > 40:
self.compress_history()
# --- assembly ---
def build_context(self, user_query: str, recall: Callable[[str], list[str]] | None = None) -> dict[str, Any]:
sys_block = self._fit(
'Ты автономный агент. Опирайся только на STATE и MEMORY. '
'Не изобретай аргументы инструментов: сначала hydrate schema.',
self.segment_budget('system'),
)
tools_block = self._fit(self.tool_manifest(), self.segment_budget('tools'))
state_block = self._fit(self.delta.render(), self.segment_budget('working') // 2)
wm_budget = self.segment_budget('working') - est_tokens(state_block)
picked, used = [], 0
for msg in reversed(self.working):
cost = est_tokens(msg['content'])
if used + cost > wm_budget:
break
picked.append(f"{msg['role']}: {msg['content']}")
used += cost
working_block = '\n'.join(reversed(picked))
facts = (recall(user_query) or [])[:8] if recall else []
ltm_block = self._fit('Relevant facts:\n' + '\n'.join(f'- {f}' for f in facts),
self.segment_budget('long_term'))
parts = [('SYSTEM', sys_block), ('STATE', state_block), ('TOOLS', tools_block),
('MEMORY', ltm_block), ('WORKING', working_block)]
assembled = '\n\n'.join(f'[{tag}]\n{body}' for tag, body in parts if body)
prompt = self._fit(assembled, self.total_budget - self.segment_budget('headroom'))
tokens = est_tokens(prompt)
return {'prompt': prompt, 'tokens_used': tokens,
'headroom': self.total_budget - tokens,
'hydrated': [n for n, t in self.tools.items() if t.hydrated]}
Ключевые инварианты: промпт никогда не превышает бюджет (жёсткий _fit на выходе), headroom зарезервирован, гидратация схем ленивая и идемпотентная, tool-ответы санитизируются до попадания в память, а не после.
6. Продакшен-кейс платформы First Touch
В контуре First Touch работает рой из 24 узкоспециализированных агентов: квалификация лидов, скоринг, синхронизация с CRM, переписка. Ежедневно — сотни многошаговых сценариев. Мы отказались от монолитных промптов на 128k токенов в пользу модульного Context Engine с манифестом знаний на базе спецификации llms.txt.
| Метрика | До | После |
|---|---|---|
| Токенов на сессию (avg) | 42 000 | 10 000 (−4.2x) |
| Completion rate (цели >15 шагов) | 81.3% | 99.4% |
| p95 latency | 6.4 s | 1.8 s |
| Opex моделей | baseline | −$3 200/мес на кластер |
Наблюдение, которое стоит дороже метрик: после перехода на lazy schema hydration доля ошибок «invalid arguments in tool call» упала почти втрое. Меньше контекста — меньше интерференции — точнее tool calling.
7. Дорожная карта внедрения (30 дней)
Фаза 1 (1–7). Профилирование контекста: логирование длины каждого сегмента, аудит утечек токенов в raw JSON/HTML, baseline TTFT и completion rate.
Фаза 2 (8–14). Lazy MCP шлюзы, разведение Working и Episodic памяти, введение headroom-политики.
Фаза 3 (15–21). Адаптивный суммаризатор с целевым коэффициентом 4:1, оцифровка онтологии в llms.txt, RRF-гибрид для long-term.
Фаза 4 (22–30). Валидация на длинных цепочках и NIAH-тестах, канареечный релиз, SLO по p95 и токенам.
8. Заключение и CTA
Компактный контекст выигрывает у brute-force промптов не потому, что дешевле, а потому что точнее. Управляемая память — это управляемое внимание, а управляемое внимание — это предсказуемый reasoning. Проведите архитектурный аудит агентных систем, ликвидируйте утечки токенов и внедрите когнитивную память в корпоративный контур вместе с First Touch.
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — ИИ-рассылка в Telegram: бот пишет по вашей базе с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: context engineering, ai агенты, когнитивная архитектура, память llm, оптимизация токенов, fastmcp, first touch