localaiforum
Войти
Руководство › Работа с моделями

Структурированный вывод и вызов функций

Попросите модель дать 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