Валидация структурированного вывода LLM на Python, которая работает надёжно
Перестаньте полагаться на интуицию. Валидируйте контракты.
Большинство руководств по «структурированному выводу» (structured output) для больших языковых моделей (LLM) не обладают должной серьезностью. Они учат вас вежливо просить модель выдавать JSON и затем надеяться, что она поступит правильно. Это не валидация. Это оптимизм, обернутый в фигурные скобки.
В собственной документации OpenAI четко проводится это различие. Режим JSON гарантирует, что вы получите валидный JSON, тогда как Структурированный вывод (Structured Outputs) обеспечивает соблюдение схемы (schema adherence), и OpenAI рекомендует использовать Структурированный вывод вместо режима JSON, whenever это возможно.

Тем не менее, это не делает полезную нагрузку (payload) надежной. JSON Schema определяет структуру и допустимые значения, Pydantic предоставляет типизированную валидацию в Python, а OpenAI явно отмечает, что ответ, соответствующий схеме, все равно может содержать неверные значения. Кроме того, отказы и неполные ответы могут обойти ожидаемую вами структуру. В продакшене валидация структурированного вывода — это конвейер, а не переключатель. Эта граница также должна существовать в более широком контексте пропускной способности, повторных попыток и ограничений планировщика на хабе по инженерной производительности LLM.
Валидация структурированного вывода — это контракт
Валидация структурированного вывода для LLM означает, что вы заранее определяете форму ответа, ограничиваете модель в производстве этой формы, насколько это возможно, и затем снова валидируете результат, прежде чем ваше приложение будет доверять ему. На практике это означает проверку обязательных полей, типов, перечислений (enums), замкнутых форм объектов и доменных правил, прежде чем полезная нагрузка попадет в вашу базу данных, пользовательский интерфейс, очередь или downstream-сервис. JSON Schema существует именно для такого вида структурной валидации, Pydantic создан для валидации недоверенных данных по аннотациям типов Python, а библиотека jsonschema в Python предоставляет прямой способ валидации экземпляра по схеме.
Также существует четкое разделение между двумя распространенными случаями использования. Если модель должна отвечать пользователю в структурированном формате, используйте формат структурированного ответа. Если модель должна вызывать инструменты или функции вашего приложения, используйте вызов функций (function calling). В документации OpenAI это различие прописано, и для вызова функций они рекомендуют включить strict: true, чтобы аргументы надежно соответствовали схеме функции.
Мое сильное мнение простое. Относитесь к каждому структурированному ответу LLM как к границе API. Как только вы начнете мыслить в терминах контрактов, а не промптов, архитектура станет чище, баги дешевле, а проблема «почему модель изобрела новое поле в продакшене» в основном исчезнет. Это реальный ответ на вопрос «что такое валидация структурированного вывода для LLM», и он намного лучше, чем «вежливо попросите модель выдать JSON».
Режим JSON — это не валидация
Если вы запомните только одну вещь из этой статьи, пусть это будет она. Режим JSON — это не валидация схемы. В справочном центре OpenAI сказано, что режим JSON не гарантирует, что вывод будет соответствовать какой-либо конкретной схеме, только то, что это валидный JSON и он парсится без ошибок. Руководство по Структурированному выводу говорит то же самое более чистым языком. И режим JSON, и Структурированный вывод могут производить валидный JSON, но только Структурированный вывод обеспечивает соблюдение схемы.
Это различие имеет большее значение, чем люди признают. В своем посте о запуске Структурированного вывода OpenAI сообщила, что gpt-4o-2024-08-06 со Структурированным выводом набрал 100 процентов в своих сложных тестах на схемы JSON, в то время как gpt-4-0613 набрал менее 40 процентов. Вам не нужно принимать эти цифры за универсальную истину, чтобы увидеть более широкую картину. Принуждение к схеме меняет поверхность отказов с «может произойти что угодно» на «контракт гораздо строже».
Все еще существуют граничные случаи, и притворство обратным образом превращает демонстрационные демоны в ночные вызовы (pager duty). В документации OpenAI указано, что модель может отказать в небезопасном запросе, и эти отказы выводятся вне вашего обычного пути схемы. Также документируются неполные ответы, включая такие случаи, как достижение max_output_tokens или прерывание фильтром контента. Поэтому на часто задаваемый вопрос «достаточно ли режима JSON для надежного вывода LLM» есть короткий и длинный ответ. Короткий ответ — нет. Длинный ответ заключается в том, что даже строгий структурированный вывод нуждается в явной обработке ошибок.
Где структурированный вывод все еще ломается
Принуждение к схеме уменьшает проблему. Оно не устраняет ее. В реальном трафике вы все еще видите сломанные или удивительные полезные нагрузки по причинам, которые имеют мало общего с формулировкой вашего промпта.
Формы отказов, для которых стоит проектировать
Модели и клиенты не согласны в деталях. Вы можете получить дополнительный текст до или после JSON, блоки Markdown с ограничителями вокруг полезной нагрузки или вызов инструмента, имя которого валидно, но аргументы которого являются JSON, не соответствующим вашей модели Pydantic. Стриминг усугубляет ситуацию, потому что вы можете валидировать незавершенный буфер. Защитный код должен исходить из предпосылки «строка на входе, возможно JSON внутри», а не «байты в потоке уже соответствуют моей модели».
Различия провайдеров и API
Не каждый хост предоставляет одинаковую поверхность структурированного вывода. Один стек может дать вам полноценное завершение, связанное со схемой, другой может гарантировать только синтаксис JSON, а локальные среды выполнения могут отставать от размещенных API. Это одна из причин, почему FAQ «как вы валидируете JSON LLM в Python» начинается с принуждения провайдером, когда оно существует, и все же заканчивается валидацией на стороне Python. Для более широкого взгляда на то, как сравниваются поставщики, см. сравнение структурированного вывода популярных провайдеров LLM. Если вы запускаете модели локально, тот же конвейер валидации применяется после нормализации формата потока, например, после извлечения с помощью Ollama, как в структурированный вывод LLM с Ollama на Python и Go. Когда среда выполнения все еще оборачивает JSON странными префиксами или следами рассуждений, ожидайте тот же класс ошибок парсинга, описанный в Проблемы со структурированным выводом Ollama GPT-OSS.
Стек Python, который действительно работает
Моя рекомендация намеренно скучна. Во-первых, позвольте провайдеру моделей обеспечить структурный контракт, когда это возможно. Во-вторых, валидируйте возвращаемую полезную нагрузку в Python с помощью Pydantic. В-третьих, используйте явную валидацию бизнес-правил для фактов, которые одна схема не может доказать. В-четвертых, тестируйте контракт с помощью фикстур и адверсарных примеров, вместо того чтобы махать скриншотом с площадки и считать задачу выполненной. Документация OpenAI по Структурированному выводу, модель валидатора Pydantic, инструменты jsonschema в Python и собственные примеры оценки структурированного вывода OpenAI все указывают в этом направлении.
Pydantic — это правильный центр тяжести для Python. Он позволяет моделировать вывод как обычные типы Python, генерировать JSON Schema с помощью model_json_schema() и валидировать сырой JSON с помощью model_validate_json(). В документации Pydantic также отмечается, что model_validate_json() обычно является лучшим путем, чем выполнение json.loads(...) сначала, а затем валидация, потому что этот двухэтапный маршрут добавляет дополнительную работу по парсингу в Python.
Если вы храните автономные файлы схем в вашем репозитории или хотите, чтобы CI валидировал полезные нагрузки фикстур независимо от кода модели, пакет jsonschema в Python дает вам самую простую проверку контракта с помощью jsonschema.validate(...). Если вы хотите это в pre-commit, check-jsonschema существует специально как CLI и хук pre-commit, построенный на jsonschema. Это очень хороший вариант для команд, которые хотят, чтобы изменения схем ревьюировались как изменения кода.
Фреймворки могут сократить количество «водопровода» (plumbing), но они не устраняют необходимость в реальной валидации. LangChain теперь автоматически выбирает нативный структурированный вывод провайдера, когда провайдер его поддерживает, и в противном случае переходит к стратегии инструментов. Instructor накладывает модели ответов Pydantic, валидацию, повторные попытки и поддержку нескольких провайдеров поверх вызовов моделей. Guardrails фокусируется на валидаторах и слоях защиты ввода-вывода. Все они полезные инструменты. Но схема и бизнес-правила все еще принадлежат вам. Если вы выбираете между библиотеками более высокого уровня, сравнение BAML и Instructor для Python является полезным дополнением к этой статье.
Минимальный пример OpenAI и Pydantic
Самый маленький пример, достойный продакшена, имеет несколько несомненных условий. Используйте замкнутый набор значений, похожих на enum, где возможно. Запрещайте дополнительные ключи. Добавляйте описания полей, чтобы схема была понятна людям и более читаема для модели. Держите корневой объект явным и скучным. OpenAI рекомендует четкие имена, а также заголовки и описания для важных ключей, JSON Schema использует enum для ограничения значений, а Pydantic может замкнуть форму объекта с помощью extra="forbid".
from typing import Literal
from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field
class TicketClassification(BaseModel):
model_config = ConfigDict(extra="forbid")
category: Literal["billing", "bug", "how_to", "abuse"] = Field(
description="Support ticket category."
)
priority: Literal["low", "medium", "high"] = Field(
description="Operational urgency."
)
needs_human: bool = Field(
description="Whether a human should review the case."
)
summary: str = Field(
description="A one sentence summary of the issue."
)
client = OpenAI()
response = client.responses.parse(
model="gpt-4o-2024-08-06",
input=[
{
"role": "system",
"content": "Classify support tickets. Return only the structured result.",
},
{
"role": "user",
"content": "Customer reports duplicate charges after refreshing checkout.",
},
],
text_format=TicketClassification,
)
result = response.output_parsed
print(result.model_dump())
Две детали в этом примере легко пропустить, и их определенно стоит учитывать. extra="forbid" на стороне Pydantic отражает идею JSON Schema additionalProperties: false, что также является требованием для строгих схем инструментов в документации OpenAI по вызову функций. И перечисления (enums) — это не косметика. Это один из простейших способов остановить модель от изобретения значения, которое ваш код не понимает.
Python SDK OpenAI поддерживает client.responses.parse(...) с моделью Pydantic, предоставленной как text_format, и распарсенный объект возвращается в response.output_parsed. Тот же SDK также поддерживает client.chat.completions.parse(...), где распарсенный объект находится в message.parsed. Если вам нужна прямая экстракция структурированных данных с минимальным связыванием, эти помощники являются самой чистой отправной точкой.
Парсинг, нормализация, затем валидация
Структурированный вывод и model_validate_json устраняют много болей парсинга, когда стек выровнен от начала до конца. В тот момент, когда вы поддерживаете провайдера, который возвращает обычный чат-текст, модель, которая оборачивает JSON в ограничители, или путь логирования, который хранит сырую строку завершения, вам нужна одна точка контроля, которая превращает текст в словарь, прежде чем запустится Pydantic.
import json
def parse_json_from_llm_text(text: str) -> dict:
cleaned = text.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[1]
cleaned = cleaned.rsplit("```", 1)[0].strip()
# Common "Sure, here is the JSON:" prefix before the object.
if not cleaned.startswith("{") and "{" in cleaned and "}" in cleaned:
start = cleaned.find("{")
end = cleaned.rfind("}")
if end > start:
cleaned = cleaned[start : end + 1]
return json.loads(cleaned)
ticket_dict = parse_json_from_llm_text(raw_completion_text)
ticket = TicketClassification.model_validate(ticket_dict)
Этот помощник намеренно скучен. Он обрабатывает блоки «json ... » и ведущую преамбулу на естественном языке, когда полезная нагрузка все еще является единственным объектом верхнего уровня. Это не полноценный экстрактор JSON. Если модель вкладывает фигурные скобки внутри строковых значений, наивное нарезка может сломаться, и правильное исправление обычно заключается в более строгом промптинге, завершении, привязанном к схеме, или специализированной библиотеке парсера.
Стриминг завершений
Если вы стримите токены чата, не запускайте json.loads или model_validate_json на каждой дельте. Буферизуйте, пока API не сообщит о завершенном сообщении (проверьте клиент на окончание потока или finish_reason), конкатенируйте текст, затем парсите один раз. То же правило применяется, когда аргументы вызова инструмента поступают кусками. Вы валидируете только после того, как строка аргументов завершена.
chunks: list[str] = []
for chunk in completion_stream:
delta = chunk.choices[0].delta.content or ""
chunks.append(delta)
raw_completion_text = "".join(chunks)
ticket = TicketClassification.model_validate_json(raw_completion_text)
Вы все равно можете пропустить raw_completion_text через parse_json_from_llm_text сначала, когда ожидаете ограничители или болтовню вокруг JSON.
Как только вы владеете парсингом обычных строк, следующим ограничением часто является не Python, а диалект JSON Schema провайдера и то, что фактический удаленный API принимает.
Ограничения схем провайдеров (прежде чем проявлять изобретательность в Python)
Не выгружайте слежу вывод любого генератора схем в API и не предполагайте, что поддерживается каждая функция JSON Schema. OpenAI поддерживает подмножество JSON Schema, требует, чтобы все поля были обязательными для Структурированного вывода, требует, чтобы корень был объектом, а не верхним уровнем anyOf, и документирует ограничения на глубину вложенности и общее количество свойств. Держите схему, обращенную к провайдеру, простой. Это не компромисс. Это хорошая инженерия.
Если вам нужен путь валидации, независимый от провайдера, или вы хотите валидировать сохраненные фикстуры и моки, Pydantic плюс jsonschema по-прежнему являются отличным сочетанием.
from jsonschema import validate as validate_json
schema = TicketClassification.model_json_schema()
payload = {
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "Checkout duplicates charges after refresh.",
}
validate_json(instance=payload, schema=schema)
ticket = TicketClassification.model_validate(payload)
print(ticket)
Этот паттерн особенно полезен в тестах, контрактных фикстурах и интеграциях, где провайдер модели не предлагает нативное принуждение структурированного вывода. Просто помните, что схема, сгенерированная локально, может быть шире, чем поддерживаемое подмножество данного провайдера, поэтому «валидно локально» не означает автоматически «принято каждым API LLM». Также отметьте, что некоторые провайдеры предварительно обрабатывают и кэшируют артефакты схем, поэтому первый запрос для новой схемы может быть медленнее, чем теплые запросы.
Вызовы инструментов — это второй контракт
Вызов функций или инструментов — это другая основная форма структурированного вывода. Модель выбирает имя и передает аргументы, которые должны соответствовать JSON Schema, которой вы владеете. OpenAI рекомендует strict: true в определениях инструментов, чтобы аргументы оставались согласованными с этой схемой. В стеках с-heavy агентностью, плохая выборка быстро превращается в невалидный JSON инструмента; держите настройки сэмплера согласованными с многоступенчатой работой, используя справочник параметров агентного вывода для Qwen и Gemma.
Нижеприведенные фрагменты кода предполагают, что вы уже отображали объект вызова инструмента провайдера в строку name и словарь arguments, например, путем парсинга tool_calls[].function в завершениях чата (аргументы строки JSON сначала становятся json.loads). dispatch_tool — это шаг после этой нормализации.
Два практических правила помогают в Python. Во-первых, валидируйте имя инструмента по явному списку разрешенных (allowlist) перед маршрутизацией выполнения. Во-вторых, валидируйте словарь аргументов с той же моделью Pydantic, которую вы используете в тестах, а не с помощью ad-hoc доступа к ключам. Режим отказа, который вы избегаете, — это «валидные аргументы JSON, неправильная форма для инструмента, который сработал», что проскальзывает мимо строковых проверок.
from typing import Any, Callable
from pydantic import BaseModel
ToolHandler = Callable[[dict[str, Any]], str]
def dispatch_tool(
*,
name: str,
arguments: dict[str, Any],
handlers: dict[str, tuple[type[BaseModel], ToolHandler]],
) -> str:
if name not in handlers:
raise ValueError(f"unsupported tool {name}")
model_cls, handler = handlers[name]
validated = model_cls.model_validate(arguments)
return handler(validated.model_dump())
handlers: dict[str, tuple[type[BaseModel], ToolHandler]] = {
"classify_ticket": (
TicketClassification,
lambda data: f"queued as {data['category']}",
),
}
Этот паттерн держит маршрутизацию и валидацию в одном месте. Ваши реальные обработчики будут богаче, но разделение должно оставаться тем же: разрешенные имена, типизированные аргументы, затем побочные эффекты.
Валидация схемы все еще нуждается в бизнес-правилах
Валидный объект — это не то же самое, что правильный объект. OpenAI говорит об этом прямо. Структурированный вывод не предотвращает ошибки внутри значений JSON-объекта. Вот почему у FAQ «почему валидация схемы и валидация бизнес-правил важны» есть прямой ответ. Потому что ответ может идеально соответствовать схеме и все еще быть неверным таким образом, который вредит бизнесу.
Вот реалистичный пример. Структура может быть валидной, но логика ценообразования все еще может быть бессмысленной.
from decimal import Decimal
from typing import Literal
from typing_extensions import Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
class Offer(BaseModel):
model_config = ConfigDict(extra="forbid")
currency: Literal["USD", "EUR", "GBP"]
amount: Decimal = Field(gt=0)
original_amount: Decimal | None
discounted: bool
@model_validator(mode="after")
def check_discount_logic(self) -> Self:
if self.discounted:
if self.original_amount is None:
raise ValueError(
"original_amount is required when discounted is true"
)
if self.original_amount <= self.amount:
raise ValueError(
"original_amount must be greater than amount"
)
return self
Этот валидатор делает то, что схемы часто делают плохо в реальных системах. Он проверяет семантику между полями после того, как вся модель была распарсена. model_validator в Pydantic существует именно для такого вида валидации целого объекта. Обратите внимание на поле Decimal | None без значения по умолчанию. Это сохраняет поле присутствующим, все еще позволяя null, что соответствует документированному паттерну OpenAI для опциональных значений при строгом Структурированном выводе.
Если вы хотите, чтобы ошибки валидации автоматически передавались обратно в модель, Instructor — это практический слой поверх Pydantic. Его документация описывает цикл повторных попыток, где ошибки валидации захватываются, форматируются как обратная связь и используются для запроса к модели попробовать снова.
import instructor
retrying_client = instructor.from_provider("openai/gpt-4o", max_retries=2)
offer = retrying_client.create(
response_model=Offer,
messages=[
{
"role": "user",
"content": (
"Extract the offer from this text. "
"Was 49.00 USD, now 19.00 USD."
),
}
],
)
Это одна из немногих удобств, которые я с удовольствием порекомендую. Автоматические повторные попытки, связанные с реальными ошибками валидации, полезны. Тихое приведение типов — нет. Слой моделей Instructor, документация по повторным попыткам и документация по валидации все опираются на эту же идею, и они правы в этом.
Вы можете реализовать ту же идею без фреймворка. Цикл мал. Спросите модель, валидируйте с Pydantic, и если валидация не удалась, отправьте детали ошибки обратно в последующем сообщении пользователя и попросите только исправленный JSON. Ограничьте попытки, ведите лог финального отказа и предоставьте контролирующую ошибку вызывающим. Когда вы уже полагаетесь на responses.parse или другие помощники, привязанные к схеме, вы можете редко использовать этот путь. Он все еще важен для режима JSON, старых конечных точек чата или любого шлюза, который передает вам сырую строку.
from openai import OpenAI
from pydantic import ValidationError
client = OpenAI()
messages = [
{"role": "system", "content": "Return only JSON that matches the ticket schema."},
{"role": "user", "content": "Customer reports duplicate charges after refreshing checkout."},
]
ticket: TicketClassification | None = None
for attempt in range(2):
completion = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=messages,
response_format={"type": "json_object"},
)
raw_text = completion.choices[0].message.content or ""
try:
ticket = TicketClassification.model_validate_json(raw_text)
break
except ValidationError as exc:
messages.append(
{
"role": "user",
"content": f"Validation failed with {exc.errors()}. Return corrected JSON only.",
}
)
else:
raise RuntimeError("exhausted structured output retries")
assert ticket is not None
В реальных сервисах вы бы прикрепляли идентификаторы трассировки, удаляли текст клиента в логах и различали восстанавливаемые ошибки валидации от отказов или неполных ответов. Важная часть заключается в том, что повторная попытка управляется реальным выводом валидатора, а не общим сообщением «попробуй еще раз».
Тестируйте, повторяйте и завершайте с отказом (fail closed)
Что должно произойти, когда валидация LLM не удалась? Не пожимание плечами. Отклоните полезную нагрузку, ведите лог отказа, повторите попытку с ограниченными попытками, если задача стоит того, и завершите с отказом (fail closed), вместо того чтобы нормализовать мусор во что-то, что только выглядит приемлемо. Это также место, где многие команды забывают явно обрабатывать отказы и неполные выходы, даже хотя документация провайдера говорит им, что эти пути существуют.
Для API Responses OpenAI, обработка отказов должна быть первоклассным кодом, а не после мысли. Переменная — это response из client.responses.create или parse, а не completion из чат-стриминга в другом месте этой статьи.
if response.status == "incomplete":
raise RuntimeError(response.incomplete_details.reason)
content = response.output[0].content[0]
if content.type == "refusal":
raise RuntimeError(content.refusal)
Это не защитный переизбыток инженерии. Это напрямую согласовано с документированными режимами отказа. Если модель отказывает, вы не держите валидную по схеме полезную нагрузку. Если ответ неполный, вы не держите валидную по схеме полезную нагрузку. Относитесь к обоим как к явным ветвям в вашем потоке управления.
Вы также должны тестировать контракт вне самого вызова модели.
import pytest
from jsonschema import validate as validate_json
from pydantic import ValidationError
def test_ticket_fixture_matches_schema():
payload = {
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "Checkout duplicates charges after refresh.",
}
validate_json(instance=payload, schema=TicketClassification.model_json_schema())
def test_discount_logic_rejects_broken_offer():
with pytest.raises(ValidationError):
Offer.model_validate(
{
"currency": "USD",
"amount": "19.00",
"original_amount": "10.00",
"discounted": True,
}
)
def test_ticket_rejects_unknown_category_string():
with pytest.raises(ValidationError):
TicketClassification.model_validate(
{
"category": "refund",
"priority": "high",
"needs_human": True,
"summary": "Customer wants a refund.",
}
)
def test_ticket_rejects_extra_keys():
with pytest.raises(ValidationError):
TicketClassification.model_validate(
{
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "Broken flow.",
"severity": "critical",
}
)
Это правильная форма стратегии тестирования для валидации вывода LLM в Python. Валидируйте золотые фикстуры с jsonschema, чтобы каждое поле в контракте было протестировано. Валидируйте семантику с Pydantic, затем добавляйте адверсарные случаи, такие как незаконные строки перечислений, запрещенные дополнительные ключи и противоречия между полями, которые вам важны. Если вы снимаете реальные выходы модели, очищайте PII и относитесь к ним как к регрессионным фикстурам.
Если ваша команда живет в стеке OpenAI, API Evals также включает рецепты оценки структурированного вывода специально для тестирования и итерации задач, которые зависят от машиночитаемых форматов. И если вы храните сырые файлы схем в репозитории, подключите check-jsonschema в CI или pre-commit. Отправляйте контракты, а не вибры.
Проверки продакшена, которые спасут вас позже
Когда валидация не удалась, ответ на FAQ прямой. Отклоните полезную нагрузку, ведите лог почему, повторите попытку с целевой обратной связью, когда задача стоит еще одной попытки, и завершите с отказом, вместо того чтобы приводить плохие данные в очередь.
Короткий операционный чек-лист помогает командам избегать повторяющихся инцидентов.
- Ведите лог версии схемы или хеша JSON Schema, который вы отправили провайдеру, чтобы вы могли точно воспроизвести отказы.
- Удаляйте входы и выходы модели в логах. Структурированные логи бесполезны, если они утекают текст клиента.
- Испускайте счетчики или метрики для частоты отказов, частоты неполных ответов, частоты ошибок валидации и частоты успешного исправления. Пики там лучше угадывания, когда вышла модель или изменение промпта.
Более широкое руководство по наблюдаемости для систем LLM помогает подключить эти сигналы в дашборды, трассы и обзоры SLO, как только счетчики существуют.
Лучшая практика не сложна. Используйте Структурированный вывод на стороне провайдера или строгие схемы инструментов, когда можете. Нормализуйте сырой текст, когда должны. Отражайте контракт в Python с Pydantic. Добавляйте валидацию бизнес-правил для того, что схема не может доказать. Обрабатывайте отказы и неполные ответы как обычные ветви. Тестируйте контракт, пока он перестанет быть демо и начнет быть программным обеспечением. Все остальное — это просто косплей промпт-инжиниринга.