Выкатывая AI-фичу в продакшен, разработчики обычно ожидают, что пользователи будут писать развернутые, структурированные промпты. В реальности в поле ввода прилетает «сделай красиво», «почини код» или «напиши текст про насосы». Нейросеть получает этот огрызок контекста и выдает максимально водянистый, шаблонный и бесполезный результат.
Первая реакция разработчика — раздуть системный промпт. Мы начинаем писать гигантские инструкции: «Если пользователь просит починить код, предположи, что это Python, используй SOLID, проверь логи..». Мы пытаемся угадать контекст за пользователя. Мы строим эвристики, парсим ввод, подставляем дефолтные значения. Но угадывание — это тупик. Проблема не в том, что модель глупая. Проблема в том, что у нее физически нет данных для решения задачи.
Если к живому senior-разработчику придет клиент и скажет «сделай мне сайт», разработчик не пойдет молча писать код на React. Он начнет задавать вопросы. Какой сайт? Для кого? Какие референсы? Нейросеть должна вести себя точно так же.
Здесь вступает в игру инверсия контроля (Inversion of Control) в проектировании AI-интерфейсов. Вместо парадигмы «пользователь командует — нейросеть угадывает и делает», мы переходим к парадигме «пользователь инициирует — нейросеть интервьюирует — нейросеть делает». Мы забираем у пользователя обязанность быть промпт-инженером и отдаем эту роль самой модели.
Ловушка наивного подхода
Самый простой способ заставить модель задавать вопросы — добавить в системный промпт фразу: «Если тебе не хватает информации, задай уточняющие вопросы». Это фатальная ошибка.
LLM, получив такую инструкцию, радостно вывалит на пользователя список из пятнадцати пунктов. «1. Какая целевая аудитория? 2. Какой tone of voice? 3. Какие ключевые слова использовать? 4..». Пользователь видит эту простыню текста, пугается и закрывает вкладку. Никто не хочет проходить допрос с пристрастием вместо получения быстрой услуги.
Чтобы режим «агента-интервьюера» работал, диалог должен быть строго дозированным. Один, максимум два вопроса за итерацию. Фокус только на критически важных деталях, без которых невозможно сделать следующий шаг. И самое главное — процесс должен быть управляемым на уровне архитектуры приложения, а не только на уровне текста промпта.
Машины состояний для диалога
Для реализации управляемого интервьюера нам нужна машина состояний (State Machine). Мы не можем позволить модели свободно генерировать текст. Мы должны загнать ее в жесткие рамки выбора действий.
В простейшем виде наша машина состояний имеет три узла:
- Оценка (Assessment): Модель анализирует текущий контекст. Достаточно ли данных для финального ответа?
- Сбор данных (Clarification): Модель формулирует один точечный вопрос пользователю.
- Исполнение (Execution): Модель генерирует итоговый результат.
Чтобы предотвратить бесконечный цикл вопросов (когда пользователь отвечает односложно, а модель продолжает допытываться), мы вводим счетчик итераций. Например, максимум три вопроса. Если после трех итераций контекст все еще неполный, машина состояний принудительно переводится в режим Execution, и модель выдает лучший результат из возможного.
Интеграция с RouterAPI: Управление циклом через Tool Calling
На практике эта машина состояний реализуется через механизм вызова функций (Tool Calling/Function Calling). Мы используем RouterAPI для маршрутизации запросов к нужной модели (например, Claude 3.5 Sonnet или GPT-4o, которые отлично справляются с вызовом инструментов и строгим следованием JSON-схемам).
Вместо того чтобы просить модель просто ответить текстом, мы передаем ей в массиве tools два доступных инструмента. Определение этих инструментов должно быть максимально строгим, чтобы исключить галлюцинации.
Первый инструмент — ask_user — предназначен для запроса дополнительной информации. Его схема включает параметры:
question(string): Сам вопрос, сформулированный максимально кратко и понятно для обывателя.reasoning(string): Внутреннее объяснение модели, зачем ей нужна эта информация. Этот параметр мы не показываем пользователю, но он критически важен для самой LLM. Проговаривание логики (Chain of Thought) перед формулировкой вопроса заставляет модель задавать более осмысленные вопросы и снижает вероятность глупых уточнений.options(array of strings, optional): Если вопрос подразумевает выбор из нескольких вариантов, модель может предложить их в виде массива, чтобы фронтенд отрендерил их как кнопки.
Второй инструмент — deliver_final_result — используется для выдачи готового результата.
content(string): Итоговый сгенерированный текст, код или структура данных.confidence_score(number): Оценка моделью того, насколько полно она решила задачу на основе имеющихся данных.
Системный промпт при этом настраивает правила игры: «Ты — эксперт-исполнитель. Твоя задача — решить проблему пользователя. Проанализируй запрос. Если для качественного решения не хватает критически важных вводных, вызови инструмент ask_user. Задавай не более одного вопроса за раз. Спрашивай только то, что действительно изменит результат. Если данных достаточно, или если ты уже задал 3 вопроса, вызови инструмент deliver_final_result. Никогда не отвечай обычным текстом, всегда используй один из инструментов».
Когда запрос уходит в RouterAPI, бэкенд переходит в режим ожидания. Если RouterAPI возвращает ответ, где finish_reason равен tool_calls, бэкенд парсит ответ. Обнаружив вызов ask_user, система извлекает вопрос (и опциональные кнопки-подсказки) и отправляет их на фронтенд. Сессия выполнения приостанавливается. В базе данных сохраняется текущий контекст диалога, включая сам вызов инструмента.
Пользователь видит в интерфейсе аккуратный вопрос от ассистента: «Уточните, пожалуйста, какую версию Laravel вы используете?». И, возможно, кнопки: «Laravel 10», «Laravel 11», «Не знаю». Пользователь отвечает.
Бэкенд берет этот ответ, формирует сообщение с ролью tool (или function, в зависимости от спецификации провайдера), содержащее результат выполнения инструмента ask_user, добавляет его в историю сообщений и снова отправляет весь массив в RouterAPI. Модель снова проходит фазу оценки. Получив ответ пользователя, она понимает, что теперь данных достаточно, и вызывает deliver_final_result. Бэкенд извлекает финальный контент и отдает его пользователю, закрывая сессию.
UX и рендеринг на клиенте
Инверсия контроля требует изменения и на стороне клиентского интерфейса. Стандартное окно чата работает хорошо, но иногда режим интервьюера лучше интегрировать прямо в рабочую область.
Например, если пользователь пишет код в редакторе и просит «оптимизировать функцию», вопрос от модели не должен перекрывать весь экран. Он может появиться в виде небольшого поп-апа (inline prompt) прямо под строкой кода. Если модель вернула параметр options в инструменте ask_user, фронтенд должен отрендерить их как кликабельные чипсы (chips), чтобы пользователь мог ответить в один клик, вообще не касаясь клавиатуры. Снижение трения (friction) при ответе на уточняющие вопросы — залог того, что пользователь дойдет до конца воронки и получит качественный результат.
Обработка краевых случаев и экономика токенов
При работе с RouterAPI в таком многошаговом режиме важно учитывать несколько технических нюансов.
Во-первых, управление состоянием. Пока пользователь думает над ответом, HTTP-соединение держать открытым нельзя. Архитектура должна быть полностью асинхронной. Состояние диалога (весь массив сообщений) должно сериализоваться и лежать в Redis или реляционной базе данных. Каждая итерация — это независимый stateless-запрос к бэкенду, который поднимает контекст из базы, обращается к RouterAPI и снова засыпает.
Во-вторых, деградация моделей и фоллбэки. Если RouterAPI переключит запрос на резервную модель (например, из-за таймаута основного провайдера), эта fallback-модель тоже должна безупречно поддерживать Tool Calling. Если резервная модель слабее и начинает путаться в JSON-схемах или пытается ответить обычным текстом, игнорируя инструменты, бэкенд должен уметь это перехватывать. В таких случаях система должна принудительно завершать цикл интервью, извлекать текст из ответа и отдавать его пользователю как финальный результат, чтобы не сломать UI.
В-третьих, стоимость токенов. Каждый новый виток цикла отправляет в API всю предыдущую историю переписки. Если пользователь прикрепил к первому сообщению лог ошибки на 50 килобайт или объемный документ, этот лог будет летать туда-сюда при каждом уточняющем вопросе. Три вопроса — это четыре запроса к API, и объем контекста растет как снежный ком. Для оптимизации костов при работе через RouterAPI необходимо применять техники кэширования промптов (Prompt Caching). Если провайдер (например, Anthropic) поддерживает кэширование, системный промпт и тяжелые исходные данные пользователя помечаются специальным флагом. При последующих запросах в рамках того же интервью тарифицируются только новые токены (вопрос модели и короткий ответ пользователя), что снижает затраты на порядки.
Смена парадигмы
Режим агента-интервьюера меняет саму суть взаимодействия с AI. Интерфейс перестает быть глухой стеной, в которую нужно бросать идеально выверенные промпты, надеясь на чудо. Он становится активным участником процесса, который берет на себя ответственность за сбор спецификации.
Разработчику больше не нужно писать монструозные костыли для парсинга невнятных запросов или пытаться предугадать все возможные намерения пользователя в одном системном промпте. Нейросеть сама выясняет, что нужно сделать. Да, это усложняет бэкенд: появляются машины состояний, асинхронная обработка вызовов функций, управление контекстом в Redis. Но это единственная рабочая стратегия, когда ваш продукт выходит за пределы гиковской аудитории и сталкивается с реальными пользователями, которые просто хотят, чтобы их проблема была решена, не изучая при этом искусство промпт-инжиниринга.