Гайды

Агент в Telegram: один ассистент на сайте и в мессенджере

Главное требование к агенту в мессенджере — одна личность и одна память на обеих поверхностях. Разбираем отображение чатов в сессии, allowlist отправителей и защиту от дублей.

Подключить модель к мессенджеру просто: получить токен, читать обновления, отвечать текстом. За вечер работает. Через неделю выясняется, что у вас два разных ассистента: тот, что на сайте, помнит ваши проекты, а тот, что в мессенджере, каждый раз знакомится заново.

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

Требование, вокруг которого стоит строить всю работу: одно ядро, одна личность, одна память, разные транспорты. Всё остальное — детали, но детали с зубами.

#Что понадобится

  • Работающий ассистент с сохраняемой историей и памятью. Если памяти нет, мессенджер её не создаст.
  • Отдельный процесс под мессенджер. Не поток внутри веб-приложения: у него другой жизненный цикл, и он должен переживать перезапуск сайта.
  • Токен бота и понимание, что он равносилен паролю: попав в чужие руки, он даёт полный доступ к переписке бота.
  • Идентификатор вашего аккаунта в мессенджере — числовой, а не имя пользователя. Имя меняется, идентификатор нет.

#Шаг 1. Развести идентичность и разговор

Две разные вещи, которые постоянно смешивают.

Идентичность — кто пишет. Один человек, у которого есть аккаунт на сайте и аккаунт в мессенджере. Память общая, потому что человек один.

Разговор — где пишет. Личный чат, групповой чат, тема внутри группы, вкладка на сайте. Это разные ленты, и смешивать их нельзя: реплика из рабочей группы не должна оказаться посреди личной переписки.

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

#Шаг 2. Построить отображение чата в сессию

Нужна таблица связей: внешний чат и тема — во внутреннюю сессию разговора.

      ВНЕШНЯЯ СТОРОНА              ВНУТРЕННЯЯ СТОРОНА

  личный чат (chat_id)      ──▶   сессия «Личное»
                                        │
  группа (chat_id)          ──▶   сессия «Рабочая группа»
   └ тема 1 (thread_id)     ──▶   сессия «Тема: релиз»
   └ тема 2 (thread_id)     ──▶   сессия «Тема: найм»
                                        │
  вкладка на сайте          ──▶   сессия «Веб»
                                        │
                                        ▼
                              ОБЩАЯ ПАМЯТЬ ВЛАДЕЛЬЦА
                              (факты, предпочтения,
                               проекты — одни на всех)

Три правила, которые стоит заложить сразу:

  • Уникальность связи. Пара «чат плюс тема» отображается ровно в одну активную сессию. Без ограничения уникальности в базе вы получите две ленты на один чат и не заметите этого месяц.
  • Группа никогда не сливается с личным чатом автоматически. Даже если человек один и тот же.
  • Фоновая задача пишет в свою сессию. Утренняя сводка, сработавшее напоминание, результат ночной обработки — отдельная лента, а не вклинивание в текущий разговор.

Как устроено хранение истории с временными метками и почему это отдельная задача — в материале про чат с памятью и битемпоральность.

#Шаг 3. Настроить allowlist отправителей

Самый важный шаг, и его нельзя откладывать «на потом»: бот в мессенджере доступен всем, кто узнал его имя.

Чек-лист, который должен быть выполнен целиком до первого запуска на боевом токене:

□ Личные сообщения принимаются только от одного числового
  идентификатора владельца. Не от имени пользователя — оно меняется.

□ Групповые чаты работают только по явному списку идентификаторов.
  Пустой список означает «ни одна группа», а не «все».

□ Неизвестный отправитель не создаёт сессию и не создаёт запись
  в истории. Не «отвечаем отказом» — вообще не заводим ничего.

□ Членство в разрешённой группе не даёт прав владельца.
  Любой участник группы может писать, но команды выполняет владелец.

□ Инструменты в группе — отдельный узкий список, независимо
  от того, кто прислал сообщение.

□ Токен бота не пишется в логи и не показывается в интерфейсе.

□ При ошибке базы или проверки доступа — отказ, а не пропуск.

Четвёртый пункт — тот, на котором ломаются самодельные реализации. Разрешить группу целиком и считать это авторизацией означает, что личным агентом управляет любой её участник.

В Persona владелец задаётся числовым идентификатором в конфигурации, разрешённые группы — отдельным списком, и это два независимых механизма, а не один. Как ту же задачу решает сторонний фреймворк Hermes Agent, у которого разделение личных сообщений и групп сделано похожим образом, — в разборе Telegram-allowlist в Hermes Agent.

#Шаг 4. Ограничить инструменты по поверхности

Групповой чат — поверхность, где текст пишут посторонние. Любая их реплика попадает в контекст, а значит может повлиять на то, что модель решит вызвать.

Практическое решение: в группе доступен минимальный набор, и он задан явным списком, а не выведен из класса риска. В Persona это ровно один инструмент — поиск в сети. Не «все читающие»: универсальный HTTP-инструмент принимает произвольные заголовки и тело, а прямое открытие страницы обходит проверку адреса, поэтому оба остаются вне группы.

Отдельно про чужие реплики в контексте: их стоит обрамлять явной меткой как недоверенный внешний текст. Это не защита, а снижение частоты — но снижение заметное.

Общая рамка распределения прав по классам и поверхностям — в руководстве по белым спискам и правам по умолчанию.

#Шаг 5. Подтверждение на необратимые действия

