# Structured output локально: JSON Schema в Ollama, llama.cpp и vLLM

Источник: https://modelium.tech/zhurnal/structured-output-lokalno-json-schema/

Для структурированного ответа задайте модельному серверу JSON Schema и затем отдельно проверьте полученный объект валидатором. Ollama принимает схему в `format`, llama.cpp — внутри `response_format`, vLLM — через OpenAI-совместимый `response_format`. Валидный JSON не доказывает правильность извлечённых фактов: сравните их с исходным документом, прежде чем сохранять или отправлять дальше.

**Сверено с документацией 25 сентября 2026 года.** Ниже — инструкции для стенда читателя, а не отчёт о запуске модели Modelium. Никаких весов и платных вычислений для этой страницы мы не использовали. Частотность запроса «structured output локально» не измерялась; основание — актуальная документация трёх runtimes.

## Что означает structured output — и чего он не обещает?

Structured output ограничивает **форму** ответа модели: вместо свободного текста сервер пытается вернуть JSON с заданными полями и типами. Ollama передаёт JSON Schema в `format`, а llama.cpp и vLLM принимают её через параметры `response_format` своих chat endpoints [s1](#src-s1)[s3](#src-s3)[s5](#src-s5). Это полезно, когда локальную модель нужно подключить к обработке документов, но форма ответа сама по себе не доказывает, что сумма или имя поставщика извлечены верно.

Проверяйте три уровня отдельно: (1) ответ — корректный JSON, (2) объект проходит *независимую* проверку схемы, (3) каждое извлечённое значение соответствует исходному тексту. По стандартному описанию JSON Schema `properties` задаёт поля, `required` — обязательные поля, `additionalProperties: false` запрещает лишние ключи; это правила формы, не проверки истинности извлечения [s7](#src-s7). Не отправляйте непроверенное значение в 1С или CRM.

Задача в примере: из строки «Поставщик Альфа, сумма 1250 рублей» получить `vendor` (строка) и `amount` (целое число). Мы сознательно не просим модель интерпретировать договор, считать НДС или оценивать надёжность поставщика. На реальных документах сравнивайте результат с оригиналом и фиксируйте пустые/спорные поля до автоматизации.

## Как подготовить одинаковую схему для трёх серверов?

Создайте каталог и файл `schema.json`. Начните с простых типов и явно запретите дополнительные поля. Эти ограничения есть в JSON Schema, но не все её возможности одинаково поддерживаются генераторами: llama.cpp документирует лишь *подмножество* стандарта и предупреждает, что неподдерживаемые ключевые слова могут быть проигнорированы без ошибки [s4](#src-s4)[s7](#src-s7).

mkdir -p structured-demo && cd structured-democat > schema.json <<'JSON'{  "type": "object",  "properties": {    "vendor": {"type": "string"},    "amount": {"type": "integer"}  },  "required": ["vendor", "amount"],  "additionalProperties": false}JSONpython3 -m json.tool schema.json > /dev/null

Последняя команда проверяет **синтаксис файла схемы**, а не полноту её поддержки конкретным сервером. Для сопоставимого теста возьмите одну и ту же исходную строку, один и тот же контракт и одну модель, если формат весов и runtime позволяют; иначе не сравнивайте качество как контролируемый бенчмарк. Подсказка в запросе должна объяснять смысл полей. llama.cpp специально отмечает, что схема ограничивает декодирование, но сама не вставляется в промпт [s4](#src-s4).

## Как получить JSON в Ollama?

Предусловие: Ollama запущена локально, выбранная модель уже доступна в `ollama list`; установка и выбор модели — в [руководстве Ollama](/zapusk/ollama/). Не подставляйте облачный URL: текущая документация Ollama отдельно предупреждает, что Ollama Cloud не поддерживает structured outputs [s1](#src-s1).

ollama listexport MODEL='впишите-имя-модели-из-списка'python3 - <<'PY'import json, osschema = json.load(open('schema.json', encoding='utf-8'))request = {    'model': os.environ['MODEL'],    'messages': [{'role': 'user', 'content': 'Извлеки vendor и amount из: Поставщик Альфа, сумма 1250 рублей. Верни только JSON.'}],    'format': schema,    'stream': False,    'options': {'temperature': 0}}with open('request.json', 'w', encoding='utf-8') as f:    json.dump(request, f, ensure_ascii=False)PYcurl --fail-with-body -sS http://127.0.0.1:11434/api/chat \  -H 'Content-Type: application/json' -d @request.json > response.json

Поле `format` здесь — **объект схемы**, а не просто строка `"json"`; Ollama показывает оба режима отдельно. `stream: false` упрощает разбор целого ответа; полезная нагрузка лежит в `message.content` [s1](#src-s1)[s2](#src-s2). Если `curl` сообщает отказ соединения, проверьте адрес, процесс и модель, прежде чем менять схему. Если ответ HTTP успешен, но объект пустой или неправильный, сначала посмотрите ответ целиком и причины ограничения генерации.

## Чем отличаются llama.cpp и vLLM?

Для llama.cpp нужен заранее поднятый `llama-server` с совместимым GGUF и chat template; [гайд по llama.cpp](/zhurnal/llama-cpp/) описывает выбор файла и запуск. Его OpenAI-совместимый chat endpoint — `http://127.0.0.1:8080/v1/chat/completions`, а простой поддерживаемый вариант схемного ответа — `response_format: {"type":"json_object","schema": ...}` на закреплённой ревизии README [s3](#src-s3). Здесь не используется Ollama-поле `format`:

export MODEL='имя-модели-на-вашем-llama-server'python3 - <<'PY'import json, osschema = json.load(open('schema.json', encoding='utf-8'))request = {  'model': os.environ['MODEL'],  'messages': [{'role': 'user', 'content': 'Извлеки vendor и amount из: Поставщик Альфа, сумма 1250 рублей. Верни JSON с этими полями.'}],  'response_format': {'type': 'json_object', 'schema': schema},  'stream': False}with open('request.json', 'w', encoding='utf-8') as f:    json.dump(request, f, ensure_ascii=False)PYcurl --fail-with-body -sS http://127.0.0.1:8080/v1/chat/completions \  -H 'Content-Type: application/json' -d @request.json > response.json

В llama.cpp даже при одинаковом JSON Schema часть ключевых слов может не ограничить генерацию; не переносите сложные `pattern`, `$ref` или `uniqueItems` в продакшен без отдельной проверки на вашей версии. Документация перечисляет ограничения и различие с общим стандартом [s4](#src-s4). Сверьте возвращённое поле `choices[0].message.content` и результат собственной валидации, а не только статус HTTP [s3](#src-s3).

Для vLLM сначала запустите совместимую модель по [гайду vLLM](/zhurnal/vllm/) и проверьте доступность сервера: его документация показывает `vllm serve` и OpenAI-совместимый сервис; необходимое железо определяется конкретной моделью, версией vLLM и настройками, здесь минимальная VRAM не заявлена [s6](#src-s6). Для схемного ответа документация приводит `response_format` с `type: json_schema` и объектом `json_schema` с именем и схемой [s5](#src-s5):

export MODEL='id-модели-которую-обслуживает-vllm'python3 - <<'PY'import json, osschema = json.load(open('schema.json', encoding='utf-8'))request = {  'model': os.environ['MODEL'],  'messages': [{'role': 'user', 'content': 'Извлеки vendor и amount из: Поставщик Альфа, сумма 1250 рублей. Верни JSON.'}],  'response_format': {'type': 'json_schema', 'json_schema': {'name': 'invoice', 'schema': schema}},  'stream': False}with open('request.json', 'w', encoding='utf-8') as f:    json.dump(request, f, ensure_ascii=False)PYcurl --fail-with-body -sS http://127.0.0.1:8000/v1/chat/completions \  -H 'Content-Type: application/json' -d @request.json > response.json

Каждый блок перезаписывает `request.json` и `response.json`: выполняйте **один** runtime за раз или сохраняйте результаты под разными именами. Адрес и ID модели проверяйте в собственном стенде. Не заключайте, что формат делает три разных модели сопоставимыми по качеству.

## Как проверить ответ, а не только увидеть фигурные скобки?

Проверка `python3 -m json.tool response.json` относится к **обёртке API** и не проверяет `message.content`. Для строгой проверки объекта установите валидатор в отдельное виртуальное окружение; затем разберите вложенную строку и сопоставьте факты вручную. JSON Schema валидирует типы и обязательные поля, но не умеет узнать исходную сумму из документа [s7](#src-s7).

python3 -m venv .venv.venv/bin/python -m pip install jsonschemaexport ENGINE=ollama   # или: llama, vllm.venv/bin/python - <<'PY'import json, osfrom jsonschema import Draft202012Validatorschema = json.load(open('schema.json', encoding='utf-8'))response = json.load(open('response.json', encoding='utf-8'))engine = os.environ['ENGINE']content = response['message']['content'] if engine == 'ollama' else response['choices'][0]['message']['content']obj = json.loads(content)Draft202012Validator.check_schema(schema)Draft202012Validator(schema).validate(obj)print(json.dumps(obj, ensure_ascii=False))assert obj['vendor'] == 'Альфа' and obj['amount'] == 1250, 'значение не совпало с исходной строкой'print('Форма и значения тестовой строки совпали; проверьте свои документы отдельно.')PY

Если нарушены типы или отсутствует поле, оставьте результат на ручную проверку, не исправляйте ответ постфактум скрытой подменой. Если схема проходит, но сумма отличается от исходника, это **ошибка извлечения**, не успех; на документах добавьте сверку с оригиналом, тесты на пустые поля и экспертный разбор спорных случаев. Особенно осторожно относитесь к юридическим и бухгалтерским данным: структурирование не создаёт достоверность.

**Следующий шаг:** выполните один из трёх путей и проверьте объект по тестовой строке, после чего перейдите к [сценарию извлечения документов для 1С](/zadachi/raspoznavanie-dokumentov-1s/) с ручной сверкой реквизитов. Это проверяемый маршрут «задача → выбранная модель → железо → runtime → команда → валидный и фактически сверенный результат», а не обещание, что модель любого размера правильно поймёт любой счёт.

## Источники

1. [Ollama — Structured Outputs](https://docs.ollama.com/capabilities/structured-outputs) — получено 2026-09-25
2. [Ollama — Chat API](https://docs.ollama.com/api/chat) — получено 2026-09-25
3. [llama.cpp — llama-server README](https://raw.githubusercontent.com/ggml-org/llama.cpp/84e76d8a23162eca70490da131945ebec1f09bf4/tools/server/README.md) — получено 2026-09-25
4. [llama.cpp — GBNF and JSON Schema limitations](https://raw.githubusercontent.com/ggml-org/llama.cpp/84e76d8a23162eca70490da131945ebec1f09bf4/grammars/README.md) — получено 2026-09-25
5. [vLLM — Structured Outputs](https://docs.vllm.ai/en/stable/features/structured_outputs/) — получено 2026-09-25
6. [vLLM — Online Serving](https://docs.vllm.ai/en/stable/serving/online_serving/) — получено 2026-09-25
7. [JSON Schema — Objects](https://json-schema.org/understanding-json-schema/reference/object) — получено 2026-09-25
