Ночью, около двух по московскому времени, сервер просыпается и запускает конвейер. Сначала сервис идёт в Telegram и скачивает новые сообщения из чатов, которые пользователь отметил галочками. Через час стартует второй воркер и превращает эти сообщения в память: режет на куски, гоняет через DeepSeek, раскладывает по таблицам, выгружает в markdown‑файлы. К утру у человека появляются дополнительная информация в сервисе второй памяти.

Сервис второй памяти — это не просто история переписки. Через полгода можно спросить «какой бюджет мы тогда согласовали на поездку» и получить ответ со ссылкой на конкретное сообщение в конкретном чате.

Как ни странно, почти вся моя работа по созданию сервиса была не в работе с LLM.

Почему я не стал делать векторную базу

Стандартный подход для такой задачи: залить переписку в векторную базу и делать RAG. Я сознательно не пошёл этим путём.

Переписка плохо делится на фрагменты (чанки). Один факт собирается из десятка реплик в разных чатах, а нарезка по 500 токенов разрывает историю на куски. Человек в одном чате Саша, в другом Александр, в третьем у него вообще непонятный ник, и поиск возвращает пять почти одинаковых фрагментов вместо одного знания. С актуальностью ещё хуже: в марте договорились об одном, в июне передумали, а в индексе лежат оба варианта с одинаковым весом.

Мне нужны были не фрагменты, а карточки: люди, проекты, темы, факты, задачи и связи между ними. И обязательная связь: от каждого факта к сообщению, из которого этот факт был вытащен. Векторная база такого не даёт из коробки.

Что в итоге получилось

Схема конвейера
Схема конвейера

Схема получилась такая. Telethon забирает сообщения по выбранным источникам (можно подключить и папку целиком, и отдельный чат). Каждое сырое сообщение получает запись в таблице состояния (ожидает, в процессе, обработано, ошибка). Планировщик в следующий прогон берёт только то, чего в этой таблице нет. Так я не плачу дважды за обработку одно и того же.

Сообщения я группирую по ключу «источник:чат», сортирую по дате и режу на куски по 100 штук. Внутри куска только один чат, чтобы модель видела связный диалог. Из куска собираю транскрипт вида [дата] Отправитель: текст, с обрезкой по 30 тысячам символов.

На каждый кусок идёт один вызов DeepSeek, и он должен вернуть строгий JSON:

{
  "daily_note": { "title": "...", "summary": "..." },
  "topics":   [{ "name": "...", "summary": "...", "facts": ["..."] }],
  "people":   [{ "name": "...", "summary": "...", "action_items": ["..."] }],
  "projects": [{ "name": "...", "summary": "...", "action_items": ["..."] }],
  "links":    [{ "from": "...", "to": "...", "type": "related_to" }]
}

В промпт я подкладываю список уже известных сущностей пользователя и прошу переиспользовать имена. Без этого один и тот же проект каждый прогон называется по‑новому, и база быстро превращается в мусорку.

Когда все куски задания готовы, запускается merge. Сущности я ищу по ключу (пользователь, тип, каноническое имя), повторные упоминания не плодят дубликаты, а обновляют summary. Факты и задачи привязываются к сущностям, связи уходят в отдельную таблицу, а каждая запись ссылается на id исходных сообщений. Это и есть нужная мне связь.

Последний шаг: выгрузка в обычные markdown‑файлы Daily, Knowledge, People, Projects. Ссылки между заметками я оформил как [[wiki-links]], так что vault открывается в Obsidian. Экспорт идемпотентный: перед записью блока я считаю его sha256, и если такой блок уже выгружался, он пропускается.

Готовая карточка выглядит примерно так:

# Поездка в Анталью

## Update

Собирались в Анталью в сентябре, бюджет около 350 000 ₽ на двоих.
[[Аня]] предложила даты, [[Саша]] уточнил расклад по отелю и перелёту.

- Отель: около 180 000 ₽
- Перелёт: около 70 000 ₽
- [ ] Уточнить даты у Ани

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

Цифры

Сейчас в базе пока несколько пользователей, на одного пользователя в среднем по 7 источников (3 папки и 4 отдельных чата), но даже они за пару месяцев теста сгенерировали десятки тысяч сообщений в 34 чатах.

Из этого получилось 281 сущность: 147 тем, 108 человек, 26 проектов. Плюс 1815 фактов, 184 задачи и 133 дневные заметки.

