- BrainTools - https://www.braintools.ru -
Чем отличается беседа с другом от разговора со случайным попутчиком в долгом путешествии? Как минимум объемом общего контекста. Там, где первый всё поймёт с пары фраз, у второго останутся только вопросы.
Книги из детства обещали мне ИИ в роли друга, напарника, наставника. Но так уж получилось, что сейчас LLM — это случайный знакомый с тяжёлой формой амнезии. Давайте попробуем это исправить.
Сообщения в чате с ИИ это лишь абстракция на уровне пользовательского интерфейса и на самом деле чтобы получить ответ на одно сообщение нужно отправить на вход LLM всю историю чата. Очень хорошо и подробно об этом рассказал пользователь Propan671 [1] вот тут — Контекст и управление им [2].
Но раз история отправляется каждый раз заново, то значит мы можем и редактировать её каждый раз перед тем как отправить. То есть фактически мы можем переписывать и свои, и прошлые ответы ИИ, создавая идеальный контекст для нашего нового сообщения. Этим мы и воспользуемся.
Представьте, что у нас есть короткий, но запутанный лог диалога, где мы успели поговорить о работе, хобби и технологиях. В сыром виде (в формате 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.
Для груди делай жим гантелей под углом."
}
]
```
Сначала мы разбиваем этот лог на отдельные предложения и складываем их в два независимых массива: один для пользователя, другой для ассистента.
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
]
Теперь мы пропускаем эти предложения через классификатор. Какой и как его сделать решим потом, сейчас подрядим для этого быструю и дешевую LLM. Она читает каждое предложение и вешает на него тег. По сути, мы создаем многомерную маску (индексы) для наших массивов:
# Какие индексы относятся к железу и ИИ?
tags = {
"user": {
"tech": [0, 1, 3],
"sport": [2, 4]
},
"model": {
"tech": [1, 3],
"sport": [2, 4],
"noise": [0] # "Отличный план!" - не несет фактов
}
}
Теперь, если пользователь спрашивает: “Так какую модель мне в итоге поставить?”, мы не скармливаем 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 этого диалога про спорт никогда не существовало. Она видит только плотный, логичный разговор двух инженеров. Именно так мы сжимаем мегабайты «водянистой» переписки в идеальный слепок памяти [3].
Почему теневой? Потому что пользователю не обязательно знать о всех этих махинациях с историей диалога. Бэкенд сам справится с этой задачей, а пользователь может вести свой диалог, как обычно:
```
[
{"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|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
What's the capital of France?<|im_end|>
<|im_start|>assistant
В примере выше показаны специальные управляющие токены из стандарта ChatML, разработанного OpenAI. Но это не способ форматирования — это архитектурное решение. На этапе обучения [4] модели оно позволяет сформировать внутри трансформера разное распределение весов для входящих запросов и собственных ответов.
В каком-то смысле в процессе диалога assistant и user могут думать каждый о своём.
На скриншотах показано как мы вписываем в историю диалога информацию, которая во время обучения модели не использовалась. В данном примере изменено авторство второго сообщения с user на model и, как мы видим, в первом случае ИИ нас поправил, во втором поверил себе на слово.
Кстати можно сказать мы только что переизобрели RAG, но без звонкой аббревиатуры вряд ли кто‑то обратит на это внимание [5]. Идея же простая — вместо инструкций, подсказок с релевантной информацией мы просто создаем диалог в котором факты озвучены самой моделью. Но будьте осторожны, LLM не всегда так просто поддается как на этом примере.
И этой информацией, фактами, может быть что угодно — берем внешний источник (API календаря, лог из таск‑трекера, письма), вытаскиваем оттуда факт и заворачиваем его в тег assistant. Модель принимает этот факт не как вводную инструкцию, с которой можно поспорить, а как собственное воспоминание:
```
[
{ // 1. Скрытый блок. Сгенерирован бэкендом на основе последних писем.
"role": "assistant",
"text": "В последнем письме Милена обозначила, что
на следующем этапе интервью на позицию AI Lead мне
нужно защитить концепцию Compound AI System с
использованием SLM."
},
{ // 2. Реальный запрос пользователя
"role": "user",
"text": "Я сейчас буду делать презентацию для звонка.
Набросай структуру из трех главных слайдов."
}
]
```
Как собрать из этой концепции рабочий код? Не знаю. Реальность сильно сложнее приведенных выше примеров. Тем не менее, как мне кажется, это уже инженерия: связать векторную базу, классификатор и логику [6] подмены контекста.
Из всех известных мне поисковых движков для создания базы с сообщениями самым перспективным выглядит Apache Lucene с серверными оболочками типа Elasticsearch и Solr. И вот почему, основные возможности движков на базе Lucene:
Инвертированный индекс: Базовая структура данных для быстрого поиска. Текст разбивается на отдельные слова (токены), и для каждого слова сохраняется список документов, в которых оно встречается.
Морфологический анализ: Поддержка стемминга (усечение слова до основы), лемматизации (приведение к словарной форме), работы с синонимами, удаления стоп-слов и генерация N-грамм.
Ранжирование: Оценка релевантности найденных документов запросу. Современные версии по умолчанию используют алгоритм BM25, который учитывает частоту термина в документе и редкость термина во всей базе.
Нечеткий поиск (Fuzzy Search): Поиск с учетом возможных опечаток пользователя, основанный на вычислении расстояния Левенштейна.
Фасетирование и агрегации: Динамическая группировка результатов по категориям, атрибутам или диапазонам (например, подсчет количества товаров заданного цвета для бокового фильтра в интернет-магазине).
Векторный поиск (kNN): Поиск по семантической близости с использованием векторных представлений (эмбеддингов), что позволяет искать по смыслу, а не по точному совпадению слов.
Мы не пытаемся заранее склеить предложения в группы. Мы делаем каждое предложение отдельным документом, но связываем их метаданными.
Запись: Сообщение разбивается на отдельные предложения (через NLP-библиотеку вроде spaCy или NLTK).
Эмбеддинг: Каждое предложение превращается в вектор.
Индексация: В Elasticsearch улетают документы вида: {"message_id": 123, "sentence_id": 5, "text": "...", "vector": [...]}
Поиск и сборка: Когда приходит новый запрос, мы ищем топ-N самых похожих предложений в базе и надеемся на то, что получится связный текст и диалог.
Смысловые срезы формируются автоматически до попадания в базу, опираясь на близость векторов, а не на абзацы или предложения.
Нарезка и векторизация: Сообщение бьется на предложения, для каждого генерируется вектор.
Кластеризация: Алгоритм (например, DBSCAN или матрица косинусного сходства) сравнивает векторы всех предложений внутри одного сообщения между собой. Если 1-е, 5-е и 7-е предложения близки друг к другу в многомерном пространстве — они объединяются в один кластер.
Индексация: В Elasticsearch улетает уже готовый смысловой срез: {"slice_text": "Предл 1. Предл 5. Предл 7.", "vector": [усредненный вектор кластера]}.
Если абзацы не сработают, попробуем делегировать задачу выделения логических нитей небольшой, быстрой языковой модели (Small Language Model).
Анализ: Входящее сообщение отдается локальной модели с системным промптом: «Разбей текст на логические независимые мысли. Для каждой мысли верни массив индексов предложений. Формат строго JSON».
Сборка: Скрипт получает массивы вида [1, 5, 7] и [1, 3, 4], физически склеивает эти предложения в строки.
Индексация: Полученные строки прогоняются через модель эмбеддингов и сохраняются в Elasticsearch как отдельные смысловые документы, ссылающиеся на родительский message_id
План такой — из всей истории диалога, пусть это десятки мегабайт текста, с помощью комбинации векторного и символьного поиска синтезировать из отдельных предложений сообщений assistant и user консистентную, но в реальности не существовавшую версию диалога, в которой будет собран весь актуальный контекст для текущего запроса пользователя.
Эту версию мы будем отправлять в LLM вместо настоящей, а результат генерации добавлять в настоящую историю, видимую пользователю. И так мы будем делать каждый раз.
Если реализуемо, то получим Stateful LLM с неограниченной памятью умещающейся в контекстное окно ~10 000–20 000 токенов.
С моими навыками реализация всего этого будет не быстрой. Но ИИ‑друга отвечающего сообщениями типа: «Ну что, опять как в прошлый раз?» мне уж слишком хочется, а значит для меня история на этой статье не заканчивается.
Автор: WantToBelieve
Источник [7]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/31150
URLs in this post:
[1] Propan671: https://habr.com/ru/users/Propan671/
[2] Контекст и управление им: https://habr.com/ru/articles/1029326/#context
[3] памяти: http://www.braintools.ru/article/4140
[4] обучения: http://www.braintools.ru/article/5125
[5] внимание: http://www.braintools.ru/article/7595
[6] логику: http://www.braintools.ru/article/7640
[7] Источник: https://habr.com/ru/articles/1042592/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1042592
Нажмите здесь для печати.