Вы когда-нибудь смотрели на ответ модели и думали: «Это JSON? Это какая-то пародия на JSON». Лишние запятые, пропущенные кавычки, вложенность, которая рассыпается при первой попытке распарсить. Знакомо.
Особенно больно, когда работаешь с маленькими локальными моделями — они экономят токены, но ценой структуры. Каталог ошибок и библиотека для восстановления — это паллиатив. Ампутация вместо лечения.
Хотите, чтобы модель сама, без костылей, выдавала идеальный JSON? Тогда добро пожаловать в мир дообучения. Рассказываю, как приручить зверя.
Почему LLM ломают JSON?
Дело не в злом умысле. Большинство open-source моделей натренированы на естественном языке, где правила пунктуации — это рекомендации. JSON же требует педантичной точности: каждая скобка на своём месте, каждая кавычка удвоена. Модель пытается угадать, а не вычислить.
Более того, даже если вы используете structured outputs (промпты вроде «Ответь строго в JSON»), модель может «забыть» закрыть скобку, особенно при длинных ответах. Когда JSON — это не опция, а требование, приходится идти на крайние меры.
Облачные провайдеры, вроде Amazon Bedrock со своим Structured Outputs, решают проблему на своей стороне, но вы привязаны к платформе и платите за каждый запрос. Локальное дообучение даёт свободу и контроль.
Важно: Дообучение — не серебряная пуля. Если вам нужно извлекать JSON из 150-страничного PDF, сначала сравните подходы. А в некоторых случаях дешевле и быстрее вообще не использовать LLM.
Решение: дообучение + грамматика
Комбинация двух техник даёт 99.9% валидного JSON:
- Дообучение (fine-tuning) — адаптируем модель под конкретный формат вывода. Она «запоминает», что после извлечения имени пациента нужно закрыть кавычку и поставить запятую.
- Grammar sampling — на этапе инференса принудительно ограничиваем генерацию правилами JSON. Модель может выбрать только те токены, которые соответствуют синтаксису.
Вместе они работают как страховка: дообучение ускоряет сходимость и улучшает качество, а грамматика гарантирует валидность. ISON против JSON — это уже другая история про оптимизацию, а мы про структуру.
Пошаговый план (будет больно, но эффективно)
1 Выбор модели и окружения
Берём самую современную open-source модель на июнь 2026 — Qwen 3.5 8B или Llama 4 7B. Они показывают отличное понимание структуры даже в базовой версии. Для дообучения используем Unsloth (ускоряет LoRA в 2 раза) и PyTorch 3.0 с CUDA 13.0.
# Установка
pip install unsloth transformers torch xformers
pip install git+https://github.com/outlines-dev/outlines.git # для грамматик
2 Создание датасета — ключевой момент
Формат — чат-темплейт (применяется к chat-template токенизатора). Каждый пример: пользовательский запрос (текст для извлечения) + ассистент (правильный JSON). Минимум 200 примеров, лучше 500-1000.
import json
from datasets import Dataset
# Примеры для извлечения данных из медицинских карт
# Вдохновлено: /article/meditsinskie-zapisi-v-json-za-15-minut-kak-zastavit-lokalnyie-llm-chitat-pocherk-vrachej/
samples = [
{
"instruction": "Извлеки информацию о пациенте из текста:",
"input": "Пациент: Иванов Иван, 45 лет. Диагноз: стенокардия. Дата: 2025-12-01",
"output": '{"name": "Иванов Иван", "age": 45, "diagnosis": "стенокардия", "date": "2025-12-01"}'
},
# ... ещё 199+ примеров
]
def format_example(ex):
return {
"text": tokenizer.apply_chat_template([
{"role": "user", "content": ex["instruction"] + "\n" + ex["input"]},
{"role": "assistant", "content": ex["output"]}
], tokenize=False)
}
dataset = Dataset.from_list([format_example(s) for s in samples])
dataset.save_to_disk("medical_json_dataset")
3 Конфигурация LoRA
LoRA (Low-Rank Adaptation) — дообучаем только небольшие веса, экономя память. Для генерации JSON ключевые модули — q_proj, v_proj, k_proj, o_proj.
from unsloth import FastLanguageModel
model, tokenizer = FastLanguageModel.from_pretrained(
"Qwen/Qwen3.5-8B-Instruct",
max_seq_length=2048,
load_in_4bit=True, # QLoRA
)
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_alpha=32,
lora_dropout=0.05,
bias="none",
use_gradient_checkpointing=True,
random_state=42,
)
4 Запуск обучения
Используем SFTTrainer из trl. Гиперпараметры проверены на практике.
from trl import SFTTrainer
from transformers import TrainingArguments
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
args=TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
num_train_epochs=3,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_strategy="epoch",
output_dir="outputs-json",
),
)
trainer.train()
model.save_pretrained("medical-json-lora")
5 Инференс с гарантией JSON (grammar)
Без грамматики даже дообученная модель иногда ошибается. Подключаем llama.cpp grammar (outlines умеет конвертировать в GBNF).
from outlines import generate, models
# Загружаем дообученную модель через llama.cpp
model = models.llamacpp("path/to/gguf", grammar="json")
generator = generate.json(model,
# Определяем схему вывода
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"diagnosis": {"type": "string"}
},
"required": ["name", "diagnosis"]
}
)
result = generator("Пациент: Петров Петр, 60 лет, диагноз: гипертония")
print(result)
# {'name': 'Петров Петр', 'age': 60, 'diagnosis': 'гипертония'}
Нюансы, которые вас закопают
- Только обучение ≠ гарантия. Без грамматики на сложных текстах (PDF с таблицами, рукописный ввод) модель может сгенерировать невалидный JSON. Loot-JSON — хорошая аварийная библиотека, но лучше не доводить.
- Размер датасета. 50 примеров — переобучение. 5000 — почти всегда избыточно. Золотая середина — 200-1000.
- Валидация на тесте. Считайте метрику: доля валидных JSON. Если < 95%, увеличивайте датасет или меняйте модель.
- Не смешивайте типы JSON. Если вам нужно извлекать и медицину, и юридические документы — делайте отдельные LoRA-адаптеры или объединяйте датасеты, но с явным промптом.
- Контекст 4K+. Большие запросы требуют больше памяти и больше примеров с длинными текстами. Используйте трюки с системным промптом, чтобы уложиться в лимит.
Практический совет: Перед тем, как дообучать, попробуйте прогнать вашу задачу через простой промпт + грамматику. Если accuracy > 80% — дообучение может быть избыточным. Экономия времени и денег. Проверено на реальных ETL-пайплайнах.
Личный опыт: мои грабли
Первый раз я дообучал Llama 3.2 на 100 примерах извлечения email-ов из писем. Результат: модель идеально извлекала email, но если в тексте его не было — она выдумывала что-то похожее (например, "user@example.com"). Причина: в датасете не было негативных примеров. Добавил 30 примеров с пустым полем "email": null — проблема ушла.
Второй раз я забыл про грамматику и на проде получил 15% невалидных JSON. Пользователи сходили с ума. Теперь даже с дообучением я всегда ставлю grammar sampling.
И последнее: ASON (асинхронный JSON) — тема, которая набирает обороты в 2026. Если ваша модель тормозит на генерации длинных JSON — присмотритесь к этому формату. Он экономит до 70% токенов без потери информации.
Итог: дообучение + грамматика = 99.9% надёжности. Не экономьте на датасете, не забывайте про валидацию, и ваш прод не упадёт из-за лишней скобки. Hugging Face Hub — лучшая платформа для хранения адаптеров. Делитесь своими LoRA, комьюнити скажет спасибо.