Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Новостной конвейер на PowerShell: три минуты от ленты источников до готовой страницы

Дата публикации: 23-10-2026 14:40:22

Две недели назад я поставил себе инженерную задачу без редакции и без бюджета: сайт должен выпускать по пять материалов в день о том, что произошло в мире LLM, причём каждый материал проверен по первоисточникам, снабжён картинкой, разметкой для поиска и не повторяет предыдущие. Человек в этом конвейере остаётся ровно в одной точке — воротах согласования, где у него десять минут на решение.Сейчас в журнале конвейера 61 карточка, из них 56 стали страницами на сайте, семь дней подряд собирается вечерняя сводка, а сам прогон от поиска до готовой страницы занимает три минуты, из которых языковая модель работает меньше минуты. Ниже — как это устроено, что пришлось переделывать и на чём PowerShell с LLM мстят друг другу. Читать далее

Основное содержимое страницы с новостью.

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

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

Охват и читатели1.3K

Кейс

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

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

Что было в требованиях

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

Всё это живёт на обычном хостинге с FTP, без серверной части и без базы данных: сайт статический, состоящий из HTML-файлов. Управляющая часть — PowerShell 5.1 на моей машине, 47 скриптов, около 9,7 тысяч строк.

Журнал как единственный источник истины

Отказаться от базы было легко, потому что все данные конвейера линейны: карточка темы, её состояние и обогащения. Поэтому журнал — это файл JSONL, по строке на материал. Каждый шаг читает строку, дописывает в неё свои поля и сохраняет обратно; никакой шаг не хранит состояние у себя.

Вот как выглядит карточка в середине обработки (поля сокращены, «…» — вырезанные источники):

{
  "заголовок": "Agent4Sci: смогут ли ИИ-агенты улучшать научные модели?",
  "url": "https://arxiv.org/abs/2610.28763",
  "рубрика": "релизы",
  "дата": "2026-10-22",
  "язык": "en",
  "лид": "Новый тестбенч Agent4Sci проверяет, способны ли агенты улучшать модели…",
  "статус": "готово",
  "тема": 169,
  "источники": [{ "url": "https://arxiv.org/abs/2610.28763", "название": "arXiv cs.AI" }, "…"],
  "факты": [["Что случилось", "…"], ["Цифры", "…"]],
  "вывод": "…",
  "полезно": [["цена/доступность", "…"], ["что нужно из железа", "…"]],
  "фактЧек": "ок",
  "faq": [{ "в": "…", "о": "…" }]
}

Постоянный адрес страницы считается из данных, а не назначается вручную: дата плюс первые восемь шестнадцатеричных знаков SHA-1 от URL первоисточника. Получается 2026-10-22-2ea2fcfc.html — коротко, стабильно, без транслита и без коллизий в пределах дня.

Ключевой приём всей схемы — гейт публикации проверяет файл, а не флаг. Материал считается опубликованным тогда и только тогда, когда на диске существует сайт\news\<slug>.html. Из этого автоматически следуют приятные вещи: сводки и ленты строятся только по существующим страницам, битых ссылок не бывает, а любая авария в середине цепочки просто оставляет карточку без страницы, что видно по счётчику, но не ломает сайт.

Цепочка одного материала: от ленты до страницы за три минуты

Каждый шаг — отдельный процесс powershell.exe -File. Это дороже по запуску, зато падение шага не убивает прогон: конвейер пишет его в список упавших и идёт дальше, а материал добирается следующим запуском. Реальные секунды сняты с лога прогона 22 октября:

Шаг

Что делает

Секунды

collect-news

опрос лент, отбор кандидатов

29 (17 кандидатов)

consolidate-sources

поиск дополнительных источников по сырому заголовку

3

adapt-ru

заголовок 45–65 знаков и лид 200–450 знаков

6

register-topics

номер темы и повторная сверка на дубли по всему журналу

3

enrich-anchors

факт в 8–14 слов на каждый источник (анкор вместо голой ссылки)

17

enrich-facts

таблица фактов, «что это значит», «что из этого полезно вам»

17

check-facts

проверка утверждений по тексту первоисточника

14

fact-heal

автопочинка неудачных проверок до публикации

15

make-news-img

картинка к материалу в локальной модели

68

faq-gen

четыре вопроса-ответа для FAQ-разметки

6

feed-gen

сборка страницы, лента, карта сайта, RSS

2

Итого: старт в 17:00:02, материал зафиксирован в 17:03:04. Дольше всего идёт не языковая модель, а картинка — 68 секунд на локальной видеокарте. Сами LLM-шаги вместе дают 46 секунд.

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

Четыре обращения к модели и их контракты

Модель в конвейере не «пишет статью». Она вызывается четыре раза, и каждый вызов — узкая задача с жёстким форматом ответа. Так дешевле, стабильнее и, главное, каждую часть можно перепроверить отдельно.

Вызов

Задача

Лимит токенов

Что вернулось на замере

adapt-ru

