У LLM есть неприятное свойство: она может очень убедительно ответить на вопрос, на который у неё нет надёжных данных. Для обычного чат‑бота это может быть просто ошибкой в тексте. Для ассистента, который способен отправлять сообщения от моего имени, это уже проблема архитектуры.

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

Именно для этой задачи я сделала AI Career Inbox Assistant — Telegram‑бота, который помогает разбирать входящие сообщения от рекрутеров. Он хранит контекст конкретных вакансий и историю диалога, извлекает из сообщений нужную информацию, сверяется с профилем кандидата и правилами ответа, а затем выбирает действие: подготовить draft, попросить меня принять решение, ничего не делать или, в узком случае, отправить безопасный автоответ.

В первой версии я думала прежде всего о качестве текста: насколько хорошо AI понимает сообщение и насколько естественно формулирует ответ. Но довольно быстро обнаружилась другая, гораздо более интересная проблема. Даже очень хороший ответ может быть неправильным действием, если система не должна была отправлять его самостоятельно.

Поэтому в этой статье я хочу разобрать не весь Career Inbox и не все его функции, а один архитектурный принцип, который оказался для меня самым важным:

что модель считает вероятным
        ≠
что системе разрешено сделать

Telegram дал мне возможность автоматизировать ответы

В Telegram Business есть возможность подключить бота, который обрабатывает сообщения и может отвечать от имени аккаунта, причём доступ бота можно ограничивать определёнными чатами. Для моего проекта это очень удобная возможность: часть сообщений действительно не требует моего участия, и если рекрутер спрашивает что‑то простое, а ответ состоит только из заранее подтверждённых фактов, нет особого смысла каждый раз писать его вручную. Но здесь появляется важная граница: техническая возможность автоматически ответить ещё не означает, что AI должен автоматически отвечать. Поэтому я не стала строить схему:

сообщение
    ↓
LLM
    ↓
ответ
    ↓
отправить
Доступ бота можно ограничить выбранными чатами и разрешениями.
Настройки автоматизации чатов в Telegram: доступ бота можно ограничить выбранными чатами и разрешениями.
Настройки автоматизации чатов в Telegram: доступ бота можно ограничить выбранными чатами и разрешениями.

Мне хотелось другой модели:

сообщение
    ↓
LLM понимает контекст
    ↓
система проверяет факты и policy
    ↓
решение о разрешённом действии
    ↓
отправка / draft / вопрос пользователю

Именно здесь для меня появилась главная идея статьи:

confidence ≠ permission

Сначала я думала о качестве ответа

Когда я только начинала делать AI Career Inbox, задача выглядела довольно привычно:

сообщение рекрутера
        ↓
       LLM
        ↓
предложенный ответ

Но довольно быстро стало понятно, что хороший текст — ещё не хороший результат. Можно написать идеально сформулированный ответ и при этом ошибиться в самом важном: имею ли я вообще право отправлять его автоматически? Например, модель может увидеть в сообщении рекрутера название технологии и решить, что у кандидата наверняка есть похожий опыт. Для генерации текста это может выглядеть правдоподобно, но для автоматической отправки от моего имени — уже нет. Поэтому я перестала рассматривать LLM как компонент, который принимает финальное решение. Она может анализировать сообщение, выделять намерение, извлекать факты и предлагать вариант ответа, но последнее слово должно принадлежать не модели.

Confidence ≠ Permission

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

LLM:
«Насколько вероятно, что я правильно поняла сообщение?»

Policy:
«Имеет ли система право выполнить действие?»

Это не одно и то же. Даже если модель очень уверена в своей интерпретации, это ещё не означает, что действие разрешено. Например:

Are you based in Serbia and do you have Tableau experience?

Первая часть может быть подтверждена моим профилем. А Tableau в профиле нет. В такой ситуации модель вполне может попытаться построить связный ответ, опираясь на похожие BI‑инструменты. Но моя система должна остановиться:

Serbia  → known
Tableau → UNKNOWN

Result → ASK_USER

Для меня это важнее, чем способность модели красиво сформулировать ответ.

