- PVSM.RU - https://www.pvsm.ru -
Работаю в компании «Сибинтек-Софт», занимаюсь поддержкой корпоративных информационных систем. Имею сертификат DevOps-инженера, в рамках задач отдела внедряю ИИ в рабочие процессы.
Как-то мой валидатор забраковал ответ, сославшись на статью, которой не существует. Причём отличился именно валидатор — тот компонент, который поставлен ловить галлюцинации генератора. LLM-судья — судья на большой языковой модели (large language model) — вынес вердикт «отклонить» и в поле обоснования (reason) подкрепил его несуществующей «теорией» с поддельным arXiv-идентификатором. Выглядело убедительно, и вручную я бы это, скорее всего, пропустил. Выручила проверка, добавленная когда-то на всякий случай: идентификатора нет в белом списке, а это флаг.
После этой истории я окончательно перестал верить в два популярных рецепта — «напишите промпт, чтобы модель не галлюцинировала» или «пусть вторая такая же модель перепроверит». У меня в итоге работает другое: конвейер из открытых пакетов, работающих последовательно в определенном порядке. Он неудобный, местами дорогой и может отклонить правильный код. Я считаю это нормальной ценой конструкции — и ниже покажу почему.
Примеры почти все будут про генерацию кода: там наглядней увидеть разницу между «выглядит правильно» и «работает» — код можно просто запустить. Для текстовых отчётов, ревью и других документов логика та же, меняются только конкретные судьи на шаге 4.
Весь конвейер — это пять шагов на восьми публичных пакетах, без единого своего фреймворка: сначала фиксируем формат, потом дешёвый фильтр самопроверки, затем сверка с базой первичных документов, далее жёсткие правила, и только в самом конце — калиброванный остаток, который либо уходит в прод, либо передается на специалиста. Единый принцип один, и он важнее любого конкретного пакета: дешёвое раньше дорогого и детерминированное раньше вероятностного.

Рисунок 1. Конвейер целиком.
Все проверки, которые наиболее часто используются, делятся на два типа.
Детектив выясняет, правда ли это: сверяет показания, ищет нестыковки, оценивает вероятности. Повторные запросы моделей, LLM-судья, сверка фактов по источникам — всё это детективы. Расхождение у детектива подсвечивает проблему, а вот совпадение не доказывает ничего: свидетели могут дружно врать.
Судья решает другой вопрос — допустимо ли это. Есть записанное правило, значит есть однозначное «да/нет». Судья не оценивает, он проверяет. И молчит там, где правило не записано, — это его главная слабость, к ней ещё вернёмся.
Тут легко запутаться, и путаются постоянно: слово «судья» в индустрии носят три разных “зверя”. Классический судья — детерминированный: правило записано, вердикт воспроизводим — линтер, тест, SELECT, схема. Далее именно этот тип судьи и будет описываться и включаться в классификацию в паре с детективами. Другой вид судьи, судья-статистик — вероятностная модель, обёрнутая в детерминированное решающее правило с калиброванной гарантией; в этом конвейере такой один, MAPIE на шаге 5, и границы его гарантии оговорены там же. А LLM-судья, несмотря на имя, — вовсе не судья, а детектив: он берётся отвечать на судейский вопрос «допустимо ли это», но вердикт у него вероятностный, правило нигде не записано, и перезапуск может дать другой ответ. Типов проверок по-прежнему два — просто судейскую мантию носят три подхода.
Детективы дешёвые и покрывают широко. Детерминированные судьи узкие, зато их вердикту можно верить. Весь конвейер, по сути, про одно: расставить их так, чтобы не платить дорогим за то, что ловится дешёвым.

