Однажды я открыла канал и увидела там собственную статью, в которой имена тегов стояли в тексте буквально - угловыми скобками посреди предложения. Рядом лежала ссылка на первоисточник, развалившаяся на половину адреса и обрывок слова. Текст перед этим прошел вычитку. Он был нормальный. Просто до читателя он доехал не в том виде, в каком я его отправляла.
Это не история про плохую модель. Это история про то, что часть требований к тексту бессмысленно просить - их нужно проверять. Дальше я разберу, какие именно требования стоит вынести в код, почему разметка ломается тише всего остального, и как устроен возврат ошибки, после которого следующая попытка чинит ровно то, что сломано, а не переписывает все заново.
Промпт - это просьба, а не гарантия
Промпт - это текстовая инструкция, которую вы даете модели перед генерацией: пиши так, не пиши эдак, кавычки такие, ссылки такие. Место удобное, правила туда складываются легко, и первое время кажется, что этого достаточно.
По моему опыту правило, записанное в промпт, соблюдается примерно всегда.
Вот это «примерно» и стоит дороже всего. Не потому, что нарушений много - их как раз мало. А потому, что момент обнаружения выбран максимально неудачно: нарушение всплывает уже у читателя. Девяносто девять текстов подряд выходят ровно такими, как вы просили, вы перестаете смотреть на эту деталь - и сотый выходит другим.
Разница между просьбой и гарантией - в том, кто отвечает за исход. Просьбу выполняет модель, и выполнение вероятностно. Гарантию дает код: проверка либо прошла, либо нет, третьего состояния у нее не бывает. Промпт при этом никуда не девается - он остается способом получить хороший текст с первой попытки. Просто он перестает быть единственным местом, где живет требование.
Что имеет смысл проверять кодом
Критерий отбора простой: в код уходит то, на что можно ответить да или нет, не читая текст. Не «интересно ли написано», а «есть ли здесь то, что должно быть».
В моем контентном конвейере кодом проверяется вот что:
- есть ли ссылка на первоисточник - не «уместна ли она», а просто есть ли
- оформлена ли ссылка словами, а не голым адресом
- не осталось ли в тексте голых адресов где-то еще
- есть ли в разметке акценты, или текст пришел сплошной серой массой
- не проехали ли прямые кавычки вместо елочек
- укладывается ли длина в заданные границы
- не встречаются ли штампы из черного списка.
Ничего умного в этих проверках нет, и в этом их ценность. Каждая занимает несколько строк, работает мгновенно и не зависит от настроения генерации. Список выглядит мелочным ровно до того момента, пока не поймаешь на нем текст, который в остальном был готов к публикации.
Здесь же проходит граница, которую я стараюсь не размывать: код проверяет форму, человек проверяет смысл. Механический гейт не заменяет вычитку. Он снимает с нее рутину - то есть ту часть, где вы глазами ищете прямую кавычку и считаете, есть ли в тексте хоть один выделенный фрагмент. После гейта вычитка занимается тем, ради чего она вообще нужна: верны ли утверждения, работает ли структура, не соврал ли текст в детали.
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))Применить у себя
Что проверить на практике
Если вы хотите завести такой гейт у себя, начните с малого - большой набор проверок сразу писать не нужно.
- Выпишите правила, которые сейчас живут только в промпте. Просто списком, как есть.
- Отметьте те, на которые можно ответить да или нет, не вникая в смысл. Это ваши кандидаты в код.
- Возьмите один такой пункт - например, голые адреса в тексте - и напишите проверку на несколько строк. Прогоните ее по десятку уже опубликованных текстов; если найдете нарушение, вы только что доказали себе, почему «примерно всегда» недостаточно.
- Отделите ошибки, которые чинятся механически, от тех, что не чинятся. Первые чините молча, вторые заворачивайте.
- Сформулируйте текст отказа так, чтобы он перечислял конкретные претензии, а не подводил итог.
- Если между генерацией и публикацией есть еще один слой, проверьте, что он делает с вашей разметкой. Совпадение ожиданий двух слоев стоит проверить один раз явно, а не выяснять на живом посте.
- Там, где сломанный результат дороже отсутствующего, ставьте предохранитель на отказ от публикации.
Как понять, что получилось
Основной признак - вы перестаете вычитывать текст на форму. Не потому, что расслабились, а потому, что форму уже проверили: если что-то не так, до вычитки текст просто не дойдет. Внимание освобождается для смысла, и это заметно по тому, какие замечания вы теперь пишете к черновикам.
Второй признак - отказы гейта перестают удивлять. Вы читаете список претензий и сразу понимаете, что править; на разбор «а что вообще имелось в виду» время не уходит.
Третий - количество исправлений после публикации стремится к нулю. Не сокращается вдвое, а именно уходит в ноль по тому классу проблем, который вы закрыли кодом.
И побочный эффект, который я оценила не сразу: у списка проверок появляется история. Каждая проверка в нем - след конкретной поломки, а не результат размышлений о качестве вообще. Перечитывая список, видно, на чем система училась и какой ценой. Это самая честная документация процесса из всех, что у меня были: ее невозможно написать заранее и незачем приукрашивать.