惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Exploit Database - CXSecurity.com
N
News and Events Feed by Topic
Latest news
Latest news
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
V
V2EX
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
Cisco Talos Blog
Cisco Talos Blog
K
Kaspersky official blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
SegmentFault 最新的问题
小众软件
小众软件
A
Arctic Wolf
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
G
GRAHAM CLULEY
罗磊的独立博客
T
Tor Project blog
C
Cisco Blogs
美团技术团队
博客园 - Franky
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
T
Threat Research - Cisco Blogs
Cyberwarzone
Cyberwarzone
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
有赞技术团队
有赞技术团队
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Security Latest
Security Latest
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
Spread Privacy
Spread Privacy
J
Java Code Geeks
C
CERT Recently Published Vulnerability Notes
大猫的无限游戏
大猫的无限游戏
S
Securelist
The Cloudflare Blog
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
阮一峰的网络日志
阮一峰的网络日志
雷峰网
雷峰网
Project Zero
Project Zero

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Mode collapse в авто-блоге: четыре пятницы подряд LLM приносила нам один и тот же «спорный вопрос про ИИ»
MatushkinVD · 2026-04-27 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение11 мин

Охват и читатели385

Ретроспектива

Это история про открытый креативный промпт к LLM, оставленный без присмотра. Через месяц он превратил пятничную рубрику нашего блога в гимнастику парафразов одного и того же тезиса. Поймал я это не на первой пятнице и даже не на второй: каждая отдельная статья сама по себе выглядела нормально. На четвёртой стало очевидно.

Расскажу, какие четыре вещи я попробовал, прежде чем признать, что промпт-инжинирингом эту штуку не лечат. По дороге всплыло, что часть проблемы сидела не в промпте, а в нашей собственной конфигурации Gemini, на которую я не сразу обратил внимание (точнее - вообще не видел). И как в итоге мы перевели генератор тем с открытого «придумай вопрос» на заранее детерминированную ротацию из курируемого пула. Без файнтюна, без RAG, без переезда на другую модель.

Контекст

У нас в NeuroVerdict (это сервис, в котором запрос параллельно идёт в пять моделей с веб-поиском, потом склеивается ответом-сводкой) есть автоматический блог. Один пост в день, разные жанры по дням недели:

День

Тип

Описание

Пн

news

сводка новостей ИИ от пяти моделей

Вт

comparison

один технический вопрос, отвечают пятеро

Ср

myth

разбор одного мифа об ИИ, одна модель

Чт

howto

практический совет, одна модель

Пт

versus

один спорный вопрос, отвечают пятеро

Сб

tool-review

разбор одного инструмента, отвечают пятеро

Вс

weekly

дайджест за неделю

Архитектура простая. Cron каждые 6 часов проверяет, есть ли пост за сегодня. Если нет, по дню недели определяется тип. Дальше двумя путями. Для одиночных типов (myth, howto) одна модель пишет статью целиком. Для пятимодельных (news, comparison, versus, weekly, tool-review) каждая из пятёрки пишет свою колонку, всё склеивается в один документ с подзаголовками-моделями.

Особняком стоят comparison, versus и tool-review. Они двухшаговые: сначала генерируется заголовок-вопрос, потом под ним отвечают пять моделей. Замысел был простой. Если все пять моделей отвечают на один и тот же вопрос, важно, чтобы вопрос был свежий и нетривиальный. Поэтому вопрос на каждый день генерировался отдельным вызовом отдельной моделью, а уже под него подключались остальные.

Двухшаговость и стала точкой, в которой что-то пошло не так.

Симптом, который я долго не замечал

Сложность ловли вот в чём. Если открыть пост от 2026-03-27 - придраться не к чему. Пять моделей действительно отвечают разнопланово, источники проставлены, формат соблюдён. Откроешь 2026-04-03 - то же. И 04-10. Каждая статья отдельно нормальная. Только если выложить в ряд несколько недель, видно, что заголовки рифмуются.

Вот реальные четыре пятницы подряд из канала NeuroVerdict, рубрика «ИИ-баттл»:

Дата

Заголовок «ИИ-баттл: …»

27.03.2026

Стоит ли создавать сверхразумный ИИ

03.04.2026

Должны ли мы создавать сверхинтеллектуальный ИИ

