Переход от чат-бота к системе, которая выполняет задачи с помощью LLM, происходит на уровне структурированного вывода и вызова функций. Здесь модель перестаёт генерировать только обычный текст и начинает формировать данные, выбирать действия и взаимодействовать с остальной инфраструктурой.
В рабочей системе «структурированный вывод» не означает, что режим JSON один раз сработал в тесте. Нужен устойчивый конвейер, который справляется с изменчивостью модели, некорректными результатами, частичными отказами, развитием схем и тем, что LLM не всегда следуют инструкциям.
Статья описывает средства контроля, необходимые рабочей реализации. Сведения об API и их ограничениях проверены 4 августа 2026 года по официальным материалам о структурированном выводе OpenAI, структурированном выводе Claude и структурированном выводе Gemini. Примеры ниже — архитектурные решения, а не результаты сравнительного теста AI Expert.
Два режима
Две связанные, но различимые возможности:
Структурированный вывод: LLM формирует ответ по заданной схеме, обычно в JSON. Он нужен, когда результат модели должен обрабатываться программно.
Вызов функций/инструментов: LLM получает набор функций, которые может вызывать, решает, какую вызвать (если вообще нужно), формирует параметры. Хост-система выполняет функцию и возвращает результат. LLM может вызвать следующие функции или сформировать финальный ответ.
API моделей обычно выставляют это через:
- Параметр формата ответа, принимающий поддерживаемое подмножество JSON Schema: у OpenAI это формат ответа с JSON-схемой, у Claude —
output_config.format, у Gemini — структурированный вывод с управлением через схему. - Массив
tools, описывающий доступные операции, и ответ с вызовом инструмента. Актуальные модели Claude также поддерживаютstrict: trueв определениях клиентских инструментов, поэтому искусственный инструмент только ради получения JSON больше не является единственным вариантом.
Обе возможности связаны: вызов функции — это разновидность структурированного вывода, где схемой служит сигнатура функции.
Паттерн 1: жёсткие, явные схемы
Самый большой выигрыш в надёжности — в ваших схемах.
Рыхлая схема:
{
"type": "object",
"properties": {
"category": { "type": "string" },
"priority": { "type": "string" }
}
}
Жёсткая схема:
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "feature_request", "complaint"],
"description": "The ticket category. Use 'technical' for product bugs and 'account' for login/password issues."
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "Use 'urgent' only for outages or business-critical impact. 'high' for blocking issues on important customers. 'medium' for standard impact. 'low' for nice-to-haves."
}
},
"required": ["category", "priority"],
"additionalProperties": false
}
Жёсткая версия:
- Ограничивает значения известным перечислением enum (никакого дрейфа в свободную форму).
- Включает описания, которые работают как встроенные промпты (модель их использует).
- Требует полей (вы не получите частичных ответов).
- Запрещает лишние поля (никаких случайных выдуманных ключей).
В продакшене у каждого поля схемы должно быть описание. Каждое перечисление enum должно быть явным. Каждое обязательное поле должно быть помечено. Это «схема как промпт» — ваша схема занимается промпт-инжинирингом.
Паттерн 2: генерация с ограничениями
Крупные поставщики поддерживают генерацию с ограничениями: на этапе декодирования модель может формировать только допустимый по схеме результат.
- OpenAI:
response_format: { type: "json_schema", json_schema: { ..., strict: true } }. - Anthropic: JSON через
output_config.formatи строгие входные данные инструментов сstrict: true. - Серверы моделей с открытыми весами: декодирование по грамматике или схеме, если его поддерживают выбранные сервер инференса и модель.
Предпочитайте ограниченный вывод, если его поддерживают поставщик, модель и необходимое подмножество схемы. Он предотвращает синтаксические ошибки и нарушение формы схемы, но не гарантирует фактическую правильность извлечённых значений и не разрешает побочные действия. Поставщики документируют неподдерживаемые ключевые слова и ограничения сложности. Компиляция грамматики может увеличить задержку первого запроса, а некоторые поставщики кэшируют скомпилированные грамматики, поэтому измеряйте холодный и прогретый режимы вместо предположения о незначительных накладных расходах.
Когда генерация с ограничениями недоступна (некоторые открытые модели, некоторые конфигурации), запасной вариант — валидация и повтор (см. Паттерн 4).
Паттерн 3: версионирование схем
Схемы эволюционируют. Вы добавляете поля. Помечаете поля как устаревшие. Меняете перечисления enum.
Изменение схемы — это изменение кода. Оно должно быть:
- Версионировано. Помечайте каждую схему номером версии.
- Протестировано. Набор для оценки прогоняется по новой схеме до развёртывания.
- Согласовано. Последующие компоненты, использующие результат, знают об изменении.
- По возможности обратно совместимо. Добавляйте новые опциональные поля; не убирайте обязательные.
Паттерн, который хорошо работает: хранить схемы как TypeScript-типы или Pydantic-модели, версионировать их в системе контроля версий, генерировать из них JSON Schema. Типы обслуживают и API модели, и код вашего приложения.
class TicketClassificationV2(BaseModel):
category: Literal["billing", "technical", "account", "feature_request", "complaint"]
priority: Literal["low", "medium", "high", "urgent"]
confidence: float = Field(ge=0, le=1, description="Confidence in this classification, 0-1")
needs_human_review: bool = Field(description="True if any field has low confidence or unusual signal")
reasoning: str = Field(description="Brief reasoning for the classification, especially for non-obvious cases")
Модель Pydantic определяет схему, валидирует результат и служит типом в коде Python. Это единый источник истины.
Паттерн 4: валидация и повтор
Даже при генерации с ограничениями валидируйте результат до использования:
from pydantic import ValidationError
def call_with_validation(prompt, schema, max_retries=2):
for attempt in range(max_retries + 1):
response = llm_call(prompt, response_format=schema)
try:
parsed = schema.model_validate_json(response.content)
return parsed
except ValidationError as e:
if attempt < max_retries:
prompt = build_retry_prompt(prompt, response.content, e)
continue
raise
Промпт для повтора должен включать исходные инструкции, предыдущий вывод модели и конкретное описание того, что пошло не так:
В вашем предыдущем ответе была ошибка валидации:
{error message}
Ваш предыдущий вывод:
{previous output}
Исправьте проблему и выдайте корректный ответ.
Повторы работают на удивление хорошо — обычно одного повтора хватает, чтобы исправиться после ошибки модели.
Ограничения: не повторяйте бесконечно (максимум 2–3 раза). Не повторяйте при ошибках, не связанных с валидацией (лимиты запросов, контент-фильтр и т. п.). Логируйте повторы, чтобы следить за их частотой (растущая доля повторов сигнализирует о дрейфе модели или проблемах с промптом).
Паттерн 5: рефлексия над результатами
Для высокорисковых вызовов функций заставляйте модель анализировать результаты инструмента до их использования.
Голый цикл:
1. Вызовите LLM с доступными инструментами.
2. Модель решает вызвать инструмент X.
3. Выполните X.
4. Верните результат модели.
5. Модель формирует финальный ответ.
Цикл с рефлексией:
1. Вызовите LLM с доступными инструментами.
2. Модель решает вызвать инструмент X.
3. Выполните X.
4. Верните результат модели.
5. Модель оценивает: результат совпадает с ожидаемым? Стоит ли на него опираться?
6. Если да — формирует финальный ответ. Если нет — вызывает другой инструмент или просит уточнение.
Так ловятся случаи вроде:
- Инструмент вернул 0 результатов, когда должен был вернуть данные → модель распознаёт пустой случай.
- Инструмент вернул ошибку → модель обрабатывает её явно, а не игнорирует.
- Инструмент вернул неожиданные данные → модель замечает это и адаптируется.
Реализация: просите модель явно оценивать результаты инструмента, возможно, через структурированный паттерн «оценить, потом действовать».
Это добавляет задержку и токены. Для высокорисковых действий (отправить письмо, провести платёж, изменить запись) оно того стоит. Для низкорискового извлечения информации — пропускайте.
Паттерн 6: идемпотентность
LLM иногда вызывают один и тот же инструмент дважды или повторяют уже успешные вызовы. Без идемпотентности вы получаете дубликаты: два возврата средств, два письма, две записи.
Паттерны идемпотентности:
Ключи идемпотентности. Каждый вызов инструмента получает уникальный ключ (генерируется на стороне клиента, передаётся в вызов). Нижестоящий API или ваша обёртка вокруг инструмента использует ключ, чтобы обнаружить дубликаты и вернуть уже существующий результат.
Семантика «найти или создать». Инструменты, создающие записи, сначала выполняют поиск. «Создать клиента с email X» сперва проверяет, есть ли клиент с email X; если есть, возвращает существующего вместо создания дубликата.
Журналы операций. Инструменты логируют каждую операцию. Обёртка проверяет журнал перед выполнением; если уже сделано — возвращает кэшированный результат.
Консервативный дизайн инструментов. Инструменты, выполняющие значимые действия, спроектированы так, чтобы требовать явного подтверждения или одобрения человеком. LLM не может случайно запустить их в плотном цикле.
Для любого инструмента с побочными эффектами проектируйте под идемпотентность. Пропуск этого шага — крупный источник продакшен-багов.
Паттерн 7: наблюдаемость вызовов инструментов
Вы должны знать, что происходит с вызовами инструментов. Для каждого вызова логируйте:
- Метку времени.
- Имя инструмента и аргументы.
- Результат (или ошибку).
- Длительность.
- Пользователя или сессию, которым он принадлежит.
- Цепочку вызовов в этом ходе (был ли этот вызов частью более длинной цепочки?).
Стройте дашборды на этих данных. Типичные разрезы:
- Объём вызовов по инструментам.
- Доля ошибок по инструментам.
- Средняя длительность по инструментам.
- Паттерны последовательностей («какие инструменты часто вызываются вместе?»).
- Вызовы несуществующих инструментов (LLM попыталась вызвать инструмент, которого нет).
Это показывает, где ваша система валится и где она обходится дорого.
Паттерн 8: выдуманные аргументы
LLM иногда выдумывают значения для параметров инструмента. Они вызовут search_customers(email="...") с адресом, не соответствующим реальному вопросу пользователя. Или вызовут book_meeting(date="...") с датой, которая нигде не упоминалась.
Способы смягчения:
Строгая схема с описаниями. «user_id должен быть упомянут ранее в диалоге. Не выдумывайте ID.»
Валидация в обёртке инструмента. Если значение неправдоподобно (user_id не существует, дата в прошлом), инструмент возвращает структурированную ошибку и модель пересматривает решение.
Рефлексия. «Перед вызовом этого инструмента подтвердите, что используемые значения опираются на диалог.»
Ограниченные описания инструментов. Инструменты, оперирующие конкретными сущностями, выставляют только ID сущностей, полученные ранее в диалоге. Не выставляйте прямой поиск.
Журналы аудита. Отлавливайте паттерны выдуманных аргументов и подстраивайте промпты и схемы.
Паттерн 9: плавная деградация
Инструменты падают. API падают. Срабатывают лимиты запросов. Правильным ответом редко является «скажи пользователю, что ничего не работает».
Паттерны:
Кэш или устаревшие данные. Если живой источник недоступен, верните кэшированные данные с пометкой, что они устарели.
Частичное выполнение. Если 3 из 5 подзадач удались — сообщите, что сделано, а что нет.
Запасные пути. Если основной инструмент падает, модель знает про резервный. Например, если search_documents падает, переходим на search_web с соответствующими оговорками.
Видимые пользователю состояния ошибок. Если инструмент реально не может завершиться, модель формирует понятное сообщение об ошибке для пользователя — а не выдуманный успех.
Модель должна знать об этих паттернах. Документируйте их в системном промпте:
Если инструмент вернул ошибку:
- Попробуйте запасной инструмент, если он есть.
- Ясно сообщите о частичных результатах, если пользователь уже предоставил информацию.
- Никогда не заявляйте об успехе, когда инструмент вернул ошибку.
Паттерн 10: потоковая передача структурированного вывода
Ради удобства пользователя стриминг частичного структурированного вывода — это здорово: пользователь видит, как результат формируется в реальном времени, а не ждёт.
Реализация:
- Большинство современных API моделей передают JSON по частям, токен за токеном.
- Парсите частичный JSON инкрементально (библиотеки вроде
partial-json-parserили собственный небольшой потоковый парсер). - Обновляйте интерфейс по мере появления полей.
Это особенно полезно для результатов из нескольких разделов. Длинное описание продукта, многочастный аналитический отчёт или проверка кода с несколькими замечаниями воспринимаются быстрее, когда передаются постепенно.
Не принимайте решений по неполному результату. Потоковая передача подходит для отображения, но перед действием дождитесь завершения и валидации структурированного результата.
Паттерн 11: вызов функций против явных «decide»-вызовов
Нативный вызов функций удобен — модель «решает», когда вызвать инструмент. Но в некоторых рабочих процессах явный «decide»-вызов надёжнее.
Пример: рабочий процесс клиентской поддержки, где модель должна выбрать между несколькими действиями.
Подход с нативным вызовом функций: дайте модели 5 инструментов (refund, send_article, escalate_to_human, ask_clarifying_question, close_ticket). Пусть решает сама.
Подход с явным decide-вызовом: сначала вызовите модель с единственным инструментом decide_action, у которого один параметр — какое действие. Затем на основании решения вызовите модель снова — уже только с релевантным инструментом.
Явный подход медленнее и многословнее, но надёжнее. Модель собраннее на каждом шаге. У хост-системы больше контроля над рабочим процессом.
Для высокорисковых рабочих процессов явный подход часто выигрывает. Для разведывательных или простых нативный вызов функций вполне сгодится.
Паттерн 12: форматирование результата инструмента
То, как вы возвращаете результат инструмента, имеет значение. Модель читает результат; формат важен.
Плохо:
{"id": "cus_123", "n": "John", "p": "12345"}
Лучше:
{
"customer_id": "cus_123",
"name": "John Doe",
"phone": "+1-555-0123",
"tier": "premium",
"open_tickets": 0
}
Лучше всего (для некоторых случаев):
Найден клиент:
- ID: cus_123
- Имя: John Doe
- Уровень: Premium
- Телефон: +1-555-0123
- Открытых обращений: 0
Этот клиент на уровне premium, открытых обращений нет.
Форма «лучше всего» — человекочитаемая, включает контекст, и модели проще использовать её в последующей генерации. Форма «лучше» — более структурирована и машиночитаема. Используйте тот формат, с которым модель лучше справляется на ваших последующих задачах (проверяйте на практике).
Для некоторых инструментов хорошо работает возврат и того, и другого («Вот результат: [нарратив]. Сырые данные: [JSON]»).
Паттерн 13: повторы с учётом схемы
Некоторые ошибки валидации непоправимы (модель фундаментально не поняла задачу). Другие — простые правки.
Полезный паттерн: классифицировать ошибку и реагировать соответственно.
def handle_validation_error(error):
if "missing required field" in str(error):
return retry_with_message("You omitted required field X. Please include it.")
elif "value not in enum" in str(error):
return retry_with_message("Value X is not in the allowed set. Choose from: ...")
elif "type mismatch" in str(error):
return retry_with_message("Field X must be a number, not a string.")
else:
# Unknown error — single generic retry
return retry_with_message("There was an error in your response. Please try again.")
Повторы, заточенные под конкретную ошибку, срабатывают чаще, чем универсальные.
Паттерн 14: компонуемость
Инструменты должны компоноваться. Маленькие сфокусированные инструменты, делающие одно дело, модель может комбинировать в сложные рабочие процессы.
Монолитный инструмент process_customer_request(query), делающий всё, — это чёрный ящик. Модель не может ни наблюдать, ни управлять внутренней логикой.
Набор сфокусированных инструментов — search_customer(email), get_recent_orders(customer_id), check_subscription_status(customer_id), escalate_to_human(reason) — модель может скомпоновать в правильный поток под каждую ситуацию.
Проектируйте инструменты с правильной гранулярностью. Каждый делает одно дело. Инструменты компонуются в рабочие процессы.
Паттерн 15: схема для «не знаю»
Тонкий паттерн: явно моделируйте неопределённость в схеме.
class CustomerInfo(BaseModel):
name: str
name_confidence: Literal["high", "medium", "low"]
needs_clarification: bool
clarification_question: Optional[str] = None
Модель может вернуть «низкую уверенность» с уточняющим вопросом вместо того, чтобы фантазировать.
Это намного лучше, чем модель, которая всегда уверенно заполняет поля — иногда выдуманными данными.
Разобранный пример: обработка счетов
Чтобы собрать всё вместе, вот система обработки счетов продакшен-уровня.
Вход: PDF-счёт во вложении к письму. Цель: извлечь структурированные данные, направить в бухгалтерскую систему.
Схема:
class LineItem(BaseModel):
description: str
quantity: float
unit_price: float
total: float
confidence: Literal["high", "medium", "low"]
class Invoice(BaseModel):
vendor_name: str
vendor_id: Optional[str] = None # null if not found in our records
invoice_number: str
invoice_date: date
due_date: Optional[date] = None
line_items: List[LineItem]
subtotal: float
tax: float
total: float
currency: str # ISO 4217
confidence: Literal["high", "medium", "low"]
needs_review: bool
review_reasons: List[str] # Specific reasons review is needed
Рабочий процесс:
- Шаг OCR: модель компьютерного зрения извлекает текст из PDF.
- Шаг извлечения: вызов LLM со схемой выше, генерация с ограничениями включена.
- Шаг валидации: Pydantic валидирует. Ошибки валидации запускают один повтор с обратной связью.
- Шаг кросс-проверки: вызов инструмента, ищущий поставщика в нашей системе. Если
vendor_nameсовпадает с известным — прикрепляетсяvendor_id. Если нет — ставитсяneeds_review=true. - Шаг проверки арифметики: проверяем
sum(line_items.total) ≈ subtotalиsubtotal + tax ≈ total. Если нет —needs_review=true. - Шаг проверки уверенности: если
confidenceнизкий или у какой-то позиции низкая уверенность —needs_review=true. - Маршрутизация: если
needs_review=true, отправляем в очередь на проверку человеком. Иначе — в бухгалтерскую систему. - Журналирование: для каждого шага сохраняются входные данные, результат, длительность и ошибки.
Обрабатываемые режимы отказа:
- Некорректный JSON: генерация с ограничениями предотвращает; повтор ловит граничные случаи.
- Выдуманные поля: схема строгая.
- Арифметические ошибки: проверены.
- Неизвестные поставщики: помечены.
- Низкая уверенность: помечена.
- Ошибки инструмента: явная обработка.
Не заимствуйте из статьи показатель полностью автоматической обработки. Создайте размеченный набор из тех форматов счетов, языков, валют, сканов и исключений, которые действительно получаете. До автоматизации согласуйте точность по каждому полю, сверку денежных сумм, допустимую долю ошибочного автоматического одобрения и поставщиков либо суммы, которые всегда требуют проверки. Сначала запустите систему в теневом режиме, учитывайте ошибки по категориям и разрешайте запись в бухгалтерскую систему только для той части потока, которая проходит согласованные пороги.
Так выглядит надёжный структурированный вывод: не единичный успешный тест режима JSON, а конвейер, который обрабатывает реальные режимы отказа.
Типичные ошибки
Несколько паттернов, которые мы видим раз за разом:
Ошибка 1: нет валидации. Pydantic, zod — что угодно — просто валидируйте. Не доверяйте модели.
Ошибка 2: размытые описания. «category: string» модели не помогает. «category: одно из billing, technical, account_access, где billing охватывает…» — помогает.
Ошибка 3: нерелевантные инструменты. Широкий каталог усложняет выбор для модели. Показывайте только те инструменты, которые нужны и разрешены в текущем состоянии, а рабочее количество определяйте тестами выбора инструментов, а не универсальным правилом «меньше десяти».
Ошибка 4: нет повтора при ошибке валидации. Один некорректный выход убивает весь поток. Делайте один повтор с обратной связью.
Ошибка 5: нет наблюдаемости. Когда вызовы инструментов валятся в продакшене, без трассировок вы не сможете поставить диагноз.
Ошибка 6: нет идемпотентности на инструментах с побочными эффектами. Дубликаты возвратов, дубликаты писем. Предсказуемый баг.
Ошибка 7: доверие к аргументам, выбранным LLM, без валидации. Выдуманные идентификаторы пользователей, выдуманные даты. Валидируйте аргументы перед выполнением.
Ошибка 8: пропуск версионирования схем. Изменения схемы ломают нижестоящих потребителей. Версионируйте.
От демо к продакшен-системе
Структурированный вывод и вызов функций переводят систему от разговоров к выполнению задач. При хорошем проектировании они позволяют безопасно применять ИИ в рабочих процессах; при плохом приводят к сложным и дорогим сбоям.
Важные паттерны: жёсткие схемы, генерация с ограничениями, валидация с повтором, рефлексия над результатами инструментов, идемпотентность, плавная деградация, обработка ошибок с учётом схемы и сквозная наблюдаемость.
Каждый из этих паттернов отделяет демо от продакшен-системы. Закладывайте их с самого начала.



