Паттерн fan-out: много вопросов к Jev за один запрос
Самый простой способ сделать систему на Jev быстрее и дешевле — перестать задавать вопросы по одному. Разбираем паттерн speculative fan-out — как отправить модели сразу всё, что может понадобиться коду, и почему лишние вопросы почти ничего не стоят.
Суть паттерна
При работе с LLM привычна цепочка: сначала узнать категорию, потом по ней решить, что спрашивать дальше. С Jev выгоднее обратное. TypeSafe рекомендует класть в запрос все вопросы, которые могут понадобиться системе, а какие ответы использовать — решать уже в коде.
Работает это благодаря двум свойствам модели:
- Параллельность. Все вопросы запроса считаются за один проход, поэтому лишний вопрос почти не добавляет времени ответа.
- Независимость. Ответ одного вопроса не становится скрытым контекстом для другого. Можно добавить или убрать вопрос, и остальные ответы от этого не изменятся.
Спекулятивными называют вопросы, ответ на которые нужен только в части сценариев. Если обращение окажется не про ошибку, ответ о серьёзности ошибки код просто проигнорирует. Если про ошибку — вы сэкономили второй запрос.
Пример: разбор обращения в поддержку
Сервису нужно определить категорию обращения. Для ошибок важны ещё серьёзность и шаги воспроизведения, для оплаты — просит ли клиент возврат, а тон клиента важен всегда. Вместо цепочки запросов отправляем всё сразу:
{
"model": "jev-latest",
"state": "Оплатил заказ 98423 в четверг, деньги списали дважды. После обновления сайта не могу войти в аккаунт. И добавьте оплату через СБП, было бы удобно. Уже начинаю злиться.",
"questions": {
"category": {
"type": "choice",
"instructions": "К какой категории относится обращение?",
"criteria": {
"bug_report": "Что-то сломано или выдаёт ошибку",
"billing": "Списания, счета, возвраты, подписки",
"feature_request": "Просьба добавить новую функцию",
"account": "Вход, доступы, профиль, безопасность",
"other": null
}
},
"bug_severity": {
"type": "score",
"instructions": "Если клиент сообщает об ошибке, насколько она серьёзна?",
"criteria": [
"Косметическая, на работу не влияет",
"Функция сломана, но есть обходной путь",
"Блокирует работу, обходного пути нет"
]
},
"has_repro_steps": {
"type": "noul",
"instructions": "Клиент описывает конкретные шаги, чтобы воспроизвести проблему?"
},
"refund_requested": {
"type": "noul",
"instructions": "Клиент прямо просит вернуть деньги или начислить компенсацию?"
},
"frustration": {
"type": "score",
"instructions": "Насколько клиент раздражён?",
"criteria": ["Спокоен, излагает факты", "Раздражён, но вежлив", "Очень зол"]
}
}
}
Запрос уходит обычным POST на https://api.typesafe.ai/v1/systemone — формат запроса и ответа разобран в справочнике по API. Вопросы bug_severity, has_repro_steps и refund_requested здесь спекулятивные: каждый нужен только в одной ветке. Формулировка «Если клиент сообщает об ошибке…» подсказывает модели, что вопрос условный, — так же пишет и TypeSafe в своих примерах.
Маршрутизация в коде
Ответы приходят одним пакетом, дальше работает обычный if:
from typesafe_sdk import TypeSafeClient
with TypeSafeClient() as client:
r = client.system_one(state=ticket_text, questions=QUESTIONS)
category = r.choices["category"]
severity = r.scores["bug_severity"]
repro = r.nouls["has_repro_steps"]
refund = r.nouls["refund_requested"]
if category.choice == "bug_report":
if severity.score > 1.5 and repro.noul > 0.6:
escalate_to_engineering(ticket_id)
else:
add_to_bug_backlog(ticket_id)
elif category.choice == "billing":
route_to_billing(ticket_id, refund_likely=refund.noul > 0.7)
elif category.choice == "feature_request":
log_feature_request(ticket_id)
else:
route_to_support(ticket_id)
# Тон важен в любой ветке
if r.scores["frustration"].score > 1.5:
flag_for_priority_response(ticket_id)
QUESTIONS — те же вопросы, что в JSON выше: SDK принимает их и словарями, и объектами Choice, Score, Noul. Уровни Score нумеруются с нуля, а score — позиция на шкале, взвешенная по вероятностям. Поэтому на трёхуровневой шкале порог 1,5 означает «ближе к блокирующей ошибке». Пороги взяты из примера TypeSafe — свои подбирайте на размеченных данных, как описано в статье об уверенности Jev.
Как собрать набор вопросов
Удобнее идти от кода, а не от модели:
- Нарисуйте дерево ветвлений обработчика: какие решения он принимает и в каком порядке.
- Для каждого узла запишите вопрос, который закрывает решение, и выберите тип — Noul, Choice или Score.
- Все вопросы к одному и тому же state объедините в один запрос, на каком бы уровне дерева они ни стояли.
- В отдельный запрос выносите только те, для которых код должен сначала что-то подгрузить или изменить state.
Так дерево из десятка ветвей превращается в один вызов API, а логика ветвления целиком остаётся в коде, где её легко читать и тестировать.
Почему это дешевле и быстрее
TypeSafe проверила паттерн в кукбуке Parallel questions. Взяли статью Википедии о GDPR (около 54 000 символов) и 13 вопросов: 8 Noul, 2 Choice и 3 Score. Каждый способ отправки прогнали по пять раз на jev-1.12:
| Способ | Запросов | Стоимость | Время |
|---|---|---|---|
| Все 13 вопросов в одном запросе | 1 | $0,000497 | 0,27 с |
| По одному вопросу на запрос | 13 | $0,006090 | 2,71 с |
Итог — примерно в 12 раз дешевле и в 10 раз быстрее. Ответы при этом не изменились: большинство совпало во всех повторах до знака, а у двух вопросов был небольшой случайный разброс — сопоставимый при обоих способах.
Экономия появляется потому, что основную часть токенов занимает state. Тринадцать одиночных запросов тринадцать раз оплачивают один и тот же документ, пакетный — один раз. Чем больше документ, тем ближе выигрыш к кратному числу вопросов. Есть и оговорка: время в таблице — сумма последовательных вызовов. Если слать одиночные запросы параллельно, разрыв по времени сократится, но разница в токенах останется.
Ещё два аргумента за fan-out:
- Лимиты. У Jev 1.13 ограничение — 1200 запросов в минуту и 250 000 токенов в секунду, причём TypeSafe предупреждает, что лимиты сейчас меняются динамически. Один запрос вместо пяти расходует минутную квоту в пять раз медленнее.
- Накладные расходы. По данным Polza.AI, каждый запрос несёт около 280 служебных токенов. Чем меньше запросов, тем меньше таких расходов.
Посчитать свой сценарий можно в калькуляторе стоимости.
Как делать не надо
В демо умного дома TypeSafe показывает антипример. Команду «выключи весь свет в доме» можно разбирать цепочкой:
- Какая это категория запроса? → команда умному дому.
- Только после ответа: какая зона и какое устройство? → весь дом, свет.
- Только после ответа: что сделать со светом? → выключить.
Цепочка минимизирует число вопросов, но выходит медленнее и дороже одного запроса со всеми вопросами. В самом демо каждый запрос пользователя проверяется длинным списком Choice-вопросов — категория, комната, устройство, действие, — и большинство ответов код отбрасывает.
Когда второй запрос оправдан
Вопросы одного запроса независимы, поэтому ответ на один нельзя использовать как вход для другого внутри того же вызова. По правилу TypeSafe, второй запрос нужен, только если код не может его собрать без первого ответа:
- ответ нужен, чтобы подгрузить дополнительные данные в state;
- ответ определяет, из чего состоит state;
- ответ задаёт варианты следующего вопроса.
В документации три таких примера. В подборе навыков для агента первый запрос ранжирует 182 навыка, второй перечитывает полные описания трёх лучших. В восстановлении структуры текста блоки, которые нужно классифицировать, появляются только после первого запроса. В иерархической классификации выбранная ветка задаёт варианты следующего уровня. Во всех остальных случаях правило одно: если вопрос можно задать к исходному state, задавайте его сразу.
Практические советы
- Пишите полный вопрос в
instructions. Ключ вопроса вродеbug_severityмодель не видит — он нужен только вашему коду. - Добавляйте вариант
other. В спекулятивных Choice он особенно важен: вопросу о типе проблемы с доставкой, заданному к письму про оплату, нужен вариант «ничего из перечисленного». - Следите за контекстом. Лимит Jev 1.13 — 64 тысячи токенов на state и все вопросы вместе и 32 тысячи на state плюс самый длинный вопрос. Длинные описания вариантов в десятках вопросов быстро съедают бюджет.
- Логируйте все ответы, даже неиспользованные. Спекулятивные ответы уже оплачены. По ним видно, как часто вопрос оказывается нужен и не пора ли его убрать.
- Проверяйте код от ИИ-ассистента. По наблюдению TypeSafe, агенты для программирования чаще людей делают по одному вопросу на вызов. Для них компания выпустила agent skill, который учит собирать вопросы в один запрос.
Fan-out хорошо сочетается с другими паттернами: из пачки Score-ответов собирают составную оценку, а категорию и сложность используют для маршрутизации по намерениям.
Частые вопросы
Замедляет ли Jev большое число вопросов в одном запросе?
По данным TypeSafe, почти нет — все вопросы считаются параллельно. Растёт только число входных токенов, потому что каждый вопрос добавляет свои инструкции и варианты ответа.
Влияют ли вопросы в одном запросе друг на друга?
Нет. Каждый вопрос оценивается независимо, ответ одного не становится контекстом для другого. В кукбуке TypeSafe ответы при пакетной и поштучной отправке совпали в пределах случайного разброса.
Сколько вопросов можно задать Jev за один запрос?
Отдельного лимита на число вопросов в документации нет. Ограничивает контекст — 64 тысячи токенов на state и все вопросы вместе и 32 тысячи на state плюс самый длинный вопрос.
Когда всё-таки нужен второй запрос?
Когда код не может собрать второй запрос без первого ответа — например, нужно подгрузить данные или выбрать варианты следующего вопроса по ветке, которую выбрала модель.