10.04.2026

Сверхинтеллект, нужен ли он человечеству

17.04.2026

Безопасно ли стремиться к сверхразумному ИИ

Один и тот же вопрос, перефразированный, четыре раза. Похожая, но более вялая динамика была на comparison-вторниках, там тянуло в «как ИИ изменит образование», цикл получался длиннее, недель шесть.

Заметил случайно (почти). Я листал собственный канал и поймал ощущение «всё уже было». Полез смотреть. Точно, было. Через символьный поиск (ctrl+F) моментально выявил дубли.

Промпт, с которого всё началось

Вот тот самый кусок из backend/services/blog.js, на котором всё держалось четыре недели:

const fallbackPrompt = {
  system: BLOG_SYSTEM,
  user:
    'Придумай один интересный вопрос на стыке ИИ, науки и общества. ' +
    'Ответь ТОЛЬКО текстом вопроса, одним предложением.',
};
const result = await searchGemini(fallbackPrompt);
const sharedQuestion = (result?.content || '').trim();

На вид всё чисто. system-prompt задаёт редакционную политику. user-prompt просит ровно одну вещь и формат ответа. Никаких лишних инструкций. Никаких примеров. Никаких ограничений. Именно «никаких ограничений» и оказалось ключевым.

Когда LLM получает открытый запрос на «придумай интересный вопрос», она не подбрасывает кубик. Она идёт по самой плотной зоне распределения, сформированного при обучении. Для словосочетания «вопрос на стыке ИИ, науки и общества» эта плотная зона - экзистенциальные сюжеты про сверхразум, alignment, сознание у машин и автономный ИИ. Они доминируют в обучающих данных, доминируют в новостных лентах, доминируют в твиттере про ИИ. Что бы вы ни подкручивали в формулировке, аттрактор всё там же.

Сначала я думал, что виноват кэш. Полез в логи Railway, посмотрел: каждый вызов уходит свежий, ответы по содержанию разные (хотя по теме одинаковые), длительности нормальные. Не кэш, не «подсунули один и тот же ответ».

Дальше всплыло то, чего я не ожидал. Я полез в свой же backend/services/gemini.js посмотреть, на какой температуре мы зовём модель. И увидел такое:

const body = {
  contents: [{ parts }],
  generationConfig: {
    maxOutputTokens: opts?.maxOutputTokens || 2048,
    temperature: opts?.temperature ?? 0.2,
  },
};

temperature: opts?.temperature ?? 0.2. Двухшаговая генерация в blog.js опции не передаёт, и мы зовём Flash Lite на температуре 0.2. Я даже не помню, когда это туда попало: видимо, ставилось ради стабильности на основном search-кейсе, и автоблог унаследовал то же значение по умолчанию. На 0.2 модель сваливается в свой самый плотный режим заметно охотнее, чем на дефолтных API-настройках около 1.0. Низкая температура - одна из причин, почему коллапс пошёл с такой надёжной точностью. Но как мы дальше увидим (попытка 3), даже если температуру поднять, картина меняется только косметически. Это симптом, а не причина.

Что я попробовал и чем это кончилось

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

Попытка 1. Добавил в промпт «не повторяй темы из предыдущих ответов, выбирай свежий угол». Эффект ноль. Предыдущих ответов модель не видит, ей нечего «не повторять». Это просто слова в промпте, не контекст.

Попытка 2. Добавил пять явных примеров вопросов на разные темы (программирование, искусство, медицина, образование, бизнес). Сработало на одну пятницу. На следующую - полу-сработало (вопрос был не про сверхразум, а про «искусство, созданное ИИ, искусство ли это»). На третью пятницу вернулось «должны ли мы дать ИИ право голоса в выборах», что снова где-то рядом с экзистенциальной осью.

Попытка 3. Поднял temperature с нашего унаследованного 0.2 до 1.2, добавил top_p: 0.95. Получил более широкий лексический разброс в формулировках, но тематически вопросы остались про этику и сверхразум, просто словарь стал поэкзотичнее. На пятой пятнице нашёл в результатах один действительно отвлечённый вопрос («должны ли школьники сдавать экзамены с открытым доступом к ИИ»), но из шести следующих пять снова свернули в «контроль над сверхинтеллектом». Чуть выше я уже писал, что это симптом, а не причина: попытка подтвердила.

