ИИ-ассистент с долговременной памятью, агентами и vision. Проблемы с Персональными Данными

от автора

Оглавление:

  • История

  • Память: факты, embeddings и забывание

  • Разные модели под разные задачи

  • Tool calling

  • Планировщик и proactive

  • Агенты и мультиагентский пайплайн

  • Vision, который знает контекст

  • Персональные данные и GigaChat

  • Что делать дальше


История

Изначально ничего общего с ассистентом не было. Мы с женой делали текстовую браузерную игру: выборы, немного тактики, бои. Она автор, я помогал адаптировать её тексты под формат игры и собирал бэкенд. Чтобы ей было удобно генерировать лор, я поднял отдельный репозиторий, прикрутил интеграцию с Claude и китайской GLM моделью, собрал админку игры на Symfony. Контента она наделала много, и он был нужен и мне: характеристики персонажей, описания боёв. Запросы вроде «покажи бой Анвара с Медведем» или «какая воля у Кид» решались через локальную LLM (Ollama).

Там же появилась первая связка, которая потом стала основой: Ollama на игровом ПК плюс Postgres с pgvector.

Эмбеддинги по лору игры — и простые запросы начинают работать быстро и бесплатно. Получился простенький RAG.

Админка с Symfony + Ollama

Админка с Symfony + Ollama

