Чем отличается беседа с другом от разговора со случайным попутчиком в долгом путешествии? Как минимум объемом общего контекста. Там, где первый всё поймёт с пары фраз, у второго останутся только вопросы.
Книги из детства обещали мне ИИ в роли друга, напарника, наставника. Но так уж получилось, что сейчас LLM — это случайный знакомый с тяжёлой формой амнезии. Давайте попробуем это исправить.
Сообщения в чате с ИИ это лишь абстракция на уровне пользовательского интерфейса и на самом деле чтобы получить ответ на одно сообщение нужно отправить на вход LLM всю историю чата. Очень хорошо и подробно об этом рассказал пользователь Propan671 вот тут — Контекст и управление им.
Но раз история отправляется каждый раз заново, то значит мы можем и редактировать её каждый раз перед тем как отправить. То есть фактически мы можем переписывать и свои, и прошлые ответы ИИ, создавая идеальный контекст для нашего нового сообщения. Этим мы и воспользуемся.
Теневой контекст: срезы памяти вместо бесконечного окна
Представьте, что у нас есть короткий, но запутанный лог диалога, где мы успели поговорить о работе, хобби и технологиях. В сыром виде (в формате JSON) эта история выглядит как череда сообщений:
```[ { "role": "user", "text": "Я собираю сервер дома. Хочу накатить туда Llama-3. Вечером иду в зал, надо пожать сотку." }, { "role": "assistant", "text": "Отличный план! Для Llama-3 тебе понадобится минимум 24GB видеопамяти. А сотка — хороший рабочий вес, береги связки." }, { "role": "user", "text": "У меня только RTX 3060 на 12 гигов. Что посоветуешь из упражнений на грудь?" }, { "role": "assistant", "text": "На 12 гигов Llama не влезет, смотри в сторону Gemma-300M. Для груди делай жим гантелей под углом." }]```
Шаг 1: Нарезка и изоляция
Сначала мы разбиваем этот лог на отдельные предложения и складываем их в два независимых массива: один для пользователя, другой для ассистента.
user_history = [ "Я собираю сервер дома.", # индекс 0 "Хочу накатить туда Llama-3.", # индекс 1 "Вечером иду в зал, надо пожать сотку.", # индекс 2 "У меня только RTX 3060 на 12 гигов.", # индекс 3 "Что посоветуешь из упражнений на грудь?" # индекс 4]model_history = [ "Отличный план!", # индекс 0 "Для Llama-3 тебе понадобится минимум 24GB видеопамяти.", # индекс 1 "А сотка — хороший рабочий вес, береги связки.", # индекс 2 "На 12 гигов Llama не влезет, смотри в сторону Gemma-300M.", # индекс 3 "Для груди делай жим гантелей под углом." # индекс 4]
Шаг 2: Разметка
Теперь мы пропускаем эти предложения через классификатор. Какой и как его сделать решим потом, сейчас подрядим для этого быструю и дешевую LLM. Она читает каждое предложение и вешает на него тег. По сути, мы создаем многомерную маску (индексы) для наших массивов:
# Какие индексы относятся к железу и ИИ?tags = { "user": { "tech": [0, 1, 3], "sport": [2, 4] }, "model": { "tech": [1, 3], "sport": [2, 4], "noise": [0] # "Отличный план!" - не несет фактов }}
Шаг 3: Синтез теневого контекста
Теперь, если пользователь спрашивает: «Так какую модель мне в итоге поставить?», мы не скармливаем LLM информацию про штангу и жим лежа. Мы берем только те индексы, которые лежат в tags['user']['tech'] и tags['model']['tech'] и нейросеть получает идеально чистый теневой контекст:
```[ { "role": "user", "text": "Я собираю сервер дома. Хочу накатить туда Llama-3. У меня только RTX 3060 на 12 гигов." },{ "role": "assistant", "text": "Для Llama-3 тебе понадобится минимум 24GB видеопамяти. На 12 гигов Llama не влезет, смотри в сторону Gemma-300M." },{ "role": "user", "text": "Так какую модель мне в итоге поставить?" }]```
Для LLM этого диалога про спорт никогда не существовало. Она видит только плотный, логичный разговор двух инженеров. Именно так мы сжимаем мегабайты «водянистой» переписки в идеальный слепок памяти.
Шаг 4: Расставляем всё по своим местам
Почему теневой? Потому что пользователю не обязательно знать о всех этих махинациях с историей диалога. Бэкенд сам справится с этой задачей, а пользователь может вести свой диалог, как обычно:
```[ {"role": "user","text": "..."}, {"role": "assistant","text": "..."}, {"role": "user","text": "..."}, { "role": "assistant", "text": "На 12 гигов Llama не влезет, смотри в сторону Gemma-300M. Для груди делай жим гантелей под углом." },{ "role": "user", "text": "Так какую модель мне в итоге поставить?" },{ "role": "assistant", "text": "Понятия не имею. Посмотри что я тебе предложил раньше - Gemma-300M. Это тебе за шутку с GPT-OSS-120B" }]```
Зачем переписывать сообщения ИИ?
Где-то в глубинах сервера массив сообщений конкатенируется в строку и уже в таком виде подаётся на вход нейронной сети.
<|im_start|>systemYou are a helpful assistant.<|im_end|><|im_start|>userWhat's the capital of France?<|im_end|><|im_start|>assistant
В примере выше показаны специальные управляющие токены из стандарта ChatML, разработанного OpenAI. Но это не способ форматирования — это архитектурное решение. На этапе обучения модели оно позволяет сформировать внутри трансформера разное распределение весов для входящих запросов и собственных ответов.
В каком-то смысле в процессе диалога assistant и user могут думать каждый о своём.
Как это можно использовать? Механика инъекции фактов
На скриншотах показано как мы вписываем в историю диалога информацию, которая во время обучения модели не использовалась. В данном примере изменено авторство второго сообщения с user на model и, как мы видим, в первом случае ИИ нас поправил, во втором поверил себе на слово.
Скрытый текст
Кстати можно сказать мы только что переизобрели RAG, но без звонкой аббревиатуры вряд ли кто‑то обратит на это внимание. Идея же простая — вместо инструкций, подсказок с релевантной информацией мы просто создаем диалог в котором факты озвучены самой моделью. Но будьте осторожны, LLM не всегда так просто поддается как на этом примере.
И этой информацией, фактами, может быть что угодно — берем внешний источник (API календаря, лог из таск‑трекера, письма), вытаскиваем оттуда факт и заворачиваем его в тег assistant. Модель принимает этот факт не как вводную инструкцию, с которой можно поспорить, а как собственное воспоминание:
```[ { // 1. Скрытый блок. Сгенерирован бэкендом на основе последних писем. "role": "assistant", "text": "В последнем письме Милена обозначила, что на следующем этапе интервью на позицию AI Lead мне нужно защитить концепцию Compound AI System с использованием SLM." }, { // 2. Реальный запрос пользователя "role": "user", "text": "Я сейчас буду делать презентацию для звонка. Набросай структуру из трех главных слайдов." }]```
От теории к вопросам практической реализации
Как собрать из этой концепции рабочий код? Не знаю. Реальность сильно сложнее приведенных выше примеров. Тем не менее, как мне кажется, это уже инженерия: связать векторную базу, классификатор и логику подмены контекста.
Из всех известных мне поисковых движков для создания базы с сообщениями самым перспективным выглядит Apache Lucene с серверными оболочками типа Elasticsearch и Solr. И вот почему, основные возможности движков на базе Lucene:
-
Инвертированный индекс: Базовая структура данных для быстрого поиска. Текст разбивается на отдельные слова (токены), и для каждого слова сохраняется список документов, в которых оно встречается.
-
Морфологический анализ: Поддержка стемминга (усечение слова до основы), лемматизации (приведение к словарной форме), работы с синонимами, удаления стоп-слов и генерация N-грамм.
-
Ранжирование: Оценка релевантности найденных документов запросу. Современные версии по умолчанию используют алгоритм BM25, который учитывает частоту термина в документе и редкость термина во всей базе.
-
Нечеткий поиск (Fuzzy Search): Поиск с учетом возможных опечаток пользователя, основанный на вычислении расстояния Левенштейна.
-
Фасетирование и агрегации: Динамическая группировка результатов по категориям, атрибутам или диапазонам (например, подсчет количества товаров заданного цвета для бокового фильтра в интернет-магазине).
-
Векторный поиск (kNN): Поиск по семантической близости с использованием векторных представлений (эмбеддингов), что позволяет искать по смыслу, а не по точному совпадению слов.
Вариант 1. Атомарная индексация + сборка на лету
Мы не пытаемся заранее склеить предложения в группы. Мы делаем каждое предложение отдельным документом, но связываем их метаданными.
-
Запись: Сообщение разбивается на отдельные предложения (через NLP-библиотеку вроде spaCy или NLTK).
-
Эмбеддинг: Каждое предложение превращается в вектор.
-
Индексация: В Elasticsearch улетают документы вида:
{"message_id": 123, "sentence_id": 5, "text": "...", "vector": [...]} -
Поиск и сборка: Когда приходит новый запрос, мы ищем топ-N самых похожих предложений в базе и надеемся на то, что получится связный текст и диалог.
Вариант 2. Векторная кластеризация при записи
Смысловые срезы формируются автоматически до попадания в базу, опираясь на близость векторов, а не на абзацы или предложения.
-
Нарезка и векторизация: Сообщение бьется на предложения, для каждого генерируется вектор.
-
Кластеризация: Алгоритм (например, DBSCAN или матрица косинусного сходства) сравнивает векторы всех предложений внутри одного сообщения между собой. Если 1-е, 5-е и 7-е предложения близки друг к другу в многомерном пространстве — они объединяются в один кластер.
-
Индексация: В Elasticsearch улетает уже готовый смысловой срез:
{"slice_text": "Предл 1. Предл 5. Предл 7.", "vector": [усредненный вектор кластера]}.
Вариант 3. SLM как маршрутизатор смыслов
Если абзацы не сработают, попробуем делегировать задачу выделения логических нитей небольшой, быстрой языковой модели (Small Language Model).
-
Анализ: Входящее сообщение отдается локальной модели с системным промптом: «Разбей текст на логические независимые мысли. Для каждой мысли верни массив индексов предложений. Формат строго JSON».
-
Сборка: Скрипт получает массивы вида
[1, 5, 7]и[1, 3, 4], физически склеивает эти предложения в строки. -
Индексация: Полученные строки прогоняются через модель эмбеддингов и сохраняются в Elasticsearch как отдельные смысловые документы, ссылающиеся на родительский
message_id
Итого:
План такой — из всей истории диалога, пусть это десятки мегабайт текста, с помощью комбинации векторного и символьного поиска синтезировать из отдельных предложений сообщений assistant и user консистентную, но в реальности не существовавшую версию диалога, в которой будет собран весь актуальный контекст для текущего запроса пользователя.
Эту версию мы будем отправлять в LLM вместо настоящей, а результат генерации добавлять в настоящую историю, видимую пользователю. И так мы будем делать каждый раз.
Если реализуемо, то получим Stateful LLM с неограниченной памятью умещающейся в контекстное окно ~10 000–20 000 токенов.
Когда релиз? Вечером. Не сегодня, в какой‑то из дней
С моими навыками реализация всего этого будет не быстрой. Но ИИ‑друга отвечающего сообщениями типа: «Ну что, опять как в прошлый раз?» мне уж слишком хочется, а значит для меня история на этой статье не заканчивается.
ссылка на оригинал статьи https://habr.com/ru/articles/1042592/