Работаю в компании «Сибинтек-Софт», занимаюсь поддержкой корпоративных информационных систем. Имею сертификат DevOps-инженера, в рамках задач отдела внедряю ИИ в рабочие процессы.

Как-то мой валидатор забраковал ответ, сославшись на статью, которой не существует. Причём отличился именно валидатор — тот компонент, который поставлен ловить галлюцинации генератора. LLM-судья — судья на большой языковой модели (large language model) — вынес вердикт «отклонить» и в поле обоснования (reason) подкрепил его несуществующей «теорией» с поддельным arXiv-идентификатором. Выглядело убедительно, и вручную я бы это, скорее всего, пропустил. Выручила проверка, добавленная когда-то на всякий случай: идентификатора нет в белом списке, а это флаг.

После этой истории я окончательно перестал верить в два популярных рецепта — «напишите промпт, чтобы модель не галлюцинировала» или «пусть вторая такая же модель перепроверит». У меня в итоге работает другое: конвейер из открытых пакетов, работающих последовательно в определенном порядке. Он неудобный, местами дорогой и может отклонить правильный код. Я считаю это нормальной ценой конструкции — и ниже покажу почему.

Примеры почти все будут про генерацию кода: там наглядней увидеть разницу между «выглядит правильно» и «работает» — код можно просто запустить. Для текстовых отчётов, ревью и других документов логика та же, меняются только конкретные судьи на шаге 4.

Коротко, если лень читать всё

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

Рисунок 1. Конвейер целиком.

Детектив и судья

Все проверки, которые наиболее часто используются, делятся на два типа.

Детектив выясняет, правда ли это: сверяет показания, ищет нестыковки, оценивает вероятности. Повторные запросы моделей, LLM-судья, сверка фактов по источникам — всё это детективы. Расхождение у детектива подсвечивает проблему, а вот совпадение не доказывает ничего: свидетели могут дружно врать.

Судья решает другой вопрос — допустимо ли это. Есть записанное правило, значит есть однозначное «да/нет». Судья не оценивает, он проверяет. И молчит там, где правило не записано, — это его главная слабость, к ней ещё вернёмся.

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

Детективы дешёвые и покрывают широко. Детерминированные судьи узкие, зато их вердикту можно верить. Весь конвейер, по сути, про одно: расставить их так, чтобы не платить дорогим за то, что ловится дешёвым.

Рисунок 2. Два вопроса к любому ответу.

Шаг 1. Перестаньте кормить прод “левым” текстом

Первая причина смерти прототипов — вовсе не галлюцинации, а фраза «Вот ваш результат:», добавленная перед валидным 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 внутри. Форму он гарантирует, а смысл остаётся на совести модели.

Шаг 2. Спросить три раза

Самый дешёвый детектив из всех, что у меня стоят [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, чем изобретать свою кластеризацию. На творческих задачах не работает вовсе — там верных ответов много по определению. И держите в голове, чем этот фильтр является на самом деле: он меряет согласованность, а не истину. Стабильная неправота проходит его на ура.

Шаг 3. Сначала база первичной информации, потом судья

Порядок внутри этого шага экономит деньги и добавляет гарантий.

Точный факт — номер уязвимости 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).

Лечится это одной строкой конфигурации: judge_llm берётся из другого семейства. Под «другим» я имею в виду другую архитектуру и другой обучающий корпус, а не другой логотип на счёте за доступ к модели. Старшая и младшая модель одного вендора для этой цели неразличимы.

Как это правило нарушают прямо сейчас. В опубликованном промышленном шлюзе непрерывной интеграции (CI-гейте) для приложений на языковых моделях [6] «независимым» судьёй в калибровочном исследовании была модель того же семейства, что и оцениваемая система: Gemini 2.5 Pro против Gemini 2.5 Flash. Автор сам оговаривает, что «смещение общего семейства исключить нельзя», — разные уровни мощности внутри семейства остаются двумя срезами одного обучающего корпуса.

Врёт не модель, а обвязка

Мы только что научились сверять факты с источниками. Так вот: врать умеет и сам слой, который эти источники достаёт, — и этот класс галлюцинаций почти никто не проверяет, потому что модель тут вообще ни при чём.

— Поисковая прослойка выдала нам «цитату» из статьи. Цитата оказалась синтез-прозой самого движка. Один arXiv-идентификатор сменил три разных «заголовка», пока скачанный PDF не показал настоящий.

— HTML-версия страницы arXiv галлюцинировала номера теорем, которых в PDF нет.

