/
Вопросы и ответы
/
Промт-инжиниринг
/

Как получить JSON из модели и не сломаться

Как получить JSON из модели и не сломаться

11 часов назад

Никита Вихров

Ответы

0

Как получить JSON из модели и не сломаться

Модель возвращает текст. Когда тебе нужен JSON — она может обернуть его в markdown-блок, добавить объяснение до и после, или вернуть почти-JSON с одной лишней запятой. json.loads() падает, прод ломается.

Есть несколько уровней защиты. Разберём от простого к надёжному.


Уровень 1: попросить правильно

Большинство проблем решается чётким промптом:

from anthropic import Anthropic

client = Anthropic()

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=512,
    system="""Отвечай только валидным JSON без каких-либо пояснений.
Не оборачивай в markdown-блоки. Не добавляй текст до или после JSON.""",
    messages=[{
        "role": "user",
        "content": """Извлеки данные из текста:
"Заказ #4521 от Ивана Петрова на сумму 3500 рублей, статус: доставлен"

Формат:
{
  "order_id": number,
  "customer": string,
  "amount": number,
  "status": string
}"""
    }]
)

print(response.content[0].text)
# → {"order_id": 4521, "customer": "Иван Петров", "amount": 3500, "status": "доставлен"}

Уровень 2: надёжный парсинг

Даже с хорошим промптом иногда прилетает json .... Чистим перед парсингом:

import json
import re


def parse_json_response(text: str) -> dict:
    """Парсит JSON из ответа модели, обрабатывая типичные обёртки"""

    # Убираем markdown-блоки
    text = re.sub(r'```(?:json)?\s*', '', text)
    text = re.sub(r'```\s*$', '', text, flags=re.MULTILINE)
    text = text.strip()

    # Ищем JSON-объект или массив если вокруг лишний текст
    json_match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
    if json_match:
        text = json_match.group(1)

    return json.loads(text)


# Использование
try:
    data = parse_json_response(response.content[0].text)
except json.JSONDecodeError as e:
    print(f"Не удалось распарсить JSON: {e}")
    print(f"Сырой ответ: {response.content[0].text}")

Уровень 3: валидация через Pydantic

Парсинг прошёл — не значит данные правильные. Модель могла вернуть строку там, где нужно число:

from pydantic import BaseModel, ValidationError
from typing import Optional


class OrderData(BaseModel):
    order_id: int
    customer: str
    amount: float
    status: str
    notes: Optional[str] = None


def extract_order(text: str) -> OrderData | None:
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=512,
        system="Отвечай только валидным JSON без пояснений.",
        messages=[{
            "role": "user",
            "content": f"""Извлеки данные заказа из текста.
Формат: {{"order_id": int, "customer": str, "amount": float, "status": str}}

Текст: {text}"""
        }]
    )

    try:
        raw = parse_json_response(response.content[0].text)
        return OrderData(**raw)
    except (json.JSONDecodeError, ValidationError) as e:
        print(f"Ошибка: {e}")
        return None


order = extract_order("Заказ #4521 от Ивана Петрова на сумму 3500 рублей, статус: доставлен")
if order:
    print(f"ID: {order.order_id}, сумма: {order.amount}")

Уровень 4: retry при ошибке

Если с первого раза не получилось — говорим модели что пошло не так:

def extract_with_retry(text: str, schema: type, max_attempts: int = 3) -> dict | None:
    messages = [{
        "role": "user",
        "content": f"Извлеки данные в JSON по схеме {schema.schema()}.\n\nТекст: {text}"
    }]

    for attempt in range(max_attempts):
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=512,
            system="Отвечай только валидным JSON.",
            messages=messages
        )

        raw_text = response.content[0].text

        try:
            raw = parse_json_response(raw_text)
            return schema(**raw)

        except (json.JSONDecodeError, ValidationError) as e:
            if attempt == max_attempts - 1:
                return None

            # Говорим модели что именно сломалось
            messages.append({"role": "assistant", "content": raw_text})
            messages.append({
                "role": "user",
                "content": f"Ошибка парсинга: {e}\nПопробуй снова, верни только валидный JSON."
            })

    return None

Что использовать в продакшне

Для большинства задач хватает уровней 1–3: чёткий промпт + чистка markdown + Pydantic-валидация. Retry добавляй если задача критичная и ошибки недопустимы.

Уровень сложности выбирай по цене ошибки: для аналитики в фоне — уровень 2, для платёжных данных — уровень 4 с логированием каждой попытки.

11 часов назад

Никита Вихров

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845