Inference — не verified fact

Отсюда появилось ещё одно правило: я разделяю то, что модель вывела из сообщения, и то, что действительно известно системе. Источником фактов о кандидате является профиль. Если в нём написано, что я работала с PostgreSQL, это можно использовать как подтверждённый факт. Если в профиле нет Tableau, система не должна превращать другие BI‑инструменты в «примерно тот же опыт». То есть:

из сообщения → inference

из профиля → verified fact

Это небольшая разница в терминах, но архитектурно она очень важна. LLM может сказать:

The candidate probably has similar experience.

Система не должна автоматически превращать probably в:

The candidate has experience.

Если факта нет, результатом остаётся UNKNOWN. И это нормально.

UNKNOWN — это полноценный результат

Обычно от AI‑системы хочется получить как можно больше определённых ответов. Мне пришлось привыкнуть к обратному: иногда правильный результат — это не YES и не NO, а UNKNOWN.

UNKNOWN

Дальше система должна:

UNKNOWN
   ↓
не придумывать
   ↓
не отправлять автоматически
   ↓
передать решение пользователю

На практике это означает, что неизвестность становится частью модели данных, а не исключением, которое нужно срочно заполнить предположением. Для моего сценария это особенно важно, потому что бот говорит от моего имени. Если AI ошибся в обычном черновике, я могу исправить текст. Если он самостоятельно отправил неверное утверждение рекрутеру, исправлять его уже поздно.

Между LLM и Telegram появился decision layer

После этого архитектура стала выглядеть примерно так:

Recruiter message
        ↓
       LLM
        ↓
intent + extracted facts
        ↓
conversation / job context
        ↓
candidate profile
        ↓
reply policy
        ↓
     decision
        ↓
┌───────────────┐
│ AUTO_REPLY    │
│ DRAFT         │
│ ASK_USER      │
│ NO_REPLY      │
│ ESCALATE      │
└───────────────┘
        ↓
      action

Это не отдельная «умная модель», а слой ограничений. LLM помогает понять, что происходит, policy решает, что разрешено делать, а Telegram‑часть отвечает за фактическое выполнение действия. Так я разделила систему на три уровня:

Understanding
     ↓
Decision
     ↓
Execution
AI разобрал сообщение и контекст вакансии, но передал окончательный выбор пользователю.
Пример решения ASK_USER: AI разобрал сообщение и контекст вакансии, но передал окончательный выбор пользователю.
Пример решения ASK_USER: AI разобрал сообщение и контекст вакансии, но передал окончательный выбор пользователю.

И мне стало намного проще понимать, где именно искать ошибку. Если модель неправильно поняла сообщение — проблема в анализе. Если модель всё поняла правильно, но система разрешила неправильное действие — проблема в policy. Если решение было правильным, но сообщение ушло два раза — это уже проблема execution/state management.

Permission важнее уверенности модели

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

«Если модель уверена, отправляй».

Правило устроено наоборот:

«Если действие разрешено policy и все необходимые факты подтверждены, его можно выполнить».

Поэтому даже очень уверенный ответ модели не может расширить список разрешённых действий. Это принципиальная граница. Модель не может сама решить:

«Я уверена на 98%, поэтому добавлю ещё один факт».

У неё просто нет такого права.

Почему я не хочу максимальной автономности

Когда говорят об AI‑ассистентах, естественно хочется убрать как можно больше ручных действий. Но для моего сценария максимальная автономность не является целью сама по себе. Мне важнее другая формула:

maximum useful automation
+
minimum uncontrolled decisions

Поэтому есть действия, которые можно автоматизировать, и есть действия, где система должна остановиться. Если сообщение содержит только подтверждённые и разрешённые факты — возможен AUTO_REPLY. Если нужен хороший текст, но отправлять его автоматически нельзя — DRAFT. Если нужно моё решение — ASK_USER. Если отвечать вообще не нужно — NO_REPLY. Если ситуация выходит за предусмотренные правила — ESCALATE. То есть «ничего не делать» тоже является нормальным результатом работы AI.

