Практика эксплуатации
Скрытая цена ремонта структурированных ответов LLM
Автоматический ремонт превращает сломанный JSON в разбираемый объект, но не доказывает, что объект сохранил исходный смысл. Разберём, как сделать эту скрытую операцию наблюдаемой и измерить три показателя: частоту ремонта, его денежную стоимость и долю семантически неверных исправлений.
Что именно скрывает «успешный» ремонт
Структурированный ответ — это результат модели, который должен соответствовать заранее заданному формату: например, JSON-объекту и его схеме. На практике конвейер часто выглядит так:
- модель возвращает текст;
- обычный JSON-парсер завершается ошибкой;
- локальная библиотека или дополнительный запрос к модели исправляет текст;
- приложение получает объект и считает вызов успешным.
Последний шаг стирает важную информацию. В метриках остаётся успешный запрос, хотя система уже понесла один или несколько видов ущерба:
- добавилась задержка;
- появились дополнительные токены или запросы;
- исходная ошибка модели перестала быть видна;
- ремонт мог удалить, придумать или переосмыслить значение;
- повторный запрос мог создать объект, не эквивалентный исходному ответу.
Минимальная модель измерений
Для каждого исходного вызова сохраняйте не только финальный объект, но и маршрут его получения. Достаточно следующих полей:
{
"request_id": "локальный-идентификатор",
"model": "фактически-использованная-модель",
"raw_parse_ok": false,
"raw_schema_ok": false,
"repair_attempted": true,
"repair_method": "local",
"repair_attempts": 1,
"repair_parse_ok": true,
"repair_schema_ok": true,
"semantic_verdict": "unknown",
"input_tokens": null,
"output_tokens": null,
"repair_input_tokens": 0,
"repair_output_tokens": 0,
"latency_ms": 840,
"repair_latency_ms": 4
}
semantic_verdict принимает значения correct, incorrect или unknown. Не подменяйте отсутствие проверки значением correct. Поля с токенами заполняйте из фактических данных вашего провайдера; если таких данных нет, оставляйте null, а не оценку, замаскированную под измерение.
Исходный ответ может содержать персональные данные или секреты. Для рабочих журналов безопаснее сохранять хеш, размер, код ошибки и обезличенный фрагмент. Полный текст допустим только в контролируемом хранилище с ограниченным сроком жизни и доступом.
Шаг 1. Разделите исходную валидацию и ремонт
Не вызывайте ремонт внутри общего обработчика исключений без отдельной телеметрии. Ниже — воспроизводимый пример на Python без внешних зависимостей. Он намеренно выполняет только безопасный локальный ремонт одного известного дефекта: запятой перед закрывающей скобкой.
import json
import re
import time
TRAILING_COMMA = re.compile(r",(\s*[}\]])")
def parse_without_repair(raw):
started = time.perf_counter()
try:
value = json.loads(raw)
return value, {
"raw_parse_ok": True,
"raw_error": None,
"raw_parse_ms": (time.perf_counter() - started) * 1000,
}
except json.JSONDecodeError as error:
return None, {
"raw_parse_ok": False,
"raw_error": error.msg,
"raw_parse_ms": (time.perf_counter() - started) * 1000,
}
def repair_known_syntax(raw):
started = time.perf_counter()
repaired = TRAILING_COMMA.sub(r"\1", raw)
changed = repaired != raw
try:
value = json.loads(repaired)
parse_ok = True
error = None
except json.JSONDecodeError as exc:
value = None
parse_ok = False
error = exc.msg
return value, {
"repair_attempted": True,
"repair_method": "remove_trailing_comma",
"repair_changed_text": changed,
"repair_parse_ok": parse_ok,
"repair_error": error,
"repair_latency_ms": (time.perf_counter() - started) * 1000,
}
raw = '{"status":"ok","items":[1,2,],}'
value, event = parse_without_repair(raw)
if not event["raw_parse_ok"]:
value, repair_event = repair_known_syntax(raw)
event.update(repair_event)
print(json.dumps(event, ensure_ascii=False))
print(json.dumps(value, ensure_ascii=False))
Сохраните пример как measure_repair.py и выполните:
python3 measure_repair.py
Команда работает локально, не обращается к сети и не меняет файлы. Ожидаемое свойство результата: raw_parse_ok равно false, а repair_parse_ok — true. Конкретное время выполнения зависит от среды и не должно сравниваться с чужими числами.
Шаг 2. Проверяйте схему отдельно
Разбираемый JSON всё ещё может иметь неверные поля и типы. Валидация должна выполняться дважды: над исходным объектом, если он разобран, и над отремонтированным объектом. Не используйте схему как механизм молчаливого заполнения данных.
Пример минимальной прикладной проверки:
def validate_result(value):
errors = []
if not isinstance(value, dict):
return ["root must be an object"]
if value.get("status") not in {"ok", "error"}:
errors.append("status must be ok or error")
if not isinstance(value.get("items"), list):
errors.append("items must be an array")
return errors
schema_errors = validate_result(value)
event["repair_schema_ok"] = not schema_errors
event["schema_errors"] = schema_errors
В рабочем проекте используйте уже принятую библиотеку проверки схем и зафиксированную версию схемы в событии. Подробнее о построении подобных контуров см. в разделе практических руководств.
Шаг 3. Посчитайте частоту и стоимость ремонта
На выбранном окне наблюдения вычислите:
repair_rate =
requests_with_repair / all_structured_output_requests
repair_success_rate =
schema_valid_repaired_results / requests_with_repair
extra_request_rate =
additional_model_requests_for_repair / all_structured_output_requests
Денежную стоимость считайте по фактическим тарифам, действовавшим для конкретного вызова. Формула для ремонта отдельным запросом:
repair_cost =
repair_input_tokens × input_price_per_token
+ repair_output_tokens × output_price_per_token
total_repair_cost =
sum(repair_cost) + other_metered_repair_charges
Не зашивайте тариф в код навсегда: цены и единицы тарификации меняются. Сохраняйте идентификатор модели, токены, валюту, источник тарифа и дату его действия. Если ремонт локальный, его API-стоимость может быть нулевой, но задержка и вычислительные ресурсы остаются.
Отдельно измерьте влияние на время ответа:
repair_latency_share =
sum(repair_latency_ms) / sum(end_to_end_latency_ms)
Среднее значение скрывает редкие дорогие случаи. Для задержки ремонта показывайте как минимум медиану и 95-й перцентиль, а для числа попыток — распределение по значениям 0, 1, 2 и более.
Шаг 4. Измерьте семантически неверные исправления
Эту долю нельзя надёжно получить ещё одним парсером. Нужен независимый эталон: заранее размеченный набор, детерминированная проверка бизнес-правил или ручная оценка. Если эталона нет, показатель остаётся неизвестным.
Сформируйте выборку только из отремонтированных результатов. Проверяющий должен видеть исходную задачу, исходный ответ модели и отремонтированный объект, но не должен считать синтаксическую валидность доказательством правильности.
Размечайте как incorrect, если ремонт:
- изменил значение поля, а не только представление;
- выбрал один вариант из неоднозначного исходного текста;
- добавил отсутствующее значение без подтверждения;
- удалил значимый элемент или превратил его в
null; - нарушил условие задачи, которое не выражено JSON-схемой.
Основная формула:
semantic_error_rate =
semantically_incorrect_repairs / reviewed_repairs
Всегда публикуйте знаменатель. Формулировка «ошибок 5%» почти бесполезна без числа проверенных ремонтов, способа отбора и правил разметки. Для неоднозначных случаев добавьте категорию uncertain и не смешивайте её с корректными результатами.
Шаг 5. Сведите показатели в один отчёт
Минимальная таблица для каждой версии модели, промпта и схемы:
| Показатель | Числитель | Знаменатель |
|---|---|---|
| Частота ремонта | Запросы с ремонтом | Все структурированные ответы |
| Успех ремонта по схеме | Исправления, прошедшие схему | Все попытки ремонта |
| Семантическая ошибка | Неверные по смыслу исправления | Проверенные исправления |
| Дополнительная стоимость | Фактическая стоимость ремонта | Все структурированные ответы или период |
| Дополнительная задержка | Время этапов ремонта | Число ремонтов и общая задержка |
Не объединяйте разные версии промпта, схемы и модели в одну строку. Иначе улучшение одной группы может скрыть ухудшение другой. После каждого изменения сравнивайте как минимум частоту исходных ошибок, частоту ремонта и семантическую ошибку ремонта.
Проверка результата
Аудит можно считать технически готовым, если выполняются все условия:
- невалидный исходный ответ отражается как ошибка, даже когда ремонт успешен;
- видно, был ли ремонт локальным или потребовал нового вызова модели;
- известны число попыток, задержка и фактические токены каждого дополнительного вызова;
- исходный и исправленный объекты проверяются одной версией схемы;
- непроверенная семантика обозначается как
unknown; - в отчёте указаны абсолютные количества рядом с процентами;
- метрики можно разложить по модели, промпту, схеме и методу ремонта.
Для контрольной проверки подайте три локальных примера: корректный JSON, JSON с однозначной синтаксической ошибкой и неоднозначный ответ. Первый не должен попадать в ремонт; второй должен сохранить факт исходной ошибки; третий не должен автоматически получать вердикт correct.
Типовые ошибки
Считать финальный JSON единственным результатом
Так теряется исходная частота отказов модели. Храните отдельные статусы до и после ремонта.
Повторять запрос до успеха без лимита
Задайте максимальное число попыток и общий бюджет времени. После исчерпания лимита возвращайте контролируемую ошибку, а не бесконечный цикл.
Просить модель «просто исправить JSON»
Без запрета на изменение значений модель может заново решить задачу. Передавайте схему, требуйте сохранять значения и всё равно проводите независимую проверку смысла.
Считать прохождение схемы семантическим успехом
Схема проверяет форму и часть ограничений. Она не знает, соответствует ли значение исходному документу, расчёту или намерению пользователя.
Логировать чувствительные данные целиком
Для диагностики часто достаточно кода ошибки, хеша, длины и безопасного фрагмента. Полные ответы требуют отдельной политики доступа и удаления.
Оценивать ремонт только на удобных случаях
Случайная или стратифицированная выборка должна включать разные типы ошибок, модели и версии схем. Иначе доля семантических ошибок будет смещена.
Ограничения метода
Ручная разметка сама может быть неоднозначной. Для важных сценариев полезны письменные критерии и повторная оценка спорных случаев. Небольшая выборка показывает направление, но не гарантирует стабильность редких ошибок.
Хеширование исходного ответа помогает обнаружить повторы, но не позволяет восстановить контекст ошибки. Полное хранение улучшает диагностику, одновременно повышая требования к безопасности.
Локальный детерминированный ремонт обычно проще аудировать, чем повторный вызов модели, однако он покрывает только известные классы дефектов. Чем шире эвристика, тем выше риск незаметного изменения смысла.
Наконец, показатели зависят от распределения реальных задач. Результаты контрольного набора нельзя автоматически переносить на рабочий трафик; оба контура следует измерять отдельно.
Практическое правило эксплуатации
Ремонт допустим как явно наблюдаемый аварийный слой, но не как способ превратить ошибку модели в «успех». Сначала измеряйте исходный отказ, затем стоимость восстановления и только после этого — пригодность результата по смыслу.
Если доля ремонта растёт, исправляйте первопричину: контракт ответа, схему, промпт, выбор модели или обработку контекста. Если семантические ошибки нельзя надёжно проверить, безопаснее вернуть контролируемый отказ или отправить результат на проверку, чем молча передать исправленный объект дальше.