— В нашей собственной библиографии сверка поймала классику: авторы верны, arXiv-id верен, а место публикации (venue) — от другой статьи тех же авторов.

Поэтому единственный источник правды — скачанный PDF. Утверждение об источнике сверяется с его первой страницей; версия на сайте, выдача поисковика и чья-либо память — модельная в том числе — источниками не считаются.

Валидатор из начала статьи — ровно этот же случай: поле обоснования от судьи — такой же выход модели, как ответ генератора, и требует такой же проверки. Мы почему-то по умолчанию верим вердиктам собственных валидаторов.

Шаг 4. Судьи, которым не нужен промпт

Всё, что формализуется правилом, проверяется правилом. Для кода — классика, работающая ровно так же, как в обычном конвейере непрерывной интеграции (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 failed\n%s",
                          cmd[0], (r.stderr or r.stdout).strip())
            ok = False
    return ok

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

Guardrails встаёт сюда же как оркестратор проверок, но следите за составом цепочки: regex-валидатор внутри него — судья, а LLM-валидатор — снова детектив, со всеми оговорками шага 2.

Шаг 5. Кто разбирает то, что осталось

После четырёх шагов остаётся серая зона: формально всё прошло, но уверенности нет.

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) — жёсткие вердикты почти даром. А полная цепочка с эскалацией по калиброванному порогу — только для того, что уходит наружу или в производственный контур.

Как внедрить за выходные

  1. Поставьте Pydantic-схему на выход модели — это один вечер, а эффект вы увидите сразу.

  2. Найдите в своих ответах точные факты и заведите под них SELECT. Дешевле любого судьи.

  3. Генерируете код — ruff + mypy --strict + pytest в контейнере, барьер готовый.

  4. Проверьте, из какого семейства ваш судья. Если из того же — вы платите за эхо.

  5. Всё остальное — когда упрётесь в потолок первых четырёх.

Если 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 — трассировка вердиктов, кто отклонил, почему и на каком шаге. Это слой наблюдаемости — той самой, без которой дрейф первыми замечают пользователи.

Источники

  1. 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.

  8. Angelopoulos, Bates (2021). A Gentle Introduction to Conformal Prediction and Distribution-Free Uncertainty Quantification. arXiv:2107.07511 — доступное введение в тему [7].

  9. Наш замер коррелированности валидаторов: https://github.com/SergeiUstiugov/judge-blindspot