Но принять правильное решение ещё недостаточно

Здесь я столкнулась с другой проблемой. Допустим, система правильно решила:

decision = AUTO_REPLY

Это ещё не гарантирует, что сообщение будет отправлено ровно один раз. Что произойдёт, если процесс упадёт после отправки в Telegram, но до сохранения результата в SQLite? При перезапуске система может решить, что действие ещё не выполнено. И попробовать отправить его снова. Поэтому пришлось разделить decision и execution state. Для pending‑действий у меня появился жизненный цикл:

PENDING
   ↓
SENDING
   ↓
SENT

При этом переход в SENDING должен выполняться так, чтобы два процесса не смогли одновременно забрать одно и то же действие. Для меня это был хороший пример того, почему AI‑архитектура заканчивается не на prompt и не на выборе модели. Можно идеально решить, что нужно сделать, и всё равно получить неправильный результат, если не продумать, как именно это действие будет выполнено.

Как я проверяла эту границу на практике

Я не пыталась сразу построить большую систему правил. Обычно цикл был намного проще:

изменить правило
      ↓
перезапустить приложение
      ↓
отправить тестовое сообщение в Telegram
      ↓
посмотреть extracted facts
      ↓
посмотреть decision
      ↓
проверить фактическое действие

Для разработки и тестирования я использовала Deploy‑F. Это был удобный способ быстро перезапускать приложение, менять конфигурацию и проверять реальные Telegram‑сценарии. При этом сама логика не привязана к Deploy‑F: проект написан на Python и использует обычный runtime, SQLite и Telegram Business API. Мне был важен именно короткий feedback loop. Не «я написала огромный prompt и надеюсь, что он работает», а:

правило
→
тестовый сценарий
→
decision
→
фактический результат
Сборка и запуск тестовой версии AI Career Inbox Assistant.
Deploy-F сборка и запуск тестовой версии AI Career Inbox Assistant.
Deploy‑F сборка и запуск тестовой версии AI Career Inbox Assistant.

Так постепенно становилось видно, где модель пытается сделать слишком много, а где ограничение действительно находится на уровне системы.

Что изменилось в архитектуре после этого

Самое главное изменение оказалось не в конкретном prompt. Я перестала считать LLM центром системы и теперь думаю о ней как об одном из компонентов:

              ┌──────────────┐
              │     LLM      │
              │ understanding│
              └──────┬───────┘
                     ↓
              ┌──────────────┐
              │    Policy    │
              │  permission  │
              └──────┬───────┘
                     ↓
              ┌──────────────┐
              │  Execution   │
              │ Telegram/API │
              └──────────────┘

И это меняет сам подход к разработке. Я больше не спрашиваю только:

«Насколько хорошо модель отвечает?»

Я задаю ещё два вопроса:

«Что модель имеет право утверждать?»

и

«Что система имеет право сделать на основании этого ответа?»

Для AI‑ассистента, который просто разговаривает с пользователем, эта граница может быть менее критичной. Но для ассистента, который действует от имени человека, она уже становится частью бизнес‑логики.

Итог

Самый полезный вывод из этого проекта для меня оказался довольно простым:

Уверенность модели не должна быть разрешением на действие.

LLM может анализировать, предполагать, извлекать факты и готовить текст. Но право на действие должно определяться отдельными правилами системы. Поэтому хороший AI‑ассистент для меня — это не тот, который умеет сделать всё самостоятельно. Это тот, который умеет понять:

я знаю
→ могу предложить

я знаю и мне разрешено
→ могу выполнить

я не знаю
→ не придумываю

мне нужно решение пользователя
→ останавливаюсь

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

Исходный код

Публичная версия проекта: https://github.com/YuliyaPV/ai‑career‑inbox‑assistant

В публичной версии проекта я не использую реальные Telegram ID, переписки, токены или рабочий профиль. Публичная версия предназначена прежде всего как шаблон архитектуры: персональный слой и правила поведения должны задаваться отдельно. Проект я разрабатывала с помощью ChatGPT, используя его как AI pair programmer.