Рисунок 2. Два вопроса к любому ответу.
Первая причина смерти прототипов — вовсе не галлюцинации, а фраза «Вот ваш результат:», добавленная перед валидным JSON. Парсер на ней падает, и весь конвейер встаёт из-за “левого” текста. Поэтому формат фиксируется схемой на стороне кода, и эта схема — уже первый судья конвейера.
from typing import Literal
from pydantic import BaseModel, Field
import instructor
class CveScanCommand(BaseModel):
tool: Literal["cve_scan"]
image: str = Field(
pattern=r"^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)*"
r":[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$"
) # registry/name:tag; сегменты не с точки или дефиса, пути ../ не проходят
severity_min: Literal["low", "medium", "high", "critical"]
client = instructor.from_provider("openai/gpt-4o-mini")
cmd = client.chat.completions.create(
response_model=CveScanCommand, # невалидный ответ -> авторетрай -> исключение
messages=[{"role": "user", "content": "Проверь registry/app:1.4.2 на CVE"}],
)
Мусор в формате до сканера теперь не доезжает. Правда, валидный ещё не значит истинный: корректно оформленный тег несуществующего образа схема пропустит с чистой совестью. Разбираться с такими случаями будут шаги 3 и 4.
Если гарантия синтаксиса нужна уже на уровне генерации, рядом стоит Outlines [2] — он физически не даст модели выдать невалидный JSON. Подвох в другом: Outlines с тем же успехом выдаст безупречный JSON с CVE-2024-99999 внутри. Форму он гарантирует, а смысл остаётся на совести модели.
Самый дешёвый детектив из всех, что у меня стоят [3]. Ядро — четыре строки без единой зависимости:
from collections import Counter
def self_consistency(ask_llm, question: str, n: int = 3) -> tuple[str, float]:
answers = [normalize(ask_llm(question)) for _ in range(n)]
best, votes = Counter(answers).most_common(1)[0]
return best, votes / n
«Дешёвый» здесь — относительно цены ошибки; в абсолюте при n=5 вы платите пятикратными токенами и пятикратной задержкой. Я начинаю с трёх прогонов, гоняю их параллельно и беру для фильтра модель дешевле генератора — этого почти всегда хватает.
На свободном тексте приём в лоб не работает: ответы надо группировать по смыслу, и проще взять готовый SelfCheckGPT, чем изобретать свою кластеризацию. На творческих задачах не работает вовсе — там верных ответов много по определению. И держите в голове, чем этот фильтр является на самом деле: он меряет согласованность, а не истину. Стабильная неправота проходит его на ура.
Порядок внутри этого шага экономит деньги и добавляет гарантий.
Точный факт — номер уязвимости CVE, версия образа, номер договора, дата приказа — вообще не должен доезжать до LLM-судьи. Для него есть SELECT, точное совпадение (exact match) по справочнику, запрос к программному интерфейсу (API) базы NVD: по сравнению с вызовом модели это бесплатно и даёт однозначное «да/нет».
Судье остаётся то, где точное совпадение физически невозможно, — выводы и вольные пересказы источников.
def verify_atomic_fact(claim: Claim) -> Verdict:
if claim.kind == "cve_id": # точный факт -> детерминированная сверка
return Verdict(exists=nvd_lookup(claim.value)) # SELECT, не мнение
if claim.kind == "image_tag":
return Verdict(exists=registry_head(claim.value))
return llm_judge_faithfulness(claim) # свободная формулировка -> Ragas
Для свободных формулировок беру Ragas (метрики достоверности faithfulness и релевантности ответа answer_relevancy) или DeepEval поверх pytest: сборка падает при метрике ниже порога, ровно как при падении покрытия (coverage). Есть подвох: внутри метрики достоверности сидит тот же LLM-судья, так что 0,95 — вероятностный сигнал, не гарантия.
Модели с пересекающимися обучающими данными ошибаются коррелированно: в большом замере совместные ошибки в 60% случаев сходились в одном и том же неверном ответе [4]. Два ассистента, обученные на одном корпусе, — это один эксперт с эхом на совместные ошибки. Справедливости ради, на панелях судей картина не лучше: девять судей дают примерно два независимых голоса [5].

