Структурированные выходы и вызов функций: паттерны для продакшена
Эксперт13 мин чтенияИИ для бизнеса

Структурированные выходы и вызов функций: паттерны для продакшена

Структурированные выходы и вызов функций — это мост от «LLM, которая генерирует текст» к «системе, которая делает работу». В продакшене важны паттерны вокруг схем, обработки ошибок, идемпотентности и аккуратной деградации — а не просто режим JSON.

Что вы сможете сделать

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

AI Expert TeamОпубликовано: 15 мая 2026 г.
Сохраняется только в этом браузере.
В этой статье

Сдвиг от «LLM-чат-бота» к «LLM-системе, которая делает работу» происходит на уровне структурированных выходов и вызова функций. Здесь LLM перестаёт просто производить прозу и начинает производить данные, принимать решения о действиях и интегрироваться с остальной инфраструктурой.

В продакшене «структурированный выход» не означает «режим JSON сработал один раз в моём тесте». Это означает устойчивый конвейер, который справляется с вариативностью модели, некорректными выходами, частичными отказами, эволюцией схем и неприглядной реальностью, где LLM не всегда следуют инструкциям.

Эта статья посвящена паттернам, которые реально держатся в продакшене. Мы предполагаем, что вы знаете основы (использовали tool_choice у OpenAI, вызов инструментов (tool use) у Anthropic, ограничения JSON Schema). Идём глубже — туда, где эти системы становятся надёжными.

Два режима

Две связанные, но различимые возможности:

Структурированный выход: LLM производит ответ, соответствующий схеме (обычно JSON). Используется, когда вам нужен ответ LLM в программно обрабатываемом формате.

Вызов функций/инструментов: LLM получает набор функций, которые может вызывать, решает, какую вызвать (если вообще нужно), формирует параметры. Хост-система выполняет функцию и возвращает результат. LLM может вызвать следующие функции или сформировать финальный ответ.

API моделей обычно выставляют это через:

  • Параметр response_format (или его аналог), принимающий JSON-схему, для обычных структурированных выходов — Structured Outputs у OpenAI, responseSchema у Google Gemini и т. д.
  • Массив tools, описывающий доступные функции, плюс ответ с вызовом инструмента при срабатывании. API вызова инструментов (tool use) у Anthropic заодно служит способом ограничить выход схемой — вы определяете один инструмент с нужной схемой и заставляете модель его вызвать.

Оба работают; они родственны. «Вызов функции» — по сути структурированный выход, где схема и есть сигнатура функции.

Паттерн 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: инструменты со строгими схемами.
  • Открытые решения: outlines, lm-format-enforcer, jsonformer, декодирование на основе грамматик в vLLM.

Пользуйтесь этим. Всегда. Это убирает целый класс отказов (некорректный JSON, выдуманные поля, пропущенные обязательные поля). Накладные расходы по производительности пренебрежимо малы.