Попытка 4. Заменил Gemini на Claude и затем на OpenAI на этой же стадии. Поведение изменилось, но коллапс остался. На Claude быстро сместилось в этику (видимо, RLHF-уклон), на OpenAI - в сравнения «GPT-X vs GPT-Y». Аттрактор у каждой модели свой, но он есть, и в открытом промпте «придумай вопрос» она в него рано или поздно сваливается.

После этих четырёх попыток у меня сформировалось простое наблюдение. Промпт-инжиниринг для борьбы с mode collapse в открытых креативных промптах не масштабируется. Он покупает две-три недели, потом аттрактор всё равно возвращается. Может другой, может тот же. Чинить открытый промпт промптом это тушить пожар бензином пожиже.

Что сработало: ротация из курируемого пула

Развязка простая, и я её сначала обходил стороной. Раз LLM в открытом выборе сваливается в аттрактор, отнимем у неё открытый выбор. Курируемый список тем, и по дню года выбирается, какой именно идёт сегодня.

// blogTopicPools.js (фрагмент)

const versusTopics = [
  'Должны ли ИИ-сервисы обязательно маркировать весь сгенерированный контент?',
  'Стоит ли запретить использование ИИ для автоматических решений о найме?',
  'Заменит ли ИИ учителей начальных классов в ближайшие 15 лет?',
  'Должен ли ИИ иметь право отказаться выполнить запрос по этическим соображениям?',
  'Нужен ли международный мораторий на обучение моделей крупнее GPT-5?',
  'Должны ли школьники сдавать экзамены с открытым доступом к ИИ?',
  // ... ещё около 45 пунктов в этом списке
];

const POOLS = {
  versus: versusTopics,
  comparison: comparisonTopics,
  'tool-review': toolReviewTopics,
  // ...
};

export function pickTopic(typeId, date = new Date()) {
  const pool = POOLS[typeId];
  if (!pool || pool.length === 0) return null;
  const start = new Date(date.getFullYear(), 0, 0);
  const dayOfYear = Math.floor((date - start) / 86_400_000);
  return pool[dayOfYear % pool.length];
}

Несколько вещей, которые надо пояснить отдельно.

Выбор детерминированный по дню года. Если cron сработает дважды за день (бывает после релиза, когда инстансы рестартуют), оба вызова получат одну и ту же тему. Это удобно для идемпотентности и для отладки: знаешь дату - знаешь номер темы.

Пул конечный. Сейчас у versus около 52 вопросов, у comparison чуть больше, у tool-review сильно меньше (новые ИИ-инструменты не выходят с такой плотностью, чтобы их хватило на 50). Раз в пару месяцев я перечитываю списки и добавляю новые темы или вычищаю устаревшие. Это компромисс. Но 50 вопросов - это год пятниц с ротацией, и нагрузка на одного редактора (на меня) терпимая. Если бы у меня было время заполнять 365 уникальных тем в год - заполнил бы. Сейчас раз в год пул прогоняется по второму кругу, пока этим живём.

Мест замены в blog.js буквально несколько строк:

// Было
const result = await withRetry(
  () => searchGemini(fallbackPrompt),
  { retries: 2, delay: 1500 },
);
sharedQuestion = (result?.content || result?.text || '')
  .trim()
  .replace(/^["«]|["»]$/g, '');
if (!sharedQuestion) {
  sharedQuestion = 'Какие главные достижения ИИ произошли в последний год?';
}

// Стало
sharedQuestion = pickTopic(contentType.id);
if (!sharedQuestion) {
  // Fallback на старый Gemini-путь, если для этого типа пул пуст
  // (например, забыли наполнить пул для новой категории).
  // ... тот же блок, что был раньше.
}

Старый Gemini-путь оставлен fallback’ом ровно для одного сценария: если завтра я добавлю новый тип поста и забуду наполнить ему пул. Тогда сервис не упадёт, отработает по-старому, в логах будет видно «pool пустой для X», я приду наполню. Из тех систем, в которых fallback написан «на любой шорох и из лучших побуждений», эта не самая страшная. Деградация в плохой режим, не отказ, и явный лог-сигнал.

