Разработка ИИ-агентов · Модуль 2 · Урок 2.4
Структурированный вывод и его парсинг
Зачем нужен структурированный вывод
Часто от модели нужен не свободный текст, а данные, которые программа сможет дальше обработать: категория, список полей, решение «да/нет» с обоснованием. Если просто попросить «ответь в JSON», модель иногда добавит пояснения до или после, вставит markdown-обёртку или слегка нарушит схему — и парсинг упадёт. Нужна надёжность.
Структурированный вывод — это получение ответа в заранее заданной форме, пригодной для машинного разбора. Это мост между «модель порассуждала» и «программа дальше действует по данным».
Способы получить структуру надёжно
- Через tool calling. Самый надёжный приём: описать нужную структуру как инструмент (схему) и заставить модель «вызвать» его. Тогда аргументы вызова и есть ваш типизированный объект, провалидированный по JSON Schema. Часто это удобнее, чем парсить текст.
- Режим строгого JSON / structured outputs. Некоторые провайдеры гарантируют валидный JSON по схеме (имена параметров версионно-зависимы — сверяйтесь с доками).
- Промпт + строгий парсинг. Если первых двух нет: чётко задать схему в промпте, попросить «только JSON без пояснений», а в коде — извлечь JSON, распарсить и провалидировать.
Что бы вы ни выбрали, относитесь к выводу модели как к недоверенному вводу: всегда парсите с обработкой ошибок и проверяйте обязательные поля.
Парсинг и восстановление
Хороший разбор структурированного ответа в Go: определить целевой тип, распарсить json.Unmarshal, затем провалидировать значения (обязательные поля, диапазоны, enum). Если разбор не удался — не падать, а попросить модель исправиться: вернуть ей текст ошибки и попросить прислать корректный JSON по схеме. Один-два таких ретрая обычно решают проблему.
Полезные мелочи: срезать markdown-ограждения (), не доверять порядку полей, задавать значения по умолчанию для необязательных, логировать сырой ответ при ошибке — это бесценно при отладке.
// Ожидаемая структура решения модели.
type Triage struct {
Category string `json:"category"`
Urgent bool `json:"urgent"`
Reason string `json:"reason"`
}
var allowed = map[string]bool{"billing": true, "tech": true, "other": true}
func parseTriage(raw string) (Triage, error) {
raw = stripFences(raw) // убрать ``` если модель их добавила
var t Triage
if err := json.Unmarshal([]byte(raw), &t); err != nil {
return t, fmt.Errorf("невалидный JSON: %w", err)
}
if !allowed[t.Category] {
return t, fmt.Errorf("недопустимая категория: %q", t.Category)
}
if t.Reason == "" {
return t, fmt.Errorf("пустое поле reason")
}
return t, nil
}func triageWithRetry(input string) (Triage, error) {
schema := "Верни ТОЛЬКО JSON: {\"category\":\"billing|tech|other\"," +
"\"urgent\":true|false,\"reason\":\"...\"}"
msgs := []Message{
{Role: "system", Content: schema},
{Role: "user", Content: input},
}
for attempt := 0; attempt < 3; attempt++ {
choice, err := callLLM(msgs, nil)
if err != nil {
return Triage{}, err
}
t, perr := parseTriage(choice.Content)
if perr == nil {
return t, nil
}
// подкладываем ответ модели и текст ошибки — просим исправиться
msgs = append(msgs,
Message{Role: "assistant", Content: choice.Content},
Message{Role: "user", Content: "Ошибка разбора: " + perr.Error() + ". Пришли корректный JSON."})
}
return Triage{}, fmt.Errorf("не удалось получить валидный JSON за 3 попытки")
}Anti-patterns
| Анти-паттерн | Почему плохо | Как правильно |
|---|---|---|
| Парсить свободный текст регэкспами | Хрупко: формат «плывёт» от запроса к запросу | Tool calling / строгий JSON-режим со схемой |
| Доверять JSON без валидации | Пропущенные/чужие значения ломают логику дальше | Проверять обязательные поля, enum, диапазоны |
| Падать при первом невалидном ответе | Случайный сбой роняет весь запрос | 1–2 ретрая: вернуть ошибку модели и попросить исправить |
| Не срезать markdown-ограждения | json ... ломает json.Unmarshal | Снимать ограждения/префиксы перед разбором |
Практическое задание
- Определите Go-тип для нужного вам структурированного ответа (например, классификацию с полями).
- Получите структуру через tool calling (схему как инструмент) ИЛИ через строгий JSON-промпт.
- Реализуйте
parse, который делаетjson.Unmarshalи валидирует поля (обязательные, enum). - Добавьте ретрай: при ошибке верните модели текст ошибки и попросите прислать корректный JSON.
- Залогируйте сырой ответ при сбое разбора — пригодится при отладке.
Проверка знаний
Какой приём обычно даёт самый надёжный структурированный вывод?
Верный ответ: B
B верно. Tool calling даёт типизированные аргументы, провалидированные по JSON Schema, — это надёжнее свободного текста. A и D хрупки; температура (C) тут ни при чём.
Что делать, если модель прислала невалидный JSON?
Верный ответ: B
B верно. Короткий цикл самоисправления (вернуть ошибку → попросить корректный JSON) обычно решает проблему, а при исчерпании попыток — честная ошибка. A слишком резко; C и D ненадёжны.