Когда генерация с ограничениями недоступна (некоторые открытые модели, некоторые конфигурации), запасной вариант — валидация и повтор (см. Паттерн 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. Call LLM with tools available.
2. Model decides to call tool X.
3. Execute X.
4. Pass result back to model.
5. Model produces final response.

Цикл с рефлексией:

1. Call LLM with tools available.
2. Model decides to call tool X.
3. Execute X.
4. Pass result back to model.
5. Model evaluates: does this result match what I expected? Should I act on it?
6. If yes, model produces final response. If no, model calls another tool or asks for clarification.

Так ловятся случаи вроде:

  • Инструмент вернул 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

Рабочий процесс:

  1. Шаг OCR: модель компьютерного зрения извлекает текст из PDF.
  2. Шаг извлечения: вызов LLM со схемой выше, генерация с ограничениями включена.
  3. Шаг валидации: Pydantic валидирует. Ошибки валидации запускают один повтор с обратной связью.
  4. Шаг кросс-проверки: вызов инструмента, ищущий поставщика в нашей системе. Если vendor_name совпадает с известным — прикрепляется vendor_id. Если нет — ставится needs_review=true.
  5. Шаг проверки арифметики: проверяем sum(line_items.total) ≈ subtotal и subtotal + tax ≈ total. Если нет — needs_review=true.
  6. Шаг проверки уверенности: если confidence низкий или у какой-то позиции низкая уверенность — needs_review=true.
  7. Маршрутизация: если needs_review=true, отправляем в очередь на проверку человеком. Иначе — в бухгалтерскую систему.
  8. Логирование: каждый шаг логируется со входом, выходом, длительностью, ошибками.

Обрабатываемые режимы отказа:

  • Некорректный JSON: генерация с ограничениями предотвращает; повтор ловит граничные случаи.
  • Выдуманные поля: схема строгая.
  • Арифметические ошибки: проверены.
  • Неизвестные поставщики: помечены.
  • Низкая уверенность: помечена.
  • Ошибки инструмента: явная обработка.

Производительность в продакшене: ~95% счетов проходят насквозь; 5% уходят на проверку. Среди автообработанных доля ошибок <0,5% (хорошо в пределах допустимого). Среди очереди на проверку ~80% подтверждаются корректными, 20% требуют правок.

Вот так выглядит продакшен-уровень структурированных выходов. Не «режим JSON сработал один раз», а конвейер, который обрабатывает реальные режимы отказа.

Типичные ошибки

Несколько паттернов, которые мы видим раз за разом:

Ошибка 1: нет валидации. Pydantic, zod — что угодно — просто валидируйте. Не доверяйте модели.

Ошибка 2: размытые описания. «category: string» модели не помогает. «category: одно из billing, technical, account_access, где billing охватывает…» — помогает.

Ошибка 3: слишком много инструментов. Доступно 30 инструментов — модель выбирает не те. Курируйте до <10 релевантных на вызов.

Ошибка 4: нет повтора при ошибке валидации. Один некорректный выход убивает весь поток. Делайте один повтор с обратной связью.

Ошибка 5: нет наблюдаемости. Когда вызовы инструментов валятся в продакшене, без трассировок вы не сможете поставить диагноз.

Ошибка 6: нет идемпотентности на инструментах с побочными эффектами. Дубликаты возвратов, дубликаты писем. Предсказуемый баг.

Ошибка 7: доверие к аргументам, выбранным LLM, без валидации. Выдуманные идентификаторы пользователей, выдуманные даты. Валидируйте аргументы перед выполнением.

Ошибка 8: пропуск версионирования схем. Изменения схемы ломают нижестоящих потребителей. Версионируйте.

От демо к продакшен-системе

Структурированные выходы и вызов функций — это мост от «LLM, которая болтает» к «LLM, которая делает работу». Сделано хорошо — открывает ИИ в продакшене. Сделано плохо — ломается интересно и дорого.

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

Каждый из этих паттернов отделяет демо от продакшен-системы. Закладывайте их с самого начала.

Читать дальше

Продолжайте тот же учебный путь со следующими практическими статьями.

Углубиться

Тщательно подобранные внешние курсы, которые глубже раскрывают эту тему.

Хельсинкский университет · MinnaLearn

Elements of AI (Основы искусственного интеллекта)

Хельсинкский университет

Самое авторитетное бесплатное введение в ИИ в Европе — создано Хельсинкским университетом, пройдено более чем миллионом человек. Без кода и без страха перед математикой, в конце — сертификат. Спокойная и достоверная версия ответа на вопрос, что такое ИИ на самом деле.

Новичок в ИИ~30 часов · в своём темпе
Coursera · DeepLearning.AI

AI for Everyone

Эндрю Ын

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

Новичок в ИИ~6 часов
HubSpot Academy

AI-Driven Customer Service

Brenna Zenaty, Adriti Gulati

Закрывает наше самое большое вертикальное слепое пятно: в каталоге ничего не говорило напрямую командам поддержки и customer success. HubSpot Academy бесплатен, хорошо сделан и освежающе конкретен — проходит ИИ-триаж тикетов и агента базы знаний, а не остаётся абстрактным, и сертификат тоже бесплатный, а не платная приманка.

Начинающий~1 час · в своём темпе (2 урока)

Все курсы в категории «ИИ для бизнеса»