заголовок и лид из англоязычного материала

900

~950 токенов, 3–4 с

enrich-anchors

по факту на каждый источник

300

177 токенов, 1,9 с

enrich-facts

факты, вывод, «полезно» (300–500 слов)

2 600

1 054 токена, 5,7 с

faq-gen

четыре пары «вопрос-ответ»

1 200

286 токенов, 2,7 с

На материал уходит 2,5–4,5 тысячи токенов — это доли цента при текущих тарифах, так что узкое место конвейера не деньги, а качество контракта. Поэтому факты принимаются только в виде [["Что случилось","…"],["Цифры","…"]], а если модель вернула набор без обязательных полей, шаг не пропускает его дальше, а переспрашивает — три попытки с паузой.

Вызов модели — обычный HTTP из PowerShell, без SDK и без внешних библиотек:

$тело = @{
  model       = $Модель
  messages    = @(@{ role = "user"; content = $промт })
  temperature = 0.4
  max_tokens  = 2600
}
if ($Рез.JsonMode)      { $тело.response_format  = @{ type = 'json_object' } }
if ($Рез.ReasoningNone) { $тело.reasoning_effort = 'none' }
$body = $тело | ConvertTo-Json -Depth 6 -Compress

$req = [System.Net.HttpWebRequest]::Create($Рез.Url)
$req.Method = "POST"; $req.ContentType = "application/json"; $req.Timeout = 120000
$req.Headers.Add("Authorization", "Bearer $Ключ")
$bytes = [Text.Encoding]::UTF8.GetBytes($body)
$req.ContentLength = $bytes.Length
$rs = $req.GetRequestStream(); $rs.Write($bytes, 0, $bytes.Length); $rs.Close()
$sr = New-Object IO.StreamReader($req.GetResponse().GetResponseStream(), [Text.Encoding]::UTF8)
$ответ = $sr.ReadToEnd() | ConvertFrom-Json

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

Модель — не константа в пяти скриптах

Первая версия конвейера была честной и глупой: имя модели, адрес API и имя переменной с ключом были прописаны в каждом скрипте, который вызывает модель. Пять файлов, пять копий одной и той же тройки значений. Любая смена модели превращалась в правку пяти файлов, а забытый шестой (обычно это скрипт, который запускается раз в сутки) выяснялся по странному поведению в логах.

Сейчас есть один резолвер. Он читает настройки сессии, находит там провайдера и модель, проверяет, что модель вообще есть в списке /v1/models этого провайдера, берёт имя переменной с ключом из настроек и возвращает готовый набор: адрес, модель, ключ, а заодно два флага режима.

. (Join-Path $PSScriptRoot 'модель-сессии.ps1')
$Рез = Get-МодельСессии
$Модель = $Рез.Модель     # если не задана вручную параметром -Модель
# $Рез.Url, $Рез.КлючИмя, $Рез.JsonMode, $Рез.ReasoningNone

Внутри — кэш на десять минут, инвалидация по времени изменения файла настроек и фолбэк на провайдера по умолчанию, если в настройках пусто или модель не найдена в каталоге. Я поменял модель в настройках один раз и получил смену автора всех материалов без правки скриптов; параметр -Модель остался как ручное переопределение на один запуск.

Пять грабель, которые стоили вечера

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

1. Reasoning-модель съедает лимит и возвращает пустоту

Модель deepseek-v4.1-flash на шлюзе B.AI — рассуждающая: кроме content она возвращает reasoning_content, и этот блок считается внутри max_tokens. При лимите 70 токенов ответ пришёл с finish_reason: length, content пустой, а в reasoning — 287 знаков размышлений про то, как надо ответить. При 400 токенах картина та же: 1090 знаков размышлений и снова пустой ответ. Со стороны это выглядит как «модель сломалась», хотя она честно отработала лимит.

Лечится флагом reasoning_effort: 'none' — режим отключает блок размышлений, ответ приходит за 2–6 секунд, и лимит снова принадлежит содержанию. Значение low не помогает: размышления всё равно едят бюджет. Вывод для любых конвейеров: если провайдер отдаёт reasoning-блок, сначала посмотрите, входит ли он в лимит, и только потом увеличивайте max_tokens — иначе будете платить за размышления, которые не попадают в результат.

2. PowerShell не различает регистр в именах переменных

Классика, на которой я попался дважды. Первый раз — так:

$S = "E:\...\скрипты"                  # папка скриптов
foreach ($s in @($карточка.источники)) { $путь = Join-Path $S $s.url }

