Проблема: LLM галлюцинируют JSON
Любой, кто выводил LLM-фичу в прод, знает эту боль. Просишь модель «вернуть JSON с полями name, email, score» — и в 99% случаев получаешь именно это. В оставшийся 1% получаешь:
- Открывающий
```jsonфенс, на котором парсер ломается. - Висячий комментарий вида
// note: score estimated. - Запятую в конце, на которой падает
JSON.parse. - Невалидную экранировку кавычки внутри строки.
- Поле, переименованное в
e_mail, потому что модели «так красивее». - Половину ответа в markdown с пояснением, что значит этот JSON.
На миллионе вызовов в месяц этот 1% — десять тысяч молчаливых прод-фейлов. Свободная генерация текста просто плохо подходит для машинных контрактов.
OpenAI выкатили response_format: { type: "json_schema", strict: true } в 2024 и фактически закрыли тему на своём стеке — декодирование модели ограничивается на уровне токенов, и вывод гарантированно валиден против схемы по грамматике. Anthropic пошли другим путём. По состоянию на 2026 год в нативном Messages API по-прежнему нет флага json_mode. Вместо этого Anthropic предлагают опереться на фичу, которая у них уже была: tool use.
В этой статье — продакшен-паттерн, который мы рекомендуем для получения 100% валидного и соответствующего схеме JSON от Claude. Покрываем нативный API через форсированный tool_use, OpenAI-совместимую обёртку response_format, генерацию схем из Pydantic и Zod, цикл валидация+ретрай и сравнение с нативным json_schema-режимом GPT‑4o.
Все примеры идут на гейтвей Claudexia с базовым URL https://api.claudexia.tech/v1 — он отдаёт и нативный Anthropic Messages API, и OpenAI-совместимый Chat Completions. Цены и список моделей — в нашем посте Цены Claude API 2026.
Ответ Anthropic: форсированный tool_use
Tool use в Claude задумывался для агентных сценариев — чтобы модель могла вызвать get_weather или search_db. Но механизм, которым она эмитит tool-вызовы, ровно то, что нам нужно для структурированного вывода: модель отдаёт блок tool_use, у которого поле input всегда — валидный JSON-объект, соответствующий input_schema инструмента.
Декодер Anthropic это обеспечивает. Модель не «вежливо просят» вернуть JSON; tool input и есть JSON. Добавьте к этому tool_choice: { type: "tool", name: "..." }, чтобы форсировать вызов ровно одного инструмента — и получите структурированный API во всём, кроме названия.
Рецепт:
- Опишите целевую форму как Pydantic-модель (Python) или Zod-схему (TypeScript).
- Сконвертируйте её в JSON Schema.
- Заверните в один инструмент с
input_schema = <ваша JSON Schema>. - Отправьте запрос с
tools=[этот_инструмент]иtool_choice={"type":"tool","name":"<имя инструмента>"}. - Прочитайте
response.content[0].input— это и есть ваш валидированный объект.
Python: Pydantic → JSON Schema → tool_use
import anthropic
from pydantic import BaseModel, Field
from typing import Literal
class InvoiceLineItem(BaseModel):
description: str
quantity: int = Field(ge=1)
unit_price_cents: int = Field(ge=0)
class Invoice(BaseModel):
invoice_number: str
issued_on: str = Field(description="ISO-8601 дата, например 2026-04-03")
currency: Literal["USD", "EUR", "RUB", "GBP"]
vendor_name: str
line_items: list[InvoiceLineItem]
total_cents: int
client = anthropic.Anthropic(
base_url="https://api.claudexia.tech/v1",
api_key="sk-cxa-...",
)
extract_tool = {
"name": "record_invoice",
"description": "Записать распарсенный инвойс в учётную систему.",
"input_schema": Invoice.model_json_schema(),
}
resp = client.messages.create(
model="claude-sonnet-4.5",
max_tokens=2048,
tools=[extract_tool],
tool_choice={"type": "tool", "name": "record_invoice"},
messages=[
{"role": "user", "content": f"Извлеки инвойс:\n\n{raw_invoice_text}"}
],
)
raw = resp.content[0].input # уже dict, уже валиден против схемы
invoice = Invoice.model_validate(raw) # дополнительная Pydantic-проверка
Значение resp.content[0].input приходит как Python-словарь, который уже парсится чисто и уже удовлетворяет JSON Schema, которую вы прислали. Дополнительный Invoice.model_validate(raw) — на всякий случай: он даёт Pydantic-приведение типов (например, строка "42" → int 42) и запускает ваши кастомные валидаторы.
TypeScript: Zod → JSON Schema → tool_use
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
const SupportTicket = z.object({
category: z.enum(["billing", "bug", "feature_request", "abuse", "other"]),
urgency: z.enum(["low", "normal", "high", "critical"]),
summary: z.string().max(280),
suggested_owner_team: z.enum(["payments", "platform", "growth", "trust_safety"]),
contains_pii: z.boolean(),
});
const client = new Anthropic({
baseURL: "https://api.claudexia.tech/v1",
apiKey: process.env.CLAUDEXIA_KEY!,
});
const resp = await client.messages.create({
model: "claude-sonnet-4.5",
max_tokens: 1024,
tools: [{
name: "classify_ticket",
description: "Классифицировать входящий тикет поддержки.",
input_schema: zodToJsonSchema(SupportTicket, { target: "openAi" }) as any,
}],
tool_choice: { type: "tool", name: "classify_ticket" },
messages: [{ role: "user", content: ticketBody }],
});
const block = resp.content.find((b) => b.type === "tool_use");
const ticket = SupportTicket.parse(block?.input);
Обратите внимание на target: "openAi" в zodToJsonSchema — он выдаёт вариант JSON Schema, который Claude (и OpenAI) принимают без претензий. Дефолтный таргет Zod эмитит $ref-паттерны, на которых Anthropic API ругается.
OpenAI-совместимый путь: response_format на гейтвее Claudexia
Если у вас уже есть кодовая база на OpenAI SDK, гейтвей Claudexia принимает форму OpenAI Chat Completions и под капотом транслирует response_format в форсированный tool_use. Переписывать ничего не надо.
from openai import OpenAI
client = OpenAI(
base_url="https://api.claudexia.tech/v1",
api_key="sk-cxa-...",
)
resp = client.chat.completions.create(
model="claude-sonnet-4.5",
messages=[{"role": "user", "content": resume_text}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "Resume",
"strict": True,
"schema": Resume.model_json_schema(),
},
},
)
resume = Resume.model_validate_json(resp.choices[0].message.content)
Это путь наименьшего сопротивления при миграции с OpenAI. Тот же SDK, та же семантика response_format, качество Claude.
Три конкретных примера
1. Извлечение инвойса
Вход: шумный текст инвойса, выдранный из PDF. Выход: модель Invoice из Python-сниппета выше. Форсированный tool_use означает, что вам больше никогда не нужно писать regex для парсинга позиций — модель возвращает типизированный список, и downstream-код напрямую читает invoice.line_items[0].unit_price_cents.
2. Классификация тикета поддержки
Используем Zod-схему SupportTicket. Энумы category и urgency ограничивают модель так, что роутить результат можно без защитных .toLowerCase() повсюду. contains_pii — булев флаг, который сразу прокидывается в пайплайн редактирования персональных данных.
3. Парсинг резюме
class WorkExperience(BaseModel):
company: str
title: str
started_on: str = Field(description="YYYY-MM")
ended_on: str | None = Field(description="YYYY-MM или null, если текущая работа")
highlights: list[str]
class Resume(BaseModel):
full_name: str
email: str | None
phone: str | None
years_of_experience: int = Field(ge=0, le=60)
skills: list[str]
experience: list[WorkExperience]
Подключаем ту же обвязку с tool_use — и получаем API парсинга резюме.
Советы по дизайну схем
После пары десятков таких пайплайнов в проде, важнее всего оказывается несколько паттернов.
- Используйте
enumагрессивно. Любое поле, чьи значения принадлежат конечному множеству, должно быть энумом. Модель сильно точнее выбирает из списка, чем генерит свободно. - Descriptions — не украшение. Claude читает
descriptionкаждого поля. «ISO-8601 дата» или «телефон в формате E.164» меняют вывод. Относитесь к ним как к промтам. - Держите вложенность мелкой. Два уровня — оптимум. На трёх-четырёх модель начинает терять, в каком
{находится, даже с форсированным tool use. - Строковые энумы лучше булевых для трёх состояний.
status: "approved" | "rejected" | "needs_review"рассуждается лучше, чем два независимых булевых флага. - Явно помечайте опциональные поля.
Optional[str]в Pydantic /.optional()в Zod. Модель аккуратно их пропустит, а не придумает"unknown". - Не просите свободный текст внутри структурных полей. Поле
summary: strв конце — нормально.summary, за которым идут ещё структурные поля, обычно проливает прозу в эти соседние поля.
Цикл валидация и ретрай
Даже с форсированным tool_use ваши бизнес-валидаторы (например, «total_cents должен равняться сумме позиций») всё равно могут падать. Паттерн прямой:
def call_with_retry(messages, max_attempts=3):
for attempt in range(max_attempts):
resp = client.messages.create(
model="claude-sonnet-4.5",
max_tokens=2048,
tools=[extract_tool],
tool_choice={"type": "tool", "name": "record_invoice"},
messages=messages,
)
raw = resp.content[0].input
try:
return Invoice.model_validate(raw)
except ValidationError as e:
messages.append({"role": "assistant", "content": resp.content})
messages.append({
"role": "user",
"content": f"Этот вывод не прошёл валидацию: {e}. Вызови инструмент ещё раз с исправленными значениями.",
})
raise RuntimeError("Валидация схемы не прошла после ретраев")
Одного ретрая обычно хватает, и на второй попытке Claude видит исходный вывод и ошибку валидации в контексте — модель надёжно сама себя поправляет.
GPT‑4o json_schema vs форсированный tool_use Claude
Оба подхода в проде дают JSON, соответствующий схеме. Практические отличия:
- Поверхность гарантии. OpenAI-овский
strict: trueобеспечивается ограниченным декодированием на уровне токенов — модель буквально не может выдать невалидный JSON. Anthropic-овский форсированныйtool_useобеспечивается валидатором на tool input. На API-поверхности разницы нет (оба возвращают валидный JSON всегда), но если Claude всё-таки промахнётся, режим отказа — «вызов вернул ошибку», а не «JSON битый». - Поддержка фич схемы. Strict-режим OpenAI запрещает
oneOf,not, рекурсивные ссылки и ещё пару фич JSON Schema. Claude вtool_useпринимает более широкий поднабор, включая некоторые$ref-паттерны (если их сгенерил правильный таргет Zod) и более богатыеpattern-строки. - Использование descriptions. Claude взвешивает описания полей сильнее, чем GPT‑4o. Если у схемы богатые
description, ждите, что Claude из них выжмет больше. - Latency. Форсированный
tool_useдобавляет небольшой фиксированный оверхед против свободной генерации, но на наших бенчмарках сопоставим сjson_schema-режимом GPT‑4o. - Стоимость миграции. Через OpenAI-совместимый эндпоинт гейтвея Claudexia вы меняете одну строчку — параметр
model— иresponse_formatпродолжает работать.
Итого
Claude не нужен флаг json_mode, потому что tool use уже даёт нечто более сильное: типизированный контракт, который форсит API. Опишите схему один раз в Pydantic или Zod, форсируйте один инструмент — и у вас структурированный вывод, не уступающий ничему на рынке. OpenAI-совместимая обёртка на гейтвее Claudexia позволяет уронить Claude в существующую response_format-кодовую базу без изменений в бизнес-логике. Добавьте цикл ретрая на свои бизнес-валидаторы — и получится JSON-пайплайн, который просто не ломается.