Бот с характером живёт не в промпте: где находится поведение LLM-бота в групповом чате. Часть 1
Открываю серию о Telegram-боте «Джонни» — язвительном инженерном персонаже, который начинался как развлечение на вечер, а закончился системой с маршрутизацией, подавлением лишних ответов, границами контекста и регрессионными проверками. В первой части — о том, почему успешный тест один на один ничего не гарантирует, и о слоевой модели, без которой поведение бота в живой группе не отладить.
Знакомая ловушка: вы дописали system prompt, придумали боту характер, задали ему пару вопросов в личной переписке — и получили осмысленные, точные, местами остроумные ответы. Возникает ощущение законченной работы.
Затем тот же бот оказывается в настоящей группе — и ощущение рассыпается. Прямой вопрос повисает без ответа. Зато в разговор двух людей, где бота никто не звал, он вклинивается. К концу вечера его реплик становится столько, что участники тянутся к кнопке mute. Разбор каждого такого случая приводит к одному и тому же наблюдению: сбой почти никогда не в содержании реплики. Сбой — в выборе момента и адресата.
Серия рассказывает, как подобный бот дорос от «ну, отвечает же» до системы, где характером управляет промпт, а поведением — несколько уровней кода вокруг него. Стартую с общей рамки: чем обманчив приватный тест, какая способность бота оказывается главной и как раскладывать дефекты по слоям вместо бесконечной правки инструкций.
Приватный тест проверяет не то
У диалога тет-а-тет с ботом отсутствует почти вся сложность группы. Любая реплика по определению обращена к нему. Параллельных веток нет. Чужих вопросов, которые можно ошибочно принять на свой счёт, нет. Следить за собственной частотой не нужно. Соседних ботов не существует. Фактически личка испытывает единственный навык — собрать связный ответ. Для современных моделей это давно решённая задача, никакой инженерии тут не требуется.
Группа же заставляет принимать решения, которых в привате не бывает в принципе:
- обращена ли конкретная реплика к боту или мимо него;
- перед ним свежий вопрос или хвост предыдущего обмена;
- лаконичное «пон» — повод молча поставить эмодзи или начало новой мысли собеседника;
- не цепляется ли бот за устаревший кусок истории вместо текущего сообщения;
- сколько раз он уже высказался за последние минуты;
- не завязалась ли у него бесконечная пинг-понг-переписка с чужим ботом.
Заметьте: качество формулировок не влияет ни на один пункт. Всё это решает обвязка — код, срабатывающий до похода в модель и после получения её ответа. Промпту эти решения недоступны физически: в нём нет счётчика недавних реплик бота, нет сведений об адресате, нет сравнения с предыдущим ответом. Отсюда первый тезис серии: способность красиво отвечать и готовность жить в группе — разные свойства, и первое не влечёт второе.
Самый ценный навык — вовремя не ответить
Следующее открытие мало кому нравится. Стоит боту стать по-настоящему живым, как на первое место выходит умение промолчать.
Механика простая, если смотреть глазами участников. Внимание группы — ресурс, и он ограничен. Каждая реплика бота этот ресурс расходует, а окупается далеко не каждая. Пропущенный уместный ответ огорчит одного человека однократно. Бот-говорун, встревающий повсюду, утомляет всех и непрерывно — финал предсказуем: mute или исключение. Несимметричность очевидна: лишнее сообщение обходится дороже недостающего.
Отсюда разворот, который на практике меняет весь проект. Изначальная цель звучала как «пусть отвечает хорошо». Настоящая цель — «пусть в большинстве ситуаций не отвечает вовсе, а голос подаёт там, где это оправдано». И это не косметика вроде «пиши пореже». Это самостоятельная логика из нескольких ступеней: распознать адресацию; отклонить реплику как не требующую ответа; придержать темп; следить за долей бота в общем потоке; разрывать затягивающиеся цепочки «человек — бот» в части случаев ограничиться эмодзи-реакцией вместо текста.
Со стороны похоже на настройки комфорта. Изнутри — компактный движок принятия решений, и его вклад в итоговое впечатление от бота выше, чем вклад любого отдельного ответа. Устройству этого движка посвящена вторая часть серии; пока достаточно зафиксировать принцип: молчание бота — не пассивность, а полноценное поведение, которое проектируют.
Слоевая модель: сначала диагноз, потом лечение
Первая реакция на любой сбой — дописать в system prompt очередное указание: «игнорируй короткие реплики», «не встревай в чужие обсуждения», «не дублируй себя». Пару дней помогает. Потом инструкции начинают противоречить друг другу, промпт распухает, а стабильности не прибавляется. Корень в том, что подавляющая часть дефектов вообще не лежит в плоскости формулировок.
Рабочей оказалась привычка перед любым фиксом отвечать на вопрос: какому слою принадлежит проблема. Раскладка выглядит так:
- routing — определяет, касается ли сообщение бота вообще;
- suppress — отсекает случаи, где отвечать не следует, даже при прямом обращении;
- context — отделяет реплику-задачу от фонового шума беседы;
- memory — обеспечивает преемственность, не протаскивая лишнее состояние;
- reaction — заменяет текст лёгким эмодзи там, где текст избыточен;
- side-effect — всякое публичное действие: отправка, реакция, файл, изображение, запись в базу;
- service — эксплуатационный контур: рестарты, журналы, бэкапы, проверки.
Сила не в самом перечне, а в дисциплине его применения: сперва симптом привязывается к слою, затем чинится внутри этого слоя. Как это выглядит на живых случаях:
| Симптом | Первый порыв | Где дефект на самом деле |
|---|---|---|
| Бот подхватил чужой вопрос из истории | переписать промпт | context (граница «задача vs фон») |
| Проигнорировал нормальное уточнение | смягчить инструкции | routing/suppress |
| Слишком частые реплики | попросить «быть лаконичнее» | cooldown/bot-share |
| Эмодзи-реакция не к месту | добавить правило в промпт | reaction (это side-effect) |
| Продублировал собственный ответ | поднять «разнообразие» | suppress/anti-duplicate |
Правило, сэкономившее в проекте больше всего часов: дефект маршрутизации не лечится стилем, а дыра в suppress не закрывается инструкцией. Раз бот заговорил там, где обязан был молчать, — виноват routing либо suppress, и точка; «модель не так поняла» — неверный диагноз. Голос — это промпт. Поведение — это слои под ним.
Тот же принцип в тридцати строках кода
Чтобы рамка не осталась теорией, ниже — нарочито упрощённая модель выбора между «ответить», «поставить реакцию», «отложить» и «промолчать». Подчеркну: это не фрагмент бота, а учебная игрушка, собранная специально для статьи. Чистый Python 3.10+, ни одной зависимости — запускайте и ломайте.
from dataclasses import dataclass
@dataclass
class Msg:
text: str
addressed: bool # адресовано боту: reply, упоминание, имя
is_reply_to_bot: bool # это reply на сообщение бота
bot_share: float # доля сообщений бота в недавнем окне, 0..1
in_cooldown: bool # бот недавно уже отвечал
LOW_VALUE = {"пон", "спс", "ок", "ясно", "угу", "+"}
def decide(m: Msg) -> str:
# routing: наш ли это ход вообще
if not (m.addressed or m.is_reply_to_bot):
return "silent"
# suppress: не доминировать и не отвечать текстом на пустое
if m.bot_share > 0.5:
return "silent"
if m.text.strip().lower() in LOW_VALUE:
return "reaction"
# cooldown: задержать, но не потерять адресную задачу
if m.in_cooldown:
return "pending"
return "answer"
Прогон на пяти сценариях:
cases = [
Msg("как поднять systemd-сервис?", True, False, 0.1, False),
Msg("спс", False, True, 0.1, False),
Msg("это вообще не боту", False, False, 0.1, False),
Msg("ещё вопрос", False, True, 0.1, True),
Msg("а тут?", True, False, 0.7, False),
]
for m in cases:
print(f"{decide(m):8} <- {m.text}")
Результат:
answer <- как поднять systemd-сервис?
reaction <- спс
silent <- это вообще не боту
pending <- ещё вопрос
silent <- а тут?
Присмотритесь к двум финальным строкам. Реплика «ещё вопрос» обращена к боту, однако попадает под cooldown — и функция возвращает pending: задачу откладываем, но не теряем. Наивная реализация здесь бы просто выбросила сообщение, и человек получил бы игнор на прямой вопрос. Реплика «а тут?» тоже адресная, но к этому моменту бот занял 70% недавнего эфира — и suppress глушит даже прямое обращение. Обе развилки пройдены до какого-либо контакта с моделью; текстом инструкции такие решения не воспроизводятся.
Оговорка ради честности: жёсткий набор LOW_VALUE оставлен исключительно ради компактности примера. Боевому боту словарь из «пон / спс / ок» противопоказан — подобные списки хрупкие и вечно отстают от живой речи. Разбору того, почему лексические триггеры проигрывают семантическим и что ставить взамен, отведена отдельная часть серии.
Границы возможностей промпта
Резонный вопрос: модели нынче умные — почему бы не переложить всю эту механику в system prompt? Потому что промпт слеп к состоянию. Ему неоткуда узнать текущий bot_share, он не в курсе, что бот высказывался двадцать секунд назад, у него нет доступа к очереди отложенных задач, и гарантированно заменить текст реакцией он тоже не в силах. Модель принимает собранный вами контекст и отдаёт текст — на этом её полномочия заканчиваются. Контроль темпа, идемпотентность, защита публичных действий — всё, что опирается на память о недавних событиях и на ограничения частоты, обитает в коде вокруг вызова, а не в строках внутри него.
Знакомый многим хаос LLM-проектов — когда всякую новую проблему закрывают ещё одним абзацем инструкций — не признак слабого промпта. Это симптом того, что на один слой навесили обязанности пяти остальных.
Куда движется серия
Перед вами общая рамка; дальше — по слою за раз, с подробностями:
- анатомия «молчания»: routing, suppress, cooldown, bot-share и защита от зацикливания с соседним ботом;
- отчего бот отвечает не тому: развод текущей задачи и фонового контекста, устаревшая история, память с привязкой к участнику;
- лексика против семантики: где списки готовых фраз подводят и в каких нишах всё же работают;
- прод-дисциплина для одного бота: побочные эффекты, dry-run-проверки, недостаточность компиляции как критерия и матрица регрессий.
Вывод
Групповой LLM-бот — задача не из области prompt engineering. Инструкции формируют характер и интонацию — верхний, витринный слой. Устойчивость же обеспечивается этажами ниже: маршрутизацией, подавлением лишнего, границами контекста, контролем публичных действий, дисциплиной проверок.
Практическая рекомендация собирающим похожее: не верьте приватным тестам и не пытайтесь чинить поведение словами. Любой фикс начинайте с вопроса «чей это слой» — с высокой вероятностью выяснится, что формулировки ни при чём. Бот, умеющий лишь отвечать, к группе не готов. Готовым его делает умение молчать.
Материалы к серии. Публичные обезличенные материалы проекта — высокоуровневая архитектура, слоевая модель runtime, открытый чек-лист регрессий, отобранные примеры ответов — собраны здесь: https://github.com/UtochkaI/johnny-telegram-bot-habr. Боевого кода, промптов и конфигурации в репозитории нет, только безопасные заметки к статьям. Код в тексте написан с нуля для иллюстрации и к коду бота отношения не имеет.





2 комментария
Добавить комментарий