По токенам пока вышло всего 10,05 млн: 3028 тысяч на входе и 7027 тысяч на выходе. Ответы дороже входа больше чем вдвое, потому что JSON со схемой занимает много места. По тарифам DeepSeek это меньше десяти долларов на все сообщения. Приятно, что персональная память на маленьких объёмах стоит копейки.

Одно задание (обработка всех новых сообщений одного пользователя за прогон) идёт в среднем 205 секунд. Самое долгое заняло почти 12 минут. Но так как все работает ночью, то 12 минут вполне приемлимо.

Что ломалось

Часть заданий отменилась на середине

В базе остался текст cancelled - model changed to v4-flash. Я менял модель на лету, а задание в этот момент выполнялось. Получилось щабавно: часть чанков посчитана одной моделью, часть другой. Данные не сломались, но такое лучше не повторять.

Агент отвечал «ничего не найдено», хотя файлы на месте

Это была самая неприятная поломка, потому что выглядело это как «память пустая». Оказалось, у OpenClaw есть баг: инструмент memory_search возвращает ложные нулевые результаты, пока внутренний поисковый движок работает нормально. Причина в закэшированном memory manager. Помогает принудительная переиндексация:

openclaw memory index --force

После этого я прибил версию образа гвоздями и запретил себе обновлять её на «latest»: мажорные версии меняют и схему конфигов, и схему внутренней базы. Один раз я уже на этом обжёгся.

Сессии Telegram живут не вечно

Telethon‑сессия может отвалиться, и тогда пользователю нужно заново авторизоваться. Сделал отдельный флоу с кодом из Telegram и сообщением в бота «переподключите аккаунт». В дашборде при повторной авторизации список чатов пересобирается, иначе остаются мёртвые ссылки на чаты, к которым уже нет доступа.

Сам Telegram из России недоступен

Это отдельная головная боль. Сначала я поднимал WARP через sing‑box, но конструкция оказалась хрупкой: то туннель отвалится, то скорость никакая. В итоге переехал на MTProto‑прокси через Cloudflare Worker. Использую открытый Flowseal/tg‑ws‑proxy, он умеет заворачивать MTProto в WebSocket через воркер.

Отдельная история: как это мониторить. Первая версия проверки просто стучалась TCP‑коннектом и говорила «всё хорошо», пока пользователи не начали жаловаться, что сообщения не приходят. Оказалось, MTProto‑прокси не отвечает на голый obfuscated‑init: он ждёт, пока клиент отправит первый фрейм. Пришлось написать healthcheck, который вручную собирает init по протоколу MTProxy (SHA256 от случайных байтов и секрета, AES‑CTR), шлёт req_pq_multi в дата‑центр и ждёт resPQ обратно. Проверяет весь путь сразу: слушатель, прокси, туннель воркера, дата‑центр. Если отвалилось любое звено, то мне приходит уведомление об этом.

Что я бы сделал иначе

Не ставил бы 100 сообщений на фрагмент как «универсальный» размер. На практике в ночной прогон попадает около 20 сообщений на чат, и лимит почти не срабатывает. Зато когда пользователь впервые подключает старую папку, прилетает сразу под две тысячи сообщений, и тогда сто штук в самый раз. То есть параметр надо подбирать под сценарий, а не «вообще».

Гораздо раньше завёл бы метрику «сколько сообщений ещё не превращено в память» по каждому пользователю. До неё я руками лазил в Postgres и считал разницу. Теперь эта цифра сразу показывает, у кого есть какие‑то проблемы.

И не экономил бы на связях. Изначально казалось, что ссылки на сообщения можно сделать потом. А оказалось, что без них невозможно ни отладку сделать, ни показать пользователю, откуда взялась память. В итоге связь оказалась нужнее, чем я думал.

Вместо итога

Вторая память для ассистента не сводится к тому, чтобы прикрутить векторную базу. Тут важны скучные вещи: состояние обработки, дедупликация, идемпотентность, связь. Красивая часть (вызов LLM) занимает процентов пять работы, остальное уходит на инфраструктуру.

Проект живой, пользователей пока пять, и я честно не знаю, что сломается на следующих ста. Но именно поэтому и пишу: если у вас есть опыт с персональной памятью, долгим хранением контекста или доступом к Telegram из России, мне интересно сравнить грабли. Мой проект называется memory.tg, это персональная память для Telegram.