Разговор с Ерланом, техническим директором логистической компании из Алматы, начался с его усталого вздоха.
«Мы потратили три недели на интеграцию ChatGPT с нашей CRM. Идея была простая: бот читает входящие заявки с почты, извлекает данные — имя клиента, адрес доставки, товары — и создаёт заказ в системе. Красиво звучит, да?»
Я кивнул. Звучало действительно красиво.
«Так вот, — продолжил Ерлан, — первую неделю всё работало отлично. А потом начался хаос. Бот вместо JSON вдруг стал писать "Конечно! Вот данные заказа:" и дальше — свободный текст. Потом он начал добавлять комментарии типа "Обратите внимание, что адрес неполный". Наша система падала с ошибкой парсинга, заказы терялись, клиенты звонили и кричали.»
Ерлан сделал паузу. «Знаешь, что самое обидное? GPT понимал заявки идеально. Он извлекал все данные правильно. Просто иногда решал ответить по-своему. И мы ничего не могли с этим сделать.»
Эта история — не исключение. Каждый, кто пробовал интегрировать большие языковые модели в бизнес-процессы, сталкивался с тем же. LLM блестяще понимают язык, но их ответы непредсказуемы по форме. А для автоматизации нужна предсказуемость. Именно для этого и появились Structured Outputs.
«До 30% интеграций с LLM терпят неудачу из-за ошибок парсинга ответов. Structured Outputs снижают этот показатель практически до нуля.»
Представьте, что вы нанимаете переводчика. Он блестяще переводит с казахского на русский, улавливает нюансы, сохраняет стиль. Но есть проблема: иногда он добавляет от себя пояснения. «Здесь, кстати, автор использует архаизм...». Для читателя — полезно. Для системы автоматического субтитрирования, которая ждёт чистый перевод — катастрофа.
Большие языковые модели — такие же «переводчики». Они обучены быть полезными, добавлять контекст, объяснять. Это прекрасно для чата с человеком. Но когда на другом конце — программа, которой нужен строго определённый JSON — творчество модели становится проблемой.
Structured Outputs решают эту проблему на корневом уровне. Вместо того чтобы просить модель «пожалуйста, верни JSON» (и надеяться, что она послушается), мы задаём жёсткую схему. И модель физически не может вернуть ничего, кроме данных, соответствующих этой схеме.
«Верни ответ в формате JSON с полями name, email, phone. Никаких пояснений!»
// Иногда:
{"name": "Алия", "email": "..."}
// Иногда:
Вот данные: {"name"...}
Результат непредсказуем
Жёсткая JSON Schema определяет структуру ответа
// Всегда:
{"name": "Алия", "email": "aliya@mail.kz", "phone": "+7..."}
100% соответствие схеме
Технически это работает через constrained decoding. Модель генерирует ответ токен за токеном, и на каждом шаге ей разрешены только те токены, которые соответствуют ожидаемой структуре. Если схема требует поле «email» — модель не сможет вместо него написать «electronic mail» или добавить пояснение.
Это не «просьба» к модели — это ограничение на уровне генерации. Разница принципиальная.
Вернёмся к примеру Ерлана. У него была задача: извлечь из письма данные для заказа. Вот как это можно сделать правильно.
Сначала определяем схему — какие данные нам нужны и в каком формате:
JSON Schema для заказа
{
"type": "object",
"properties": {
"customer_name": {
"type": "string",
"description": "Имя и фамилия клиента"
},
"phone": {
"type": "string",
"pattern": "^\\+7[0-9]{10}$",
"description": "Телефон в формате +7XXXXXXXXXX"
},
"delivery_address": {
"type": "object",
"properties": {
"city": {"type": "string"},
"street": {"type": "string"},
"building": {"type": "string"},
"apartment": {"type": "string"}
},
"required": ["city", "street", "building"]
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"price": {"type": "number"}
},
"required": ["name", "quantity"]
}
},
"delivery_date": {
"type": "string",
"format": "date"
},
"notes": {
"type": "string"
}
},
"required": ["customer_name", "phone", "delivery_address", "items"]
}
Обратите внимание на несколько важных моментов в этой схеме:
Теперь когда модель получает письмо «Здравствуйте, хочу заказать доставку. Ахметов Бауржан, телефон 8-705-123-45-67. Нужен холодильник Samsung и микроволновка. Доставьте на Абая 150, кв 42, Алматы. Желательно в субботу.» — она вернёт:
Гарантированный вывод
{
"customer_name": "Ахметов Бауржан",
"phone": "+77051234567",
"delivery_address": {
"city": "Алматы",
"street": "Абая",
"building": "150",
"apartment": "42"
},
"items": [
{"name": "Холодильник Samsung", "quantity": 1},
{"name": "Микроволновка", "quantity": 1}
],
"delivery_date": "2025-01-04",
"notes": "Клиент предпочитает доставку в субботу"
}
Никаких «Вот извлечённые данные:», никаких комментариев, никаких неожиданностей. Чистый JSON, который можно напрямую передать в CRM.
О том, как интегрировать AI-бота с CRM и ERP системами, читайте в нашей статье Интеграция AI-бота с ERP и CRM.
Хорошая новость: все основные провайдеры LLM уже поддерживают структурированные ответы. Плохая новость: у каждого свой подход и свои ограничения.
| Провайдер | Функция | Особенности | Ограничения |
|---|---|---|---|
| OpenAI | response_format с JSON Schema | Полная поддержка JSON Schema, включая вложенные объекты и массивы | Только GPT-4o и новее |
| Anthropic (Claude) | Tool use с JSON Schema | Через механизм вызова инструментов, очень надёжно | Немного сложнее в настройке |
| Google (Gemini) | response_schema | Встроенная поддержка в API | Некоторые типы не поддерживаются |
| YandexGPT | Ограниченно через промпты | Можно добиться через чёткие инструкции | Нет нативной поддержки схем |
| GigaChat | Функции (functions) | Работает через function calling | Меньшая гибкость схем |
Если вы работаете с локальными российскими моделями — YandexGPT или GigaChat — структурированные ответы реализуются сложнее. Подробнее об этом мы писали в статье Интеграция CRM с Yandex GPT и GigaChat.
Для проектов, где важна надёжность, рекомендую использовать OpenAI или Anthropic — у них самая зрелая реализация Structured Outputs.
Если вы работаете с Python (а большинство AI-интеграций пишутся на Python), есть элегантный способ работы со структурированными ответами — через Pydantic.
Pydantic — это библиотека для валидации данных. Вы описываете структуру как Python-класс, и Pydantic проверяет, что данные соответствуют этой структуре. А ещё Pydantic умеет генерировать JSON Schema автоматически.
Пример с Pydantic и OpenAI
from pydantic import BaseModel, Field
from openai import OpenAI
from typing import Optional
# Определяем структуру данных
class DeliveryAddress(BaseModel):
city: str = Field(description="Город доставки")
street: str = Field(description="Улица")
building: str = Field(description="Номер дома")
apartment: Optional[str] = Field(default=None, description="Квартира")
class OrderItem(BaseModel):
name: str = Field(description="Название товара")
quantity: int = Field(ge=1, description="Количество")
price: Optional[float] = Field(default=None, description="Цена за единицу")
class Order(BaseModel):
customer_name: str = Field(description="ФИО клиента")
phone: str = Field(pattern=r"^\+7\d{10}$", description="Телефон")
delivery_address: DeliveryAddress
items: list[OrderItem]
notes: Optional[str] = Field(default=None, description="Примечания")
# Вызов API с гарантированной структурой
client = OpenAI()
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Извлеки данные заказа из сообщения клиента."},
{"role": "user", "content": email_text}
],
response_format=Order
)
# Получаем типизированный объект
order = response.choices[0].message.parsed
# Теперь можно работать с данными как с обычным Python-объектом
print(f"Клиент: {order.customer_name}")
print(f"Город: {order.delivery_address.city}")
for item in order.items:
print(f" - {item.name} x {item.quantity}")
Что здесь происходит:
Главное преимущество: если модель вернёт что-то не то — вы узнаете сразу, при парсинге. Не через час, когда CRM упадёт. И не на проде.
IDE знает структуру данных и подсказывает поля
Ошибки ловятся сразу, а не в рантайме
Изменения в схеме видны всему коду
Давайте разберём полноценный пример — тот самый, с которого мы начали. Компания получает заявки на почту. Нужно автоматически создавать лиды в CRM.
Вот типичное письмо:
От: gulnara.k@company.kz
Тема: Запрос на поставку офисной мебели
Текст:
Добрый день!
Меня зовут Гульнара Касымова, я менеджер по закупкам компании TechStart. Мы планируем обновление офиса и ищем поставщика мебели.
Нас интересуют:
- Рабочие столы (примерно 25 штук)
- Офисные кресла (25 штук)
- Шкафы для документов (10 штук)
Бюджет около 5-6 млн тенге. Хотелось бы получить коммерческое предложение.
Мой телефон: +7 701 555 33 22
С уважением, Гульнара
Определяем структуру лида для CRM:
Структура лида для CRM
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class LeadSource(str, Enum):
EMAIL = "email"
PHONE = "phone"
WEBSITE = "website"
REFERRAL = "referral"
class LeadPriority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class Product(BaseModel):
name: str
quantity: Optional[int] = None
notes: Optional[str] = None
class Lead(BaseModel):
contact_name: str = Field(description="ФИО контактного лица")
company_name: Optional[str] = Field(default=None, description="Название компании")
position: Optional[str] = Field(default=None, description="Должность")
email: str = Field(description="Email адрес")
phone: Optional[str] = Field(default=None, description="Телефон")
products_of_interest: list[Product] = Field(description="Интересующие товары")
estimated_budget: Optional[str] = Field(default=None, description="Примерный бюджет")
source: LeadSource = Field(default=LeadSource.EMAIL)
priority: LeadPriority = Field(description="Приоритет лида")
summary: str = Field(description="Краткое описание запроса в 1-2 предложения")
next_action: str = Field(description="Рекомендуемое следующее действие")
Отправляем письмо в модель с этой схемой и получаем:
Результат парсинга
{
"contact_name": "Гульнара Касымова",
"company_name": "TechStart",
"position": "Менеджер по закупкам",
"email": "gulnara.k@company.kz",
"phone": "+77015553322",
"products_of_interest": [
{"name": "Рабочие столы", "quantity": 25},
{"name": "Офисные кресла", "quantity": 25},
{"name": "Шкафы для документов", "quantity": 10}
],
"estimated_budget": "5-6 млн тенге",
"source": "email",
"priority": "high",
"summary": "Компания TechStart планирует обновление офиса, интересуется закупкой офисной мебели на 5-6 млн тенге",
"next_action": "Подготовить и отправить коммерческое предложение на офисную мебель"
}
Обратите внимание: модель не только извлекла данные, но и:
Теперь этот JSON можно напрямую передать в API вашей CRM. Подробнее о работе с REST API в CRM — в нашей статье REST API для CRM: проектирование и интеграция.
Покажем, как настроить AI-парсинг писем, сообщений из мессенджеров и форм на сайте. Лиды попадают в CRM за секунды, а не за часы.
Узнать подробнееStructured Outputs минимизируют ошибки формата, но не гарантируют качество содержимого. Модель может вернуть синтаксически правильный JSON, в котором данные неполные или неправильные.
Например, письмо без явного телефона. Модель должна вернуть phone, потому что он required. Что она сделает? Может выдумать номер. Или поставить пустую строку. Или попытаться угадать по email.
Для таких случаев нужна дополнительная валидация:
JSON Schema / Pydantic проверяют структуру и типы данных. Встроено в Structured Outputs.
Проверка бизнес-логики: телефон существует, email валиден, сумма в разумных пределах.
Оценка уверенности модели. Низкая уверенность → отправить на проверку человеку.
Практический паттерн — retry с уточнением:
Retry с валидацией
def parse_lead_with_retry(email_text: str, max_retries: int = 2) -> Lead:
for attempt in range(max_retries + 1):
try:
# Получаем структурированный ответ
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": email_text}
],
response_format=Lead
)
lead = response.choices[0].message.parsed
# Бизнес-валидация
errors = validate_lead(lead)
if not errors:
return lead
if attempt < max_retries:
# Retry с информацией об ошибках
email_text = f"{email_text}\n\n[Ошибки валидации: {errors}. Исправь.]"
continue
# Если retry не помогли — отправляем на ручную проверку
return mark_for_review(lead, errors)
except Exception as e:
if attempt == max_retries:
raise
continue
Ключевой момент: если модель ошиблась, мы не просто повторяем запрос — мы говорим ей, что было не так. Это значительно повышает шансы на успех во второй попытке.
И последняя линия защиты: если автоматика не справилась — отправляем на проверку человеку. Лучше задержать обработку на 10 минут, чем создать в CRM мусорный лид с неправильными данными.
Наблюдая за проектами, которые мы ведём в Казахстане, я выделил несколько ошибок, которые повторяются из раза в раз.
20 вложенных уровней, опциональные поля везде, сложные условия. Модель путается. Упрощайте: если поле редко нужно — вынесите в отдельный запрос.
Поле называется «status», а что туда писать — непонятно. Добавляйте описания к каждому полю, особенно к enum-значениям.
Поле required, данных нет — модель выдумывает. Используйте Optional для полей, которые могут отсутствовать в исходных данных.
Если модель не смогла распарсить — код падает. Добавьте try/except, логирование и fallback на ручную обработку.
Ещё одна частая проблема — несоответствие схемы и промпта. Схема говорит «верни priority», а промпт ничего не говорит о приоритетах. Модель угадывает. Убедитесь, что промпт объясняет контекст каждого поля.
О том, как правильно составлять ТЗ на AI-бота, включая описание структур данных, читайте в статье Шаблон технического задания на LLM-бота.
Structured Outputs — мощный инструмент, но не универсальный. Есть сценарии, где они создают больше проблем, чем решают.
| Используйте Structured Outputs | Не используйте |
|---|---|
| Извлечение данных из текста в CRM/ERP | Креативные задачи: написание статей, генерация идей |
| API-интеграции, где нужен точный формат | Диалоговые боты, где важна естественность |
| Автоматическая классификация: категории, теги | Задачи с непредсказуемой структурой ответа |
| Парсинг документов: счета, накладные, резюме | Суммаризация, где длина варьируется |
| Генерация структурированного контента: FAQ, товары | Объяснения и пояснения для пользователя |
Общее правило: если ответ должен попасть в систему (база данных, API, CRM) — используйте Structured Outputs. Если ответ читает человек — скорее всего, не нужны.
Кстати, можно комбинировать. Бот общается с клиентом свободным текстом, но когда нужно создать заявку — отдельным запросом извлекает данные в структурированном формате. Лучшее из двух миров.
Помните Ерлана из начала статьи? После перехода на Structured Outputs его интеграция заработала стабильно. Три месяца — ни одной ошибки парсинга. Заказы из почты попадают в CRM за секунды. Клиенты довольны, менеджеры не тратят время на ручной ввод.
«Самое смешное, — сказал он мне недавно, — что код стал проще. Раньше у нас было 200 строк регулярок и try/except на каждом шагу. Теперь — 50 строк Pydantic-классов и один вызов API. И работает надёжнее.»
Structured Outputs — это не просто техническая фича. Это переход от «AI как эксперимент» к «AI как надёжный компонент системы». Переход от надежды, что модель ответит правильно, к уверенности в формате ответа.
Если вы интегрируете AI в бизнес-процессы — будь то CRM, ERP, или любая другая система — structured outputs должны стать вашим стандартом. Не потому что это модно. А потому что это работает.
Проектируем и разрабатываем AI-интеграции для CRM, ERP и других бизнес-систем. Structured Outputs, валидация, отказоустойчивость — всё включено.
Обсудить проектАрхитектура и паттерны интеграции
Как проектировать API для интеграций
Особенности работы с локальными LLM
Как описывать требования к AI-системам