Практика AI-агентов
Структурированный вывод AI-агента вместо хрупкого текста
Свободный ответ модели удобен человеку, но ненадёжен как вход следующего этапа. Исправим это с помощью контракта данных, строгой валидации и предсказуемой обработки сбоев.
Почему свободный текст ломает конвейер
Структурированный вывод — это ответ модели, форма которого заранее определена машинно проверяемой схемой. В отличие от обычного текста, он задаёт имена полей, их типы, допустимые значения и обязательность.
Представим два этапа: первый анализирует обращение, второй назначает исполнителя. Если первый возвращает фразу «Срочно, похоже на проблему с оплатой», второму приходится угадывать категорию и приоритет. Формулировка может измениться, появится Markdown или пояснение перед JSON — и простой парсер перестанет работать.
Надёжная граница между этапами выглядит иначе:
{
"category": "billing",
"priority": "high",
"summary": "Платёж списан, но заказ не активирован",
"needs_human": false
}
Это не гарантирует истинность анализа, но гарантирует проверяемую форму результата. Формат и смысл следует контролировать отдельно.
Шаг 1. Определите контракт данных
Начинайте не с промпта, а с решения следующего этапа. В примере маршрутизатору нужны категория, приоритет, краткое описание и признак ручной проверки. Не добавляйте поля «на всякий случай»: каждое поле увеличивает число способов получить некорректный ответ.
JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "agent-analysis.schema.json",
"title": "AgentAnalysis",
"type": "object",
"additionalProperties": false,
"required": [
"category",
"priority",
"summary",
"needs_human"
],
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high"]
},
"summary": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"needs_human": {
"type": "boolean"
}
}
}
additionalProperties: false запрещает неожиданные поля. enum ограничивает значения, а required не позволяет молча пропустить данные, необходимые следующему этапу.
Сохраните схему как agent-analysis.schema.json. Команда ниже только проверяет синтаксис JSON и не отправляет данные во внешние сервисы:
python3 -m json.tool agent-analysis.schema.json > /dev/null && echo "JSON syntax: OK"
Шаг 2. Передайте схему модели
Если используемый интерфейс модели поддерживает вывод по JSON Schema, передавайте схему через его параметр структурированного ответа. Точное имя параметра зависит от выбранного API или библиотеки; сверяйтесь с документацией вашей версии.
Смысл конфигурации должен быть таким:
{
"response_format": {
"type": "json_schema",
"name": "agent_analysis",
"strict": true,
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"category",
"priority",
"summary",
"needs_human"
],
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high"]
},
"summary": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"needs_human": {
"type": "boolean"
}
}
}
}
}
Это пример конфигурации, а не универсальный запрос к конкретному провайдеру. Не вставляйте ключи доступа в исходный код, логи или HTML. Получайте секреты из защищённого хранилища или переменных окружения, принятых в вашей среде.
Инструкция агенту
Проанализируй обращение для маршрутизации.
Правила:
- category выбирай только из значений схемы;
- priority=high используй только при явном риске остановки работы
или существенной потери;
- если данных недостаточно для безопасной маршрутизации,
установи needs_human=true;
- не добавляй сведения, которых нет во входе;
- верни результат в заданной структурированной форме.
Схема контролирует форму, а инструкция — критерии выбора значений. Одно не заменяет другое.
Шаг 3. Валидируйте ответ на своей стороне
Даже при строгом режиме не передавайте результат дальше без локальной проверки. Ответ может быть обрезан, транспорт может вернуть ошибку, а возможности конкретной модели или библиотеки могут отличаться.
Ниже — воспроизводимый пример на Python. Для проверки схемы используется пакет jsonschema.
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install jsonschema
Перед установкой зависимости в рабочем проекте проверьте её допустимость по правилам вашей команды и зафиксируйте согласованную версию в файле зависимостей.
import json
from pathlib import Path
from jsonschema import Draft202012Validator
from jsonschema.exceptions import ValidationError
class AgentOutputError(Exception):
pass
def load_schema(path: str) -> dict:
return json.loads(Path(path).read_text(encoding="utf-8"))
def parse_agent_output(raw: str, schema: dict) -> dict:
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise AgentOutputError(
f"Ответ не является корректным JSON: строка {exc.lineno}, "
f"столбец {exc.colno}"
) from exc
validator = Draft202012Validator(schema)
errors = sorted(
validator.iter_errors(data),
key=lambda error: list(error.absolute_path),
)
if errors:
first = errors[0]
location = ".".join(map(str, first.absolute_path)) or "<root>"
raise AgentOutputError(
f"Ответ не соответствует схеме: {location}: {first.message}"
)
return data
schema = load_schema("agent-analysis.schema.json")
raw_response = """{
"category": "billing",
"priority": "high",
"summary": "Платёж списан, но заказ не активирован",
"needs_human": false
}"""
try:
result = parse_agent_output(raw_response, schema)
except AgentOutputError as exc:
print(f"REJECTED: {exc}")
else:
print(f"ACCEPTED: {result['category']} / {result['priority']}")
raw_response здесь — демонстрационный ответ, а не результат реального вызова модели. В приложении подставьте текстовое содержимое, полученное от вашего клиентского слоя.
Шаг 4. Обрабатывайте ошибки как отдельные состояния
Не объединяйте все сбои в «модель ответила неправильно». Минимально полезная классификация:
| Состояние | Пример | Действие |
|---|---|---|
| Транспортная ошибка | Тайм-аут или недоступность сервиса | Ограниченный повтор с задержкой |
| Обрезанный ответ | Достигнут лимит вывода | Не парсить как полный результат; повторить с меньшей задачей или большим допустимым лимитом |
| Отказ модели | Вместо данных получен сигнал отказа | Не маскировать под ошибку JSON; направить в безопасный сценарий |
| Некорректный JSON | Лишний текст или оборванная строка | Один исправляющий повтор либо ручная обработка |
| Нарушение схемы | priority: "urgent" |
Повторить с кратким описанием нарушения |
| Сомнительный смысл | Форма верна, категория не подтверждается входом | Бизнес-проверка или needs_human=true |
Ограниченный повтор
def run_with_validation(call_agent, source_text, schema, max_attempts=2):
last_error = None
for attempt in range(max_attempts):
raw = call_agent(
source_text=source_text,
validation_error=str(last_error) if last_error else None,
)
try:
return {
"status": "ok",
"data": parse_agent_output(raw, schema),
}
except AgentOutputError as exc:
last_error = exc
return {
"status": "manual_review",
"error_code": "INVALID_AGENT_OUTPUT",
"message": str(last_error),
}
call_agent — намеренно условная функция: подключите к ней уже выбранный вами клиент. Повтор ограничен двумя попытками, чтобы неисправимый вход не создавал бесконечный цикл и непредсказуемые расходы.
Не передавайте модели трассировки стека, секреты и внутренние системные сведения. Для исправляющего запроса достаточно безопасного сообщения вроде: «Поле priority должно быть одним из: low, normal, high».
Шаг 5. Добавьте смысловые ограничения
JSON Schema проверит типы, но не все бизнес-правила. Например, ручная проверка может быть обязательна для категории other. Такое правило проще и надёжнее выразить обычным кодом:
def validate_business_rules(data: dict) -> None:
if data["category"] == "other" and not data["needs_human"]:
raise AgentOutputError(
"Для category=other требуется needs_human=true"
)
Вызывайте эту функцию после проверки JSON Schema и до запуска следующего этапа. Детерминированные правила лучше держать в коде: их легче проверять, изменять и наблюдать.
Проверка результата
Сохраните Python-код как validate_output.py, затем выполните:
. .venv/bin/activate
python3 validate_output.py
Для приведённого демонстрационного объекта ожидаемый вывод:
ACCEPTED: billing / high
Затем вручную замените "high" на "urgent". Валидатор должен отклонить объект с сообщением о поле priority. Удалите needs_human — объект также должен быть отклонён. Добавьте неизвестное поле — сработает запрет additionalProperties.
Проверка считается успешной, если следующий этап получает данные только после прохождения обеих границ:
- структурная проверка по JSON Schema;
- проверка бизнес-правил в коде.
Типовые ошибки
Извлекать JSON регулярным выражением
Конструкции вроде поиска текста между первой { и последней } ломаются на вложенных объектах, строках со скобками и нескольких фрагментах JSON. Используйте структурированный режим модели и стандартный JSON-парсер.
Просить «ответить JSON» только в промпте
Текстовая просьба полезна, но сама по себе не является контрактом. Модель может добавить пояснение, изменить имя поля или вернуть строку вместо логического значения.
Исправлять данные молча
Автоматическая замена "urgent" на "high" скрывает нарушение контракта. Если нормализация необходима, задайте явную таблицу преобразований, журналируйте её применение и не используйте для неоднозначных значений.
Считать валидный объект правильным
Объект может соответствовать схеме и при этом содержать неверный вывод модели. Проверяйте критичные решения детерминированными правилами, а при недостатке данных направляйте результат человеку.
Записывать полный вход и ответ в журнал
В них могут находиться персональные или конфиденциальные данные. Для наблюдаемости обычно достаточно кода ошибки, номера попытки, версии схемы, длительности и технического идентификатора операции.
Ограничения подхода
- Схема гарантирует форму, но не фактическую корректность результата.
- Не все модели и клиентские библиотеки поддерживают одинаковые части JSON Schema.
- Слишком сложная схема повышает вероятность отказов и затрудняет развитие контракта.
- Изменение обязательного поля или значения
enumможет сломать потребителей. - Повторный запрос увеличивает задержку и стоимость, поэтому число попыток должно быть ограничено.
- Для действий с высоким риском структурированный вывод не заменяет авторизацию, бизнес-проверки и подтверждение человеком.
Версионируйте контракт при несовместимых изменениях, например через отдельное поле schema_version или разные имена схем. Сначала обновляйте потребителя так, чтобы он понимал новую версию, и только затем переключайте производителя.
Короткий контрольный список
- Поля определены потребностями следующего этапа.
- Указаны
required, типы, ограничения иadditionalProperties. - Схема передаётся через структурированный режим, если он доступен.
- Ответ повторно валидируется в приложении.
- Бизнес-правила проверяются отдельно от JSON Schema.
- Транспортные ошибки, отказ и нарушение схемы различаются.
- Повторы ограничены, а запасной сценарий определён заранее.
- В журналы не попадают секреты и лишние пользовательские данные.