Подстраховка для одношаговых типов: DB lookback как anti-repeat hint

Есть деталь, которую хочется выделить отдельно. Пул лечит двухшаговые типы (comparison, versus, tool-review), потому что там есть «вопрос», который нужно однозначно зафиксировать. С одношаговыми типами (myth, howto, news, weekly) сложнее. Там у нас не один вопрос, а одна «тема» (для myth это пул из 50 мифов про ИИ, для howto - 50 советов, и так далее), и внутри этой темы у LLM остаётся свобода развернуть. Mode collapse и сюда прокрался, но уже на следующем уровне: не «один вопрос четыре раза», а «один и тот же ракурс одной и той же темы три раза».

Ловится это вторым слоем. Перед генерацией мы достаём из БД заголовки последних 5 постов того же тега и просим модель не повторяться:

async function fetchRecentTitlesByTag(tag, limit = 5) {
  const res = await pool().query(
    `SELECT title FROM blog_posts
     WHERE lang = 'ru' AND $1 = ANY(tags)
     ORDER BY created_at DESC
     LIMIT $2`,
    [tag, limit],
  );
  return res.rows.map((r) => r.title).filter(Boolean);
}

function buildAvoidFragment(recentTitles) {
  if (!recentTitles?.length) return '';
  const list = recentTitles.slice(0, 5).map((t) => `«${t}»`).join(', ');
  return `Не повторяй темы недавних постов: ${list}. ` +
         `Выбери существенно другой угол.`;
}

Эта подсказка прирастает к user-промпту через [promptFn(topic, avoid)].filter(Boolean).join(' '). Сэмпл у меня пока маленький: одношаговых постов после деплоя я успел собрать пару десятков, и большие выводы делать рано. Тем не менее, по этим точкам видно, что Gemini Flash Lite и OpenAI GPT-4.1 Mini подсказку соблюдают надёжно, Claude Haiku 4.5 в большинстве случаев. Grok иногда повторяет один из перечисленных заголовков почти дословно. Perplexity ведёт себя непредсказуемо на длинном контексте, изредка подсказку «теряет» где-то по пути.

В двухшаговые типы anti-repeat hint мы намеренно НЕ кладём, и это важно. Там тема уже зафиксирована детерминированной ротацией. Если ещё и сказать «не повторяй темы», модель начнёт уворачиваться от темы, которую её попросили раскрыть. Получится противоречивая инструкция, и в худшем случае модель забивает на оба слоя и пишет что-то третье.

Что показал первый постфикс-цикл

Деплой blogTopicPools.js и подмену двухшагового пути в blog.js я выкатил 17 апреля рано утром. Это был последний день в серии коллапсов: пятничный пост от 17 апреля сгенерировался ещё на старом коде, потому что cron к этому моменту уже прошёл. С 18 апреля все двухшаговые типы пошли через пул.

К моменту, когда я пишу эту статью (26 апреля), постфикс-точки такие:

  • 18 апреля, суббота, tool-review - тема из пула.

  • 21 апреля, вторник, comparison - тема из пула.

  • 24 апреля, пятница, versus - тема из пула, совершенно другая часть списка, ничего про сверхразум.

  • 25 апреля, суббота, tool-review - тема из пула.

Четыре постфикс-поста двухшаговых типов, все по контракту. Это пока ещё не статистика, но уже больше, чем одна точка. Главное, ни один не повторил сверхразум-привычку.

Основания ожидать, что коллапс не вернётся, у меня сейчас в первую очередь механистические, а не эмпирические. Внутри pickTopic() я могу глазами проследить весь путь: пул прочитан, индекс посчитан, элемент возвращён. Формула - три строки. Если по этому контракту что-то снова повторится, это будет либо bug в индексации, либо я сам отредактировал пул и положил туда дубликат. Оба сценария чинятся локально, без переразвёртывания всей механики выбора. Месяц подержу глаза открытыми. Если что-то пойдёт не так, обновлю статью.

Почему я не пошёл по пути «n-gram diversity check + перегенерация»

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

Стоимость. Каждая перегенерация - это ещё один Gemini-вызов. По цене Flash Lite копейки. Но если когда-нибудь сценарий «mode collapsed на месяц» вернётся уже на чём-нибудь другом, мы будем платить за 3-5 перегенераций в день, чтобы получить одну приемлемую. Плюс это латентность 2-5 секунд на каждую попытку. Пайплайн становится неустойчивее.

