Как получать от LLM стабильный JSON-ответ
Практика3 минРедакция Innovialabs
Коротко
Самый надёжный способ — ответ, ограниченный JSON-схемой (structured outputs), если модель и API его поддерживают; JSON mode гарантирует только валидный синтаксис, а одна инструкция в промпте — ничего. В любом случае проверяйте ответ по схеме в коде и повторяйте запрос при ошибке.
Три способа получить JSON
Когда ответ LLM читает не человек, а код, нужен предсказуемый JSON. Есть три уровня надёжности:
| Способ | Что гарантирует | Риски |
|---|---|---|
| Только промпт | Ничего: модель старается следовать инструкции | Лишний текст, markdown-обёртка, пропущенные поля |
| JSON mode | Синтаксически валидный JSON | Поля и типы могут не совпасть с ожидаемыми |
| Ответ по схеме (structured outputs) | JSON соответствует переданной схеме | Поддерживается не всеми моделями и не всеми конструкциями схем |
Выбор простой: если API поддерживает ответ по схеме — используйте его. Если нет — JSON mode плюс строгая валидация. Промпт без всего этого годится только для прототипа.
Ещё один вариант — tool calling (function calling): вы описываете «инструмент» со схемой параметров, и модель возвращает аргументы в виде структуры. Многие используют его именно как способ получить JSON.
Как писать промпт для JSON
Даже с JSON mode промпт важен — модель должна понимать смысл полей.
- Покажите точную схему или пример объекта.
- Опишите, что значит каждое поле и какие значения допустимы.
- Скажите, что делать при отсутствии данных:
null, пустой массив или специальное значение. - Попросите вернуть только JSON, без пояснений и без markdown.
Извлеки данные из заявки. Верни только JSON без пояснений:
{
"name": string,
"phone": string | null,
"service": "web" | "mobile" | "bot" | "other",
"budget_mentioned": boolean
}
Если поле не указано в тексте — ставь null.
Валидация: всегда проверяйте ответ
Валидный синтаксис ещё не значит правильные данные. Проверяйте ответ по схеме в коде — например, с помощью Pydantic в Python или Zod в TypeScript.
from typing import Literal, Optional
from pydantic import BaseModel, ValidationError
class Lead(BaseModel):
name: str
phone: Optional[str]
service: Literal["web", "mobile", "bot", "other"]
budget_mentioned: bool
def parse_lead(raw: str) -> Lead | None:
try:
return Lead.model_validate_json(raw)
except ValidationError:
return None
Схема в коде работает как единый источник правды: из неё же можно сгенерировать JSON Schema для запроса к API.
Повторный запрос при ошибке
Даже при хорошей настройке ответы иногда не проходят проверку. Рабочий паттерн:
- Отправить запрос, получить ответ.
- Провалидировать.
- При ошибке — повторить запрос, добавив текст ошибки валидации: «Ответ не прошёл проверку: поле service имеет недопустимое значение. Исправь и верни только JSON».
- Ограничить число попыток (обычно хватает одной-двух).
- Если всё равно не вышло — записать в лог и вернуть понятную ошибку или отправить на ручную обработку.
Не повторяйте бесконечно и не «чините» JSON регулярными выражениями — это скрывает проблему.
Частые ошибки парсинга
- Markdown-обёртка. Модель возвращает JSON внутри блока кода с тройными кавычками. Решение: ответ по схеме или JSON mode; на крайний случай — аккуратно убрать обёртку перед парсингом.
- Текст до или после JSON. «Вот результат:» перед объектом. Решение: явный запрет в промпте и валидация.
- Обрезанный ответ. Лимит токенов закончился посреди объекта. Решение: увеличить лимит, проверять причину остановки генерации в ответе API.
- Неверные типы.
"true"вместоtrue, число строкой. Решение: схема и валидация, при необходимости — приведение типов на стороне кода. - Выдуманные значения. Модель заполняет поле, которого нет в тексте. Решение: разрешить
nullи прямо сказать, что угадывать нельзя. - Слишком сложная схема. Глубокая вложенность и десятки полей повышают риск ошибок. Решение: упростить или разбить на несколько запросов.