Вторая проблема выросла из иллюстраций. Для игры их нужно было много. Сначала генерация промптов для Stable Diffusion жила в админке, потом я вынес это в отдельное приложение — я уже писал про него (https://habr.com/ru/articles/1030628/). Но главное не это. Главное, что пока я всё это собирал, я постоянно и подолгу общался с ИИ. И ловил себя на том, что раз за разом пишу одно и то же: «у меня RTX 4070 Ti Super, 16 ГБ VRAM, мне надо вот это …».

И в какой-то момент пришёл к логичному: «Если ИИ и так всё это знает обо мне, почему я должен повторять?» Так родилась идея ассистента, который помнит тебя между разговорами, умеет работать с документами и реально что-то делает, а не просто отвечает на вопросы.

Что я хотел получить:

  • память: ассистент сам вытаскивает факты из диалога и хранит их;

  • ассистенты: чаты с готовым промптом, по сути агенты;

  • база знаний: прикрепляешь ссылку или файл, и ИИ работает прямо в этом контексте;

  • и конкретные мои «хотелки»: «прочитай статью и перескажи коротко», «найди салонный фильтр для моей машины» (именно моей), «найди простую чёрную майку на маркетпйлесах» (без допроса про рост и размер), «напомни в 15:00 о …», «какие у меня планы на завтра?».

Стек выбрал такой: бэкенд на Go, фронт на Vue 3 как PWA и Capacitor для сборки APK под Android, в базе PostgreSQL с расширением pgvector (потому что уже было), LLM через облачные провайдеры. Об этом далее.

Общая архитектурная схема

Общая архитектурная схема

Память: факты, embeddings и забывание

Память делится на две части, и это важно их не смешивать.

Первая часть — это векторный поиск по контексту. Когда приходит сообщение пользователя, я собираю промпт: системный шаблон, профиль, последние сообщения истории и релевантные куски из базы знаний через RAG. История и база знаний живут на pgvector. Я выбрал pgvector, а не отдельную векторную базу типа Qdrant или Weaviate, по простой причине (выше писал): у меня и так PostgreSQL, профиль и факты там же.

Вторая часть — это факты. Это не просто «история переписки», а структурированные утверждения про пользователя, которые ассистент сам вытаскивает из диалога. После каждого сообщения работает extraction: отдельный вызов модели читает пару последних сообщений и возвращает JSON с фактами. «У пользователя Toyota Camry 2019 года», «аллергия на пыльцу», «работает ночью».

Пример — как это выглядит под капотом. Представим, что пользователь написал:

У меня Toyota Camry 2019 года. В среду был на медосмотре, всё нормально, но врач сказал сдавать кровь каждые полгода. И напомни завтра в 15:00 забрать заказ из пункта выдачи.

Что extraction вытаскивает из этого сообщения:

{ “facts”: [

{“fact”: “У пользователя Toyota Camry 2019 года”, “category”: “personal”, “kind”: “fact”},

{“fact”: “Пользователь должен сдавать кровь каждые полгода”, “category”: “health”, “kind”: “fact”}

],

“tasks”: [ {“description”: “Забрать заказ из пункта выдачи”, “deadline”: “2026-06-23T12:00:00Z”}

],

“completed_tasks”: []

}

Обратите внимание на дедлайн: «завтра в 15:00» превратилось в 2026-06-23T12:00:00Z — конкретная дата и 12:00 UTC.

Факты потом подмешиваются в системный промпт при следующем разговоре. И тут выясняется, что просто копить их нельзя: их становится слишком много, они устаревают, начинают конфликтовать — один факт говорит одно, а другой, добавленный через неделю, почти противоположное. Память без управления быстро превращается в помойку, и качество ответов падает.

Поэтому поверх накопления пришлось строить ещё несколько механизмов. Сначала нормализация времени: «сегодня», «вчера», «ночью» в тексте факта заменяются на конкретную дату, иначе через неделю факт теряет смысл. Потом веса: у каждого факта есть вес. При сохранении нового факта система ищет похожий по смыслу (similarity выше 0.70) и, если находит, считает это повтором — вес старого факта растёт, а дубль не плодится. В обратную сторону вес падает сам: планы старше 30 дней и события старше 14 дней постепенно уменьшаются до нуля. Факт с весом 0 физически не удаляется, но в промпт больше не попадает. Не всё стареет одинаково: имя и семью важно держать вечно, а «что ел на обед неделю назад» можно смело забывать. Дальше — summary: старые факты периодически агрегируются и сжимаются, чтобы не тянуть в промпт всё подряд.

И всё равно весов и summary мало: иногда факт нужно выкинуть сознательно и сразу. Для этого есть забывание прямо из чата. Пишешь «забудь про носовое кровотечение» — модель вытаскивает суть («носовое кровотечение»), и система удаляет все похожие факты.

Про нос — пример реальный. Я разбил нос и попросил чат «Фитнес-тренер» подстроить мне тренировку на сегодня. Проблема всплыла позже: чат запомнил про нос и стал постоянно его учитывать — уменьшать нагрузки «чтобы не было проблем с давлением» и раз за разом спрашивать: «Как сегодня твой нос?». Пришлось написать «забудь про носовое кровотечение» — и вопросы закончились.

Изоляция памяти (space)

С играми всплыла и вторая проблема — утечка контекста. Однажды фитнес-тренер посоветовал мне «не брать большие веса после вчерашнего удара топором по голове». Удар топором был в чате-игре D&D, а попал в советы по тренировкам. Поэтому факты разделили по пространствам (space): у некоторых чатов своя локальная память, и, к примеру, игровые факты в ней и остаются, не смешиваясь с основным профилем. Глобальное живёт отдельно от того, сколько HP у моего орка.

Кстати, про игры. Играть с ИИ — отдельный кайф: контекст держится, и сюжет меняется прямо в диалоге. Моя ситуация: играю орком в D&D, захожу в очередную комнату, на меня кидается маг с ножиком. Вместо того чтобы сразу бить, я заговорил с ним: «Мы прошли кучу комнат и завалили куда более страшных противников, ты будешь просто очередной жертвой; давай лучше выберемся, я познакомлю тебя с прекрасными девами и угощу вином, только умойся, а то от тебя плохо пахнет (я использовал другое слово)». Маг сел, загрустил и сказал, что с ним ещё никто так не говорил, и отдал ключ от следующей комнаты.

Диаграмма жизненного цикла факта: extraction → хранение → summary/decay → попадание в промпт

Диаграмма жизненного цикла факта: extraction → хранение → summary/decay → попадание в промпт

Разные модели под разные задачи

Первоначально у меня была одна модель на всё.

Дело в том, что задачи очень разные по требованиям. Чат должен быть умным и быстрым. Extraction — это вообще не чат: модели скармливают короткий промпт и просят вернуть строгий JSON, тут важна дешевизна и следование формату, а не глубина рассуждений. Vision — отдельный класс моделей. Пихать одну дорогую vision-модель на extraction — дороже.

Поэтому в архитектуре появилось разделение: chat-модель, extraction-модель и vision-модель, каждая со своим назначением. В конфиге провайдера это просто три поля, и у каждой свой модельный ID.

Для ориентира — цены у одного из агрегаторов (не буду писать какой) на момент написания:

Задача

Модель

Input / Output, ₽ за 1M токенов

Чат (основная)

Gemini 2.5 Flash

33 / 276

Чат (эконом)

Qwen 3.5 Flash

4 / 44

Extraction

GPT-5 Nano

4 / 31

Vision

Qwen3 VL Flash

3 / 33

Embeddings

text‑embedding-3-small

3

Extraction гоняется на дешёвой Nano, основное общение — на Gemini, и только когда в чате появляется фото, работает vision-модель. Себестоимость среднего пользователя при такой схеме получается вменяемой.

Важный момент про саму интеграцию. Я не стал привязываться к одному провайдеру. Все LLM-вызовы идут через единый OpenAI-совместимый клиент, а провайдеры лежат в таблице с приоритетами. Если основной падает или отдаёт 5xx, запрос уходит на следующий в цепочке. Это спасает и при сбоях, и при перегрузке.


Tool calling

Чтобы ассистент не просто говорил, а действовал, нужен tool calling. Я использую стандартный OpenAI-формат: описываю функции, модель сама решает их вызвать, tool_choice выставляется в auto, когда функции переданы.

Зачем это нужно. В чат-играх (D&D, детектив, выживалка на острове) ассистент поначалу запоминал состояние персонажа как факт: «здоровье 3/30 HP». Но HP в бою меняется каждый ход, и сохранённый факт мгновенно устаревал — в новом бою модель приписывала персонажу старое здоровье и путалась. Запоминать через extraction то, что постоянно меняется, нельзя.

Tool calling чинит это иначе: модель не хранит HP как факт, а вызывает функцию и получает актуальное состояние персонажа. Точное значение по запросу — без устаревшей памяти и без угадывания. В этом и суть tools: давать модели доступ к живым данным вместо того, чтобы она их тащила из фактов.


Планировщик и proactive

Следующая ступень после «отвечает, когда спросили» — ассистент, действует сам. Хорошо работает с чатом «Планировщик» (но не только там). Он не привязан к одному чату: собирает факты из всех разговоров и видит задачи пользователя, поэтому картина у него цельная, а не обрывок текущего диалога.

Работает в двух режимах.

Проактивный: утром планировщик сам пишет планы на день, а в течение дня напоминает о задаче, время которой подошло.

Реактивный: на «какие у меня планы на завтра?» отвечает по реальным задачам, а на «добавь задачу на завтра» или «для такого-то факта укажи время» — вносит правки. Задачи и факты можно не только читать, но и менять прямо из чата.


Агенты и мультиагентский пайплайн

Ассистенты в моём понимании — это чаты с готовым системным промптом. Завёл чат «травник», задал ему роль — и он придерживается её в ответах, опираясь на твою память и базу знаний.

Но интереснее, когда агенты работают не поодиночке, а в цепочке. Возьмём пример: приложением пользуется моя жена. Она выращивает травы и делает из них натуральную хендмейд-косметику. Допустим, нужно сделать контент про какое-то растение. Сначала используем чат «травник» — он вытаскивает суть и факты по теме. Его результат уходит копирайтеру, который превращает это в читаемый текст. Затем подключается SMM-стратег и адаптирует текст под конкретную площадку: заголовок, тон, формат. Плюс она обучила модель своему стилю письма (по примеру своих же статей). На выходе — не один ответ модели, а готовый материал, прошедший через трёх специалистов и сохранивший стиль автора.

Мультиагентский пайплайн можно применить и для других задач:

  • обучение и экзаменация;

  • генератор идей → критик → финализатор;

  • вопрос, прошедший через разных советчиков и критиков;

  • шопинг: ищем девайс — один агент собирает характеристики, другой подбирает примеры, третий сверяет с бюджетом.

Придумать можно ещё многое. Близкая аналогия — то, как агенты работают в разработке ПО: архитектор -> кодер -> ревьюер.

Только что пока я пишу статью прибежала радостная жена и говорит: смотри, я задала вопрос, а он мне написал что написал:

1 скриншот

1 скриншот
2 скриншот

2 скриншот

Тот же принцип работает и в поиске по маркетплейсам. Когда я говорю «найди салонный фильтр для моей машины», в запрос уходит не абстрактное «модель и год», а конкретно мой автомобиль из памяти. Профиль инжектируется в поиск, и результат получается под меня, а не усреднённый. Разница с обычным «найдите фильтр» колоссальная.


Vision, который знает контекст

Vision-модели работают куда интереснее, когда им не просто показываешь картинку, а даёшь контекст из памяти.

Если просто кинуть фотографию в модель, она опишет, что видит. А если в тот же промпт положить факты про пользователя и его окружение, модель начинает «узнавать». На фото семья — и ассистент не просто видит «несколько человек», а может соотнести их с тем, что знает: кто есть кто. Это уже не распознавание, а понимание.

Технически это мультимодальный контент в OpenAI-формате: текст плюс image_url. Никакой магии, просто правильная сборка промпта. Но эффект заметный.


Персональные данные и GigaChat

Технически всё описанное работает. Но на иностранной модели или на локальной Ollama: и чат, и vision, и embeddings, и tool calling. Проблемы начались, когда я решил выложить всё это в общий доступ.

Проблема в персональных данных. Многие российские агрегаторы юридически находятся в РФ, но физически у них нет своих GPU, и запросы уходят за рубеж даже к простым моделям. Какие точно я не могу сказать и они то не раскроют, как я понимаю. Для продукта, который помнит про пользователя всё, это означает, что все эти данные уезжают в другую страну. С учётом 152-ФЗ это не та ситуация, в которую хочется добровольно лезть.

Логичная мысль — перейти на отечественные модели, чья инфраструктура реально в РФ. Попробовал GigaChat, как самая известная и (наверное) умная. И тут начались проблемы.

GigaChat не OpenAI-совместимый. У него своя авторизация, свой формат промптов, свой формат vision, свои короткие embeddings. По уровню рассуждений он примерно соответствует GPT-4o-mini (как мне показалось). Техническую несовместимость ещё можно докрутить, но основных проблем оказалось несколько.

Extraction фактов не отработал так, как мне нужно. Summary, на котором держится агрегация и сжатие памяти, тоже не зашло. Tool calling на момент моего тестирования в моём сценарии не отработал адекватно. И цены: самая простая модель Lite стоит ~19 тыс. рублей за 300 млн токенов — GigaChat сильно дороже.

Пример самой простой модели Lite для ИП.

Пример самой простой модели Lite для ИП.

Самое наглядное получилось с vision. Я сфотографировал мак без цветка — в таком состоянии растение узнать непросто.

Qwen3 VL Flash. Какая-то другая модель еще написала про опасность и проблемы с этим маком (хотя это декоративный).

Qwen3 VL Flash. Какая-то другая модель еще написала про опасность и проблемы с этим маком (хотя это декоративный).
GigaChat Pro (бесплатного ключа хватает на три картинки)

GigaChat Pro (бесплатного ключа хватает на три картинки)

Qwen3 VL Flash определил, что это за растение. GigaChat увидел просто «какое-то растение». И это не придирка к частному примеру, а разница в пригодности под задачу.


Что делать дальше

Коротко: Проект готов технически. Поэтому прежде чем вкладывать ещё месяцы в допиливание GigaChat или в свою GPU-инфраструктуру, я хочу понять одно — нужна ли вообще кому-то такая штука.

Вот и сам вопрос. Как по-вашему, что мне делать с этим проектом (не нашел, как сделать опрос):

  1. Добить и выложить в опенсорс, пусть живёт как есть. Бэкенд можно использовать как для работы через агрегаторы, так и с локальной Ollama.

  2. Тянуть в «серую зону»: остаться на зарубежных моделях через агрегатор, оформить согласие на обработку и не париться. Как я понимаю куча ИИ-чатов в русторе так и живут.

  3. Не нужно. Ниша занята, продукт не взлетит, оставить как опыт.

  4. Свой вариант в комментариях.

Если кому-то интересно или есть мысли — напишите. Выкинуть жалко: сейчас используем только для себя, а лезть туда, где я не силён, — не буду.

ссылка на оригинал статьи https://habr.com/ru/articles/1050300/