Порог сходства. N-gram-сходство ловит лексическое повторение. Парафраз ловит плохо. «Стоит ли создавать сверхразумный ИИ» и «Безопасно ли стремиться к сверхинтеллекту» по триграммам разнятся, хотя по смыслу - один вопрос. Чтобы поймать парафраз, нужны эмбеддинги и семантическое сравнение. У этого свои проблемы: на чьих эмбеддингах считать; как калибровать порог; что делать с легитимными возвратами темы (например, «искусственный интеллект и право» - это широкая тема, по ней можно писать раз в три месяца под разными углами).

Детерминизм. N-gram check со случайной перегенерацией - недетерминированный. На один и тот же день можно получить разный пост в зависимости от того, на какой попытке сработал фильтр. Это плохо для идемпотентности. Если cron сработает дважды (а у нас он именно так устроен - каждые 6 часов с advisory-lock на день), мы можем получить два разных итоговых поста, и тот, который запишется в БД первым, не обязательно самый «лучший» по фильтру.

Простота. Пул и ротация по дню года - это О(1)-функция и JSON-файл на пару КБ. Любая semantic-deduplication-pipeline - это эмбеддинг-модель, индекс, порог, мониторинг, повторное тестирование. Если задачу можно решить на одном слое, лучше так: меньше слоёв, меньше мест, где что-то может пойти не туда.

Я не утверждаю, что для всех команд правильнее именно пул. Если у вас тематика блога подразумевает 10000+ возможных тем и вы не готовы их курировать - RAG поверх корпуса свежих новостей может оказаться разумнее. Если вы публикуете что-то, где «свежий парафраз старой темы» это плюс (например, daily-take на горячую новость), сценарий вообще другой и пул вам не нужен. Но для нашего размера (один человек редактирует, ~50-150 тем на тип, постов ~300 в год) пул выигрывает с большим запасом.

Мини-чек-лист, если у вас похожая ситуация

Положу как короткий ориентир, чтобы у тех, кто пришёл сюда по поиску, было что унести:

  1. Соберите хотя бы 3-4 недели подряд истории генерации. На одной точке коллапс не виден. На двух только начинают слабо рифмоваться формулировки.

  2. Проверьте, можно ли заменить «открытый выбор темы» на «выбор из конечного списка». В большинстве реальных задач можно: статьи по тегам, рассылки по сегментам, квизы по разделам.

  3. Сделайте выборку детерминированной по дате (день года, неделя года, ISO-неделя - что подходит). Это даёт идемпотентность бесплатно.

  4. Anti-repeat hint в промпт через DB-lookback по последним N - дешёвая страховка под пулом, не вместо него.

  5. Если избежать открытого выбора нельзя в принципе - ведите реестр уже использованных тем и блокируйте повторы детерминированно. Это всё ещё дешевле эмбеддинг-pipeline.

Что я для себя из этого вынес

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

Мне теперь хочется в каждом проекте, где LLM что-то «выбирает» из открытого пространства, поставить чек: а есть ли тут пул, из которого можно фиксированно крутить? Иногда такой пул есть в готовом виде (корпус ваших артикулов, тегов, категорий). Иногда его нужно завести. В обоих случаях это часто проще, чем сделать честный «выбор без пула».

И отдельно про температуру. Когда лезете в свой код, который генерит контент пачками, проверьте, на какой температуре зовётся модель. Я был уверен, что у нас 0.8-1.0, потому что так дефолтно у API. Оказалось 0.2, потому что когда-то ставилось ради другого сценария. Низкая температура поверх открытого промпта - это ракета в коллапс-аттрактор, и я её сам себе в руки вложил.

Послесловие

Что меня в этой истории удивляет больше всего - то, что симптом провисел четыре недели. Я генерирую контент сам, читаю свой же канал, и всё равно не сразу заметил. На уровне отдельного поста рифмы не слышно. Это к разговору про слабые сигналы в системах с человеком в петле: не верьте, что глаза достаточно. Раз в пару недель сравнивать выходные ленты по тегам имеет смысл вынести в отдельную регулярную задачу, иначе руки до этого не дойдут.