DD · Наблюдаемость и отладка ночных прогонов: чтобы «утром сломалось, но где» стало «вот тут, вот почему»
Углублённый модуль поверх курса. Продолжает Модуль 4 («Иммунная система», §7.5 Observability) и Модуль 8.3 (панель), закрывает пробел №5 из редакторской вычитки курса (
COURSE-GAPS.md). Тон и метки честности — как в основном курсе: 🟢 проверено по первоисточнику, 🟡 иллюстративно/эвристика/первоисточник не найден, 🔴 исправлено.
0. Зачем это отдельный модуль
Guardrails (Модуль 4) решают «когда систему нужно остановить». Устойчивость роя (DD-ustoichivost-roya.md) решает «что делать, когда сломалось — не удвоить ущерб». Этот модуль — третья, недостающая нога: как понять, что именно произошло, когда ты просыпаешься и видишь на панели «канал lightning: 3 из 12 видео не вышли». Без него полная ночная автономия — это не автономия, а лотерея: система работала сама, но ты не можешь проверить, ПОЧЕМУ она приняла то или иное решение, и вынужден либо слепо доверять, либо откатывать всё вручную.
Медицинская аналогия для всего модуля: анамнез. Врач, который видит пациента впервые утром, не может лечить по одному симптому («температура 39») — ему нужна хронологическая карта: что кололи, когда, в какой дозе, как реагировал организм на каждом шаге. Без карты каждое утро — это диагностика с нуля вслепую. Логи ночного прогона — это ровно такая карта, только не для одного пациента, а для конвейера ⓪→⑨, который в эту ночь работал сам.
Второй кусок аналогии, отдельно важный для судейства (Модуль 2, judge panel): лабораторный анализ, который при пересдаче в тот же день иногда даёт другое число — не потому что прибор сломан, а потому что живая биохимия немного шумит даже в одинаковых условиях. LLM-судьи и критики устроены похоже: одна и та же «кровь» (промпт), тот же «прибор» (модель, temperature=0) — и всё равно возможен небольшой разброс. Это не баг, который надо «починить», а свойство, с которым нужно уметь работать (§4).
Третья причина, почему этот модуль стоит строить ДО, а не ПОСЛЕ включения полной автономии: без него веер MVP консультант-фабрики (Модуль 2, Урок 2.3; Модуль 9, Урок 9.3) физически нельзя будет отладить. Когда ночью параллельно работают 3-10 explorer-агентов, а не один линейный конвейер, «что-то сломалось» без сквозного correlation_id превращается в «что-то сломалось у КОГО-ТО из десяти, и логи всех десяти перемешаны в одних и тех же контейнерах» — задача расследования растёт не линейно, а комбинаторно. Наблюдаемость — это то, что делает параллелизм отлаживаемым, а не просто быстрым.
0.5 Мониторинг и наблюдаемость — это не два слова для одного и того же
По-человечески. Кардиомонитор у кровати пациента показывает пульс, давление, сатурацию — прибор честно скажет, ЧТО СЕЙЧАС происходит с телом, и запищит, если число вышло за норму. Но он не скажет, ПОЧЕМУ давление упало именно в этот момент — для этого нужен врач, который сопоставит показания монитора с историей болезни, назначениями и событиями последнего часа. Первое — мониторинг; второе — то, ради чего нужна вся карта пациента целиком, то есть наблюдаемость.
Как называется по-настоящему (English). Monitoring отвечает на вопрос «работает ли система прямо сейчас» — известные заранее метрики (латентность, доля ошибок, объём очереди), обычно на дашборде с графиками и порогами тревоги. Observability отвечает на вопрос «почему система повела себя именно так» — способность реконструировать ПРИЧИНУ поведения по данным, которые не обязательно были заранее предусмотрены как «метрика». Разница практическая, а не философская: мониторинг покажет Лучиану, что видео сгенерировалось за 40 минут вместо обычных 10 (число вне нормы); наблюдаемость — покажет, что причина именно в зомби-задаче, пиннящей слот аккаунта, а не в том, что провайдер стал медленнее в целом. 🟢 [это разделение — прямая формулировка из конспекта nadezhnost.md §7.5, ссылающегося на индустриальный источник reliability-cost-security.md, не изобретение курса].
Практический вывод для завода. Дашборд burn-rate/latency на портале :4700 (Модуль 8.3) — это мониторинг, он уже частично спроектирован. Всё, что описано ниже в этом модуле (structured logging, correlation ID, детерминизм, тихие сбои) — это слой НАБЛЮДАЕМОСТИ под этим дашбордом: без него панель честно покажет «что-то не так», но не даст ответить на следующий вопрос, который Лучиан задаст сам себе через десять секунд после того, как это увидит — «и что мне с этим делать прямо сейчас».
1. Structured logging: не print, а карточка, а не записка на салфетке
По-человечески. «Что-то пошло не так, кажется, около трёх ночи» — это записка на салфетке, не медицинская карта. Она не годится для расследования, потому что в ней нет структуры: кто пациент, какой был показатель, в каком отделении. Структурный лог — это карта: у каждой записи фиксированные поля, которые можно потом отсортировать, отфильтровать, сравнить между записями, а не читать глазами тысячи строк текста в надежде наткнуться взглядом на нужную.
Как называется по-настоящему (English). Structured logging — запись событий в машиночитаемом формате (обычно JSON, одна строка = одно событие), в противоположность print debugging / неструктурированному текстовому логу, где сообщение — это произвольная строка текста «для человека». Ключевое отличие не в том, что структурный лог «красивее», а в том, что он фильтруется и агрегируется программой, а не только читается глазами.
Разница на конкретном примере, до и после. Было (типичный print-стиль, который сейчас реально встречается в логах многих сервисов завода):
Starting generation for scene 3...
Retry 2
Error: 403 no accounts available
Стало (та же информация, но структурно):
{"ts":"2026-07-12T03:14:07Z","level":"info","service":"photo-video-generator","stage":"stage_visual_rules","correlation_id":"lightning:2026-07-12-run1","task_id":"lightning:visual:scene-3","event":"generation_started"}
{"ts":"2026-07-12T03:14:11Z","level":"warn","service":"photo-video-generator","stage":"stage_visual_rules","correlation_id":"lightning:2026-07-12-run1","task_id":"lightning:visual:scene-3","event":"retry","attempt":2}
{"ts":"2026-07-12T03:14:12Z","level":"error","service":"photo-video-generator","stage":"stage_visual_rules","correlation_id":"lightning:2026-07-12-run1","task_id":"lightning:visual:scene-3","event":"tool_call_failed","error_class":"capacity"}
На глаз вторая версия многословнее — но именно она отвечает на вопрос «покажи мне ВСЁ, что произошло со сценой 3 канала lightning этой ночью» одной командой фильтрации, а не ручным чтением. Первая версия отвечает на этот же вопрос только если читать файл целиком глазами и держать контекст в голове — что работает для одного сервиса и одного прогона, но рассыпается уже при двух параллельных задачах в одном контейнере.
Почему print/произвольный console.log — тупик для ночной автономии. Если сервис пишет console.log("генерация видео началась"), а через час — console.log("ошибка, повтор"), то узнать, к какой ИМЕННО генерации относится вторая строка, можно только по порядку появления в файле — что рушится, как только в логе одновременно пишут два параллельных прогона (а на заводе Лучиана это норма: resource-governor уже даёт до 20 параллельных картинок и 5 видео одновременно). Текстовый лог из параллельного мира — это как если бы все врачи больницы писали заметки в один общий блокнот без имени пациента: технически всё записано, практически не восстановить, что к чему относится.
Минимальная схема, которая реально работает (расширение уже существующего формата decision-лога из nadezhnost.md §7.5 — те же поля, но обязательные для КАЖДОГО события, не только решений):
{
"ts": "2026-07-12T03:14:07Z",
"level": "error",
"service": "photo-video-generator",
"stage": "stage_visual_rules",
"correlation_id": "lightning:2026-07-12-run1",
"task_id": "lightning:visual:scene-3",
"event": "tool_call_failed",
"tool": "nano_banana_pro",
"attempt": 2,
"latency_ms": 4200,
"cost_usd": 0.0,
"error_class": "capacity",
"message": "403 no accounts available"
}
Пять полей обязательны ВСЕГДА, независимо от сервиса: ts (когда), service (кто пишет), level (насколько серьёзно — debug/info/warn/error), correlation_id (§2 — какой это прогон целиком) и task_id (какая именно задача внутри прогона). Остальное — по контексту события. Это не новая инфраструктура с нуля: на заводе уже есть почти такая же схема для контракта error-bank (08-contracts.md: generator, project_id, stage, error, traceback?) — задача не изобретать, а распространить ту же дисциплину на УСПЕШНЫЕ события, а не только на ошибки, и добавить два поля (correlation_id, task_id), которых сейчас не хватает.
Для любопытных. level — не декоративное поле, а фильтр по умолчанию: в проде обычно включён info и выше, debug — только когда осознанно расследуешь конкретный прогон (иначе объём логов взрывается, а полезной информации в них не прибавляется). Второе практическое следствие структурности — можно посчитать метрику ПРЯМО из потока логов без отдельной системы мониторинга: «сколько tool_call_failed с error_class=capacity за последний час» — это одна команда jq, а не ручной просмотр.
Практическая оговорка про диск (прямая связь с его собственным риском «диск на 91%», Модуль 11.3, этап 1). Структурные логи многословнее старого print — это плата за возможность их фильтровать. Без ротации они физически заполнят диск быстрее, чем текстовые. Минимум для Docker — задать json-file-драйверу лимит прямо в docker-compose.yml каждого сервиса:
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "5"
Это не «оптимизация на будущее», а обязательное условие ДО включения структурного логирования на всех 16+ сервисах разом — иначе решение проблемы наблюдаемости само создаст проблему места на диске, причём ровно того рода «тихую», о которой §5.
Живой якорь: зайди на любой контейнер завода и посмотри, что он сейчас пишет в stdout — docker logs --tail 20 channel-hq. Если там произвольный текст без фигурных скобок {} — это и есть тот самый print, который предстоит заменить.
2. Correlation ID: один браслет на пациента через всю больницу
По-человечески. Пациента госпитализируют — на запястье надевают браслет с номером. Он идёт на рентген, в лабораторию, к хирургу, в реанимацию — на КАЖДОМ бланке из этих отделений стоит один и тот же номер браслета. Только благодаря этому номеру утренний обход может собрать пять разрозненных бумажек в одну историю одного человека, а не гадать, какой анализ крови к какому пациенту относится.
Как называется по-настоящему (English). Correlation ID (иногда trace ID / request ID) — уникальный идентификатор, который присваивается задаче в момент её создания и передаётся БЕЗ ИЗМЕНЕНИЙ через каждый сервис, который эту задачу трогает. Смежное понятие — distributed tracing (распределённая трассировка): реконструкция полного пути запроса через несколько независимых сервисов по этому общему ID.
Прямая связь с уже спроектированным контрактом. В DD-ustoichivost-roya.md §2 уже предложена схема task_packet/result_packet с полями task_id, idempotency_key, parent_task_id. Correlation ID — это НЕ новое поле, а уточнение роли уже существующего task_id верхнего уровня: у прогона lightning:2026-07-12-run1 (весь конвейер ⓪→⑨ для одного видео) есть один correlation_id, а у каждой стадии внутри (lightning:script:..., lightning:visual:scene-3, lightning:voice:...) — свой task_id, но ОДИН И ТОТ ЖЕ correlation_id, унаследованный от родителя. Правило распространения простое и обязано быть механическим (закон №3 — скрипт, а не ИИ-решение на каждом шаге): каждый сервис, вызывая следующий, обязан ПРОКИНУТЬ correlation_id дальше, никогда не генерировать новый для того же прогона.
channel-hq (создаёт correlation_id="lightning:2026-07-12-run1")
→ stage_script (task_id=".../script", correlation_id=та же)
→ memory-bank.read (task_id=".../script/mem-read", correlation_id=та же)
→ stage_visual_rules (task_id=".../visual", correlation_id=та же)
→ fastgen /generate (task_id=".../visual/scene-3", correlation_id=та же)
→ error-bank.report (correlation_id=та же — попадает в тот же анамнез!)
→ stage_voice ...
Практическое правило внедрения на существующем стеке, без переписывания API. Не нужен отдельный трассировочный сервис (Jaeger/Zipkin и подобные — это оверинжиниринг для объёма Лучиана прямо сейчас, 🟡 архитектурный совет, не измеренный факт). Минимум: (1) correlation_id кладётся в HTTP-заголовок при каждом межсервисном вызове (например, X-Correlation-Id) — это тот же паттерн, что уже используют holder-конвенция "<gen>:<job>" и брокер медиа-задач (jobId) из 08-contracts.md, просто явно вынесенный на уровень заголовка, а не спрятанный внутри тела запроса; (2) каждый сервис при логировании ОБЯЗАН прочитать этот заголовок и вставить его в каждую строку своего структурного лога (§1); (3) error-bank.report уже принимает project_id — добавить туда же correlation_id как обязательное поле, а не опциональное.
Частая ошибка внедрения, о которой стоит знать заранее. Correlation ID — это НЕ то же самое, что project_id или channel. project_id называет ПАЦИЕНТА (какой конкретно ролик/канал), а correlation_id называет КОНКРЕТНЫЙ ВИЗИТ этого пациента — один и тот же project_id за свою жизнь проходит через несколько отдельных прогонов (первая генерация, повторная генерация после правки, ре-аплоуд после страйка), и у КАЖДОГО такого прогона должен быть свой correlation_id, иначе трассировка одной конкретной ночной попытки смешается с историей всех остальных попыток того же видео за месяц — ты получишь анамнез всей жизни пациента вместо анамнеза одного текущего визита, когда на самом деле нужен именно второй.
Живой якорь: открой 08-contracts.md и найди контракт error-bank.report — увидишь ровно то поле (project_id), рядом с которым по этому рецепту должен появиться correlation_id.
Соотношение с уже существующим jobId медиа-брокера. У resource-governor уже есть свой идентификатор — jobId в ответе POST /media/image//media/video ({jobId}), и это НЕ конкурент correlation_id, а вложенная сущность на уровень ниже: jobId называет один конкретный платный вызов генератора (как номер конкретного анализа крови), correlation_id — весь визит целиком (как номер браслета пациента). Правильная связь — оба поля живут РЯДОМ в одной строке лога: {"correlation_id": "lightning:2026-07-12-run1", "job_id": "img-8271", ...}. Тогда по correlation_id собирается вся ночь, а по job_id внутри неё — конкретный платный вызов, если спор именно про списанный кредит, а не про весь прогон.
3. Как читать лог/трассировку одного прогона на практике
По-человечески. Утренний обход не читает подряд ВСЮ картотеку больницы — врач называет номер браслета конкретного пациента, и ему приносят только его записи, отсортированные по времени. Ровно так должна работать отладка ночного прогона: не «открыть все логи всех 16 контейнеров и читать», а «дать один correlation_id — получить хронологическую ленту событий именно этого прогона».
Прежде чем открывать логи — важно понимать порядок действий, а не хвататься за первый попавшийся контейнер. Утренний обход всегда начинается с ОДНОГО вопроса: «какой прогон вообще нужно расследовать» — панель (Модуль 8.3) или Telegram-алерт называет correlation_id, и только после этого имеет смысл заходить в терминал. Открывать логи наугад, без известного correlation_id, — то же самое, что листать всю картотеку больницы в поисках одного пациента вместо того, чтобы посмотреть его номер на браслете.
Живой рецепт на существующем стеке (без новой инфраструктуры — Docker уже пишет структурные JSON-логи, если сервис печатает их в stdout):
# собрать полную ленту одного прогона из ОДНОГО контейнера
docker logs channel-hq 2>&1 | jq -c 'select(.correlation_id == "lightning:2026-07-12-run1")'
# то же самое сразу по НЕСКОЛЬКИМ контейнерам, отсортировано по времени —
# это и есть распределённая трассировка "на коленке", без Jaeger
for c in channel-hq photo-video-generator moss-lab memory-bank; do
docker logs "$c" --since 24h 2>&1 | jq -c --arg c "$c" \
'select(.correlation_id == "lightning:2026-07-12-run1") + {container:$c}'
done | jq -s 'sort_by(.ts)'
Вторая команда — это буквально «утренний обход» на минимальной инфраструктуре: без единой строчки нового кода на заводе, только требование §1-§2 (структурный лог + сквозной ID). Дальше это можно один раз обернуть в скилл (trace-run.sh <correlation_id>) — прямое применение его же правила «повторил дважды → оформи скиллом».
Отдельно про сам traceback внутри одной записи — не путать трассировку прогона (§2-3) со стек-трейсом одной ошибки. Контракт error-bank.report уже принимает необязательное поле traceback — это ДРУГОЙ уровень детализации, чем цепочка событий по correlation_id: не «что происходило между сервисами», а «через какие именно строки кода ВНУТРИ одного сервиса прошёл вызов, прежде чем сломаться». Правило чтения (DD-azbuka.md §8) остаётся в силе и здесь: искать не первую попавшуюся строку с Error, а самую содержательную фразу ошибки и код вокруг неё, направление чтения (сверху вниз или снизу вверх) зависит от языка сервиса. Разница в контексте ночного прогона в том, что traceback без correlation_id рядом почти бесполезен для мультиагентного расследования: он скажет, ГДЕ в коде сломалось, но не скажет, какому именно из параллельных прогонов эта ошибка принадлежит — оба поля нужны вместе, одно без другого закрывает только половину вопроса «что случилось этой ночью».
На что смотреть в собранной ленте, по порядку (метод, а не хаос):
1. Первая строка с "level":"error" — не последняя, а самая ранняя: как и в §8 «Азбуки» (DD-azbuka.md), дальше почти всегда идёт лавина повторов одной и той же первопричины, а не десять разных проблем.
2. Разрыв во времени между соседними событиями больше, чем обычно занимает эта стадия — сигнал зависшего/забытого шага (см. §5 про тихие сбои), даже если ошибки как таковой нет.
3. attempt > 1 рядом — сколько раз стадия уже пыталась, прежде чем сломаться (см. DD-ustoichivost-roya.md §4 про retry-политику) — если attempt дошёл до максимума прямо перед последней записью, это Loop of Death, а не разовый сбой.
4. Совпадение correlation_id в error-bank, resource-governor и логе конкретного генератора одновременно — если событие видно только в ОДНОМ месте, а не сквозной цепочкой, это первый признак, что propagation (§2) где-то разорвался, и это баг наблюдаемости, а не баг бизнес-логики.
Куда это дальше растёт (не строить сейчас, но знать словарь). Индустриальный термин для «дашборда со всеми трассами кликабельно» — trace viewer / APM (Application Performance Monitoring, например Jaeger, Grafana Tempo, Honeycomb). Для его объёма (десятки видео в сутки, не миллионы запросов) это преждевременная сложность 🟡 — jq-скрипт выше закрывает 90% практической потребности за один вечер работы, тогда как разворачивание Jaeger — это отдельный сервис, ещё один контейнер, ещё одна точка отказа. Правильный момент для апгрейда — когда ручная трассировка через jq начнёт занимать больше 10-15 минут на инцидент регулярно, не раньше.
Разбор одной вымышленной, но типичной ночи — чтобы метод не остался абстракцией. Утром на панели: канал lightning — 9 из 12 видео опубликованы, 3 в статусе «не завершено». Расследование по методу выше, а не по наитию:
- Берёшь
correlation_idиз панели для первого зависшего видео — панель обязана его показывать именно потому, что теперь он есть в каждом событии (§2). trace-run.sh lightning:2026-07-12-run4собирает ленту из всех контейнеров. Самая ранняяerror-запись:stage_voice,event: tool_call_failed,error_class: capacity,attempt: 3.- Следующая по времени запись с тем же
correlation_id— неerror, а полное молчание на 42 минуты, хотя таймаут стадии — 10 минут. Это уже не «ошибка», а тихий сбой (§5): стадия не упала явно, она просто перестала подавать признаки жизни. - Проверка соседнего сервиса:
docker ps | grep moss-lab— контейнер в списке есть, но в статусеunhealthy. Причина, судя по совпадению по времени с предыдущимcapacity-отказом, — исчерпание слотов пулаvoice(concurrency=1, см.DD-ustoichivost-roya.md§5), а не поломка самого сервиса. - Диагноз собирается не из одной строки, а из последовательности: пул
voiceбыл занят дольше обычного → третья попытка исчерпала retry-лимит → стадия должна была явно перейти вfailed, но осталась вrunningбез heartbeat — то есть баг НЕ в озвучке, а в отсутствии postcondition-перехода в явный отказ после исчерпания попыток (прямая связь с §5 и с рецептом §6, пункт 5).
Три оставшихся видео из двенадцати расследуются той же последовательностью команд за минуты, а не заново с нуля — ровно то преимущество метода, о котором раздел 0.
Управление уровнем детализации во время активного расследования. Держать debug-уровень включённым постоянно на всех 16+ сервисах — верный способ утопить самого себя в объёме и одновременно приблизить проблему диска (§1). Правильный паттерн — точечный, не глобальный: пока конкретный correlation_id активен и подозрителен, можно временно попросить ИМЕННО этот прогон логировать подробнее (например, передав в task_packet флаг debug:true, который сервис читает и на его основании поднимает уровень только для событий с этим correlation_id), не трогая уровень логирования всего сервиса целиком для всех остальных параллельных задач. Это тот же принцип избирательности, что и в матрице риска Модуля 4 — не «включить всё» и не «выключить всё», а решение по конкретному случаю.
4. Детерминизм и воспроизводимость: почему одна и та же «кровь» иногда даёт разный анализ
По-человечески. Пересдай один и тот же анализ крови дважды подряд в одной лаборатории — числа могут чуть отличаться, хотя пациент, прибор и методика одни и те же: живая биохимия и сама измерительная система немного шумят. Разумный врач не паникует от разницы в третьем знаке после запятой, но насторожится, если разница меняет ДИАГНОЗ. LLM-судья или evaluator-агент (Модуль 2, Модуль 4 §4) — устроен похоже: даже при, казалось бы, «одинаковых» условиях он не гарантированно выдаст один и тот же вердикт дважды.
Как называется по-настоящему (English). Determinism — свойство системы всегда давать одинаковый результат при одинаковом входе; reproducibility — способность ВОСПРОИЗВЕСТИ конкретный прошлый результат позже, по записанным условиям. Temperature — параметр «случайности» выбора следующего слова у модели (0 = всегда самый вероятный токен, «жадный» выбор; выше — больше разнообразия). Seed — число, фиксирующее конкретную реализацию генератора случайности, чтобы при повторном запуске с тем же seed получить тот же путь выбора.
Ключевой факт, который стоит знать ДО того, как строить критика/судью на temperature=0 как на гарантии: даже temperature=0 НЕ гарантирует 100%-но одинаковый ответ модели при повторном запросе. 🟢 [Anthropic прямо пишет в своей документации, что «even with temperature 0.0, the results will not be fully deterministic» — источник: vincentschmalbach.com со ссылкой на документацию Anthropic]. Причины — не «баг», а следствия того, КАК современные модели физически считаются: батч-инференс на GPU (несколько запросов пользователей считаются вместе, и то, с кем именно твой запрос попал в один батч, слегка меняет порядок операций), неассоциативность операций с плавающей точкой на параллельном железе (порядок сложения чисел меняет результат в последнем знаке — а если вероятности двух токенов почти равны, такая мелочь может поменять победителя), и у моделей с архитектурой Mixture-of-Experts — конкуренция токенов за «мощности» одного и того же эксперта при высокой нагрузке. 🟢 [эти три причины — задокументированное свойство batched GPU-инференса и MoE-роутинга, не гипотеза].
Практический эквивалент system_fingerprint для Claude, если своего аналога нет. Раз явной метки версии бэкенда, как у OpenAI, у Claude API нет, её функцию должен взять на себя сам лог: фиксировать полное имя модели (например, claude-sonnet-5-20260115, а не просто «sonnet» — если версия модели незаметно обновится у провайдера, старые вердикты критика останутся привязаны к конкретной версии, под которой были вынесены) и дату вызова. Это дёшево сделать сейчас (одно поле в схеме §1) и дорого добавить задним числом, когда уже накопится месяц вердиктов без этой метки и появится вопрос «а судья тогда и судья сейчас — вообще одна и та же модель?».
Параметр seed в API OpenAI — тоже НЕ полная гарантия, а «лучшее усилие». Официальная документация прямо формулирует это как «best effort to sample deterministically» и отдельно вводит system_fingerprint — метку версии бэкенда, которую нужно сверять между запросами: если она изменилась, воспроизводимость не обещана даже с тем же seed. 🟢 [источник: OpenAI Cookbook, «Reproducible outputs with the seed parameter»]. Отдельная деталь для его стека: у Claude API нет публичного параметра seed (в отличие от OpenAI) — попытка передать его через OpenAI-совместимые обёртки прямо документирована как неподдерживаемая. 🟢 [подтверждено по официальной документации Claude Platform и открытому issue в litellm о том, что параметр seed для Anthropic не поддерживается]. Значит, для мозга завода (подписка Claude, claude -p) инструмент «зафиксировать seed и получить точную копию прошлого ответа» просто недоступен как таковой — воспроизводимость нужно строить НЕ на детерминизме модели, а на записи входов и выходов (см. ниже), что даёт документированность результата, а не гарантию его повторения.
Что это значит практически для judge panel и критика-публикатора (Модуль 2, Модуль 4 §4). Три следствия, которые стоит заложить в архитектуру прямо сейчас, а не открывать методом проб:
1. Небольшой разброс вердикта на границе порога — это не поломка критика, а ожидаемый шум измерительного прибора. Если чек-лист публикации даёт pass при score 0.71 и fail при 0.69 на двух подряд прогонах ОДНОГО и того же видео — правильная реакция не «чинить критика», а поставить буферную зону вокруг порога (например, 0.65–0.75 → эскалация человеку, а не автоматическое решение в любую сторону), совсем как лаборатория не бьёт тревогу на разницу в третьем знаке.
2. Для действительно дорогих решений (не рядовое видео, а, например, финальный отбор в турнире веера MVP, Модуль 2, Урок 2.3) — гони критика несколько раз и агрегируй, а не доверяй одному прогону. Индустриальное имя приёма — self-consistency / majority voting: N независимых прогонов одного и того же судейского промпта, финальное решение — по большинству или медиане, а не по единственному ответу. Это прямое расширение уже спроектированного «конструктора судейской панели» (Модуль 2.3) — панель из разных углов ОДНОВременно решает и задачу разнородности критериев, и задачу устойчивости к шуму одного прогона.
3. Раз воспроизвести момент решения кнопкой нельзя (нет seed) — записывай момент решения целиком. Практический эквивалент воспроизводимости для системы без гарантированного детерминизма — это НЕ «повторный запуск даст то же самое», а «у меня есть точная запись того, что произошло»: полный текст промпта критика, версия модели, ВСЕ входные артефакты (ссылка на скрипт/видео/скриншот, не пересказ), сырой ответ модели и итоговый вердикт — всё это одной строкой в структурный лог (§1), с тем же correlation_id. Тогда при споре «почему критик пропустил это видео» не нужно гадать или пытаться воссоздать условия — нужно просто прочитать запись.
{
"ts": "2026-07-12T05:02:11Z",
"level": "info",
"service": "critic-evaluator",
"correlation_id": "lightning:2026-07-12-run1",
"task_id": "lightning:critic:final",
"event": "judge_verdict",
"model": "claude-sonnet-5",
"temperature": 0,
"checklist_version": "v3",
"input_artifact_ref": "projects/lightning-01/final.mp4",
"score": 0.71,
"verdict": "pass_borderline",
"criteria_scores": {"hook": 0.9, "sync": 0.8, "facts": 0.6, "visual": 0.75},
"raw_reasoning_ref": "s3://.../critic-reasoning-2026-07-12-run1.txt"
}
Короткая сводка выбора инструмента под ставку решения (чтобы не переусложнять там, где не нужно):
| Ставка решения | Что делать | Аналогия |
|---|---|---|
| Рядовое видео, порог не на границе | Один прогон критика, temperature=0, лог входа/выхода | Обычный анализ, число далеко от нормы — врач не пересдаёт |
| Рядовое видео, score рядом с порогом | Буферная зона → эскалация человеку, не авто-решение | Число на границе нормы — врач смотрит внимательнее, не гадает |
| Финал турнира MVP / дорогая публикация | N независимых прогонов судьи, решение по большинству (self-consistency) | Спорный диагноз — консилиум, не мнение одного врача |
Оговорка про токеномику (Модуль 5), чтобы приём не превратился в новый источник раздутых расходов. Self-consistency в N прогонов буквально означает N× стоимость этого конкретного решения — это оправдано для финала турнира MVP или публикации на репутационно значимый канал, но НЕ должно стать привычкой «на всякий случай» для рядового видео из ежедневного потока: правило то же, что и везде в курсе — дорогой приём применяется точечно, там, где цена ошибки выше цены нескольких лишних вызовов, а не как настройка по умолчанию для всего конвейера.
Живой якорь: попроси Claude Code дважды подряд оценить один и тот же артефакт (например, один и тот же скрипт видео) по одному и тому же чек-листу в двух отдельных чистых сессиях — сравни оценки. Если они разошлись хоть немного при одинаковом промпте — это не глюк, а живая иллюстрация всего раздела 4, увиденная своими глазами, а не прочитанная как утверждение.
5. Тихие сбои: болезнь без симптомов опаснее очевидной ошибки
По-человечески. Пациент, который кричит от боли, УЖЕ получает помощь — симптом сам зовёт врача. Опаснее всего болезнь, которая не болит и не показывает внешних признаков, пока не станет слишком поздно что-то делать — именно поэтому в медицине существуют скрининги и плановые чекапы, а не только реакция на жалобы. Программный аналог: сервис, который не упал, не написал error, а просто тихо сделал не то или ничего — с точки зрения мониторинга «всё зелёное», хотя по факту видео не вышло, скрипт пустой, а факт-чек молча пропущен.
Как называется по-настоящему (English). Silent failure — отказ без явного сигнала об ошибке: код возвращает 200 OK с пустым или мусорным содержимым, поток данных тихо обрывается, стадия помечает себя done, хотя реальный артефакт не создан. Отличие от обычного отказа принципиальное: обычная ошибка сама зовёт на помощь (алерт, error-лог, упавший статус); тихий сбой никого не зовёт — узнать о нём можно только явно проверив РЕЗУЛЬТАТ, а не статус вызова.
Где на заводе это уже случалось — конкретный прецедент, а не гипотеза. Грабля из 06-errors-and-gotchas.md, разобранная и в DD-azbuka.md: proxyExecute для IG Stories не имел поля .successful, и код считал успехом сам факт «вернулся какой-то id» — по сути, «ответ пришёл» приняли за «задача реально выполнена», хотя это разные вещи. Внешне — никакой ошибки, id есть, всё выглядит штатно; по факту Stories не выходили. Это учебниковый silent failure: тихий сбой почти всегда прячется именно в месте, где код путает «получил ответ» с «получил ПРАВИЛЬНЫЙ ответ».
Как ловить это методом, а не удачей — три инструмента, дополняющих друг друга:
- Postcondition-проверка вместо доверия статусу. После КАЖДОЙ стадии, которая создаёт артефакт, — явная проверка самого артефакта, не только кода ответа: у видео есть длительность больше 0 и она совпадает по порядку величины с ожидаемой; у сгенерированного скрипта не пустая строка и не повтор промпта слово в слово; у SEO-пакета все обязательные поля непусты. Это прямое расширение уже спроектированного
result_packet(DD-ustoichivost-roya.md§2) — полеstatus:"done"должно ставиться ТОЛЬКО после того, как postcondition-проверка прошла, а не сразу, как только внешний API отдал200.
# псевдокод — то же правило можно приложить к ЛЮБОЙ стадии конвейера,
# меняется только сама проверка, не структура вызова
def finalize_stage(task_id, correlation_id, artifact_path):
if not artifact_exists(artifact_path):
return mark_failed(task_id, correlation_id, reason="artifact_missing")
if get_duration_seconds(artifact_path) <= 0:
return mark_failed(task_id, correlation_id, reason="empty_artifact")
# только после проверки — статус done, не раньше
return mark_done(task_id, correlation_id, artifact_path)
Механический скептицизм в одну строку кода стоит дешевле, чем ночь тихой генерации мусора, которую утром примут за успех.
2. Heartbeat / «часы идут» вместо «задача жива». Стадия, которая обязана закончиться за 10 минут, а идёт 40 без единой новой строки лога (не ошибки — вообще НИЧЕГО), — это тихий сбой типа «зависла» даже без единого error. Возраст последней записи лога по этому task_id — тот же сигнал, что уже предложен в DD-ustoichivost-roya.md §3 для чекпоинтов («висит running дольше таймаута × 2 → needs_recovery»), только применённый не к статус-машине, а к самому потоку логов: если по correlation_id за N минут не появилось ни одной новой строки, а стадия не помечена done, — это и есть сигнал тревоги, причём именно тот случай, когда полагаться на «нет ошибки = всё хорошо» опаснее всего.
3. Регулярный скрининг задним числом, не только реакция в моменте. Раз в сутки — механический (не LLM, закон №3) обход завершённых done-задач за ночь: сколько видео реально появилось на диске против того, сколько стадий отчиталось done; совпадает ли количество файлов обложек с количеством одобренных видео. Расхождение — прямой индикатор тихого сбоя где-то в цепочке, даже если ни одна отдельная стадия не подала признаков проблемы. Это ровно тот же принцип, что и плановый чекап у здорового на вид пациента.
Смежная гигиена, которую легко упустить именно потому, что структурные логи стали подробнее. Чем детальнее лог, тем выше риск случайно записать в него то, чего там быть не должно, — полный .env при печати printenv для отладки, необрезанный сырой ответ провайдера с ключом внутри URL, персональные данные из заявки клиента в консультант-фабрике. Правило то же, что для .env в DD-азбуке («секреты — только в .env, никогда в код/репозиторий/чат с ИИ») — расширить на логи буквально: значения полей вроде api_key, token, oauth в структурном логе маскируются (sk-***) на уровне самой функции логирования, а не оставляются на совесть разработчика в моменте написания конкретной строки.
Метрика для калибровки ожиданий, а не повод для паники. Частота отказов вызовов инструментов (tool calling) в проде оценивается практиками в 3–15% 🟡 [оценка из полевых кейс-стади, а не измерение конкретно завода Лучиана — на 8+ стадийном конвейере это означает, что штатный retry-лимит обязателен, а не «если повезёт»]. Это не про тихие сбои напрямую, но задаёт масштаб: даже при идеальной наблюдаемости часть вызовов будет падать явно — задача этого раздела не довести отказы до нуля, а гарантировать, что КАЖДЫЙ отказ (явный или тихий) оставляет след, который можно прочитать утром.
6. Собираем в один рецепт под его docker-конвейер
Ничего из перечисленного выше не требует новой инфраструктуры, нового языка или нового контейнера — весь модуль укладывается в изменение СХЕМЫ данных (что пишется в лог, какое поле передаётся дальше) и пары небольших скриптов поверх уже работающего resource-governor/error-bank/docker. Это важно проговорить отдельно, потому что «наблюдаемость» в общей литературе часто подаётся как повод развернуть тяжёлый APM-стек — для его объёма это неверный первый шаг: сначала дисциплина в существующих логах, и только когда объём реально перерастёт jq-скрипты (§3), имеет смысл разворачивать что-то тяжелее. Минимальный набор изменений, который реализует весь модуль на СУЩЕСТВУЮЩЕМ стеке — без нового сервиса, без Jaeger, без миграции:
- Каждый сервис печатает структурный JSON в stdout (§1) — Docker УЖЕ это подхватывает штатным
json-file-драйвером, дополнительной инфраструктуры не требуется. correlation_idгенерируется РОВНО ОДИН РАЗ, на входе в конвейер (channel-hq, при старте прогона), и передаётся дальше HTTP-заголовком (§2); каждый сервис обязан включить его в каждую строку своего лога.error-bank.report(уже существующий контракт) дополняется полемcorrelation_id— это одна строка изменений в схеме, а не новый сервис.- Скрипт
trace-run.sh <correlation_id>(§3) — обёртка надdocker logs+jqпо всем контейнерам сетиfactory; один раз написать, дальше — «живой якорь» на каждое утро. result_packetкаждой стадии получает обязательную postcondition-проверку ПЕРЕДstatus:"done"(§5, пункт 1) — не полагаться на код ответа внешнего API.- Judge/critic-логи (§4) пишутся с полным
raw_reasoning_refи версией модели/чек-листа — это НЕ детерминизм, а страховка на случай спора о вердикте. - Ежесуточный механический скрипт-скрининг (§5, пункт 3): число
doneв логах против числа реальных файлов на диске — расхождение = алерт в Telegram.
Где здесь деньги. Каждый час, который сейчас уходит на ручное «а что вообще случилось ночью» — это час, отвоёванный у роли человека-клея (боль №1 из профиля), и прямая предпосылка для того, чтобы доверить системе десятки каналов и веер MVP консультант-фабрики без риска молча потерять деньги на невидимом сбое. Отдельно: postcondition-проверки и correlation ID напрямую защищают от того самого сценария, которого боится Лучиан — «монстр, кушающий $1000 в минуту» тихо, без единого явного error в логе, потому что именно ТИХИЕ сбои (зомби-задачи, зависшие поллинги, повторные платные вызовы без дедупликации) исторически стоили заводу реальных денег (MOSS ~$38/сутки вхолостую, зомби FastGen), а не громкие явные падения — те как раз сами себя останавливали. Есть и прямой торговый аргумент: когда режим консультант-фабрики дойдёт до продажи автоматизаций чужим бизнесам (Модуль 10.1), «покажи мне точную карту того, что твоя система делала этой ночью и почему» — это ровно тот вопрос, который задаст скептичный клиент из «скучной индустрии» (недвижимость, стройка, финансы) перед тем, как заплатить; без сквозной трассировки ответить на него нечем, кроме «доверься мне».
Живой якорь на весь модуль: после первого же реального ночного сбоя — не чини руками по наитию, а сначала пройди метод целиком: возьми correlation_id, собери ленту (§3), найди самую раннюю запись, реши — это явная ошибка (§3), шум измерения (§4) или тихий сбой (§5). Если через 15 минут диагноз ещё не ясен — это сигнал, что где-то в цепочке §1-§2 пропущено звено, а не повод копать глубже вслепую.
Итог модуля в одной фразе. Наблюдаемость не делает систему надёжнее сама по себе — она делает видимым то, что уже происходит, чтобы решения о надёжности (Модуль 4) и о судействе (Модуль 2) принимались по фактам, а не по интуиции. Без этого модуля остальные три опоры курса — guardrails, устойчивость роя, evals — работают вслепую: guardrails остановят операцию, но не объяснят почему; ретрай перезапустит стадию, но не покажет, какая именно причина повторяется из ночи в ночь; критик вынесет вердикт, но при споре не на что будет опереться. Анамнез — это то, что превращает три отдельных механизма в одну систему, которой можно доверять по частям, а не только целиком и вслепую.
Чек-лист внедрения
- Structured logging везде, минимум обязательных полей
ts/service/level/correlation_id/task_id— распространить существующую схемуerror-bankна успешные события, не только ошибки. - Один
correlation_idна весь прогон, генерируется единственный раз на входе в конвейер, передаётся HTTP-заголовком через каждый межсервисный вызов без исключений. error-bank.reportдополнить полемcorrelation_id— минимальное изменение существующего контракта.- Скрипт трассировки одного прогона (
trace-run.sh <correlation_id>поверхdocker logs+jq) — оформить как скилл после первого же ручного использования (закон «повторил дважды»). - Метод чтения ленты: искать САМУЮ РАННЮЮ
error-запись, смотреть на аномальные разрывы времени, проверятьattempt, сверять совпадениеcorrelation_idво всех местах, где он должен быть. - Осознать пределы детерминизма LLM: temperature=0 не гарантирует идентичный ответ; у Claude API нет параметра
seed; строить не «повторяемость по кнопке», а полную запись входа/выхода судейского вердикта. - Буферная зона вокруг порога критика вместо жёсткой границы pass/fail; для дорогих решений — несколько независимых прогонов судьи + агрегация (self-consistency), не один запрос.
- Postcondition-проверка перед
status:"done"для каждой стадии, создающей артефакт — не доверять коду ответа внешнего API как признаку реального успеха. - Heartbeat по возрасту последней лог-записи на
task_id, а не только по статус-машине — сигнал «тихо зависло», даже когда ошибки как таковой нет. - Ежесуточный механический скрининг (число
doneпротив реальных файлов на диске) — не LLM-аудит, обычный скрипт, отчёт в Telegram при расхождении.
Мини-проверка себя (3 вопроса, ответь вслух, потом сверься с подсказкой в скобках):
1. Панель показывает: видео не опубликовано, но ни одной строки error по нему нет. Что это и с чего начнётся расследование? (подсказка: тихий сбой — искать не ошибку, а разрыв во времени между последней записью лога и текущим моментом, по тому же correlation_id).
2. Критик дважды подряд оценил ОДИН И ТОТ ЖЕ ролик по одному промпту при temperature=0 и получил чуть разные числа. Это баг критика? (подсказка: нет — модель физически не гарантирует полный детерминизм даже при temperature=0; вопрос не «баг ли это», а «далеко ли число от порога решения»).
3. У видео lightning-04 было три отдельные попытки генерации за месяц (первая, две повторные после правок). Почему им нельзя присвоить один общий correlation_id на все три? (подсказка: correlation_id — это визит, project_id — пациент; смешение визитов ломает трассировку конкретной ночи).
Куда это в курсе
Прямое инженерное углубление Модуля 4 (§7.5 Observability — там объясняется ЗАЧЕМ нужна трассировка решений для замыкания стадии ⑨ LEARNING, здесь — КАК её физически собрать) и Модуля 8, Урок 8.3 (единая веб-панель — панель без сквозного correlation_id и структурных логов нечем будет наполнять содержательно, кроме голых чисел burn-rate без объяснения причины). Прямо продолжает Модуль 6, Урок 6.3 («Ночь без человека», где впервые упомянут silent failure без метода его ловить) и дополняет DD-ustoichivost-roya.md — тот модуль про то, что делать, когда сломалось (retry, чекпоинты, изоляция), этот — про то, как УВИДЕТЬ, что сломалось, и не спутать шум измерения с реальной проблемой при судействе (Модуль 2, Уроки 2.1 и 2.3, где введён verifier pattern и эхо-камера — детерминизм из §4 объясняет ещё одну причину, почему независимый прогон критика важен, помимо изоляции рассуждений). Питает этапы Модуля 11 (Roadmap): пункт (2) «структурированный лог решений в memory-bank» и пункт (5) «включить оркестратор с новой матрицей гейтов» технически невозможны без дисциплины из этого модуля — без неё оркестратор будет принимать решения, которые потом нельзя ни проверить, ни объяснить, а замкнутая LEARNING (Модуль 3.4) не сможет отличить «видео провалилось из-за плохой темы» от «видео провалилось из-за тихого сбоя на стадии рендера», что превратит обучение системы в обучение на шуме вместо сигнала. Закрывает пробел №5 из COURSE-GAPS.md целиком: и метод трассировки, и структурный лог решений, ранее упомянутый в roadmap только как пункт списка без объяснения механики.