Add D12 failure-resilience model: three-class failure taxonomy, F3 idempotency fix bounded to at-most-once, progress-surfacing spec grounded in durable-execution prior art

This commit is contained in:
Claude (backend session) 2026-07-04 23:10:32 +03:00
parent 737d4c6a68
commit d15cdb3b71
6 changed files with 137 additions and 8 deletions

View file

@ -253,6 +253,13 @@ keep-alive (Ф12), инъекция глоссария (`selective`), пор
**Техдолг вперёд (не блокирует Веху 2):** capabilities эскалационных моделей НЕ в snapshot (только стадии) — закрыть при заводке цикла эскалации (тот же класс D5.2, но пути исполнения ещё нет); `memoryVersion` — заглушка до банка памяти v2 (при инъекции памяти заполнить запрос); Kimi `max_tokens≥16000` не выразим в capability-схеме (при wiring эскалации).
> **Ответ оркестратора на 4 вопроса coverage-гейта + failure-model (04.07) → полный контракт в [05-decisions-log.md D12](architecture/05-decisions-log.md), заземлён ресёрчем durable-execution.** Отличная Веха 1. Кратко:
> - **4 вопроса — все A.** Q1: наивный split, `refusal_bench.py` = источник истины, Go зеркалит бит-в-бит, изменения лок-степ Python-first (§3.7 quote-absorbing → v1.1, свою доку реконсиль сам). Q2: минимальный single-hop — **не противоречит D4** (это escalation на детерминированном контент-провале, не outage-своп). Q3: только excision_suspect (верхняя аномалия ложно флагает — ru 1.41.9×; D10 ждёт памяти v2). Q4: opt-in до валидации precision владельцем.
> - **Таксономия отказа — ТРИ класса**, не бинар: транзиентный/бюджетный → same-model retry ({length,empty}); детерминированный контент-провал (echo/cjk/excision/refusal, **HTTP 200 — детект в app-слое post-settle, не транспорт**) → single-hop escalation на другую модель (1 хоп→флаг, отдельная ось в request_hash, budget-cap, re-gate, канал B изолирован ТИПОМ); outage/системный → circuit-breaker → **пауза+resume** (не mass-swap).
> - **F3 фикс:** idempotency-key = существующий `request_hash`, заморожен на входе, заголовком где поддержано — но ⚠ **проверено: OpenAI поддерживает, DeepSeek/xAI/Gemini/Kimi НЕТ** → capability-флаг `supports_idempotency_key`; для центрального стека **at-most-once cap** на класс «billed-but-unconfirmed» (≤1 доп. вызов, не MaxAttempts1) + консервативный settle оценки на post-send timeout. Честная модель — «at-most-once billed, replay бесплатен».
> - **Global call-budget guard:** попытки/чанк = retries + 1 hop, **НЕ сбрасывается на фолбэке** (иначе retry×fallback = 30+ оплаченных, LiteLLM #19985). Editor pinned per book (фолбэк только на draft-стадии — editor задаёт стиль).
> - **Прогресс (реальный пробел, ответ на «подсвечивать»):** `tmctl status --config` — READ-ONLY проекция `chunk_status`(уже построен)+jobs+spend+request_log, `--json`; **book manifest на snapshot** (знаменатель N/M); статусы done/in-progress/flagged/skipped/pending (flagged≠failed, всегда REASON); **паспорт качества главы** (JUnit+SonarQube-стиль); ETA от EWMA не present-state; никакого синтетического time-бара. Не сейчас, но заложить manifest в snapshot-момент.
**Гигиена xAI (напоминание):** в `models.yaml` у провайдера `xai` — комментарий-контракт «прод-clean без data-sharing». `XAI_API_KEY` в `.env` подтвердить как прод-аккаунт перед первым боевым grok-вызовом (в Вехе 1 grok в стадии не зовётся).
**Веха 2 (следующая):** runner chapter/chunk-loop + `chunk_status`/disposition (A, D2) — `flag_reason`-константы, `classify(text,finish)` (refusal/echo до length), ось attempt, exit-коды 0/2/1, фиксы F4 (усечённый length → flagged) + edit-`max_tokens` от длины черновика.
@ -415,4 +422,4 @@ keep-alive (Ф12), инъекция глоссария (`selective`), пор
> **[СЕССИЯ ЗАКРЫТА, 04.07]** Выходы: research/13 (+§«Честные слабые места»), architecture/06, `eval/memory_hotpath.py` (спека), `eval/retrieval_bench.py` (эмбеддинги). Эксп-05 снят как низкосигнальный. **Перед кодом реализующей сессии — прочитать research/13 §«Честные слабые места»:** F1 подан как решённый, но там развилка (snapshot материализует байты vs store версионирует глоссарий); post-check в русской морфологии — несущий, но невалидированный (померить до доверия как гейту); «пусто=безопасно» — размен на ложный-пропуск, который тоже тихий; ставка на детерминированный no-LLM путь — cost-driven, не evidence-driven (DelTA валидирует LLM-в-цикле). Гигиена: фоновых процессов нет, ollama полигона не тронут, `eval/.venv` дополнен (torch-cpu/sentence-transformers/openai/dotenv — переиспользуемо).
> **[ДОБАВЛЕНО после закрытия] Локалка × банк памяти — тест экстракции (дыра G1)** → `eval/extract_bench.py`. Локалка касается банка в 2 точках: эмбеддинги (сделано, bge-m3) и экстракция терминов (write-path). Индикативно (3 кейса zh+ja): **локаль хорошо СПОТИТ сущности** (qwen3-abliterated:8b recall 1.0, 0 галлюцинаций, ~15с) но **плохо РЕНДЕРИТ dst** (阿Q→«А Цзы», 羅生門→«Россомаха») → **разделить: спот локально (дёшево) / рендер dst — облако+человек** (уточняет Р4/G1, где «экстракция=локаль» слито в одно). Побочно: deepseek-v4-flash молча вернул ПУСТОЙ ответ на части чанков (祝福 воспроизводимо) — тот empty-paid-response, ради которого существуют D2/coverage-гейт. Аггрегат deepseek по recall не репортил (контаминирован пустыми) — только per-case. Оговорка: 3 кейса, индикативно.
> **[ДОБАВЛЕНО после закрытия] Локалка × банк памяти — экстракция терминов, ВЕРИФИЦИРОВАННЫЙ бенчмарк (дыра G1)** → [experiments/06-local-extraction.md](experiments/06-local-extraction.md), `eval/extract_bench.py`. 13 моделей × 12 кейсов (zh/ja/en + эдж: катакана-запад, буддийские имена, алиасы), запросы через `refusal_bench.call_provider`+`providers.json` (квирки верны: deepseek thinking-ON+max_tokens 8000 — прошлый пустой был МОЯ ошибка бюджета, не свойство модели), **каждый ответ health-верифицирован** (пустой/echo не засчитан как recall 0). Итог: **(1) СПОТ сущностей решён у всех, локаль вкл.** (recall ~0.98 на всех языках, 0 галлюцинаций). **(2) РЕНДЕР dst — разрыв с ЯЗЫКОВЫМ градиентом у локали:** en 0.82 / ja 0.53 / **zh 0.26** (阿Q→«Ах-Чу», 赵太爷→«Тяо Тай-Я» — Палладий-мусор → **эмпирика для B6**); облако-топ zh 0.75/ja 0.98/en 1.0. **(3) бо́льшая локаль не помогает** (30b render 0.55≈8b, ×3 медленнее). **(4) deepseek thinking помогает рендеру** (0.85 vs 0.58 off; на экстракции thinking-off здоров — echo бьёт перевод, не JSON). Рендер-value: **gemini-2.5-flash** (0.92/1.4с). **Дизайн-следствие (уточняет Р4/G1):** спот=дешёвая 8b (все языки); рендер dst язык-зависим — zh обязательно облако/человек + таблица Палладия (B6), en локаль потянет. ruadapt ненадёжен (5/12 health).

View file

@ -6,6 +6,41 @@
---
## D12. Модель устойчивости к отказам (веха coverage-гейта, 04.07) — заземлена на durable-execution prior-art
4 вопроса бэкенда + большой вопрос владельца «падать ли / фолбечиться / как не терять книгу». Заземлено ресёрчем (Temporal / Step Functions / Airflow / River / Stripe idempotency / OpenRouter/LiteLLM fallback; проверено по первоисточникам).
**4 вопроса — все A:**
- **Q1 (сегментация): порт 1:1 наивного split.** `refusal_bench.py::split_sentences` = **источник истины** (он же приёмочный оракул); Go зеркалит бит-в-бит; любое изменение — Python-first, лок-степ. §3.7 «поглощение кавычек» → v1.1 (координированно). Иначе Go-гейт и Python-оракул разъедутся.
- **Q2 (эскалация): минимальный single-hop.** НЕ противоречит D4: D4 запрещал массовый outage-своп, а это точечная эскалация ОДНОГО чанка на детерминированном контент-провале. Нужна сейчас (DeepSeek-эхо воспроизводим). Минимально: 1 хоп → флаг; полные цепочки — шаг 7.
- **Q3 (детект): только excision_suspect (нижняя граница).** Верхняя аномалия-дописывание ложно флагает (ru-выход 1.41.9× исходника) — нужен entity-счёт+судья (Ф2); coverage_fail/D10 блокирован памятью v2.
- **Q4 (активация): opt-in (`enabled:false`), тестами.** Боевой флип — после валидации precision владельцем на реальных чанках (порог N флагов — за владельцем); до починки диалоговой сегментации флип ложно флагал бы диалого-плотные главы.
**Таксономия отказа — ТРИ класса (не бинар «фолбэк vs пауза» — это мис-фрейминг):**
| Класс | Триггер | Реакция |
|---|---|---|
| Транзиентный / бюджетный | 429/5xx/timeout; `finish=length`/пустой | same-model retry (ось `attempt`, бо́льший max_tokens); ретраятся только `{length, empty}` |
| **Детерминированный контент-провал** | echo / cjk_artifact / excision / refusal — **HTTP 200!** детектит app-слой post-settle, НЕ транспорт | **single-hop escalation** на ДРУГУЮ модель (same-model retry бессмыслен); 1 хоп → флаг; отдельная ось модели в request_hash (не `attempt`); `escalation.budget_usd`; результат **ре-гейтится**; канал B изолирован ТИПОМ (permissive/nil-guard в failover.go), пишет новую TM-запись (model_id в ключе) |
| Outage / системный | 503-storm, 429-исчерпание, terminal-4xx, ceiling — **не per-chunk** | circuit-breaker (Azure Closed/Open/Half-Open, ~30с health-окно) → **пауза джобы + resume** (НЕ mass-swap: D4, стиль поплывёт + TM-инвалидация); супервизор рестартит `tmctl` |
**F3 фикс (оплаченный неидемпотентный вызов):**
- Provider idempotency-key = **существующий `request_hash`**, заморожен на входе (константа через транспорт-ретраи и resume), в заголовке `Idempotency-Key` где поддержано. ⚠ **Матрица (проверено по первоисточникам): OpenAI поддерживает; DeepSeek / xAI / Gemini / Kimi — НЕТ.** Центральный стек (draft-DeepSeek, editor-grok, judge-Gemini) provider-dedup не имеет → **не архитектурить F3 вокруг него.** Capability-флаг `supports_idempotency_key` в models.yaml (в готовый capability-слой).
- Для непокрытого стека — **at-most-once cap**: класс «billed-but-unconfirmed» (2xx с нечитаемым НЕусечённым телом / чистый timeout после отправки) получает бюджет **≤1 доп. вызова**, не `MaxAttempts1` (Temporal «maxAttempts=1 для non-idempotent»). Восстанавливает приёмку «≤1 вызов». + **консервативный settle оценки (не Release)** на post-send-timeout, чтобы потолок не был слеп (расширить существующий `BilledDecodeError`-путь); ledger-состояние AMBIGUOUS-BILLED. Честная модель — **«at-most-once billed, replay бесплатен»**, не exactly-once (saga/exactly-once на оплаченный вызов не мапятся).
**Consistency-aware fallback by stage:** draft-фолбэк свободно (editor ре-нормализует стиль — 2605.13368); **editor pinned per book** (на отказе editor — пауза / same-family, не чужая модель); model-per-chunk логировать для последующего uniformity-пасса. **Global call-budget guard:** суммарные попытки на чанк = `retries_on_primary + 1 fallback hop`, **НЕ сбрасывается на фолбэке** (иначе retry×fallback = 30+ оплаченных вызовов — LiteLLM #19985).
**Прогресс — единственный реальный пробел (ответ на «подсвечивать прогресс»):**
- `tmctl status --config book.yaml`**READ-ONLY** проекция `chunk_status`+`jobs`+`spend`+`request_log` (Temporal query-handler аналог; 0 LLM, 0 replay); `--json` (стабильные disposition/flag_reason enum как контракт) для CI/IDE. `chunk_status` бэкенд УЖЕ построил — read-model есть; нет только команды и знаменателя.
- **Book manifest на snapshot** (всего глав/чанков-на-главу) — иначе честный N/M невозможен. Вести **unit-счётом** (done/in-progress/flagged/skipped/pending), % только из N/M; **никакого синтетического time-бара** (fictional-bar анти-паттерн, Cloud Four).
- Словарь статусов (Airflow/Step-Functions): flagged≠failed (`jobs.status.failed` = infra-only), skipped с upstream-reason; **всегда REASON** (GitLab «explain why skipped»).
- **Паспорт качества главы** (JUnit `<testsuite>` + SonarQube gate): chunks total/ok/flagged/skipped, worst flag_reason, coverage-вердикт (pass|attention|fail по 5002500-char порогам), $, версия — против «проклятия 300-й главы» (04-unhappy §10).
- ETA от **сглаженного throughput** (EWMA α≈2/(n+1)), не present-state; unit-счёт ведущий, ETA вторично. Деньги в status: committed/reserved/ceiling%, F3-честный кумулятив, projected book cost = (cost/done-chunk)×remaining. Re-drivable флагнутые (Step Functions redrive), не трогать DispOK.
**Не строить (cautions верификатора):** distributed-движок (Temporal/Step Functions инфра — мы single-node/один файл, брать паттерны не инфру и не их HA-числа); прозрачный cross-provider fallback главного пути (D4); детектор контент-провала в транспорте (обязан быть app-слой post-settle — транспорт видит 200); real-time SSE-прогресс (Ф3, IDE вне MVP); наивный параллелизм чанков (гонка `Runner.clients` + «коммит терминов только на границе джоб»).
---
## Вторая волна развязок Фазы 1 (04.07, вопросы бэкенда после survey) — D8D11
Онбординг-поправки бэкенда **ратифицированы**: (1) дефолт-редактор grok-4.3 @ temp 0.4 thinking-OFF (не Kimi — «Kimi-editor 400» как повод для capability-слоя снят; живые 400 дают Gemini/gpt-5); (2) `deepseek-v4-pro` нигде не подтверждён → слаг канала A резолвить через `/models`, не хардкодить (deepseek-chat депрекейт **2026-07-24, ~3 недели** — миграция черновика срочная); (3) Gemini wire-слаг `gemini-3.1-pro-preview` (суффикс несущий, голый → 404); (4) models.yaml на диске отстаёт — бэкенд обновляет сам (его зона); (5) F4 (усечённый length течёт в edit как OK) чинить в A, F3 (retryable-but-paid переоплата) учесть явно. A/B/C-план бэкенда принят как есть.

View file

@ -36,7 +36,7 @@
| B3 | **Разрешение алиасов** (名/字/号/прозвища/титулы: «один персонаж = 36 людей») | С / 🟠🔇 | Alias-граф с типами (имя/цзы/хао/прозвище/титул) + name-tables (фамилия/имя раздельно); NER-гейт «новая форма имени-подобного токена = блок/флаг» | DET+LLM(экстракция)+HUMAN | схема: `aliases`→граф (D7 v1); NER-гейт | 12 | 🔶 (04-unhappy §1) |
| B4 | **Дрейф «термин → утверждённый перевод»** по ходу книги (approved-термин передан иначе) | С / 🟠🔇 | Post-check регэксп по **склонённым** формам (через `decl`/лемматизатор): approved `src`/алиас во входе → лемма `dst` в выходе? нет → флаг редактору; прономинализация не штрафуется, только `approved` (процедура Р7) | DET | гейт консистентности глоссария (Р7); поле `decl` | 1 | ✅ (Р7) — но см. E-post-check |
| B5 | **Западные имена через катакану/фонозапись** (约翰→«Юэхань» вместо «Джон», ヴァイオレット, васэй-эйго マンション=квартира) | С / 🟡 | Поле `translit_policy` для псевдоевропейских имён; словарь васэй-эйго (v1.1) | DET | схема: `translit_policy` (D7 v1) | 1 (поле) / 2 (словарь) | ✅ (04-unhappy §9, D7) |
| B6 | **Нонконформность стандарту транслитерации** (zh→ru — Палладий, ja→ru — Поливанов): имя может быть **внутренне консистентным, но систематически неверным** по стандарту — ось корректности, ортогональная самосогласованности | С / 🟠🔇 | Валидация `dst` имён собственных против таблиц Палладия/Поливанова на коммите термина; отклонение — флаг/политика проекта | DET+HUMAN | схема: правило на коммите `dst`; пресеты стандарта | 2 | **не покрыто ни одним доком** — крупнейший zh/ja→ru-специфичный пробел (находка критика) |
| B6 | **Нонконформность стандарту транслитерации** (zh→ru — Палладий, ja→ru — Поливанов): имя может быть **внутренне консистентным, но систематически неверным** по стандарту — ось корректности, ортогональная самосогласованности | С / 🟠🔇 | Валидация `dst` имён собственных против таблиц Палладия/Поливанова на коммите термина; отклонение — флаг/политика проекта | DET+HUMAN | схема: правило на коммите `dst`; пресеты стандарта | 2 | 🔶 **эмпирически подтверждён (exp06):** локаль на zh даёт Палладий-мусор (阿Q→«Ах-Чу», 赵太爷→«Тяо Тай-Я»), рендер zh 0.26; даже облако-топ лишь 0.75 (часть zh→ru реалий без единого канона) → детерминированная таблица Палладия/Поливанова обязательна, «сильной моделью» не закрывается |
## C. Спойлеры и род (темпоральная ось)
@ -66,7 +66,7 @@
| # | Сценарий отказа | Вер./Тяж. | Механизм защиты | Слой | Где в коде/схеме | Фаза | Статус |
|---|---|---|---|---|---|---|---|
| F1 | **`snapshotID` не покрывает память → тихий расходящийся ре-пэй (D5.2)**. Подтверждено кодом: `snapshotID` (`runner.go:145-163`) хэширует brief/chunker/план-стадий/сэмплинг, но **не** версию approved-глоссария и **не** параметры сборки контекста; `RequestHash` (`render.go:158`) включает контент `msgs`; `render.go:23-29` декларирует рендер «чистой функцией snapshot». Как только Фаза 1 инъектит память в `msgs` (`render.go:133-136`): изменённый глоссарий/STM меняет `request_hash`, но не `snapshotID` → resnapshot-гейт (`runner.go:260`) молчит → ре-ран старой главы промахивается мимо чекпоинта и **переоплачивает расходящимся переводом**, ломая инвариант Р6 | В / 🔴🔇 | **Свернуть в snapshot ОБА:** (1) состояние памяти (версия-счётчик approved-глоссария + резюме/series-bible на момент старта джобы — рендер читает **замороженную** версию, не live) и (2) context-assembly-конфиг (`glossary_injection`, `stm_depth`, `overlap`). Тогда рендер снова чист по snapshot, а изменение памяти громко триггерит resnapshot-гейт. impl-notes §3.2 это уже **предписывает** («зафиксированное состояние approved-глоссария (версия-счётчик)») — код отстаёт | DET | `runner.snapshotID()` struct; snapshot payload; сборщик читает pinned-версию | **до инъекции Фазы 1** | ❌ **общий гейт Фазы 1**, держит D1 и D5; закрыть ДО инъекции памяти |
| F1 | **`snapshotID` не покрывает память → тихий расходящийся ре-пэй (D5.2)**. Подтверждено кодом: `snapshotID` (`runner.go:145-163`) хэширует brief/chunker/план-стадий/сэмплинг, но **не** версию approved-глоссария и **не** параметры сборки контекста; `RequestHash` (`render.go:158`) включает контент `msgs`; `render.go:23-29` декларирует рендер «чистой функцией snapshot». Как только Фаза 1 инъектит память в `msgs` (`render.go:133-136`): изменённый глоссарий/STM меняет `request_hash`, но не `snapshotID` → resnapshot-гейт (`runner.go:260`) молчит → ре-ран старой главы промахивается мимо чекпоинта и **переоплачивает расходящимся переводом**, ломая инвариант Р6 | В / 🔴🔇 | **Свернуть в snapshot ОБА:** (1) состояние памяти (версия-счётчик approved-глоссария + резюме/series-bible на момент старта джобы — рендер читает **замороженную** версию, не live) и (2) context-assembly-конфиг (`glossary_injection`, `stm_depth`, `overlap`). Тогда рендер снова чист по snapshot. **✅ УЖЕ СДЕЛАНО (commit 5eaf2d9): snapshot содержит `context_assembly` (`GlossaryInjection`/`GlossaryTokenBudget`) + `MemoryVersion` — материализует память → хеш (выбран мой рекомендованный «материализующий» вариант); `Capability` тоже свёрнут. `memoryVersion()` пока хеширует константу-заглушку `tm-memory-v1\x00`, т.к. банк памяти v2 ещё не приземлился (комментарий в коде явно фиксирует инвариант).** | DET | `runner.snapshotID()`/`memoryVersion()`/`contextSnap` | сделано; активируется с v2 | ✅ структурно закрыто (5eaf2d9). **ОСТАЁТСЯ узко: при приземлении глоссария заменить константу в `memoryVersion()` на хеш ORDER BY-стабильных материализованных строк (замороженных, не live) — иначе F1 тихо регрессирует.** |
| F2 | **Токен-бюджет: eviction не определён**. При превышении бюджета (matched + sticky > лимит) **какая запись выпадает — не задано**; выпавшая нужная запись = тихая деградация | С / 🟡🔇 | Явная политика приоритета/вытеснения (World Info inclusion-groups: approved>auto; персонажи чанка>фоновые) **+ логировать факт вытеснения** (не молча) | DET | сборщик; `retrieval-state` запись | 1 | ❌ политика и лог вытеснения не специфицированы (находка критика) |
| F3 | **Конкурентность/порядок в SQLite**: параллельный перевод чанков + запись auto-записей ломает `first-translation-wins` и sticky (оба предполагают последовательность); modernc — один писатель | С / 🟡 | Явное правило: auto-экстракция и коммит терминов — только на **границе джоб** (impl-notes §3.2 это уже фиксирует); sticky/порядок считаются от детерминированной пересборки чекпоинтов, не от wall-clock порядка | DET | граница джоб; write-пул store | 1 | 🔶 назвать явно (находка критика) |
| F4 | **Один SQLite-файл на книгу = SPOF**: повреждение файла молча теряет весь глоссарий+резюме | Н / 🟠🔇 | Бэкап/интеграл-чек/версионирование файла книги; экспорт TMX/TBX как побочный бэкап | DET | store; экспорт | 2 | 🔶 нет строки бэкапа/интеграла (находка критика) |
@ -76,7 +76,7 @@
| # | Сценарий отказа | Вер./Тяж. | Механизм защиты | Слой | Где в коде/схеме | Фаза | Статус |
|---|---|---|---|---|---|---|---|
| G1 | **Качество автоэкстракции (bootstrap)**: все направления валидируют *retrieval*, но не *популяцию*. Мусор/пропуск главного персонажа на входе → горячий путь верно инъектит мусор / молча опускает — **корневой отказ выше всего измеренного** | С / 🟠🔇 | Мерить precision/recall экстракции на своём голд-сете; first-encounter unknown имя`status=auto/unconfirmed`, гейт (батч-судья/человек раз в главу) **до** промоушена в approved-ключ, распространяемый на будущие главы; alarm «имя-подобный токен без записи → неизвестная сущность» (DelTA Proper-Noun-Record паттерн) | DET(алярм)+LLM(экстракция)+HUMAN | pre-пасс NER; статус-машина термина | 12 | ❌ популяция не измерена (находка критика) |
| G1 | **Качество автоэкстракции (bootstrap)**: все направления валидируют *retrieval*, но не *популяцию*. Мусор/пропуск главного персонажа на входе → горячий путь верно инъектит мусор / молча опускает — **корневой отказ выше всего измеренного** | С / 🟠🔇 | **Разделить задачу экстракции на две (индикативно замерено, `eval/extract_bench.py`): (а) СПОТ кандидатов-спанов — локаль годится** (qwen3-abliterated:8b recall 1.0 на zh+ja, 0 галлюцинаций, ~15с/чанк — дёшево на стенде); **(б) РЕНДЕРинг dst — локаль НЕ годится** (0.00.5: 阿Q→«А Цзы», 羅生門→«Россомаха», 秀才→«шоуцай» — как exp03), dst даёт сильная/облачная модель + человек. First-encounter unknown`status=auto/unconfirmed`, гейт (батч-судья/человек) **до** промоушена в approved; alarm «имя-подобный токен без записи → неизвестная сущность» (DelTA-паттерн). ⚠ deepseek-v4-flash молча вернул **пустой** ответ на части чанков (祝福 воспроизводимо) — это тот empty-paid-response, что ловят D2/coverage-гейт | DET(алярм)+LLM(спот=локаль / рендер=облако)+HUMAN | pre-пасс NER; статус-машина термина | 12 | 🔶 верифицировано (exp06, 13 моделей × 12 кейсов zh/ja/en): спот локальный OK (recall ~0.98 все языки), рендер локали язык-зависим (en 0.82 / ja 0.53 / **zh 0.26**) → спот=8b, рендер dst=облако(gemini-flash value)/человек, на zh + таблица Палладия B6 |
| G2 | **Человеческая ёмкость — недоказанная петля всей safety-аргументации**: почти каждая защита кончается «флаг → редактору/человеку». Объём флагов на главу никто не оценил; при высоком flag-rate «route to human» пусто → деградация тихая на практике | С / 🟠🔇 | Оценить flag-volume на главу на пилоте; alarm-пороги и aggregate «memory-coverage»-сигнал; безлюдный режим: auto→approved с записью в журнал (Р7), но с явной пометкой качества | DET(телеметрия)+HUMAN | `retrieval-state` запись; отчёт «паспорт качества главы» | 12 | ❌ flag-rate × throughput не смоделирован (находка критика) |
---
@ -98,6 +98,8 @@
4. **Трёхсторонний disposition инъекции (A2)** + `retrieval-state` per-chunk запись (A1/F2/G2: `n_exact_hits, n_sticky, n_ambiguous_flagged, n_spoiler_blocked, embedding_tier_used`) — наблюдаемость деградации с первого дня.
5. **Отложено в v1.1/Фазу 2 (не блокирует v1-схему, но заложить поля/место):** транслит-стандарты Палладий/Поливанов (B6), гейт фактичности резюме (D1), whole-book эмбеддинг-слой low-trust (A7/D2), бэкап/интеграл файла книги (F4), морфо-гейт глагольного рода (C3), измерение экстракции и flag-volume (G1/G2).
> ⚠ **Перед реализацией прочитай `research/13` §«Честные слабые места рекомендаций».** Два пункта прямо влияют на этот реестр: **F1** подан как решённый фикс, но там реальная развилка (snapshot материализует байты инъекции vs store версионирует глоссарий — я склоняюсь к «байты», но не доказал); **E1 post-check** — несущий гейт, но его надёжность в русской морфологии сама невалидирована (склонение-осознанный матч шумит → ложные флаги → редакторы игнорируют → safety рушится). Померить пост-check на русском ДО того, как доверять ему как гейту. Исполняемый эталон механизма (не продукт) — `eval/memory_hotpath.py` (T1T10 → Go-юниттесты).
## In-house eval — обязателен (числа литературы не переносятся)
**Ни один процитированный источник не меряет zh/ja→ru** (DelTA/TransAgents/Karpinska/WMT25 — En-центричны или ru только как *источник*; вендорские memory-числа — про chat-память о пользователе, при честной методологии их бьёт ванильный RAG). Поэтому перед заморозкой ставки нужен **свой замер на реальном стеке и направлении** — детали, acceptance-критерии и протокол (bank-vs-long-context baseline, «random-terminology» адверсариальный arm из WMT25) — в `docs/research/13-memory-bank-validation.md` §Вердикт. Реестр рисков не заменяет этот замер — он делает провалы **явными**, что и есть ответ на опасение владельца: **тихую деградацию мы конвертируем в громкую (post-check + disposition-лог + retrieval-state), а не устраняем её обещанием.**

View file

@ -8,9 +8,9 @@
| Провайдер | base_url | Модель (на 2026-07-04) | env-ключ |
|---|---|---|---|
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat``deepseek-v4-flash` (деприкация 24.07.2026) | `DEEPSEEK_API_KEY` |
| xAI Grok | `https://api.x.ai/v1` | `grok-4-fast`, `grok-4.3` (флагман) | `XAI_API_KEY` |
| GLM (Z.ai) | `https://api.z.ai/api/paas/v4` | `glm-4.6` | `ZAI_API_KEY` |
| DeepSeek | `https://api.deepseek.com/v1` | **`deepseek-v4-flash`** (`deepseek-chat` не в /models, но работает как алиас до депрекации 24.07.2026; эхает zh — см. ниже, брать v4-flash с thinking) | `DEEPSEEK_API_KEY` |
| xAI Grok | `https://api.x.ai/v1` | `grok-4.20-0309-non-reasoning` (явный не-reasoning слаг; `grok-4-fast` не в /v1/models, но работает как алиас — проверено HTTP 200; флагман `grok-4.3`) | `XAI_API_KEY` |
| GLM (Z.ai) | `https://api.z.ai/api/paas/v4` | `glm-4.5-air` (дёшево) / `glm-4.6`; в /models: glm-4.7, glm-5, glm-5.1, glm-5.2 | `ZAI_API_KEY` |
| Gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-3.1-pro-preview` | `GEMINI_API_KEY` |
| Kimi (Moonshot) | `https://api.moonshot.ai/v1` (intl, **не** `.cn`) | `kimi-k2.6` (доступны k2.5, k2.7-code) | `KIMI_API_KEY` |
| OpenAI | `https://api.openai.com/v1` | `gpt-5-mini` | `OPENAI_API_KEY` |
@ -24,7 +24,8 @@
| Провайдер | Поведение по умолчанию | Как управлять (для перевода) |
|---|---|---|
| GLM-4.6 | thinking ON, не влезал в 180с | отключить: `extra_body: {"thinking": {"type": "disabled"}}` |
| **DeepSeek v4-flash** | гибридная reasoning-модель, thinking ON по умолчанию | **Управление (официальные доки, api-docs.deepseek.com/guides/thinking_mode, проверено 04.07): в body `extra_body={"thinking":{"type":"enabled"\|"disabled"}}`** (дефолт enabled). Т.е. выключить МОЖНО, параметр есть. НО для роли ПЕРЕВОДЧИКА не выключать: `disabled`→non-thinking режим, который на плотном CJK **ЭХАЕТ исходник**. Оставлять thinking ВКЛ; reasoning→`reasoning_content`, `content` чистый; `max_tokens` ≥8000 (иначе reasoning съест бюджет → `content` пуст, finish=length — это НЕ отказ). ⚠ **thinking-режим НЕ поддерживает `temperature`/`top_p`/penalties** (шлём — молча игнорируются). `reasoning_effort`: только `high`/`max` (low/medium→high, xhigh→max); `none` НЕ существует → 400. Для дешёвой роли ЭКСТРАКЦИИ (не перевод) thinking можно выключить — echo бьёт по переводу, не по JSON-выводу (перепроверить). |
| GLM-4.5-air / 4.6 | thinking ON, не влезал в 180с | отключить: `extra_body: {"thinking": {"type": "disabled"}}` |
| Gemini 2.5 Flash | thinking ON, съедал `max_tokens`, **молча обрезал перевод** | отключить: `extra_body.google.thinking_config.thinking_budget: 0` |
| **Gemini 3.1 Pro** | thinking **обязателен** | `thinking_budget:0` → HTTP 400 «only works in thinking mode». НЕ отключать; дать большой `max_tokens` (≥8000) |
| gpt-5-mini (reasoning) | reasoning ON, выжигал бюджет → пустой ответ | `reasoning_effort: "minimal"`; **без `temperature`**; `max_completion_tokens` вместо `max_tokens` |
@ -59,6 +60,7 @@
- `deepseek-chat``deepseek-v4-flash` к **24.07.2026**.
- **Grok 4.1 Fast retired 15.05.2026** → слаги молча редиректят на `grok-4.3` (цена ×9). Ретайрнулся дешёвый тир, НЕ сам xAI: grok-4.3 остаётся основным пермиссивным тиром канала B.
- **Аудит /models 04.07.2026 (воркфлоу) — с поправкой на перепроверку:** и `deepseek-chat`, и `grok-4-fast` НЕ в списках /models (там v4-flash/v4-pro у DeepSeek; версионные слаги у xAI), НО **оба работают как алиасы** (перепроверено вживую: HTTP 200, переводят). Агент-аудитор написал «сняты» — это была неточность: «нет в /models» ≠ «не работает». deepseek-chat депрекируется 24.07.2026. У xAI reasoning/non-reasoning — РАЗНЫЕ слаги (`grok-4.20-0309-non-reasoning` / `-reasoning` / `-multi-agent`); для перевода брать non-reasoning. xAI `usage` отдаёт нестандартные `cost_in_usd_ticks`/`num_sources_used` — игнорировать. **Урок: имя модели проверять не только `/models`, но и пробным вызовом — алиас может работать, даже если его нет в списке; и наоборот.** Реальная причина сменить deepseek на v4-flash — не «chat умер», а ЭХО на zh (лечится thinking-on), см. раздел thinking.
- Claude Sonnet 5 промо $2/$10 до **31.08.2026** (не закладывать в долгий прайс).
- **ПОПРАВКА (04.07, владелец): удалён только Anthropic (за дороговизну). OpenAI ОСТАЁТСЯ** (ключ есть — GPT-5 nano/mini). Живой набор ключей у бэкенда и полигона: DEEPSEEK, ZAI, KIMI, OPENAI, GEMINI, XAI. Источник истины по стеку — `architecture/05-decisions-log.md` (D3) и Р4. Ранее эта строка ошибочно исключала OpenAI — исправлено оркестратором.

View file

@ -0,0 +1,63 @@
# Эксперимент 06. Экстракция терминов: локаль vs облако (write-path банка памяти)
Дата: 2026-07-04. Закрывает дыру **G1** реестра рисков (`architecture/06`: «качество популяции глоссария не измерено»). Скрипт: `eval/extract_bench.py`; сырьё: `eval/data/extract_bench/results.json`.
**Зачем.** Горячий путь банка памяти модель НЕ использует (детерминированный матч). Локаль касается банка в 2 точках: эмбеддинги (exp там же → bge-m3) и **экстракция кандидатов в глоссарий из сырого текста** (bootstrap write-path). Полигон exp03 мерил локаль как ПЕРЕВОДЧИКА (вердикт: не годится). Экстракция — другая, более лёгкая задача; здесь меряем её отдельно и **верифицированно**.
**Метод (в ответ на «сделай нормально, не обрывочно»):**
- **13 моделей:** 6 локальных (`qwen3-abliterated:8b/9b`, `qwen3:8b`, `qwen3-vojo`, `ruadapt-qwen3:8b`, `qwen3-abliterated:30b-a3b`) + 7 облачных (`deepseek-v4-flash`, `grok-4.20-non-reasoning`, `glm-4.5-air`, `kimi-k2.6`, `gemini-2.5-flash`, `gpt-5-mini`, + `deepseek-nothink` как дешёвый вариант без reasoning).
- **12 кейсов, 3 языка, эдж-кейсы:** zh (имена+реалии 送灶/祝福/秀才, алиас 王癞胡=王胡), ja (羅生門/朱雀大路, **западные имена через катакану** メロス→Мелос / セリヌンティウス→Селинунтий, буддийское 禅智内供 по эталону Стругацкого), en (Джон Картер/Конан/Филби — эдж на транскрипцию). Gold — сущности + канонический RU (accept-регэксп: Палладий/Поливанов/устоявшиеся).
- **Запросы — через рабочий `refusal_bench.call_provider` + канонический `providers.json`** (квирки уже верны: deepseek thinking-ON+`max_tokens 8000` — иначе reasoning съедает бюджет и `content` пуст; kimi temp=1/24k; gpt-5-mini `max_completion_tokens`+no-temp; gemini/glm thinking-off). Управление thinking у DeepSeek — `extra_body={"thinking":{"type":"..."}}` (офиц. доки, см. `00-provider-quirks`).
- **Каждый ответ верифицируется на здоровость** (пустой/echo/http/unparseable → ретрай с бо́льшим бюджетом → явный `HEALTH=failed`, **НЕ** засчитывается как recall 0 — урок прошлого обрывочного прогона). Метрики детерминированные, без судьи.
## Результаты
Агрегат по `health==ok` (recall = спот сущностей; render = доля найденных с каноническим RU; halluc = извлечённое, чего нет в тексте):
| Модель | тип | recall | render | halluc | n | сек | ok/all |
|---|---|---|---|---|---|---|---|
| qwen3-abliterated:8b | local | 0.98 | 0.48 | 0.0 | 3.9 | 5.1 | 12/12 |
| qwen3.5-abliterated:9b | local | 1.00 | 0.54 | 0.0 | 3.2 | 6.6 | 12/12 |
| qwen3:8b | local | 0.98 | 0.49 | 0.0 | 3.8 | 4.9 | 12/12 |
| qwen3-vojo | local | 0.98 | 0.51 | 0.0 | 3.4 | 3.4 | 12/12 |
| qwen3-abliterated:30b-a3b | local | 0.91 | 0.55 | 0.0 | 3.1 | 13.5 | 12/12 |
| ruadapt-qwen3:8b | local | 1.00 | 0.72 | 0.07 | 3.6 | 3.7 | **5/12** |
| **deepseek-v4-flash** | cloud | 0.97 | 0.85 | 0.0 | 3.3 | 8.9 | 12/12 |
| grok-4.20-non-reasoning | cloud | 0.98 | 0.69 | 0.0 | 3.2 | **1.1** | 12/12 |
| glm-4.5-air | cloud | 0.98 | 0.68 | 0.0 | 3.2 | 1.9 | 12/12 |
| **kimi-k2.6** | cloud | 1.00 | **0.96** | 0.0 | 3.5 | 38.2 | 12/12 |
| **gemini-2.5-flash** | cloud | 0.97 | 0.92 | 0.0 | 3.4 | **1.4** | 12/12 |
| gpt-5-mini | cloud | 0.95 | 0.67 | 0.0 | 4.4 | 2.5 | 12/12 |
| deepseek-nothink | cloud | 0.98 | 0.58 | 0.0 | 3.5 | 1.6 | 12/12 |
**Ключевая разбивка — render по языку** (пул из 5 локальных dense/MoE vs топ-3 облака deepseek/kimi/gemini):
| Группа | zh | ja | en |
|---|---|---|---|
| **локаль (пул)** | **0.26** | 0.53 | 0.82 |
| облако-топ | 0.75 | 0.98 | 1.00 |
Recall по языку у всех ~0.951.0 (спот не зависит от языка).
## Выводы (верифицированные)
1. **СПОТ сущностей — решённая задача для ВСЕХ, локаль включительно** (recall 0.951.0 на zh/ja/en). Кандидат-споттинг write-path можно гнать на дешёвой локальной 8b. Даже 0 галлюцинаций (кроме ruadapt).
2. **РЕНДЕР канонического RU — вот где разрыв, и он с крутым ЯЗЫКОВЫМ градиентом у локали:** en 0.82 (западные имена локаль транскрибирует нормально — Джон Картер/Конан), ja 0.53 (средне), **zh 0.26 (локаль проваливается)**. Облако-топ сильно везде (zh 0.75, ja 0.98, en 1.0).
3. **Провалы локали на zh — это прямая нонконформность стандарту Палладия** (реестр **B6**, эмпирически подтверждён): 阿Q→«Ах-Чу», 赵太爷→«Тяо Тай-Я», 祥林嫂→«Сяньлинсяо», 送灶→«Сув-цзя», 祝福→«Бэ-цзю», 羅生門→«Ро-сёмон», 朱雀大路→«Чжу-сюкэ-дайро» — фонетический мусор вместо Палладий-канона («А-кью», «Чжао»). Локаль не знает системы транскрипции zh→ru; это не «плохой перевод», а систематически неверная по стандарту транслитерация.
4. **Бо́льшая локаль НЕ помогает:** 30b-a3b render 0.55 ≈ 8b (0.480.55), но в 34× медленнее (13.5с vs 35с) и recall даже ниже (0.91). Для спота брать дешёвую 8b, не 30b.
5. **deepseek: thinking ПОМОГАЕТ рендеру** (0.85 с thinking vs 0.58 без) — reasoning вытаскивает канон реалий (согласуется с exp03). Но thinking-off deepseek на экстракции **здоров** (12/12, echo бьёт перевод, не JSON-вывод) — просто рендерит хуже (≈уровень локали).
6. **Лучший рендер-value — gemini-2.5-flash** (render 0.92 за 1.4с); kimi лучший по качеству (0.96), но 38с/дорого; deepseek-think 0.85/8.9с. grok/glm/gpt-5-mini слабее по рендеру (0.670.69).
7. **ruadapt-qwen3 — ненадёжен** (5/12 health, единственный с галлюцинациями) — как и предупреждал exp03 «понимание разрушено ru-адаптацией».
## Следствие для дизайна (write-path, уточняет Р4/G1)
Экстракцию разбить на две подзадачи, и разбивка **язык-зависима**:
- **(а) СПОТ кандидатов-спанов — локальная 8b** (recall ~0.98 на всех языках, дёшево/быстро, 0 галлюцинаций). Это дешёвый пре-пасс NER.
- **(б) РЕНДЕР dst — зависит от языка источника:** для **zh — обязательно облако/человек + детерминированная таблица Палладия** (локаль даёт мусор 0.26; даже облако лишь 0.75 — часть zh→ru реалий не имеет единого канона, нужен словарь B6); для **ja — облако** (локаль 0.53); для **en/западных имён — локаль потянет** (0.82), облако для надёжности. Рендер-модель по value — gemini-2.5-flash.
- Промоушен `auto→approved`с человеком/судьёй (G1), особенно на zh, где даже сильная модель не гарантирует Палладий.
## Ограничения
12 кейсов (умеренно, не сотни), 1 прогон на ячейку (temp низкая), gold-render = мой набор accept-регэкспов канона (для спорных реалий — 送灶/洛中 — канон нестрогий, render занижен честно). Render — объективно более трудная для авто-оценки ось, чем recall; человеческая сверка на выборке усилит. Но языковой град**иент (zh≪ja≪en у локали) устойчив и с этим N.
Провенанс: `eval/extract_bench.py` (переиспользует `refusal_bench.call_provider` + `providers.json`), `results.json` с per-кейс `records`. Реальные PD-тексты `eval/data/samples`.

View file

@ -221,6 +221,26 @@
---
## Честные слабые места рекомендаций (что подано увереннее, чем доказано)
Раздел добавлен на закрытии сессии умышленно: по этим рекомендациям пишут код, поэтому места, где моя уверенность ниже, чем звучит в тексте выше, — здесь явно. Отсортировано по важности для реализации.
1. **F1 (фикс snapshotID) — УЖЕ РЕАЛИЗОВАН бэкендом (commit 5eaf2d9), тем самым «материализующим» вариантом, к которому я склонялся** (snapshot содержит `context_assembly` + `MemoryVersion` = хеш материализованной памяти; `memoryVersion()` пока хеширует константу-заглушку до приземления банка v2, комментарий в коде явно держит инвариант). **Каветат снят.** Узкий остаток: при приземлении глоссария заменить заглушку на реальный хеш замороженных материализованных строк — иначе инвариант тихо регрессирует. (Я держал это как «открытый гейт, а фикс — лишь направление» по УСТАРЕВШЕМУ чтению кода начала сессии — на деле бэкенд уже ушёл далеко вперёд и это сделал.)
2. **Пост-check в русском — несущий И невалидированный.** Весь safety-аргумент реестра держится на пост-check как «главном детекторе тихой деградации». Но проверка «утв. `dst` присутствует» в русском требует **склонение-осознанного матча** (лемматизатор), и он ошибается: ложно-отрицательное (форма есть, но матч промахнулся) → ложный флаг → шум → редакторы перестают верить флагам → safety рушится. В `memory_hotpath` я использовал игрушечные регэксп-корни. **Надёжность пост-check в русской морфологии — сама по себе непроверенный research-вопрос; если он шумит, вся защита слабеет.** Замерить до того, как доверять ему как гейту.
3. **«Пусто = безопасно» — подано слишком сильно; дизайн меняет ложную-инъекцию на ложный-ПРОПУСК, а пропуск тоже тихий.** Recall точного матча в моём бенчмарке — **0.571**: ~43% релевантных записей НЕ инъектируются (косвенные упоминания, перефраз, местоимения сверх sticky). Пропущенная запись → несогласованный рендеринг → ровно тот дрейф, которого боимся. Precision-first — защитимая ставка, но это **размен**, и net-положителен ли он — именно то, что должен померить Ф2.5. Я эту напряжённость в собственной рекомендации сгладил.
4. **`retrieval_bench`: корпус, gold И ловушки я построил сам → риск само-подтверждения; малый N; ловушки — «лёгкая» версия.** Мои ловушки (送灶/修/炎/气) — случаи, где точного многосимвольного термина в тексте НЕТ, поэтому substring выигрывает тривиально. **Трудную версию** — полный термин, который сам по себе омоним/совпадает с обычным словом целиком — я не тестировал. «Substring trap-safe» доказано для лёгкой ловушки, не для трудной. И bge-m3 > Qwen3 близко — на реальных данных может перевернуться. Числа индикативны с большими доверительными интервалами.
5. **«Разделяющего cosine-порога нет» — свойство МОЕГО представления, не закон эмбеддингов.** Я эмбедил карточки «src — dst. note». Другое представление (только dst / NL-глосса / sparse-голова bge-m3) может разделять лучше. Читать как «наивный cosine на этой карточке не гейтит», НЕ «векторные пороги бесполезны».
6. **Латентность brute-force — это numpy, не Go.** «<20 мс на 100k» оптимистичный пол; рукописный Go-цикл с десериализацией BLOB будет медленнее (ресёрч это и флагал scalar-Go оверхед). Перемерить в Go до того, как доверять триггеру миграции.
7. **Конкретные вендор/академ-числа опираются на веб-чтение моих ресёрч-агентов, не на мою личную перепроверку.** Верификаторы ловили overclaim'ы, но остаточные ошибки возможны (перепутанный arXiv-ID, чуть неверная цифра). DelTA +4.58, Zep 84→58.44, Mem0 72.9/66.88 — доверять на уровне *направления*; перед цитированием точной цифры как факта — перепроверить первоисточник.
8. **Самое глубокое: я рекомендовал ДЕШЁВЫЙ НЕВАЛИДИРОВАННЫЙ путь поверх ДОРОГОГО ДОКАЗАННОГО.** Сильнейшее доказательство (DelTA) валидирует память **с LLM-в-цикле** per-предложение. Наш детерминированный no-LLM горячий путь — это ставка по экономике, а **не** по доказательствам. Защитима на COGS, но это ставка, и я должен сказать прямо: она cost-driven, не evidence-driven.
## Источники (проверены 2026-07-04; с корректировками верификаторов)
**Retrieval-robustness / RAG-failure:** Power of Noise (SIGIR 2024, [2401.14887](https://arxiv.org/abs/2401.14887)); Yoran et al. robust-to-irrelevant (ICLR 2024, [2310.01558](https://arxiv.org/abs/2310.01558)); RGB / Benchmarking LLMs in RAG — noise robustness + negative rejection, **есть китайский тестбед** (AAAI 2024, [2309.01431](https://arxiv.org/abs/2309.01431)); CRAG ([2401.15884](https://arxiv.org/abs/2401.15884)) + независимая репродукция/объяснимость ([2603.16169](https://arxiv.org/html/2603.16169)); over-trust инъектированных терминов ([2510.00829](https://arxiv.org/abs/2510.00829)).