Структурированный вывод и вызов функций
Попросите модель дать JSON — и она охотно выдаст почти-JSON: висячая запятая, ключ без кавычек, дружелюбное предложение перед первой скобкой. Свободное сэмплирование всегда может выбросить неверный токен, потому что сэмплер ничего не знает о вашей схеме. Структурированный вывод закрывает этот разрыв, а вызов функций — тот же механизм в другой шляпе.
Почему свободный JSON ломается
Каждый сгенерированный токен — сэмпл из распределения. На каждом шаге какая-то масса вероятности — маленькая, но не нулевая при температуре выше 0 — лежит на токенах, ломающих ваш формат: закрывающая скобка до открывающей, запятая после последнего элемента массива, строчное true написанное как True (питоновская привычка, впечатанная в обучающие данные). Перемножьте «почти всегда валидно» по 200 выходным токенам — и получите перемежающиеся, невоспроизводимые поломки: худший вид бага. Даже температура 0 делает вывод лишь детерминированным, а не схемо-валидным.
Лестница решений
| Ступень | Механизм | Гарантия | Где |
|---|---|---|---|
| Только промпт («ответь валидным JSON») | Вежливая просьба | Никакой — сэмплирование всё равно может ошибиться | любой движок |
| JSON-режим | Промпт плюс ограничение синтаксисом JSON | Синтаксически валидный JSON; форма не гарантирована | OpenAI-совместимые API, Ollama format: "json" |
| Схемо-ограниченный | Схема → грамматика → маскирование логитов | Вывод всегда парсится и соответствует схеме | llama.cpp (json_schema / GBNF), vLLM guided_json, Ollama structured outputs, outlines / xgrammar |
Каждая ступень стоит гибкости и покупает детерминизм. Используйте верхнюю всегда, когда потребитель — код.
Грамматически-ограниченное декодирование
Скомпилируйте схему в грамматику. На каждом шаге декодирования каждому токену, который не может продолжить валидный документ, логит устанавливается в −∞ до сэмплирования. Неверный вывод не маловероятен — он невозможен. llama.cpp поставляет грамматики GBNF; грамматика десятичного числа — одна строка:
root ::= "-"? [0-9]+ ("." [0-9]+)?
Та же идея оборачивает JSON Schema. Против локального сервера llama.cpp (OpenAI-совместимый эндпоинт):
import json, requests
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
"component": {"type": "string",
"enum": ["auth", "upload", "ui", "api"]},
"severity": {"type": "string",
"enum": ["low", "medium", "high"]}
},
"required": ["title", "component", "severity"],
"additionalProperties": False
}
r = requests.post("http://localhost:8080/v1/chat/completions", json={
"messages": [{"role": "user",
"content": "Extract the bug: " + report}],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "bug", "strict": True,
"schema": schema}
},
"temperature": 0
})
bug = json.loads(r.json()["choices"][0]["message"]["content"])
Ollama делает то же с JSON Schema в поле format; vLLM с guided_json. Тот же механизм, три написания — точные имена полей вашего движка в параметрах движка.
Ограниченное — не правильное
Грамматика гарантирует форму, а не истину. Модель может выдать {"severity": "low"} на репорт о краше или набить строковое поле наполнителем. Маскирование может и вредить открытым полям: когда модель хочет абзац, а грамматика запрещает перевод строки, качество падает. Практичные правила:
- Узкие enum-ы вместо свободных строк всегда, когда пространство значений известно.
requiredдля каждого потребляемого поля;additionalProperties: falseпротив выдумок.- Держите строковые поля короткими; длинную прозу выводите из-под ограниченного ответа.
- Всё равно положите один разобранный пример желаемого вывода в промпт — он рулит содержанием, чего грамматика не может.
Вызов функций и инструментов
Вызов инструментов — структурированный вывод плюс конвенция: модель возвращает, какую функцию вызвать и с какими аргументами; ваш код исполняет её и дописывает результат. Форма OpenAI tools — диалект де-факто, который имитируют локальные движки:
{
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["C", "F"]}
},
"required": ["city"]
}
}
}]
}
Модель отвечает tool_calls в сообщении; вы возвращаете вывод сообщением role: "tool", и цикл продолжается. Модели обучены под конкретные диалекты инструментов — шаблоны линий Hermes и Qwen лучше всего поддержаны локальными движками (llama.cpp с --jinja, Ollama, vLLM с автоматическим выбором инструментов). Модель без обучения инструментам выдаёт правдоподобные вызовы с галлюцинированными именами аргументов, что бы вы ни ограничивали, поэтому сначала выберите модель под задачу: выбор модели.
Проверяйте и парс, и содержание
Две проверки после каждого вызова. Первая — парсинг: json.loads проходит (грамматика обычно это гарантирует; JSON-режим не всегда). Вторая — семантика: обязательные ключи на месте, enum-ы в диапазоне, числа правдоподобны, строки непусты. При провале повторите один раз, вписав ошибку валидации обратно в промпт — ограниченные повторы дёшевы и обычно проходят с первого отскока.
Именно этот цикл — выдать вызов, провалидировать, исполнить, наблюдать — агенты гоняют весь день: агенты.
Связанные разделы: агенты · промптинг · сэмплирование · rag