Большие языковые модели обучаются генерировать непрерывный естественный текст. Однако современные корпоративные архитектуры (микросервисы, базы данных, фронтенд-клиенты) требуют строгих форматов сериализации — в первую очередь JSON.
Классический подход «Пожалуйста, ответь строго в формате JSON» с последующим вызовом JSON.parse(response.text) в производственной среде неизбежно приводит к сбоям:
```json ... ```) или добавляет вежливые преамбулы («Конечно, вот данные:»).userId вместо требуемого user_id).Для решения этой проблемы ведущие провайдеры (OpenAI, Anthropic, Google) внедрили технологию Structured Outputs. На уровне инференс-рантайма применяется управляемое декодирование (Constrained Decoding / Grammar Masking): на каждом шаге генерации маска логитов физически запрещает модели сэмплировать токены, нарушающие формальную грамматику заданной схемы JSON Schema.
В Vercel AI SDK этот механизм инкапсулирован в функции generateObject и streamObject, интегрированных со стандартной библиотекой схем Zod.
Взглянем на принципиальное отличие наивного парсинга текста от аппаратного управляемого декодирования:
+-----------------------------------------------------------------------------------+
| НАИВНЫЙ ПОДХОД VS СТРУКТУРИРОВАННЫЙ ВЫВОД (ZOD) |
+-----------------------------------------------------------------------------------+
| А. НАИВНЫЙ ПОДХОД (Промпт "Верни JSON" + JSON.parse): |
| Модель: "Конечно! Вот данные: ```json { \"price\": 100, } ``` Надеюсь помог!" |
| Рантайм: JSON.parse(...) -> SyntaxError: Unexpected token 'К', "Конечно! В"... |
| Результат: Крах микросервиса / HTTP 500 |
+-----------------------------------------------------------------------------------+
| Б. УПРАВЛЯЕМОЕ ДЕКОДИРОВАНИЕ (generateObject + Zod Schema): |
| Zod -> Компиляция в JSON Schema -> Загрузка конечного автомата (CFG) на GPU |
| Шаг 1: Маска разрешает только символ '{' |
| Шаг 2: Маска разрешает только токен '"price"' |
| Шаг 3: Маска разрешает только ':' и далее числовое значение |
| Результат: 100% синтаксически валидный JSON + строгий тип TypeScript |
+-----------------------------------------------------------------------------------+Разработчик описывает целевую структуру данных с помощью примитивов z.object, z.string, z.array, дополняя поля семантическими подсказками .describe().
Vercel AI SDK автоматически конвертирует схему Zod в спецификацию JSON Schema Draft-07/2020-12, понятную API языковой модели.
Сервер инференса строит граф допустимых переходов (CFG / Context-Free Grammar). Токены, нарушающие синтаксис JSON или структуру схемы, получают вероятность 0.
Полученный чистый JSON-текст валидируется методом schema.safeParse() для проверки кастомных бизнес-правил и ограничений диапазонов чисел.
Разработчик получает объект result.object, тип которого статически выведен как z.infer<typeof Schema>, с автодополнением в IDE.
За счет чего режим Structured Outputs (например, в моделях OpenAI с json_schema) гарантирует отсутствие синтаксических ошибок в JSON?
Ниже представлен производственный сервис анализа резюме кандидатов. Мы описываем схему с помощью Zod, а Vercel AI SDK гарантирует вывод строгого типа:
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// 1. Декларация строгой схемы данных
export const CandidateEvaluationSchema = z.object({
fullName: z.string().describe('Полное имя кандидата'),
seniorityLevel: z.enum(['junior', 'middle', 'senior', 'lead']).describe('Оцененный уровень грейда'),
yearsOfExperience: z.number().int().min(0).max(50).describe('Полное количество лет коммерческого опыта'),
primarySkills: z.array(z.string()).min(1).describe('Ключевой технологический стек кандидата'),
strengths: z.array(z.string()).describe('Сильные стороны и достижения'),
redFlags: z.array(z.string()).describe('Потенциальные риски или пробелы в опыте'),
contactInfo: z.object({
email: z.string().email().optional().describe('Корпоративный или личный email'),
github: z.string().url().optional().describe('Ссылка на профиль GitHub'),
}),
hiringRecommendation: z.boolean().describe('Рекомендация к прохождению на техническое интервью'),
});
// 2. Статический вывод типа TypeScript
export type CandidateEvaluation = z.infer<typeof CandidateEvaluationSchema>;
/**
* Парсит текст резюме в гарантированно валидный объект TypeScript
*/
export async function evaluateCandidateResume(
rawResumeText: string
): Promise<CandidateEvaluation> {
const result = await generateObject({
model: openai('gpt-4o'),
schema: CandidateEvaluationSchema,
schemaName: 'CandidateEvaluation',
schemaDescription: 'Формализованная оценка квалификации IT-специалиста по тексту резюме',
system: 'Ты — Senior Staff инженер и технический рекрутер. Оцени кандидата строго объективно.',
prompt: `Проанализируй текст следующего резюме:\n\n${rawResumeText}`,
temperature: 0.1, // Низкая температура для максимальной точности фактов
});
// result.object имеет гарантированный тип CandidateEvaluation!
return result.object;
}Несмотря на аппаратные ограничения Structured Outputs, возможны сетевые сбои или ошибки валидации бизнес-ограничений (например, z.string().email() может не пройти, если кандидат не указал email). Для таких сценариев применяется безопасный паттерн с повторными попытками:
import { generateObject, TypeValidationError, JSONParseError } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z, ZodSchema } from 'zod';
export async function executeSafeStructuredTask<T>(
schema: ZodSchema<T>,
prompt: string,
systemPrompt: string,
maxRetries = 2
): Promise<T> {
let attempts = 0;
while (attempts <= maxRetries) {
try {
attempts++;
const result = await generateObject({
model: openai('gpt-4o-mini'),
schema,
system: systemPrompt,
prompt,
temperature: 0,
});
return result.object;
} catch (err: unknown) {
if (TypeValidationError.isInstance(err)) {
console.warn(`[Попытка ${attempts}] Ошибка валидации структуры: ${err.message}`);
if (attempts > maxRetries) {
throw new Error(`Не удалось сгенерировать валидный объект после ${maxRetries} попыток: ${err.value}`);
}
} else if (JSONParseError.isInstance(err)) {
console.error(`[Попытка ${attempts}] Фатальная ошибка парсинга JSON: ${err.text}`);
if (attempts > maxRetries) throw err;
} else {
const errMsg = err instanceof Error ? err.message : 'Unknown error';
console.error(`Непредвиденный сбой генерации: ${errMsg}`);
throw err;
}
}
}
throw new Error('Исчерпан лимит повторных попыток генерации');
}Конвертация типов Zod в стандартное дерево JSON Schema Draft-07. Генерация системных описаний полей из метаданных .describe().
Все свойства по умолчанию помечаются как required для предотвращения пропуска ключей.
Отправка параметров запроса в API OpenAI/Anthropic с указанием строгой схемы и флага strict: true.
Провайдер компилирует схему в бинарный конечный автомат парсера.
Генерация текста объекта с динамическим маскированием логитов. Гарантируется соблюдение синтаксиса JSON и типов полей схемы.
Исключена возможность появления висячих запятых или сломанных скобок.
Декодирование полученного JSON в JavaScript-объект и запуск клиентских валидаторов Zod (email, url, regex, min/max).
При несоответствии схемы выбрасывается типизированная ошибка TypeValidationError.
Зачем при объявлении полей схемы Zod для `generateObject` необходимо указывать `.describe('описание поля')`?
Попытка передать в generateObject валидаторы с произвольным кодом JavaScript: z.string().refine(val => checkDatabase(val)). Инференс-сервер модели не может исполнить сторонний JavaScript-код внутри маскирования токенов, поэтому такие проверки игнорируются при генерации или вызывают ошибку компиляции схемы в JSON Schema.
Решение: Для ограничений модели использовать стандартные примитивы (z.enum, z.number().min(), z.string().regex()), а кастомную бизнес-логику выполнять после получения объекта.
Запрос на извлечение 500 элементов в один массив через generateObject({ schema: z.array(...) }). Генерация блокирует сервер на 30–60 секунд, пока модель полностью не сформирует массив. Если соединение оборвется на 499-м элементе, весь результат пропадет.
Решение: Для больших списков использовать streamObject с элементом массива output: 'array', позволяющий обрабатывать каждый сгенерированный элемент по отдельности по мере его завершения.
Разработчики полагают, что если объект прошел валидацию Zod, то данные в нем верны. Structured Outputs гарантирует только форму (синтаксис и типы данных), но не содержание. Если модель решит, что у кандидата 150 лет опыта, поле типа number будет валидным, но фактически ложным.
Решение: Использовать строгие ограничения диапазонов (.min(), .max()) и верификацию фактов на уровне бизнес-логики приложения.
Вызов generateObject({ schema }) без передачи параметров schemaName и schemaDescription. Модель хуже понимает глобальную цель структуры, что может снижать точность извлечения сущностей.
Решение: Всегда явно указывать назначение схемы через schemaName и schemaDescription.