vegman.dev
Все статьи

Гейт качества: механические проверки вместо доверия

2026-05-14статья

Однажды я открыла канал и увидела там собственную статью, в которой имена тегов стояли в тексте буквально - угловыми скобками посреди предложения. Рядом лежала ссылка на первоисточник, развалившаяся на половину адреса и обрывок слова. Текст перед этим прошел вычитку. Он был нормальный. Просто до читателя он доехал не в том виде, в каком я его отправляла.

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

Промпт - это просьба, а не гарантия

Промпт - это текстовая инструкция, которую вы даете модели перед генерацией: пиши так, не пиши эдак, кавычки такие, ссылки такие. Место удобное, правила туда складываются легко, и первое время кажется, что этого достаточно.

По моему опыту правило, записанное в промпт, соблюдается примерно всегда.

Вот это «примерно» и стоит дороже всего. Не потому, что нарушений много - их как раз мало. А потому, что момент обнаружения выбран максимально неудачно: нарушение всплывает уже у читателя. Девяносто девять текстов подряд выходят ровно такими, как вы просили, вы перестаете смотреть на эту деталь - и сотый выходит другим.

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

Что имеет смысл проверять кодом

Критерий отбора простой: в код уходит то, на что можно ответить да или нет, не читая текст. Не «интересно ли написано», а «есть ли здесь то, что должно быть».

В моем контентном конвейере кодом проверяется вот что:

  • есть ли ссылка на первоисточник - не «уместна ли она», а просто есть ли
  • оформлена ли ссылка словами, а не голым адресом
  • не осталось ли в тексте голых адресов где-то еще
  • есть ли в разметке акценты, или текст пришел сплошной серой массой
  • не проехали ли прямые кавычки вместо елочек
  • укладывается ли длина в заданные границы
  • не встречаются ли штампы из черного списка.

Ничего умного в этих проверках нет, и в этом их ценность. Каждая занимает несколько строк, работает мгновенно и не зависит от настроения генерации. Список выглядит мелочным ровно до того момента, пока не поймаешь на нем текст, который в остальном был готов к публикации.

Здесь же проходит граница, которую я стараюсь не размывать: код проверяет форму, человек проверяет смысл. Механический гейт не заменяет вычитку. Он снимает с нее рутину - то есть ту часть, где вы глазами ищете прямую кавычку и считаете, есть ли в тексте хоть один выделенный фрагмент. После гейта вычитка занимается тем, ради чего она вообще нужна: верны ли утверждения, работает ли структура, не соврал ли текст в детали.

flowchart TB
  D["Черновик от модели"] --> G{"Механические проверки"}
  G --> G1["Разметка сбалансирована"]
  G --> G2["Цифры есть в фактуре"]
  G --> G3["Ссылки оформлены словами"]
  G --> G4["Имен заказчиков нет"]
  G1 & G2 & G3 & G4 -->|"хоть одна не прошла"| BACK["Точный список замечаний<br/>автору: правка, а не<br/>новая генерация"]
  G1 & G2 & G3 & G4 -->|"все прошли"| HUMAN["Вычитка о смысле:<br/>верно ли, работает ли<br/>структура"]
Гейт отвечает только на вопросы формы; смысл остается человеку

Разметка: почему она ломается тише всего

Разметка - отдельный класс проблем, и он неприятнее остальных.

Мессенджер, в который уходит текст, принимает лишь несколько тегов. Если разметка несбалансированна - открыли выделение и не закрыли, вложили один тег в другой не так, - он не показывает текст криво. Он отвергает сообщение целиком. То есть ошибка в одном символе превращается в «публикации не было», и узнаете вы об этом только по факту отсутствия поста.

Поэтому у меня разметка обрабатывается в два приема. Сначала скрипт чинит то, что чинится механически: переносы строк, списки - вещи, где правильный вариант выводится однозначно и без догадок. А то, что механически не чинится, он не пытается угадать: заворачивает и возвращает автору.

Теперь про тот самый сбой из начала статьи. Текст ушел в канал с буквальными тегами и разваленной ссылкой, потому что слой публикации экранировал разметку - то есть превращал служебные символы в обычные, - а конвейер эту разметку кладет намеренно. Два слоя, каждый по-своему прав, и результат виден только на выходе, у читателя.

После починки я добавила предохранитель: если в готовом сообщении осталась экранированная разметка, публикация не происходит вообще. Не «публикуем и правим», не «публикуем с оговоркой» - просто не публикуем.

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

Как ошибка должна возвращаться автору

Гейт, который умеет говорить только «не прошло», почти бесполезен. Он сообщает, что где-то что-то не так, и оставляет вас искать это самостоятельно - то есть возвращает ровно ту работу, ради избавления от которой вы его писали.

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

Это важно и когда исправляете вы сами, и когда текст переписывает модель. Общий отказ провоцирует переписать все заново - а вместе с проблемным местом уезжают и удачные куски, которые уже были готовы. Точный список превращает второй проход в правку, а не в новую генерацию.

# Кириллица только с заглавной буквы: иначе короткое имя совпадет с обычным словом
CLIENT_RE = re.compile("|".join(CLIENT_NAMES))


def client_names(text):
    """Имена заказчиков, просочившиеся в публичный текст."""
    return sorted({m.group(0) for m in CLIENT_RE.finditer(text or "")})


# в проверке статьи:
found = client_names(text)
if found:
    errors.append("имена заказчиков в тексте: " + ", ".join(found))
Правило из промпта, переписанное в проверку: гейт заворачивает текст, если имя заказчика всплыло

Применить у себя

Что проверить на практике

Если вы хотите завести такой гейт у себя, начните с малого - большой набор проверок сразу писать не нужно.

  • Выпишите правила, которые сейчас живут только в промпте. Просто списком, как есть.
  • Отметьте те, на которые можно ответить да или нет, не вникая в смысл. Это ваши кандидаты в код.
  • Возьмите один такой пункт - например, голые адреса в тексте - и напишите проверку на несколько строк. Прогоните ее по десятку уже опубликованных текстов; если найдете нарушение, вы только что доказали себе, почему «примерно всегда» недостаточно.
  • Отделите ошибки, которые чинятся механически, от тех, что не чинятся. Первые чините молча, вторые заворачивайте.
  • Сформулируйте текст отказа так, чтобы он перечислял конкретные претензии, а не подводил итог.
  • Если между генерацией и публикацией есть еще один слой, проверьте, что он делает с вашей разметкой. Совпадение ожиданий двух слоев стоит проверить один раз явно, а не выяснять на живом посте.
  • Там, где сломанный результат дороже отсутствующего, ставьте предохранитель на отказ от публикации.

Как понять, что получилось

Основной признак - вы перестаете вычитывать текст на форму. Не потому, что расслабились, а потому, что форму уже проверили: если что-то не так, до вычитки текст просто не дойдет. Внимание освобождается для смысла, и это заметно по тому, какие замечания вы теперь пишете к черновикам.

Второй признак - отказы гейта перестают удивлять. Вы читаете список претензий и сразу понимаете, что править; на разбор «а что вообще имелось в виду» время не уходит.

Третий - количество исправлений после публикации стремится к нулю. Не сокращается вдвое, а именно уходит в ноль по тому классу проблем, который вы закрыли кодом.

И побочный эффект, который я оценила не сразу: у списка проверок появляется история. Каждая проверка в нем - след конкретной поломки, а не результат размышлений о качестве вообще. Перечитывая список, видно, на чем система училась и какой ценой. Это самая честная документация процесса из всех, что у меня были: ее невозможно написать заранее и незачем приукрашивать.