Рисунок 3. Почему согласие двух родственных судей ничего не доказывает.
В нашем контрольном замере два детерминированных клона одной модели дали φ = +1,0 на классе wrong_operator («неверный оператор») — совпали во всех ошибках без исключения (https://github.com/SergeiUstiugov/judge-blindspot [1]).
Лечится это одной строкой конфигурации: judge_llm берётся из другого семейства. Под «другим» я имею в виду другую архитектуру и другой обучающий корпус, а не другой логотип на счёте за доступ к модели. Старшая и младшая модель одного вендора для этой цели неразличимы.
Как это правило нарушают прямо сейчас. В опубликованном промышленном шлюзе непрерывной интеграции (CI-гейте) для приложений на языковых моделях [6] «независимым» судьёй в калибровочном исследовании была модель того же семейства, что и оцениваемая система: Gemini 2.5 Pro против Gemini 2.5 Flash. Автор сам оговаривает, что «смещение общего семейства исключить нельзя», — разные уровни мощности внутри семейства остаются двумя срезами одного обучающего корпуса.
Мы только что научились сверять факты с источниками. Так вот: врать умеет и сам слой, который эти источники достаёт, — и этот класс галлюцинаций почти никто не проверяет, потому что модель тут вообще ни при чём.
— Поисковая прослойка выдала нам «цитату» из статьи. Цитата оказалась синтез-прозой самого движка. Один arXiv-идентификатор сменил три разных «заголовка», пока скачанный PDF не показал настоящий.
— HTML-версия страницы arXiv галлюцинировала номера теорем, которых в PDF нет.
— В нашей собственной библиографии сверка поймала классику: авторы верны, arXiv-id верен, а место публикации (venue) — от другой статьи тех же авторов.
Поэтому единственный источник правды — скачанный PDF. Утверждение об источнике сверяется с его первой страницей; версия на сайте, выдача поисковика и чья-либо память — модельная в том числе — источниками не считаются.
Валидатор из начала статьи — ровно этот же случай: поле обоснования от судьи — такой же выход модели, как ответ генератора, и требует такой же проверки. Мы почему-то по умолчанию верим вердиктам собственных валидаторов.
Всё, что формализуется правилом, проверяется правилом. Для кода — классика, работающая ровно так же, как в обычном конвейере непрерывной интеграции (CI):
import logging, subprocess
def hard_gate(path: str) -> bool:
checks = [["ruff", "check", path], # ruff первым: падает быстрее всех
["mypy", "--strict", path],
["pytest", "-q", "tests/"]]
ok = True
for cmd in checks:
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
logging.error("hard_gate: %s failedn%s",
cmd[0], (r.stderr or r.stdout).strip())
ok = False
return ok
Одно требование безопасности, без которого этот фрагмент кода вреден: pytest импортирует и исполняет сгенерированный моделью код. Гоняйте его в одноразовом контейнере без сети и лишних прав — запуск в рабочем окружении означает, что вы своими руками собрали исполнитель произвольного кода из интернета и назвали его гейтом.
Guardrails встаёт сюда же как оркестратор проверок, но следите за составом цепочки: regex-валидатор внутри него — судья, а LLM-валидатор — снова детектив, со всеми оговорками шага 2.
После четырёх шагов остаётся серая зона: формально всё прошло, но уверенности нет.
MAPIE без математики выглядит так: у каждого ответа уже есть числовые следы предыдущих шагов — доля согласия из шага 2, достоверность из шага 3, оценка похожести из поиска (retrieval), длина ответа, число утверждений. Это не абстрактный данные, это ваши собственные логи. По ним обучается классификатор «ответ ошибочен / нет», а конформный слой [7, 8] превращает его предсказание в обещание покрытия.
Ключевое отличие от обычного классификатора умещается в одну строку: MAPIE возвращает не «да/нет», а множество вариантов. Если в множестве больше одного класса, система признаёт «не знаю» и передает запрос специалисту.
from mapie.classification import SplitConformalClassifier
clf = RandomForestClassifier().fit(X_train, y_train)
mapie_clf = SplitConformalClassifier(clf, confidence_level=0.9, prefit=True)
mapie_clf.conformalize(X_calib, y_calib) # отложенная выборка!
y_pred, y_sets = mapie_clf.predict_set(X_new)
ambiguous = y_sets.sum(axis=1).squeeze() != 1 # >1 класса = «не знаю»
escalate_to_human(X_new[ambiguous])
Эскалацию здесь часто путают с браковкой, а это разные выходы. Однозначное «ошибочен» — автоматический отказ или регенерация, человек его не видит. Специалисту уходит только то, про что классификатор сказал «не знаю», — и уходит к профильному спецу: дежурный оператор с «не знаю» классификатора ничего сделать не сможет.
На наших синтетических данных при гарантии 90% к специалисту ушло 30% потока. Треть, да. Дёшево эта гарантия не даётся: хотите меньше эскалаций — платите либо уровнем гарантии, либо качеством скоринга.
Прежде чем рекомендовать конвейер, я прогнал его на реальных задачах. Заодно проверил популярный миф в индустрии «модель помощнее = меньше проблем».
17 строго-исполнимых задач генерации кода — от алгоритмов и структур данных до вызовов API, — три источника. Оракулом было исполнение: код либо проходит заранее написанные тесты с известным ответом, либо нет — мнения второй модели в этом замере не спрашивали.

Рисунок 4. Исполнимость кода из трёх источников на одном оракуле-по-исполнению.
Из этого замера я отметил три вещи.
Сильнейшая из проверенных моделей провалила пять задач из семнадцати — пять решений, которые выглядят безупречно и не работают. Расшифрую пятёрку: настоящие ошибки исполнения — две из пяти, ещё три — расхождение кода и теста в недостаточно специфицированном контракте, где обе стороны по-своему правы.
Пост-обработка меня, честно говоря, ошарашила. Кэширование результатов и шаблонизация под переиспользование, задуманные как удобство, уронили рабочий код с 12 из 17 до 1 из 17: шаблонизатор разрывал контекст, на который опирался сгенерированный код.
А ради третьего наблюдения всё и писалось. Переход на самую новую и тяжёлую модель не отменяет ни одного шага проверки: мощность двигает долю рабочего кода вверх, но не к единице. «Безупречно выглядит» и «работает» — разные свойства, и различить их умеет только исполнение.
Детерминированный валидатор в этом прогоне показал профиль, который сначала выглядит как провал:
Он ни разу не пропустил битый код. И забраковал треть рабочего.
В обычной разработке за такое настройщика выгонят. В промышленном контуре это единственно верная настройка, потому что цена двух ошибок несопоставима: отбракованный годный код — потеря времени, его перепишут; битый код, пропущенный в производственный контур, — авария, остановка линии, в пределе — угроза жизни.
Поэтому валидатор обязан гнать точность к единице ценой охвата. Лучше сто раз перепроверить рабочее, чем один раз пропустить дефект туда, где он дорого стоит. Валидатор с неполным охватом — не слабый, а правильно настроенный под цену ошибки.
Тот же профиль независимо зафиксирован снаружи. В промышленном гейте [6] провели слепую калибровку: из 20 ответов, отклонённых гейтом, человек и LLM-судья сочли содержательно приемлемыми 13 — гейт зарубил их по структурным признакам, невидимым в тексте. И зеркально: из 40 пропущенных гейтом ответов судья отклонил 9 за выдуманные названия функций и уход в шаблонные общие ответы (FAQ). То есть разные типы проверок ловят разные классы отказов, и ни одна не подменяет другую — комбинация обязательна.
— Судья того же семейства. «Второе мнение» оказывается эхом первого — про это был целый раздел выше. Меняйте архитектуру и обучающие данные — смена вендора сама по себе ничего не гарантирует.
— Вера полю обоснования (reason). Судья галлюцинирует в обосновании собственного вердикта — с этого кейса началась статья, и после него вывод судьи у меня проходит те же проверки, что и вывод генератора.
— LLM-судья на точных фактах. Платить за мнение там, где есть SELECT, — просто выброшенные деньги. Точное значение сверяется с базой, судья остаётся на свободных формулировках.
— pytest в рабочем окружении. См. врезку в шаге 4: без одноразового контейнера вы исполняете произвольный код от модели у себя.
— Пороги из туториала. Чужой порог на вашем потоке либо не ловит ничего, либо блокирует всё. Сначала померьте свою базовую линию, потом ставьте порог под неё — скучно, но другого способа нет.
— Конвейер без наблюдаемости. Дрейф замечают пользователи, а не вы. Минимум — панель мониторинга (dashboard) по долям эскалаций и отказов и перекалибровка по событию.
Три ограничения, о которых надо сказать прямо, иначе получится реклама.
Гарантии конвейера — про контур, не про мир. Все проверки говорят о соответствии ответа вашим схемам, правилам и базам. Ложь, согласованная с ложной базой, пройдёт насквозь и не поморщится.
Шаги 2 и 3 — эвристики. Их вклад — экономика фильтрации, не доказательство. Гарантии дают только шаги 1, 4 и калиброванный 5 — и то узкие и условные [1].
Для АСУ ТП, медтеха и авионики это кирпич, а не стена. Конвейер может быть одним из диагностических барьеров, но обоснование безопасности (safety case) строится отдельно и по своим стандартам. Как это встраивается в IEC 61508 и ISO 13849 — разбираю в отдельном тексте; здесь для этого нет места.
И про деньги: пять шагов на каждый запрос — это дорого и медленно. Целиком конвейер имеет смысл в асинхронных сценариях: отчёты, ночные регрессии и прочая фоновая валидация, где никто не ждёт ответа секундами. В синхронных не тащите всё во все запросы: для черновиков и подсказок хватит шага 1 — он стоит копейки и ловит половину боли. Для кода во внутренние задачи и отчётов для людей добавьте сверку с базой и линтеры (шаги 3 и 4) — жёсткие вердикты почти даром. А полная цепочка с эскалацией по калиброванному порогу — только для того, что уходит наружу или в производственный контур.
Поставьте Pydantic-схему на выход модели — это один вечер, а эффект вы увидите сразу.
Найдите в своих ответах точные факты и заведите под них SELECT. Дешевле любого судьи.
Генерируете код — ruff + mypy --strict + pytest в контейнере, барьер готовый.
Проверьте, из какого семейства ваш судья. Если из того же — вы платите за эхо.
Всё остальное — когда упрётесь в потолок первых четырёх.
Если Ragas, DeepEval и MAPIE пока вам ничего не говорят — не трогайте их. Начните с шагов 1, 3 (в части SELECT) и 4: это три вечера и никакой теории вероятностей. Шаги 2 и 5 достраивайте по мере необходимости — когда надоест руками разбирать, какие ответы передавать специалисту.
Итог простой и скучный: строгая схема на входе, дешёвые фильтры раньше дорогих, детерминированная сверка раньше LLM-судьи, правила везде, где их можно записать, и калиброванный остаток на выходе. Конвейер, который отклоняет часть правильного кода, — и делает это осознанно.
А что отклоняет ваш? Пишите в комментарии кейсы «балл высокий — ошибка реальная» и ваши доли эскалаций — соберу самые интересные в следующий пост.
Все восемь — публичные, ставятся менеджером пакетов pip; в скобках — кто они в терминах статьи. Фрагменты кода (snippets) Pydantic (2.13) и MAPIE (1.4) исполнены как есть, паттерн Instructor сверен с 1.15; Ragas и DeepEval даны схематично — сверяйте с документацией своей версии, API меняется быстро.
Pydantic (судья) — схема на выходе модели: не сходится — исключение.
Instructor (судья) — обёртка над интерфейсом модели с автоповтором (autoretry) под схему Pydantic.
Outlines (судья) — ограниченная генерация: невалидный JSON физически невозможен, про смысл см. шаг 1.
Guardrails (и то и другое) — оркестратор валидаторов: regex внутри него — судья, LLM-валидатор — детектив.
Ragas (детектив) — метрики достоверности и релевантности для ответов с поиском по базе (RAG).
DeepEval (детектив) — те же метрики, но оформленные как pytest-тесты: гейт в CI по порогу.
SelfCheckGPT (детектив) — самосогласованность для свободного текста, с кластеризацией по смыслу.
MAPIE (судья-статистик) — конформный слой, превращающий скоры в обещание покрытия.
И отдельно, уже вне конвейера: TruLens — трассировка вердиктов, кто отклонил, почему и на каком шаге. Это слой наблюдаемости — той самой, без которой дрейф первыми замечают пользователи.
Kalai, Nachum, Vempala, Zhang (2025). Why Language Models Hallucinate. arXiv:2509.04664 [2]
Willard & Louf (2023). Efficient Guided Generation for Large Language Models (Outlines). arXiv:2307.09702 [3]
Wang et al. (2022). Self-Consistency Improves Chain of Thought Reasoning in Language Models. arXiv:2203.11171 [4]
Kim, Garg, Peng, Garg (2025). Correlated Errors in Large Language Models. arXiv:2506.07962 [5]
Kohli (2026). Nine Judges, Two Effective Votes: Correlated Errors Undermine LLM Evaluation Panels. arXiv:2605.29800 [6]
Maiorano (2026). Automated Self-Testing as a Quality Gate: Evidence-Driven Release Management for LLM Applications. arXiv:2603.15676 [7]
Vovk, Gammerman, Shafer (2005). Algorithmic Learning in a Random World. Springer — математический фундамент конформного предсказания, на котором стоит MAPIE.
Angelopoulos, Bates (2021). A Gentle Introduction to Conformal Prediction and Distribution-Free Uncertainty Quantification. arXiv:2107.07511 [8] — доступное введение в тему [7].
Наш замер коррелированности валидаторов: https://github.com/SergeiUstiugov/judge-blindspot [1]
Автор: sergeiustiugov
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/validatsiya/456655
Ссылки в тексте:
[1] https://github.com/SergeiUstiugov/judge-blindspot: https://github.com/SergeiUstiugov/judge-blindspot
[2] arXiv:2509.04664: https://arxiv.org/abs/2509.04664
[3] arXiv:2307.09702: https://arxiv.org/abs/2307.09702
[4] arXiv:2203.11171: https://arxiv.org/abs/2203.11171
[5] arXiv:2506.07962: https://arxiv.org/abs/2506.07962
[6] arXiv:2605.29800: https://arxiv.org/abs/2605.29800
[7] arXiv:2603.15676: https://arxiv.org/abs/2603.15676
[8] arXiv:2107.07511: https://arxiv.org/abs/2107.07511
[9] Источник: https://habr.com/ru/articles/1070866/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1070866
Нажмите здесь для печати.