Как 3-5 слов-меток вместо 50 строк инструкций помогают LLM понимать жанр запроса, настроение пользователя и уровень собственной уверенности. Сравниваю с «Системой 1» Канемана. В прошлой статье про STM + МНЕМОС цикл сообщения был без этого слоя — сейчас покажу, где он реально стоит и что берёт из памяти.
Это четвёртая статья из серии «Как я собрал self-improving AI-ассистента». Предыдущие:
Проблема: промпт раздувался, LLM тонула
После STM + МНЕМОС ассистент уже помнил: оперативная память держала тему и цифры внутри диалога, долгосрочная — училась между диалогами. Но была другая беда: каждый запрос тянул за собой простыню инструкций.
пользователь → 5000 символов промпта → LLM генерирует ответ
Промпт рос: «не галлюцинируй», «будь вежливой», «проверяй цифры», «уточняй если не уверена», «используй женский род», «сначала проверь календарь», «для валют используй ЦБ РФ»... Правил становилось больше, LLM путалась, ответы становились «серединой по больнице».
А главное — LLM не знала контекста, который человек улавливает мгновенно:
- Это знакомая тема или я спрашиваю впервые?
- Пользователь злится или в хорошем настроении?
- В этой теме уже были ошибки?
- Это заказ на мебель или просьба посчитать спецификацию?
- Нужен короткий ответ или глубокая проработка?
Память знает ответы на эти вопросы. Но она не говорит об этом LLM напрямую — она лишь кладёт данные в хранилище. Между памятью и LLM не хватало слоя, который превратит данные памяти в подсказку для модели. Я пытался компенсировать это инструкциями. Инструкции росли — промпт пух — LLM терялась. Замкнутый круг.
Решение: ассистент должен «пощупать» запрос ДО ответа
В психологии есть модель Канемана:
- Система 1 — быстрая, интуитивная, работает автоматически;
- Система 2 — медленная, рациональная, включается для сложных задач. У людей это две системы, работающие вместе.
У LLM всё наоборот — она всегда «Система 2»: получает весь контекст и медленно перебирает. А «Системы 1» у неё нет.
Я решил: сделаю ассистенту Систему 1. Назвал её Интуитивный Контур — слой, который за миллисекунды до ответа оценивает запрос и добавляет в промпт 3-5 коротких меток.
Где он стоит — наконец-то полная картина:
Пользователь пишет
↓
STM L1: extract_topic → тема запроса
STM L1: should_anchor? → тема/цифры в якорь
STM L2: load_history → последние сообщения
МНЕМОС: search_experiences → топ-3 примера
↓
Prompt Assembler: собирает system_prompt
↓
Контур (1ms): читает STM + МНЕМОС → метки ←── вот здесь!
↓
LLM отвечает (видит метки + промпт)
↓
Sufler оценивает → STM/МНЕМОС учатся
В прошлой статье цикл заканчивался на Prompt Assembler. Контур — это слой между сборкой промпта и LLM. Он не хранит данные, как STM или МНЕМОС. Он их читает и превращает в метки-подсказки.
Ключевая идея: Контур не генерирует ответ. Он создаёт «атмосферу» для мышления. LLM получает не голый запрос, а 3-5 слов меток — и интерпретирует их сама.
Контур не хранит — он читает
Это самое важное. Контур — не еще одна система памяти, а, скажем так, "фундаментальная надстройка "над уже имеющимся модулями. Он берёт данные из STM и МНЕМОС и «озвучивает» их состояние для LLM:
| Что читает контур | Откуда | Это система |
|---|---|---|
familiarity (знакома ли тема) | ChromaDB, async | МНЕМОС L1 |
microgoals (текущие темы диалога) | якоря | STM L1 |
numanchors (глубина диалога) | якоря | STM L1 |
risk_topics (где были ошибки) | Sufler fails | МНЕМОС L3 |
mood, streaks (настроение) | Чутьё | отдельный модуль |
trust (доверие, самообучение) | UserModel по Sufler | МНЕМОС L1/L3 |
Контур
│
├── читает STM → темы, якоря, цифры (оперативная память)
├── читает МНЕМОС → знакомость, риск-темы, доверие (долгосрочная)
├── читает Чутьё → настроение, streak
│
└── отдаёт LLM → [УВЕР: HIGH] [ЖАНР: ЗАКАЗ] [РИСК: ПОДБОР]
Контур ничего не хранит — он «Система 1», которая заглядывает в обе памяти и коротко говорит LLM: «вот ситуация, реагируй».
StateVector: сердце контура
Всё, что Контур знает о запросе, — это объект StateVector. Семь полей, каждое — эвристика или данные, прочитанные из STM/МНЕМОС:
@dataclassclass StateVector:
familiarity: float = 0.5 # ← из МНЕМОС (ChromaDB async)
criticality: float = 0.0 # ← эвристики + Чутьё
complexity: float = 0.0 # ← эвристики + STM (анкоры)
mood: str = "нейтрально" # ← Чутьё
confidence: str = "MEDIUM" # ← familiarity + trust
genre: str = "диалог" # legacy keyword
method: str = "" # ← LLM-классификация
risk_topics: list = [] # ← МНЕМОС L3 (Sufler fails)
trust: float = 0.5 # ← самообучение по Sufler
def topromptfragment(self) -> str:
parts = [f"[УВЕР: {self.confidence}]"]
if self.genre != "диалог":
parts.append(f"[ЖАНР: {self.genre.upper()}]")
if self.mood not in ("нейтрально", "спокоен"):
parts.append(f"[МООД: {self.mood}]")
if self.risk_topics:
parts.append(f"[РИСК: {self.risk_topics[0]}]")
return " ".join(parts)
На выходе — фраза вида:
[УВЕР: HIGH] [ЖАНР: ЗАКАЗ] [РИСК: ПОДБОР]
Три слова. Вместо 50 строк инструкций. LLM сама понимает: «тема знакомая, это заказ, в подборе аналогов были ошибки — будь аккуратнее».
Как Контур «оценивает» запрос
Знакомость (familiarity) — асинхронный ChromaDB
Первый вопрос: а встречал ли ассистент такую тему раньше? Для этого нужен поиск по МНЕМОС — но он занимает 200-500 мс. Блокировать чат нельзя.
Решение — кэш + фон:
class FamiliarityCache:TTL = 300 # 5 минут
async def get(self, userid, threadid):
key = (userid, threadid)
if key in self.cache and свежий:
return self.cache[key] # 0ms — хит
entry = {"familiarity": 0.5, "genre": "", "timestamp": now}
self.cache[key] = entry
asyncio.createtask(self.asyncfill(userid, thread_id, key)) # фон!
return entry
Первый запрос в треде получает familiarity=0.5 (дефолт) и запускает поиск в фоне. Со второго сообщения — точное значение из кэша, 0 миллисекунд. Память на 100 пользователей — ~300 KB. Никакой нагрузки.
Глубина диалога (complexity) — из STM
Анкоры STM — это «save points» диалога: каждая смена темы создаёт якорь. Контур смотрит на их количество:
цифры "1800x400x500" → +0.3 (возможный Matematicus)
длина сообщения > 50 слов → +0.3 (нужен глубокий ответ)
> 3 анкоров в STM → +0.2 (долгий диалог, тема разрослась)
Много якорей = диалог долгий и ветвистый = LLM должна глубже копать. Мало = короткая переписка = ответ покороче.
Критичность — keyword-based
Иногда важнее не «знакомая ли тема», а «горит ли у пользователя»:
CRITICAL_WORDS = {
"срочно", "горим", "суд", "претензия", "жалоба",
"налоговая", "санкции", "блокировка", "штраф", "иск"
}
«Срочно нужен шкаф к пятнице» → criticality растёт. Плюс: аномально короткое сообщение (стресс) + негативный streak от Чутья → LLM становится внимательнее.
Метод — семантическая классификация (v4)
Самое важное обновление. Раньше жанр определялся keyword'ами — и это давало ложные срабатывания:
«какое сечение кабеля» → детект «заказ» ❌ (это же про кабель, а не мебель!)
Теперь метод классифицирует LLM — одна лёгкая классификация, ~100 токенов:
METHODVALUES = {"анализ", "бизнес", "сео", "расчёт", "заказ","поиск", "документы", "секретарь", "диалог"}
async def classifymethod(message: str) -> str:
# Fast-path: короткие без цифр → диалог (0мс)
if len(message.split()) <= 2 and not re.search(r'\d', message):
return "диалог"
# LLM-классификация (лёгкая модель, без хардкода)
...
Метод управляет загрузкой промптов:
| Метод | Модуль | Что грузится |
|---|---|---|
| анализ | Logicus | правила method=анализ |
| бизнес | бизнес-промпты | method=бизнес |
| сео | app/seo | method=сео |
| расчёт | Matematicus | method=расчёт |
| заказ | PrCoordinator | отдельный процессор |
| поиск | Deep Search | method=поиск |
| документы | документы | method=документы |
| секретарь | календарь/заметки | method=секретарь |
| диалог | — | только base |
Самообучение: Контур учится на оценках Sufler
У Контура есть UserModel — память о пользователе, которая обновляется после каждого ответа (по оценке Sufler, без дополнительных LLM-вызовов):
class UserModel:trust: float = 0.5
frequent_topics: list = []
frustration_topics: list = []
avg_score: float = 0.0
def update(self, score, topic):
self.trust = self.trust 0.9 + (score / 5.0) 0.1
if score <= 2:
self.frustration_topics.append(topic) # зона боли
if score >= 4:
self.frequent_topics.append(topic) # сильные темы
Доверие влияет на уверенность:
if trust < 0.3:
sv.confidence = "LOW" # пользователь часто недоволен → осторожнее
elif trust > 0.7:
sv.confidence = "HIGH" # доволен → можно смелее
Модель хранится в general_settings, переживает рестарт, на 100 юзеров — ~50 KB памяти.
Как это выглядит в бою
Пример 1: заказ мебели (STM помнит тему)
STM: якорь «заказ мебели» + цифры 1800x400x500Контур: familiarity=0.8 (из МНЕМОС), complexity=0.7 (анкоры STM), method="заказ"
Пользователь: «Шкаф 1800x400x500 нужен срочно к пятнице, подбери аналог»
Без контура: LLM объясняет про шкафы, теряет размеры
С контуром: [УВЕР: HIGH] [ЖАНР: ЗАКАЗ] [РИСК: ПОДБОР]
LLM: «Вижу активный заказ на шкафы. 1800×400×500 —
аналог Бриз Б-2 (совпадение 87%). Срок до пятницы принят.
Добавить в заказ?»
Пример 2: расчёт с НДС (МНЕМОС помнит ошибку)
STM: якорь «НДС, спецификация»МНЕМОС L3: в теме НДС была ошибка (Sufler fail)
Пользователь: «Расчёт спецификации с НДС 20%»
Без контура: LLM считает с ошибкой в НДС
С контуром: [УВЕР: MEDIUM] [ЖАНР: РАСЧЁТ] [РИСК: НДС] [МООД: деловой]
LLM: «Давайте посчитаем. По опыту — с НДС бывают ошибки,
проверю 20%. Разрешите использовать Matematicus?»
Видите? Контур сам ничего не знает про шкафы и НДС. Он прочитал STM (тема, цифры), МНЕМОС (знакомая тема, были ошибки) — и превратил это в метку. Данные дали памяти, «озвучил» их контур.
Сравнение: Контур vs «просто хороший промпт»
| Фича | Обычный промпт | Интуитивный Контур |
|---|---|---|
| Размер инструкций | 50+ строк | 3-5 меток |
| Жанр запроса | keyword-детект (ошибки) | LLM-классификация (метод) |
| Настроение | нет | Чутьё (mood) |
| Знакома ли тема | нет | МНЕМОС async (familiarity) |
| Глубина диалога | нет | STM (анкоры → complexity) |
| Где были ошибки | нет | МНЕМОС L3 → [РИСК: тема] |
| Доверие | нет | UserModel (самообучение) |
| Промпт для «привет» | 11 142 симв | 3 892 симв |
| Строк в main.py | — | 0 |
Три системы — одна картина
Чтобы не запутаться в трёх статьях:
| Система | Роль | Живёт | Пример |
|---|---|---|---|
| STM | оперативная память | один диалог | якорь «шкаф 1800×400, 2 шт» |
| МНЕМОС | долгосрочная память | вечно | «в НДС была ошибка» |
| Контур | Система 1, читает обе | переживает рестарт (UserModel) | [ЖАНР: ЗАКАЗ] [РИСК: ПОДБОР] |
«0 строк в main.py» — это принцип
Вся логика в app/contour.py (~600 строк) + константы вынесены в app/constants.py, чтобы не было циклических импортов (контур → prcoordinator → auth → контур). Главная боль архитектуры — изолированность: контур можно выключить, и ассистент просто станет «менее внимательным», но не сломается.
Сравнение с тем, что есть у других
Я не нашёл аналогов «pre-thinking слоя» как отдельного компонента у LLM-ассистентов. Ближайшие параллели:
- Mem0 / Letta — память, но без оценки «настроения» и «жанра»
- Function calling — определяет «какой инструмент», но не «в каком состоянии пользователь»
- Prompt engineering — пытается решить всё инструкциями
Контур — по сути system prompt на стероидах: вместо «запомни 20 правил» LLM получает «вот оценка ситуации — реагируй». А данные для этой оценки берутся из STM и МНЕМОС, которые я описал в прошлых статьях.
Что дальше
В следующих статьях:
- Замкнутый цикл самообучения AI-ассистента
- Matematicus — 3-level matching для точных расчётов
Вопросы к читателям:
- Пробовали ли вы pre-processing слой перед LLM? Или решаете всё промптом?
- Как вы связываете «оценку ситуации» с памятью? Контур должен читать STM/МНЕМОС напрямую или получать готовые метрики?
- Стоит ли передавать criticality в промпт или это должно оставаться внутренним сигналом?
Если есть вопросы по конкретному компоненту — пишите в комментариях.
Комментарии
Для комментирования авторизуйтесь на сайте или используйте ВКонтакте