Потому что $S и $s для PowerShell — одна и та же переменная. Внутри цикла папка скриптов превратилась в объект-ссылку, и Join-Path упал с сообщением про диск с именем @{url=https.

Второй раз — тише и потому дороже. Скрипт вечерней сводки держал путь к своему кэшу в переменной $ФайлКэша. Резолвер модели, который подключается в том же файле через точку (dot-sourcing), объявляет свою переменную с тем же именем для кэша выбора модели. Подключение переписало путь, и текст сводки бодро записался в файл кэша модели, затерев его содержимое. Ошибки не было ни одной: запись прошла успешно, просто не туда. Нашлось это только после того, как я поставил в код временную печать пути — она показала имя файла модели вместо ожидаемого.

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

3. Файл .ps1 без BOM читается как CP1251

Запуск powershell.exe -File script.ps1 для файла без BOM в UTF-8 означает чтение в системной кодировке. В русской локали это CP1251, и весь текст с кириллицей внутри скрипта превращается в мусор — вплоть до ParserError на строке с сообщением. Причём интерактивный запуск того же файла работает: там кодировка определяется иначе. Я лечу это записью BOM после любой правки скрипта, и в половине случаев именно забытый BOM — причина «мистического» падения по расписанию, когда руками всё отлично.

4. Строка "$переменная:" не парсится

Сообщение Variable reference is not valid. ':' was not followed by a valid variable name — про то, что двоеточие после имени переменной в двойных кавычках трактуется как квалификатор диска. Строка вида "Итоги за $дата:" не компилируется; надо ${дата}:. Мелочь, но она прилетает именно в шаблонах сообщений, то есть в самом конце работы, когда всё уже почти готово.

5. Публикация без картинки — это тихая дыра

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

Согласование за десять минут, или Где всё-таки нужен человек

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

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

Выгрузка, карта сайта и поисковики

Деплой — FTP со сверкой MD5 по каждому файлу: сравнивать размеры бесполезно, правка вида ?v= в ссылке на стили не меняет длину файла, зато меняет содержимое. Перед выгрузкой сайт пересобирается целиком: лента, страницы, карта сайта, RSS, отдельная карта для новостей.

С поисковиками три канала, и все три идут после успешной выгрузки: карта сайта отправляется через API Search Console, отдельные адреса — через Indexing API (с паузой два дня на адрес, иначе квота сгорает на повторах), плюс IndexNow для Яндекса и Bing. Сбой любого из каналов не ломает деплой: уведомления складываются в очередь и отправляются следующим прогоном. Это не теоретическая аккуратность: API Вебмастера Яндекса лежит у меня тринадцатый день, очередь терпеливо копится, а сайт от этого не останавливается.

Отдельный урок про FTP-хостинг: листинг не показывает дотфайлы. .htaccess с правилами редиректов и заголовками безопасности в листинге не виден вообще — ни в коротком, ни в длинном формате, при том что файл существует и работает. Вывод: наличие скрытых файлов на таком хостинге проверяется запросом файла, а не листингом. Иначе однажды выгрузите сайт «как есть» и потеряете редиректы, не заметив этого.

Что показали цифры за две недели

Показатель

Значение

Опубликовано материалов

56 (из 61 карточки журнала)

Вечерних сводок

7, каждая со своим разбором дня

Скриптов в конвейере / строк

47 / ~9 700

Время прогона одного материала

~3 минуты

Расход токенов на материал

2,5–4,5 тыс.

Адресов в карте сайта / в индексе Google (23.10)

76 / 54 (71 %)

Оповещений IndexNow отправлено

45

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

Что бы я сделал иначе

Вынести модель в резолвер сразу, а не после того, как пять скриптов разошлись в поведении. Сделать все шаги идемпотентными с кэшем результата с первого дня: я пришёл к этому только на сводках, когда понял, что повторный прогон за прошедшую дату переписывает уже опубликованный текст, и любая отладка дёргает страницу, которую поисковик только что проиндексировал. И третье: не отдавать код 200 на */index.html. Внутренние ссылки шаблона вели на файл, а канонический адрес был без файла — через месяц поисковик насчитал тринадцать «страниц-вариантов» и прислал письмо о проблемах индексации. Лечится одной строкой в .htaccess, но обнаруживается поздно.

Итог

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

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Конвейер из 10 агентов без строчки кода: три промпта, 40 минут и прогон при зале09.710-07-2026
2ИИ-Автопилот: поток принятых задач вырос в тринадцать раз18.8231-07-2026
3Одна статья, 90 языковых версий и Google: как пет‑проект превратился в платформу09.9205-08-2026
4Алгоритм был правильным. Ошибка была в контракте графа-17.5713-08-2026
5Как заставить LLM выбирать осмысленные фрагменты из часовой расшифровки: почему «найди интересные моменты» не работает0528-06-2026
6Как я искал себе читалку и придумал новый формат электронных книг для изучающих язык0508-07-2026
7Как я искал себе читалку и придумал новый формат электронных книг для изучающих язык05.508-07-2026
8Как желание быстрее читать чужой код превратилось в войну с недетерминизмом LLM0528-06-2026
9Как оптимизировать инференс LLM: кеширование, время ответа и GPU-ресурсы011.508-07-2026
10Создаём DSL на C#: генерация кода, BlackSilence и Hot Reload04.907-10-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 9.11. Источник: habr.com.