Мессенджер добавляет удобную вещь — кнопки. Их стоит использовать по назначению: вызовы, которые выполняют код или удаляют данные, не выполняются из текста, а паркуются и ждут нажатия.

Два требования к реализации:

  • Кнопка несёт только непрозрачный идентификатор. Не имя инструмента и не аргументы: полезная нагрузка кнопки приходит от клиента и не должна определять, что именно выполнится. Имя и аргументы перечитываются из строки в базе по этому идентификатору.
  • У отложенного действия есть срок жизни. Кнопка, нажатая через сутки, не должна выполнить команду, о которой все забыли.

Так это и сделано у нас: вызовы классов «выполнение» и «разрушение», пришедшие с поверхности мессенджера, не выполняются на месте, а превращаются в карточку с двумя кнопками, и если подтверждать негде — действие не выполняется вовсе.

#Шаг 6. Дедупликация и доставка

Мессенджер доставляет обновления с гарантией «хотя бы один раз». Перезапуск процесса в середине обработки приведёт к тому, что то же сообщение придёт снова.

  • Каждое обновление имеет номер. Обработка помечается в базе до начала работы, повторное обновление с тем же номером отбрасывается.
  • Ответ и запись в очередь отправки — в одной транзакции. Иначе после сбоя вы получите либо ответ, которого нет в истории, либо запись в истории без отправленного ответа.
  • Один читатель обновлений. Два параллельных процесса, читающих одну очередь, дадут двойные ответы.

#Проверка результата

Подключение считается готовым, когда проходят шесть проб:

  1. Факт, рассказанный в мессенджере, вспоминается на сайте.
  2. Сообщение от постороннего в личку не создаёт ни сессии, ни записи в истории.
  3. Участник разрешённой группы не может заставить агента выполнить команду.
  4. Запрос на удаление из мессенджера приводит к карточке с кнопками, а не к удалению.
  5. Перезапуск процесса в середине ответа не создаёт второго вопроса в истории и не теряет завершённый ответ.
  6. Реплики из группы не появляются в личной ленте.

Пятая проба неудобна в исполнении — придётся убить процесс в нужный момент, — и именно она отделяет работающее подключение от демонстрационного.

#Что может пойти не так

Бот не видит сообщений в группе. Это ожидаемое поведение: у ботов в группах ограниченный доступ к сообщениям, и без изменения настроек бот получает в основном упоминания, ответы и команды. Не считайте это поломкой и не пытайтесь обойти — объясните в интерфейсе.

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

Стриминг превращается в спам. Отправлять каждый фрагмент ответа отдельным сообщением нельзя. Работает редактирование одного сообщения раз в секунду с небольшим.

Токен утёк в лог. Проверьте, что он не пишется при ошибках подключения, — это самое частое место утечки.

Одна сессия на все группы. Симптом — обсуждение из одной группы всплывает в другой. Лечится ограничением уникальности в таблице связей.

Мессенджер живёт внутри веб-процесса. Перезапуск сайта роняет бота, а тяжёлый цикл опроса мешает отдавать страницы. Отдельный процесс — не архитектурная роскошь, а условие работоспособности.

Напоминания приходят не в то время. Обычно дело в часовом поясе, привязанном к сессии, а не к человеку; разбор — в материале про расписание на естественном языке.

#Когда мессенджер не нужен

Если вы работаете с ассистентом только за компьютером — не подключайте. Мессенджер добавляет отдельный процесс, отдельную поверхность атаки и отдельный набор ошибок доставки, а взамен даёт удобство, которое нужно не всем.

Если у вас нет памяти, общей между поверхностями, — сначала сделайте её. Иначе вы получите второго ассистента, не знающего того, что знает первый.

Если вы хотите пустить в бота коллег или семью — учитывайте, что в Persona доступ к личному ассистенту устроен как один владелец плюс список разрешённых групп, а не как многопользовательская система с ролями. Общая картина того, что здесь работает, а что остаётся проектом, — в разборе агентной архитектуры Persona.

#Частые вопросы

#Может ли бот прочитать мою переписку за прошлый год?

Нет. Обычный бот получает только обновления, пришедшие после того, как его подключили, и в группах видит ограниченный набор сообщений. Ретроспективный импорт истории через обычный интерфейс ботов недоступен, и обещать его нельзя.

#Как сделать так, чтобы память была общая с сайтом?

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

#Безопасно ли добавлять бота в групповой чат?

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

#Что будет, если процесс упадёт в середине ответа?

При правильной реализации — ничего страшного: обновление помечается обработанным по своему номеру, поэтому повторная доставка не создаст второй вопрос, а запись ответа и постановка его в очередь отправки происходят одной транзакцией. Без этого вы получите дубли и потерянные ответы.

#Нужен ли отдельный процесс под мессенджер?

Практически — да. Внутри веб-процесса цикл опроса мешает обслуживать запросы, а перезапуск сайта роняет бота. Для небольшой установки процесс мессенджера и фоновый обработчик можно объединить, но отделить их от веб-приложения стоит.

#Как ограничить, кто может писать боту?

Числовым идентификатором владельца для личных сообщений и явным списком идентификаторов для групп. Имя пользователя для этого не годится: оно меняется и может быть занято другим человеком.

#Вывод

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

Если у вас пока нет общей памяти — начните с неё, иначе получите второго ассистента вместо второй двери к первому. И проверьте подключение шестью пробами, включая неудобную: убейте процесс посреди ответа и посмотрите, что окажется в истории.

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт