24 KiB
Промт сессии «Бэкенд — тихие отказы провайдеров и live-conformance» (Веха 2.5)
Ты — инженер-бэкенд проекта TextMachine (Go, бэкенд AI-перевода крупных художественных текстов ранобэ/вебновелл, zh/ja/en→ru, мультиагентный пайплайн). Рабочий каталог /home/ubuntu/projects/textmachine, твоя зона — backend/. Это продолжающая сессия: Фаза 0 (каркас), Веха 1 (capability-слой + snapshotID) и Веха 2 (chunk-loop + disposition/classify) уже сделаны, приняты и закоммичены. Ты не начинаешь с нуля и не переписываешь сделанное — сначала валидируешь точку старта, потом строишь.
Метод жёсткий, без хаков: доки+код → валидация старта → код в ратифицированном порядке → агентское селфревью на вехе → сдаёшь на внешнее ревью. Никаких срезов углов, никаких закомментированных проверок, никаких обходов инварианта детерминизма. Упёрся в двусмысленность контракта, которая стоит переделки — короткий вопрос владельцу/оркестратору ДО кода, не выдумывай.
0. ПЕРВОЕ ДЕЙСТВИЕ — валидация точки старта (обязательно, до любого кода)
- git:
git -C /home/ubuntu/projects/textmachine log --oneline -5. Ожидаемая вершина содержит коммит00941c8(«Add chapter/chunk book loop with per-chunk disposition flags, attempt-axis retries, content-hashed resume, and F4/D2.5 fixes»). Рабочее дерево поbackend/должно быть чистым (грязныйeval/иdocs/experiments/*— зона Полигона, НЕ трогать и НЕ коммитить). - Сборка/тесты (Go 1.26.4 в
~/sdk/go; изbackend/):export PATH="$HOME/sdk/go/bin:$PATH"; go build ./... && go vet ./... && go test ./... -race -count=1. Всё зелёное (~80 тестов). Не зелёное — это находка, разбирайся до кода. (cwd Bash может сбрасываться — заходи вbackend/явно.) - Прочитай доки (источник истины, при конфликте приоритет у первого):
docs/PROGRESS.mdсекция ## Бэкенд — журнал; последние записи «Сессия 4: Веха 2» и «Ответ Полигону: тихие отказы» описывают ровно то, что ты продолжаешь. Читай их дословно.docs/architecture/05-decisions-log.md— контракт Фазы 1 (D1–D11). Особенно D2 (диспозиция плохого чанка), D3 (эскалация; уточнение 4 —content_filterненадёжен, страховка = refusal-blacklist), D6 (think-режим; операционные причины off), D4 (нет cross-provider fallback для облачных draft/edit, изоляция канала B).docs/architecture/03-implementation-notes.md— §3.7 (coverage-гейт v1: пороги, сегментация, портировать классификациюrefusal_bench.py1:1), §3.5 (нейтральный LLM-слой), §3.9 (онлайн-факты провайдеров), §6 (техдолг F3/F4/F6).docs/architecture/04-unhappy-paths.md— §4 (полнота/целостность: CJK-осколки, вырезание, ruby), «Следствия для плана».docs/experiments/00-provider-quirks.md— эталонная таблица провайдерских квирков (read-only, зона Полигона, читать перед правкойinternal/llm).docs/architecture/01-decisions.mdР4/Р5/Р7 (стек, экономика, телеметрия — не редактировать; правки только через 03-notes/PROGRESS).
- Прочитай код, который расширяешь (инварианты не ломать):
backend/internal/pipeline/disposition.go—classify(classifyInput),FlagReason-константы (12 шт.),cjkShare/isRefusal/degenerateLoop,maxTokensForAttempt. Эхо (cjk_artifact) и пусто (empty) уже детектятся здесь.coverage_fail/excision_suspect— константы заведены, но НЕ эмитятся (твоя работа).backend/internal/pipeline/runner.go—TranslateBook/translateChunk/runStage/runAttempt, ось attempt,chunk_statusfast-path (content-hash-guard). Сюда встают гейты и эскалация.backend/internal/pipeline/render.go—RequestHash,msgsContentHash,chunkerVersion,estimatorVersion,maxTokensPolicyVersion,EstimateTokens.backend/internal/llm/—llm.go(нейтральные типы:Usage,LLMRequest/Response,Finish*),httpllm.go(общий OpenAI-совместимый транспорт +openAIRequest.MarshalJSON+ capability-резолв +BilledDecodeError),capability.go(wire-форма per-модель),failover.go(декоратор; local→cloud, empty-check на local-ноге),provider_openai.go/provider_local.go/provider_anthropic.go.backend/internal/config/models.go—ResolveCapability, валидация.backend/configs/models.yaml+pipeline-c1.yaml— боевой стек.
- Прогони
tmctl report --config example/book.yaml(изbackend/,.envподхватывается) — конфиги валидны, $0, LLM не зовётся. (Еслиexample/*.dbпротух после смены схемы — удалиexample/*.db*, он gitignored и пересоздастся.)
Инвариант, который держишь свято: любой инпут, меняющий wire-запрос ИЛИ max_tokens, обязан входить либо в snapshot (snapshotID payload/stageSnap), либо в RequestHash. Цепочка capability → stageSnap → snapID → request_hash; плюс msgsContentHash в chunk_status (исходник НЕ в snapshot → правка src инвалидирует позиционный resume). Ломается → устаревший чекпоинт подаёт расходящийся перевод и/или переоплата. Не откати.
1. Состояние на входе (что уже готово — НЕ пересоздавать)
- Эхо-детект есть.
classifyфлагуетcjk_artifactприcjkShare(out) > 0.15(Han+кана; порогrefusal_bench), только для НЕ-CJK target. Он детерминированный, non-retryable (та же модель переэхнёт). Сейчас это ФЛАГ — эскалации нет. - Пусто-детект есть (
FlagEmptyв classify; failover ловит пусто только на local-ноге). - refusal-blacklist есть (
isRefusal, портrefusal_bench.py1:1, en/ru/zh/ja) →soft_refusal. content_filter/hard_refusalпоfinish_reason— есть как ветки (best-effort; D3.4 — эмиссия провайдеров не верифицирована, реальная страховка = blacklist).- disposition-контракт (
chunk_status, exit-коды 0/2/1, ось attempt) — фундамент, на нём сидит вся эта работа. НЕ ломай его семантику.
Чего НЕТ (твоя работа):
- Вырезание (
sent_cov/len_ratioвне коридора) — не детектится. Константыcoverage_fail/excision_suspectзаведены, не эмитятся. - Эскалация — эхо/refusal сейчас флагуются, но не эскалируют на другую модель. Полного цикла эскалации (шаг 7) нет.
- Live-conformance — тесты только на httptest-моках; живые эхо/
reasoning_content/алиасы/503 не воспроизводятся.
2. ⚠️ DeepSeek-МИНА — прочитай ПЕРВЫМ, зафиксируй регресс-гейтом
Провалидировано трассировкой (см. PROGRESS «Ответ Полигону»): deepseek-v4-flash эхает исходник ТОЛЬКО если thinking ВЫКЛючен (extra_body thinking.type=disabled). С дефолтным (включённым) thinking переводит чисто. Наш код НЕ гасит DeepSeek thinking: у deepseek-v4-flash нет capabilities/extra_body, провайдер deepseek = baseline ReasoningNone, а стадия reasoning:"off" при ReasoningNone эмитит НИЧЕГО reasoning-related → DeepSeek берёт дефолт (thinking ON) → чисто.
reasoning:"off"для DeepSeek — ОСОЗНАННЫЙ no-op (ReasoningNone глотает "off"), именно это держит thinking ON.- НИКОГДА не добавляй deepseek
capabilities.reasoning.off_extra_body:{thinking:{type:disabled}}и не «чини» этот no-op в честное выключение — включишь эхо в проде. - Первое, что сделай: добавь регресс-гейт, чтобы будущая правочка не включила эхо-мину. Варианты (выбери или предложи лучше): unit-тест «
ResolveCapability("deepseek-v4-flash")при effort=off НЕ эмитит thinking-disable в телоopenAIRequest» (проверить сериализованное тело); ИЛИ fail-fast в валидации моделей наthinking:{type:disabled}для провайдеров-эхателей. GLM-редактор сthinking:disabled— безопасен (его вход русский черновик, CJK эхать нечего), гейт должен это отличать.
3. МИССИЯ — робастность к тихим отказам + live-conformance (Веха 2.5)
Три тихих отказа = HTTP 200, перевода нет: эхо (исходник вместо перевода), вырезание (куски пропали), пусто (reasoning съел бюджет). Эхо/пусто детектятся, но эхо только флагуется. Вырезание не детектится. Приёмка Фазы 1 (§04-unhappy §4 — «anti-hallucination guarantee» как продуктовая фича) требует ловить весь класс.
Порядок работ (согласуй финальную последовательность с оркестратором — часть пересекается с ратифицированными шагами 6/7):
A. Coverage-гейт вырезания (конкретизация шага 6) — детектит excision_suspect/coverage_fail
- Портируй классификацию из
eval/refusal_bench.py::classify_output1:1 (§3.7 это предписывает): сегментация предложений[。!?.!?…]со схлопыванием терминаторов и поглощением кавычек/скобок; метрика — символы без пробелов (не токены); пороги per-пара из эксперимента 01/02 (нижние zh<2.2 / ja<1.4 / en<0.70 = вырезание, верхние >4.2/>2.6/>1.4 = аномалия;sent_cov<0.75; минимальная длина применения). Пороги живут в конфиге гейта (Gates.Coverage.LenRatio/SentCovMin/MinChunkChars— схема уже есть вpipeline.go), не в коде. - Это конфигурируемый гейт (
gates.coverage.enabled), в отличие от intrinsic-classify (эхо/пусто всегда включены). Раннер, НЕ адаптер.CheckRunnableсейчас валитgatesEnabled()— сними этот блок КОГДА гейт реально исполним (иначе fail-open против Р7). Гейт считает по РЕЗУЛЬТАТУ стадии (нужен src+out) — встаёт вrunStage/runAttemptпослеclassify, до коммита «ok». - Эмитируй
excision_suspect(низкое покрытие/длина) иcoverage_failпо D10-знаменателю если возьмёшь и глоссарную консистентность (согласуй скоуп — может быть отдельно). Вырезание детерминировано per-модель обычно → скорее эскалация, чем same-model retry (см. B). - Валидируй гейт мини-набором 20–30 фрагментов с выпиленными предложениями (приёмка: ловит ≥90% искусственных пропусков — критерий Фазы 1).
B. Эскалация детерминированных провалов (cjk_artifact/excision_suspect/refusal) — срез шага 7
- Полигон прав (Q2): эхо/вырезание должны ЭСКАЛИРОВАТЬ на другую модель, не просто флажок (same-model retry бессмыслен — переэхнёт/перевырежет/переплатит).
cjk_artifactуже non-retryable в classify ИМЕННО под это. - Решение скоупа (согласуй с оркестратором): полный цикл эскалации (именованные цепочки, re-gate, budget) — большой шаг 7. Возможен минимальный срез: single-hop «фолбэк-черновик» — per-стадия именованная запасная модель, пробуется ОДИН раз на детерминированный флаг (
cjk_artifact/excision_suspect/hard_refusal), под адреснымescalation.budget_usd-потолком, с новой осью вrequest_hash(attempt уже несёт регенерации — эскалации нужна СВОЯ ось модели/цепочки, иначе фолбэк попадёт в чекпоинт провалившегося; продумай ключ). Результат фолбэка тоже гейтится (re-gate) — иначе эскалированный вывод коммитится непроверенным (03-notes §3.8). - Инварианты D4/D3: cross-provider fallback для облачных draft/edit был запрещён (стиль/термины плывут,
model_idв ключе TM обнуляет хиты) — но ЭСКАЛАЦИЯ ПО ПРОВАЛУ ГЕЙТА это другое (осознанная, пишет новую TM-запись). Различай. Канал B (18+) изолируй типом (поле permissive/channel), не комментарием — fallback канала B = только пермиссивный провайдер или fail-loud на nil (D4.1,failover.gonil-guard). Эскалация капабилити эскалационных моделей в snapshot (сейчас только стадии — техдолг D5.2, закрой при заводке цикла). content_filterbest-effort (D3.4): не строй логику на нём как на надёжной ветке; blacklist + coverage — реальная страховка.
C. Live-conformance харнесс
- Go live-интеграционные тесты (твоя зона): build-tag
//go:build liveИЛИ env-gated (TM_LIVE=1), вне дефолтногоgo test/CI, на живых ключахbackend/.env. Per-adapter: бьёт живого провайдера коротким переводом, ассертит валидный перевод (не эхо/пусто/обрезка), парсит usage (reasoning_contentvscontent— Kimi/DeepSeek кладут ответ вcontent, reasoning вreasoning_content; читатьcontent), проверяет алиасы/канонизацию слага в ответе. Деньги живые — вызовы копеечные, но реальные; не гонять в CI, помечать. - Python-оракул Полигона = приёмочный ГЕЙТ перед фиксацией адаптеров (кросс-провайдерный, зона Полигона — координируй, не пиши его сам). Твои Go-тесты — per-adapter wire+parse; оракул — «каждый адаптер даёт валидный перевод». Оба.
D. Провайдерская домашка (реализовать «крайне грамотно»)
Перед правкой любого адаптера — прочитай официальные доки провайдера (ссылки в шапке backend/configs/models.yaml и §3.9), сверься с docs/experiments/00-provider-quirks.md, при сомнении — бесплатный GET /models (curl --noproxy '*' -H "Authorization: Bearer $KEY" https://api.<...>/models; completion — деньги). Обязательно провалидируй/пометь: слаги живьём (deepseek-v4-pro цену /models не отдаёт — фактчек перед wiring эскалации), reasoning_content vs content, оба варианта cache-полей DeepSeek, канонизацию слага в ответе (PriceForResponse), finish_reason эмиссию (эмитят ли DS/GLM/Kimi/grok content_filter вообще — это открытый вопрос Полигону). Расхождение цен/usage/кэша с конфигом — отдельная находка, флагуй, не молчи.
4. Инварианты (жёстко)
- Детерминизм священен. Всё wire-меняющее → snapshot или
request_hash. Новая ось эскалации (модель/цепочка) — в ключ. Меняешь сегментацию гейта — это не wire, но пороги в конфиг (не хардкод).classify/гейты — чистые/детерминированные (resume воспроизводит вердикт из чекпоинта без ре-биллинга). - Деньги. reserve→settle сбалансированы на каждом пути; эскалированный вызов оплачивается и чекпоинтится атомарно (§3.3); ни один платный вызов не «исчезает» мимо ledger; эскалация уважает
escalation.budget_usd. F3 (retryable-но-оплаченные доMaxAttempts−1) — не заявляй плоский «≤1» вне SIGKILL-пути. - Нейтральность адаптера.
internal/llmпровайдер-нейтрален, без семантики перевода (src/target/покрытие). Валидация тихих отказов живёт в РАННЕРЕ (disposition/гейт), не в адаптере. failover — транспорт, не контент. - disposition-контракт Вехи 2 не ломать:
jobs.status=инфра, плохой чанк=disposition,chunk_statusрезолв поверх чекпоинтов, exit 0/2/1, три анти-веджа. - Без хаков. Никаких «потом починю», закомментированных гейтов, обходов детерминизма.
5. Правила проекта
- Метод: доки+код → валидация → код → агентское селфревью на вехе (адверсариальный многоагентный Workflow: дименсии → находки → независимая верификация CONFIRMED/REFUTED с конкретным сценарием инпут→неверный-выход → исправь подтверждённые + регресс-тесты, мутационно-проверенные — сломай, тест должен упасть) → сдаёшь оркестратору/владельцу.
- Не редактировать
docs/research/*,01/02-decisions— правки только через03-implementation-notes.mdиPROGRESS.md(## Бэкенд). Не трогать/не коммититьeval/иdocs/experiments/*(зона Полигона; читать можно). Не коммититьsettings.local.json,*.db. - Коммиты: одно предложение на английском ≤30 слов, без трейлера
Co-Authored-By, без многострочного тела. Наmainпрямо (паттерн проекта), толькоbackend/+PROGRESS, никогдаeval/. - PUML не рендерить в картинки (у владельца PlantUML-расширение VS Code).
- Реальные вызовы = живые деньги (ключи
backend/.env: DEEPSEEK/ZAI/KIMI/OPENAI/GEMINI/XAI). Live-тесты — копеечные, opt-in, вне CI; массовых циклов по живым провайдерам до готовности бюджета/гейтов не запускать. Гигиена xAI:XAI_API_KEYобязан быть ЧИСТЫМ прод-аккаунтом без data-sharing (комментарий-контракт вmodels.yaml). - Автопамять проекта (
MEMORY.md+ provider-wiring-gotchas) — сверься со слагами/гигиеной, обнови по находкам.
6. Сборка/прогон
Из backend/: export PATH="$HOME/sdk/go/bin:$PATH"; go build ./... / go vet ./... / go test ./... -race -count=1 — зелёные на каждом шаге. Стенд: WSL2/GTX 1070; local-адаптер с Proxy: nil. Файлы БД только на ext4 (не /mnt/c). /models-запросы бесплатны, completion — деньги.
7. Открытые вопросы (уточни ДО кода, если стоят переделки)
- Скоуп эскалации: минимальный single-hop фолбэк-черновик СЕЙЧАС vs полный цикл эскалации (шаг 7) — за оркестратором (владеет Р4/цепочками/бюджетом). Ключ эскалации (ось модели/цепочки в
request_hash) — контракт-несущее, спроектируй точно. - Порог допустимого N флагов / precision-граница coverage-гейта — за владельцем (гейтит приёмку, не код).
content_filter-эмиссия провайдеров — проба Полигона; не строй на ней надёжную ветку.- Порядок vs ратифицированные шаги (3 чанкер / 4 память v2 / 5 четыре гейта / 6 coverage+CJK / 7 эскалация): эта миссия — конкретизация 6 + срез 7 + live. Согласуй последовательность с оркестратором (можно как «Веха 2.5» параллельно/до чанкера — фундамент Вехи 2 её держит).
Welcome to the backend. Валидируй старт, зафиксируй DeepSeek-мину гейтом, потом строй.