Комментарии (6)


  1. OlegEgoism
    16.08.2026 07:03

    P.S. Про pytest в изолированном контейнере — спасибо, что напомнили. У нас джун чуть не уронил прод, запустив subprocess.run() в сгенерированном коде на рабочей машине. С тех пор все LLM-тесты гоняем строго в одноразовых Docker-контейнерах без сети и с read-only маунтами.


    1. TldrWiki
      16.08.2026 07:03

      У вас доступ к проду с рабочей машины?


    1. sergeiustiugov Автор
      16.08.2026 07:03

      Доброе утро!

      Спасибо, что поделились — это как раз практическое подтверждение сценария из врезки к шагу 4.

      Здесь важно не столько наличие Docker само по себе, сколько модель доверия: сгенерированный код по умолчанию считается недоверенным и получает минимальные полномочия — одноразовая среда, отсутствие сети, исходники доступны только для чтения. Контейнеризация для DevOps давно стала базовой практикой; существенна она именно там, где исполняется чужой — в данном случае модельно сгенерированный — код, а не там, где просто привычно.

      subprocess.run() особенно показателен: он позволяет коду запускать внешние программы. Если модель сформировала исполняемую команду, путь или аргументы неудачно либо под влиянием недоверенного контекста, этот процесс на рабочей машине действует с правами пользователя, запустившего тест. Именно от такого класса ошибок и нужна изоляция.

      Оговорка: одноразовый контейнер без сети заметно уменьшает радиус поражения, но не должен быть единственным рубежом. Если рабочая станция в принципе имеет пути к production-контуру — через VPN, учётные данные, CI/CD, внутренние API или облачные секреты, — нужны и ограничения на уровне сетевой топологии, IAM и управления секретами. Сегментация доступа и изоляция исполнения должны работать как независимые контуры защиты.

      Интересно, как у вас устроен следующий этап: код, прошедший тесты в песочнице, обязательно попадает на human review или при каких-то условиях может автоматически пройти дальше по конвейеру?


    1. Vasiliy_II
      16.08.2026 07:03

      Зачем джуну прод?


  1. Dvarvfich
    16.08.2026 07:03

    У меня был почти зеркальный кейс, только не с кодом, а с генерацией чек-листа по ТЗ.

    HTTP 200, JSON валиден, схема пройдена, результат сохранился — с точки зрения пайплайна всё зелёное. Открываю результат: 271 строка, где рядом с нормальными проверками модель добавила цели исследования, элементы матрицы рисков, служебные формулировки про готовность и вопросы, на которые исходное ТЗ вообще не отвечало. После этого для меня окончательно развалилось «валидный = пригодный».

    Для таких артефактов нет удобного execution oracle, как у кода, поэтому пришлось переносить жёсткий гейт ближе к источнику: существенный пункт результата должен иметь связь с конкретным требованием или фрагментом исходного документа. То, что источником не подтверждается, не становится успешным результатом — максимум уходит на ручную проверку.

    И есть ещё обратная проблема: модель может ничего не выдумать, но тихо пропустить половину критичных требований. Поэтому одной проверки faithfulness тоже мало, нужен ещё контроль покрытия.

    По сути, у меня получилось очень похоже на ваш принцип «детерминированное раньше вероятностного», только детерминированная часть строится вокруг traceability и контракта результата, а LLM остаётся скорее детективом, чем судьёй.

    Интересно, как бы вы строили такой гейт для текстового артефакта без исполнимого оракула: claim → source binding, отдельный контроль покрытия или уже human review на остатке?


  1. sergeiustiugov Автор
    16.08.2026 07:03

    Спасибо — вы переоткрыли подход сами. Поправлю рамку и заодно уточню то, что в моём коротком ответе прозвучало слишком сильно.

    Подход один и универсальный: детерминированное раньше вероятностного, отклонять раньше, чем принимать. Код я взял для витрины не потому, что метод про код, а потому, что у кода есть исполнение — автоматический источник положительного свидетельства. Только это не «стопроцентный оракул»: тест говорит про поведение на покрытых входах и в данной среде, а не про соответствие требованиям в целом. У меня в замере три из пяти провалов были не ошибками кода, а расхождением кода и недоспецифицированного теста — то есть даже у кода оракул частичный.

    У текста исполнимого оракула нет вовсе. Поэтому положительный вердикт для текста — не «верно», а составной и ограниченный: трассируемо + не противоречит модели требований + покрыт критичный инвентарь + соответствует контракту. Прошёл гейт — значит автоматика не нашла повода забраковать, а не «пригодно». Вот эта асимметрия и есть суть: у кода есть чем добыть положительное свидетельство, у текста — почти только отрицательное.

    По вашей развилке — не «или-или», а три слоя, детерминированное первым:

    1. claim → source binding — да, но слабая нижняя граница. Пункт может верно ссылаться на REQ-017 и всё равно нарушать контракт: усилить модальность (может → должен), ввести число, которого в ТЗ нет. Поэтому сильная проверка — не «есть ли источник», а «не противоречит ли пункт формальной модели требований». ТЗ сначала разбирается системным анализом в модель (ID, модальности, числовые границы, взаимоисключения), и пункт валится по противоречию ей правилом (if item.modality==MUST and req.modality==MAY: reject), а не мнением LLM. Вот это уже больше, чем судьи и детективы.

    2. покрытие — обязательно и ортогонально первому. Faithfulness ловит лишнее, покрытие — тихо потерянное. Меряется только против замкнутого инвентаря требований, с весом по критичности: пропущенное критичное — жёсткий reject, а не warning.

    3. human review — только на калиброванном остатке: слабая привязка, спорное покрытие критичного, конфликт проверок. Не 271 строка, а серая зона с причиной.

    Честная граница: всё упирается в качество модели ТЗ. Если ТЗ — проза, то сам шаг «разобрать в формальную модель» — это ещё один вызов LLM, то есть генератор, которому нужен свой гейт: пропустил извлекатель половину требований — покрытие покажет 100% на неполном множестве. Уязвимость просто сдвигается вверх по конвейеру.

    Этот класс — оракул для неисполнимого LLM-софта через привязку к требованиям — разбирает Wang et al., ASE'26 (REAG, Requirements-Augmented Generation, arXiv:2608.12970): оракул строят из требований, а не из эталонного ответа, и упираются в то же узкое место — точность привязки к источнику, а не генерация.

    Поэтому вопрос по существу: ТЗ у вас структурировано (нумерованные требования, покрытие разностью множеств) или инвентарь извлекаете из прозы? Там и проходит реальная граница детерминированного ядра — в прозе половина «детерминированного» на деле детектив.