Land exp14 batch-2: 3-family frontier shows the comprehension fix is gpt-5.4-specific (grok inverts, gemini inconclusive), re-gate flags ~10 fidelity chunks, bigger chunks are cheaper and better

This commit is contained in:
Claude (backend session) 2026-07-12 03:43:47 +03:00
parent 174192b1eb
commit a19107479d
8 changed files with 515 additions and 8 deletions

View file

@ -18,7 +18,13 @@
- **Претензия 2 внутри-чанк (смысл) = дешёвая МОДЕЛЬ (capability floor).** T1 (gl.7 двойное отрицание «все знали»): glm-5 **инвертирует** в «никто не знал» НЕЗАВИСИМО от промпта (P0 и P1a); **gpt-5.4 чинит** НЕЗАВИСИМО от промпта (F-base и F-disc). T2 (物是人非) — тот же знак. Промпт претензию 2 НЕ закрывает.
- **Претензия 2 кросс-граница = НЕ доминирует в срезе.** T5/T6 локально самодостаточны; ±1-reference (A-ref) дал байт-идентичный выход (чинить нечего); whole-chapter (P1c) единственный явно связал антецедент (1 датапоинт).
**Near-term рекомендация (на ратификацию оркестратора, НЕ смена дефолта):** (1) дискурс-reflow-контент Ван Цуй→editor-промпт + рассмотреть отдельный target-only reflow-пасс (A-2pass) под COGS; (2) претензию 2 — **селективная фронтир-эскалация редактора по смысл-риску** (не тотальный фронтир — он абзацы портит); (3) CJK-гард при deformat; (4) НЕ строить Ф2-кросс-чанк-память под слабый сигнал. **ПАРТИЯ 2 (осталось):** кривая нарезки, слепые пакеты Ступени II (гл.19/17/18, D=0.061/0.246/0.440), полная ранжирующая панель +leave-one-out, fidelity re-gate (D34.3), 3 финалиста, хендофф. Планка = 2 претензии владельца (ИНТЕРИМ пре-скрин, НЕ пилот Ф2.5). Смену дефолта ратифицирует оркестратор.
**ПАРТИЯ 2 ВЫПОЛНЕНА (по запросу владельца добавлены Gemini+Grok фронтир-редакторы — честный тест в €2.3; итог обеих партий ≈$8.5 across 6 ключей, каждый под лимитом).** Ключевые УТОЧНЕНИЯ:
- **T1-«capability floor» — MODEL-SPECIFIC, не общий фронтир (поправка партии-1 n=1):** gpt-5.4 чинит инверсию двойного отрицания, а **grok-4.3 (фронтир, reasoning-ON) её НЕ чинит — инвертирует как дешёвый glm** (заякорился на черновике; мини-проба grok на СВЕЖЕМ предложении верна → провал именно в РЕДАКТОРСКОЙ задаче). Gemini-редактор ИНКОНКЛЮЗИВЕН (mandatory-thinking съел max_tokens=8000 → обрыв finish=length; урок: Gemini-editor нужен ≥1620k ток × $12/M = COGS-враждебно; €2.3 исчерпан). grok-4.5 — регион-лок (403, оба ключа). ⇒ смысл (П2) чинит **gpt-5.4 конкретно**, не «любой фронтир».
- **Абзацы (П1): grok-disc ЛУЧШИЙ (mean 4.22)** но CJK+провал T1; A-2pass 3.33; **кривая нарезки: крупнее чанк = ДЕШЕВЛЕ выходных токенов И лучше абзацы, 0 ретраев** (оптимум 3200c/глава) → поддержка `edit=крупная единица` (D35.7) по COGS И качеству.
- **Fidelity re-gate (D34.3): пере-прогон НЕ чист** — средняя верность 8894, но **13 катастроф + 21 инверсия редактуры на ~10 чанках** (ch10.1 both-judge fid 60); класс инверсий D34.3 подтверждён на хвосте → флаги на span-ревью, пере-прогон засчитан НЕ полностью.
- **Ранжирование финалистов (гл.19/17/18): стиль F-disc(gpt-5.4)>A-2pass>P0 (робастно); holistic-верность P0>прочих — НО P0 несёт T1-катастрофу, панель её не поймала = ЛИТЕРАЛ-СМЕЩЕНИЕ судей** (урок exp12/мемо eval-aggregation) → доверять span-уровню, не holistic-скаляру.
**Слепой пакет владельцу готов:** `exp14/monitor/monitor14.md` (3 финалиста × 3 главы, нейтральные метки, ключ отдельно) — человеческий вердикт «лучше фана» (D30.7). **Near-term рекомендация** (ратификация оркестратора): дискурс-промпт + крупная edit-единица (дешевле И лучше) под omission-гард; селективная эскалация на gpt-5.4 (не grok/gemini) по смысл-риску; CJK-гард на deformat/grok; ~10 re-gate-флагов на ревью. Планка = 2 претензии (ИНТЕРИМ пре-скрин, НЕ пилот Ф2.5). Детали — `experiments/14-quality-empirics.md` §2/§2Б/§3.
## Ресёрчер — рычаги качества (`research/18-quality-levers.md`), 2026-07-12 (D35.5, вход полигону)

View file

@ -247,8 +247,76 @@ usage→$: `(in·pin + (comp + reason)·pout)/1e6`; `reason`= из `completion`
3. **CJK-гард обязателен** при любом deformat-черновике (A-layout урок).
4. **НЕ строить** сложную Ф2-кросс-чанк-память под этот срез (сигнал слабый); whole-chapter/edit=глава — держать кандидатом под доказанный ROI.
**Оговорки (методика-guard):** trap-набор мал (6 трапов, prev-boundary, без катафоры); фронтир = 1 модель (gpt-5.4), Gemini/grok переводчиками не гонялись (бюджет/эхо); T1-вывод — 1 катастроф-датапоинт (но детерм.-чистый); срез — PD-смежная ранняя арка (без 18+); это ИНТЕРИМ пре-скрин (D35), НЕ вердикт пилота Ф2.5.
**Оговорки партии 1 (пересмотрены партией 2):** T1-вывод «capability floor» партии 1 стоял на 1 фронтир-модели (gpt-5.4) → партия 2 (3 семьи) его УТОЧНИЛА (см. §2Б.1 — floor model-SPECIFIC, не общий). Прочее: срез PD-смежный без 18+; ИНТЕРИМ пре-скрин (D35), не пилот Ф2.5.
## 3. Fidelity re-gate (ПАРАЛЛЕЛЬНО, D34.3 — независим от §1-§2)
---
Span-цитирующий, reasoning-ON, раздельно стиль/верность, охота на КЛАСС ИНВЕРСИЙ редактуры (ch11/0 аутлайер: editor расширил 3009→4631, sim 0.064), катастроф-скрин терминов у победителя, author≠reviewer (не glm-5/deepseek), leave-one-out. Вход — `rerun/rerun-parallel.txt` (57 чанков, выровнено ОРИГ|ПЕРЕВОД) + `records.json`. **Пере-прогон НЕ засчитан до пройденного re-gate.** *(результаты — ниже, после прогона.)*
## 2Б. ПАРТИЯ 2 — 3-семейный фронтир + fidelity re-gate + ранжирование финалистов + кривая нарезки
> Трата партии 2: Gemini $2.20 (≈€2.0, на границе €2.3 — больше не тратил), Kimi +$2.7, OpenAI +$0.9, ZAI +$0.14, XAI $0.15, DeepSeek $0.02. **Итог обеих партий ≈ $8.5** across 6 ключей (каждый под лимитом). grok-4.5 — региональный лок (`403 not available in your region`, ОБА ключа, не бюджет); тестим доступный grok-4.3.
### 2Б.1. Capability floor — MODEL-SPECIFIC, не общий фронтир (ключевая поправка партии 1)
3-семейный фронтир-редактор на trap-наборе (черновик flash общий; варьируем editor-модель × промпт):
| Editor-семья | T1 двойн. отриц. (детерм.) | mean предл./абзац | доля 1-предл. | CJK | len/P0 |
|---|---|---|---|---|---|
| glm-5 (дешёвый) P0/P1a/A-2pass | ✗ **инверсия** (все промпты) | 1.91→3.33 | 0.47→0.19 | 2 | ~1.0 |
| **gpt-5.4** F-base/F-disc | **✓ чинит** (оба промпта) | 1.58 / 2.45 | 0.58 / 0.37 | 0 | ~1.02 |
| **grok-4.3 ON** X-base/X-disc | ✗ **инверсия** (оба! якорится на черновике) | 1.74 / **4.22** | 0.50 / 0.26 | 2 / 6 | ~0.97 |
| gemini-3.1-pro G-base/G-disc | **инконклюзивно** (обрыв) | 1.52 / 1.95 | 0.60 / 0.53 | ≤1 | **0.650.76 ⚠** |
- **T1 «capability floor» — НЕ универсален: gpt-5.4 чинит инверсию черновика, а grok-4.3 (фронтир, reasoning-ON) её НЕ чинит — инвертирует так же, как дешёвый glm.** Grok-редактор заякорился на ошибке черновика (мини-проба grok на СВЕЖЕМ предложении `他没有一个不知道的` дала верно → провал именно в РЕДАКТОРСКОЙ задаче «поймай ошибку черновика»). ⇒ **это не «любой фронтир чинит», а СПЕЦИФИЧЕСКАЯ компетенция gpt-5.4** (ловить двойное отрицание/инверсию черновика). Партия 1 (n=1 фронтир) это переобобщила — партия 2 поправила.
- **grok-4.3 + дискурс (X-disc) — ЛУЧШИЕ абзацы из всех (mean 4.22)**, но CJK-утечка 6 и провал T1. Профиль: отличная переверстка, слабое понимание.
- **gemini-3.1-pro как редактор — ИНКОНКЛЮЗИВНО (харнес-дефект + COGS):** mandatory-thinking съел `max_tokens=8000``finish=length`, выход обрезан (comp 317646 ток, len/P0 0.650.76 = потеря контента). Честный урок: **Gemini-редактору нужен `max_tokens`≥1620k, а при $12/M (thinking биллится как output) это ~$0.150.25/вызов — COGS-враждебно** (×10 к glm/grok, ×23 к gpt-5.4). €2.3 исчерпан на обрезанных вызовах → Gemini-качество не оценено (мой мис-конфиг max_tokens; вписано в урок).
### 2Б.2. Fidelity re-gate (D34.3) — пере-прогон НЕ чист
Span-цитирующий, раздельно верность/стиль, охота на класс инверсий редактуры (final vs draft vs src), author≠reviewer (gpt-5-mini + kimi, НЕ glm/deepseek), leave-one-out. 55 чанков (ch5.0/ch20.0 — пустые экспорт-баг, вне скора).
- **Средняя верность: gpt-5-mini 93.7 (min 60), kimi 87.6 (min 60); leave-one-out 87.6↔93.7.** Широко приемлемо, НО с хвостом.
- **13 КАТАСТРОФ-флагов + 21 ИНВЕРСИЯ РЕДАКТУРЫ** на ~10 чанках. **ch10.1 — катастрофа обоими судьями (fid 60)**; прочие: 1.2, 5.1, 6.0, 9.1, 11.0, 12.0, 13.1, 15.1, 16.0, 20.1. Класс, которого боялся D34.3 (билингв-редактор купил гладкость ценой смысла), ПОДТВЕРЖДЁН на хвосте.
- **Вердикт re-gate: пере-прогон НЕ проходит чисто — ~10 чанков с span-цитированными инверсиями/катастрофами на ревью оркестратору/владельцу** (часть может быть FP LLM-судьи — верифицировать спаны; но ch10.1 both-judge — вероятно реален). Пере-прогон засчитан НЕ полностью (D1.1/D34.3).
### 2Б.3. Ранжирование 3 финалистов (Ступень II, гл.19/17/18) — style↑ у reflow, но HOLISTIC fidelity литерал-смещён
Финалисты: **Fin-A=P0 · Fin-B=A-2pass · Fin-C=F-disc (gpt-5.4)**. Панель gpt-5-mini+kimi+gemini, 3 повтора со свапом, leave-one-out (Borda, 2=лучший):
- **Стиль:** Fin-C **1.43** > Fin-B 0.95 > Fin-A 0.62 (робастно во всех leave-one-out) → **gpt-5.4-дискурс читается лучше, A-2pass второй, прод P0 худший.**
- **Верность (holistic):** Fin-A(P0) **1.53** > Fin-C 0.76 > Fin-B 0.71 (робастно) — **НО P0 содержит T1-КАТАСТРОФУ (инверсию «все знали»→«никто»), которую F-disc чинит.** Панель holistic-верности этого НЕ поймала → **литерал-смещение судей** (награждают консервативную близость P0 к черновику, слепы к его span-катастрофе — точный урок exp12/мемо `eval-aggregation`).
- **Синтез:** доверять SPAN-уровню (F-disc чинит T1, P0 инвертирует) выше, чем holistic-скаляру верности. Reflow/фронтир поднимают СТИЛЬ; на верность есть НАПРЯЖЕНИЕ (агрессивная переверстка ↔ omission-риск), но «P0 вернее» — артефакт литерал-bias, не факт.
### 2Б.4. Кривая нарезки — крупнее чанк = ДЕШЕВЛЕ И лучше абзацы (0 ретраев)
ch17+ch13 × 800/1600/3200/whole (жадная упаковка; flash-draft→glm-5-дискурс; реальный токенайзер):
| размер | ch17 out-ток / mean_spp | ch13 out-ток / mean_spp |
|---|---|---|
| 800c | 15159 / 2.59 | 12171 / 3.48 |
| 1600c (прод) | 10284 / 3.03 | 8043 / 3.95 |
| 3200c | 8691 / 3.67 | 6090 / 4.09 |
| whole | 7782 / 3.59 | 4560 / 3.88 |
- **Монотонно: крупнее чанк → МЕНЬШЕ выходных токенов (дешевле) И ЛУЧШЕ абзацы** (mean_spp↑, доля-1-предл.↓). 0 ретраев на всех размерах (retry blast-radius не наблюдён на этих 2 главах). Оптимум ~**3200c / whole** по обеим осям.
- Мелкий чанк (800c) — дороже (дублирующий overhead/повторы) И рубленее. Опровергает «мельче = дешевле» для output-домината; на ЭТОМ тексте перевёрнутой-U вниз не видно (lost-in-middle не бьёт до главы). **Поддерживает `edit=крупная единица / глава` (D35.7) — И по COGS, И по качеству.** Оговорка: 2 главы, 1 текст, 0 наблюдённых ретраев (retry-adjusted не активировался).
### 2Б.5. Итоговая capability-vs-activation карта (обе партии)
| Претензия | Потолок | Уточнённое доказательство | Ход |
|---|---|---|---|
| (1) абзацы | **ПРОМПТ + РАЗМЕР** (активация) | дискурс-промпт (P1a/F-disc/X-disc) + 2-пасс (A-2pass 3.33) + крупный чанк (кривая) двигают KPI; grok-disc лучший (4.22); фронтир-модель НЕ нужна | катить дискурс-промпт + крупная edit-единица; A-2pass если COGS позволит |
| (2) внутри-чанк смысл | **MODEL-SPECIFIC** (gpt-5.4), не общий floor | gpt-5.4 чинит T1; glm-5 И grok-4.3 инвертируют; gemini инконклюзивно | селективная эскалация на **gpt-5.4** по смысл-риску (grok НЕ годится — тоже инвертирует) |
| (2) кросс-граница | не доминирует в срезе | A-ref null; P1c связал (1 датапоинт) | не строить Ф2-память под слабый сигнал |
| верность пере-прогона | **есть хвост порчи** | re-gate: 13 катастроф + 21 инверсия на ~10 чанках | ревью флагов + omission-гард на reflow-армы |
### 2Б.6. Пересмотренная near-term рекомендация (на ратификацию оркестратора)
1. **Абзацы (П1):** дискурс-reflow-промпт (Ван Цуй) + сдвиг edit-единицы к КРУПНОЙ (3200c/глава — дешевле И лучше по кривой). A-2pass даёт топ-KPI, но добавляет пасс — взвесить vs крупный чанк (крупный чанк даёт похожий лифт БЕЗ +пасса и ДЕШЕВЛЕ). **Оба под omission-гард** (reflow-армы просели по holistic-верности — следить за атомами).
2. **Смысл (П2):** селективная эскалация редактора на **gpt-5.4 конкретно** по смысл-риску (двойное отрицание/chengyu/инверсия черновика). **grok и gemini НЕ заменяют** (grok инвертирует, gemini COGS-враждебен/инконклюзивен). Не тотальный фронтир (портит абзацы у F-base, дорого).
3. **CJK-гард** обязателен на deformat (A-layout) и на grok-выходах (утечка 6).
4. **Пере-прогон:** ~10 re-gate-флагов на ревью до «засчитан».
5. **Слепые пакеты Ступени II** (`exp14/monitor/monitor14.md`) — владельцу на человеческий вердикт «лучше фана» (арбитр, D30.7).
**Оговорки (методика-guard):** trap-набор мал (6, prev-boundary, без катафоры); T1 — детерм.-чистый, но 1 конструкция; holistic-fidelity судьи литерал-смещены (доверять спанам); gemini недооценён (мис-конфиг max_tokens, €2.3 исчерпан); кривая — 2 главы/0 ретраев; ИНТЕРИМ пре-скрин (D35), НЕ пилот Ф2.5. Смену дефолта ратифицирует ОРКЕСТРАТОР.
## 3. Fidelity re-gate (D34.3) — свод в §2Б.2
Пройден с результатом **«не чист»**: средняя верность gpt-5-mini 93.7 / kimi 87.6 (leave-one-out 87.6↔93.7), но **13 катастроф-флагов + 21 инверсия редактуры на ~10 чанках** (ch10.1 — both-judge катастрофа fid 60). Класс инверсий редактуры (D34.3) подтверждён на хвосте. Флаги — на span-верификацию оркестратором/владельцем; пере-прогон засчитан НЕ полностью. Вход: `rerun/records.json` (source/draft/final, author≠reviewer gpt-5-mini+kimi). Артефакты: `exp14/regate_verdicts.jsonl`.

View file

@ -145,8 +145,9 @@ def main():
ref = build_reference(a.arm)
run_editor(a.arm, "discourse", [u for u in chunk_units(a.scope) if u["id"] in ref],
a.model, reference_by=ref, dry=a.dry)
elif a.arm in ("F-base", "F-disc"):
variant = "baseline" if a.arm == "F-base" else "discourse"
elif a.arm in ("F-base", "F-disc", "G-base", "G-disc", "X-base", "X-disc"):
# F-* = gpt-5.4, G-* = gemini-3.1-pro-preview, X-* = grok-4.3 reasoning-ON (все фронтир-редакторы)
variant = "baseline" if a.arm.endswith("-base") else "discourse"
run_editor(a.arm, variant, chunk_units(a.scope), a.model, dry=a.dry)
else:
raise SystemExit(f"unknown arm {a.arm}")

View file

@ -42,10 +42,12 @@ TRAPS = [
JUDGE_POOL = { # judge_model → family (для self-family exclusion)
"gpt-5-mini": "openai", "kimi-k2.6": "moonshot", "gemini-3.1-pro-preview": "google",
}
ARM_FAMILY = { # editor-модель арма → family (author≠reviewer)
ARM_FAMILY = { # editor-модель арма → family (author≠reviewer, self-family exclusion)
"P0": "zai", "P1a": "zai", "P1b": "zai", "P1c": "zai", "A-layout": "zai",
"A-2pass": "zai", "A-ref-prev": "zai", "A-ref-both": "zai",
"F-base": "openai", "F-disc": "openai",
"F-base": "openai", "F-disc": "openai", # gpt-5.4
"G-base": "google", "G-disc": "google", # gemini-3.1-pro-preview
"X-base": "xai", "X-disc": "xai", # grok-4.3 reasoning-ON
}
# draft общий = deepseek → deepseek не судья (self-family к черновику, где рождается T1)

63
eval/exp14_monitor.py Normal file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""exp14 Ступень II — слепой пакет владельцу: 3 ФИНАЛИСТА на 3 главах (гл.19/17/18), под
нейтральными метками, ключ ОТДЕЛЬНО. Копирайт: книжный текст ВНЕ git (exp14/monitor/).
Финалисты: Fin-A=P0 (текущий прод baseline) · Fin-B=A-2pass (лучший near-term, дешёвый) ·
Fin-C=F-disc (фронтир gpt-5.4, дискурс). Per-глава seeded-shuffle меток (V1..V3 РАЗНАЯ
модель в каждой главе; ключ владелец не открывает до ранжирования).
"""
from __future__ import annotations
import json, random
from pathlib import Path
import exp14_common as C
ARMS = C.DIAG / "arms"
MAT = json.load(open(C.DIAG / "material.json"))
OUT = C.DIAG / "monitor"; OUT.mkdir(exist_ok=True)
FINALISTS = {"Fin-A": "P0", "Fin-B": "A-2pass", "Fin-C": "F-disc"}
CHAPTERS = [19, 17, 18]
SALT = "exp14-monitor-blind-v1"
def chapter_text(arm: str, ch: int) -> str:
chunks = sorted(u["chunk_idx"] for u in MAT["stage2"][str(ch)])
parts = []
for ck in chunks:
p = ARMS / arm / f"{ch}.{ck}.txt"
parts.append(p.read_text(encoding="utf-8") if p.exists() else f"[[MISSING {arm} {ch}.{ck}]]")
return "\n\n".join(parts)
def main():
missing = [(f, a, ch) for f, a in FINALISTS.items() for ch in CHAPTERS
for ck in sorted(u["chunk_idx"] for u in MAT["stage2"][str(ch)])
if not (ARMS / a / f"{ch}.{ck}.txt").exists()]
if missing:
print("⚠ отсутствуют выходы (арм ещё не готов):", missing[:10]); return
key = {}
md = ["# Слепое чтение — exp14 Ступень II (3 варианта × 3 главы)\n",
"Прочти каждую главу в ТРЁХ вариантах (V1/V2/V3 — порядок в каждой главе РАЗНЫЙ). "
"Для каждой главы: (1) какой вариант читается лучше как русская проза «лучше фана» — ранжируй V1>V2>V3; "
"(2) закрыты ли обе претензии (абзацы/конвенции; понимание связей/смысла); (3) любые смысловые ляпы. "
"Ключ (какая модель под какой меткой) — в отдельном файле `_КЛЮЧ.json`, открой ПОСЛЕ.\n",
"D-плотность диалога: гл.19=0.061 (нарратив), гл.17=0.246 (смешанная), гл.18=0.440 (диалог).\n"]
for ch in CHAPTERS:
order = list(FINALISTS); random.Random(f"{SALT}-{ch}").shuffle(order)
labels = {f"V{i+1}": order[i] for i in range(len(order))}
key[str(ch)] = {lab: {"finalist": fin, "arm": FINALISTS[fin]} for lab, fin in labels.items()}
md.append(f"\n\n---\n\n## Глава {ch}\n")
for i in range(len(order)):
lab = f"V{i+1}"; fin = labels[lab]
md.append(f"\n### {lab}\n\n{chapter_text(FINALISTS[fin], ch)}\n")
(OUT / "monitor14.md").write_text("\n".join(md), encoding="utf-8")
(OUT / "_КЛЮЧ.json").write_text(json.dumps(
{"salt": SALT, "finalists": FINALISTS, "per_chapter": key}, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"{OUT/'monitor14.md'} (3 главы × 3 слепых варианта)")
print(f"{OUT/'_КЛЮЧ.json'} (НЕ открывать до ранжирования)")
for ch in CHAPTERS:
print(f" гл.{ch}: " + ", ".join(f"{lab}={key[str(ch)][lab]['finalist']}" for lab in ("V1","V2","V3")))
if __name__ == "__main__":
main()

120
eval/exp14_rank.py Normal file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""exp14 Ступень II — ранжирующая панель финалистов (P0/A-2pass/F-disc) на главах 19/17/18.
РАЗДЕЛЬНО стиль/верность, span-цитаты, N повторов со свапом меток, leave-one-judge-out.
Судьи gpt-5-mini + kimi + gemini(точечно). Self-family (F-disc=openai) снимается leave-one-out.
Guard мемо eval-aggregation: НЕ собирать ложную сходимость.
Использование: exp14_rank.py [--repeats 3] [--dry]
"""
from __future__ import annotations
import argparse, json, random, re, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import exp14_common as C
ARMS = C.DIAG / "arms"
MAT = json.load(open(C.DIAG / "material.json"))
FINALISTS = {"Fin-A": "P0", "Fin-B": "A-2pass", "Fin-C": "F-disc"}
CHAPTERS = [19, 17, 18]
AXES = {"style": "русская художественность/абзацы/конвенции (естественность прозы, красная строка, тире-диалог)",
"fidelity": "верность смыслу (нет инверсий полярности/участника, нет потери атомов; можно ценой лёгкой шероховатости)"}
JUDGES = ["gpt-5-mini", "kimi-k2.6", "gemini-3.1-pro-preview"]
CAPS = {"gpt-5-mini": 0.10, "kimi-k2.6": 0.15, "gemini-3.1-pro-preview": 0.30}
def chapter_text(arm, ch):
chunks = sorted(u["chunk_idx"] for u in MAT["stage2"][str(ch)])
return "\n\n".join((ARMS / arm / f"{ch}.{ck}.txt").read_text(encoding="utf-8") for ck in chunks)
def run():
ap = argparse.ArgumentParser(); ap.add_argument("--repeats", type=int, default=3)
ap.add_argument("--dry", action="store_true"); a = ap.parse_args()
sp = C.Spender(C.DIAG / "rank_costs.jsonl", CAPS)
vpath = C.DIAG / "rank_verdicts.jsonl"; out = []; done = set()
if vpath.exists():
for l in vpath.read_text(encoding="utf-8").splitlines():
if l.strip():
v = json.loads(l); out.append(v); done.add((v["ch"], v["axis"], v["judge"], v["rep"]))
tasks = []
for ch in CHAPTERS:
for axis in AXES:
for rep in range(a.repeats):
order = list(FINALISTS); random.Random(f"exp14-rank-{ch}-{axis}-{rep}").shuffle(order)
labels = {f"V{i+1}": order[i] for i in range(len(order))}
for jm in JUDGES:
if jm == "gemini-3.1-pro-preview" and rep > 0:
continue # gemini точечно: 1 повтор (бюджет €2.6)
if (ch, axis, jm, rep) in done:
continue
tasks.append((ch, axis, rep, jm, labels))
print(f"rank tasks: {len(tasks)} new ({len(done)} done)")
if a.dry:
from collections import Counter
print(Counter(t[3] for t in tasks)); return
for ch, axis, rep, jm, labels in tasks:
blocks = "\n\n".join(f"=== {lab} ===\n{chapter_text(FINALISTS[fin], ch)}" for lab, fin in labels.items())
sysmsg = ("Ты — литературный судья zh→ru. Оцени ТРИ перевода одной главы по ОДНОЙ оси. "
"Ранжируй от лучшего к худшему. Приведи 2-4 span-цитаты-обоснования (≤15 слов каждая). "
'Ответь СТРОГО JSON без markdown: {"ranking":["V?","V?","V?"],'
'"evidence":[{"label":"V?","quote":"≤15 слов","note":"почему"}]}.')
user = f"ОСЬ ОЦЕНКИ: {AXES[axis]}\n\nТРИ ВАРИАНТА:\n\n{blocks}\n\nРанжируй по оси. JSON."
s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 6000))
in_est = C.count_tokens(sysmsg + user, "glm")
ok, pred = sp.guard(jm, in_est, s["max_tokens"])
if not ok:
print(f" OVER-CAP {ch} {axis} {jm} pred=${pred:.3f}"); continue
text, err, usage = C.call(s, [{"role": "system", "content": sysmsg},
{"role": "user", "content": user}], timeout=360)
cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "ch": ch,
"axis": axis, "rep": rep, "err": err, "usage": usage})
rk = parse_rank(text) if not err else None
v = {"ch": ch, "axis": axis, "rep": rep, "judge": jm, "labels": labels,
"ranking_labels": rk, "err": err}
# map labels→finalist
if rk:
v["ranking_fin"] = [labels.get(x) for x in rk]
out.append(v)
with open(vpath, "a", encoding="utf-8") as f:
f.write(json.dumps(v, ensure_ascii=False) + "\n")
print(f" ch{ch} {axis:<8} rep{rep} {jm:<18}{v.get('ranking_fin')} ${cst:.4f}")
summarize(out)
def parse_rank(text):
m = re.search(r'"ranking"\s*:\s*(\[[^\]]*\])', text)
if not m:
return None
try:
arr = json.loads(m.group(1))
return [str(x) for x in arr]
except Exception:
return re.findall(r'V\d', m.group(1))
def summarize(out):
from collections import defaultdict
import statistics
# Borda: 1st=2pts,2nd=1,3rd=0; per axis; per judge for leave-one-out
def borda(rows):
pts = defaultdict(list)
for v in rows:
rf = v.get("ranking_fin")
if not rf or len(rf) != 3 or None in rf:
continue
for i, fin in enumerate(rf):
pts[fin].append(2 - i)
return {f: round(statistics.mean(p), 2) for f, p in pts.items()}
print("\n=== РАНЖИРОВАНИЕ (Borda mean, 2=лучший) ===")
for axis in AXES:
rows = [v for v in out if v["axis"] == axis]
print(f"\n{axis}: overall {borda(rows)}")
judges = sorted(set(v["judge"] for v in rows))
for j in judges:
print(f" only {j}: {borda([v for v in rows if v['judge']==j])}")
for j in judges: # leave-one-out
print(f" leave-out {j}: {borda([v for v in rows if v['judge']!=j])}")
if __name__ == "__main__":
run()

125
eval/exp14_regate.py Normal file
View file

@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""exp14 §3 — Fidelity re-gate пере-прогона (D34.3, ОБЯЗАТЕЛЕН; независим от §1-§2).
Span-цитирующий, РАЗДЕЛЬНО стиль/верность, охота на КЛАСС ИНВЕРСИЙ РЕДАКТУРЫ (final vs draft
vs src: билингв-редактор мог купить гладкость ценой смысла), катастроф-скрин терминов.
authorreviewer: судьи НЕ glm-5 (редактор) и НЕ deepseek (черновик) кросс-семейно
gpt-5-mini + kimi (+ gemini точечно на флагах). leave-one-out. $ персист.
Вход: rerun/records.json (source/draft/final). ch5.0/ch20.0 final пуст (экспорт-баг D35.4a),
скорятся отдельно (не в среднем). Выход: exp14/regate_verdicts.jsonl + свод.
Использование: exp14_regate.py [--dry] [--limit N]
"""
from __future__ import annotations
import argparse, json, re, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import exp14_common as C
RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun")
RECS = json.load(open(RERUN / "records.json"))
JUDGES = ["gpt-5-mini", "kimi-k2.6"] # кросс-семейно к glm-5/deepseek; gemini — точечно вне этого прохода
CAPS = {"gpt-5-mini": 0.06, "kimi-k2.6": 0.10, "gemini-3.1-pro-preview": 0.30}
SYS = ("Ты — билингвальный редактор-эксперт zh→ru, СУДЬЯ ВЕРНОСТИ. Тебе даны: ИСХОДНИК (zh), "
"ЧЕРНОВИК (ru) и ФИНАЛ после редактуры (ru). Оцени ВЕРНОСТЬ ФИНАЛА исходнику РАЗДЕЛЬНО от стиля. "
"Особо ищи КЛАСС ИНВЕРСИЙ РЕДАКТУРЫ: правки, где ФИНАЛ отошёл от смысла исходника ДАЛЬШЕ черновика "
"(редактор внёс смысл, которого в исходнике нет; перевернул полярность/участника/число; выбросил атом). "
"Ответь СТРОГО JSON без markdown: "
'{"fidelity":0-100,"style":0-100,"n_mistranslation":int,"n_omission":int,'
'"n_edit_inversion":int,"catastrophe":true|false,'
'"flags":[{"type":"inversion|omission|term|addition","span_src":"≤12 слов zh",'
'"quote_final":"≤15 слов ru","why":"кратко"}]}. '
"catastrophe=true при инверсии полярности/участника/действия ИЛИ потере ключевого атома (заголовок/событие/термин).")
def run():
ap = argparse.ArgumentParser(); ap.add_argument("--dry", action="store_true")
ap.add_argument("--limit", type=int, default=0); a = ap.parse_args()
sp = C.Spender(C.DIAG / "regate_costs.jsonl", CAPS)
vpath = C.DIAG / "regate_verdicts.jsonl"
done = set(); out = []
if vpath.exists():
for l in vpath.read_text(encoding="utf-8").splitlines():
if l.strip():
v = json.loads(l); out.append(v); done.add((v["chunk"], v["judge"]))
recs = [r for r in RECS if (r["chapter"], r["chunk_idx"]) not in {(5, 0), (20, 0)}
and (r.get("final") or "").strip()]
empties = [(r["chapter"], r["chunk_idx"]) for r in RECS if not (r.get("final") or "").strip()]
if a.limit:
recs = recs[:a.limit]
tasks = [(r, jm) for r in recs for jm in JUDGES
if (f"{r['chapter']}.{r['chunk_idx']}", jm) not in done]
print(f"re-gate: {len(recs)} chunks × {len(JUDGES)} judges = {len(tasks)} new tasks "
f"({len(done)} done); empty-final (экспорт-баг, вне скора): {empties}")
if a.dry:
est = sum(C.predict_cost(jm, 4000, 4000) for _, jm in tasks)
print(f"predicted ≤ ${est:.2f}"); return
for r, jm in tasks:
cid = f"{r['chapter']}.{r['chunk_idx']}"
user = (f"ИСХОДНИК (zh):\n{r['source']}\n\nЧЕРНОВИК (ru):\n{r['draft']}\n\n"
f"ФИНАЛ после редактуры (ru):\n{r['final']}\n\nОцени верность ФИНАЛА. Ответь JSON.")
# gpt-5-mini: reasoning ⊆ completion → нужен запас (medium@4000 съедал бюджет → empty); low@10000
s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 10000),
reasoning_effort=("low" if jm == "gpt-5-mini" else None))
in_est = C.count_tokens(SYS + user, "deepseek")
ok, pred = sp.guard(jm, in_est, s["max_tokens"])
if not ok:
print(f" OVER-CAP {cid} {jm} pred=${pred:.3f}"); continue
text, err, usage = C.call(s, [{"role": "system", "content": SYS},
{"role": "user", "content": user}], timeout=300)
cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "chunk": cid,
"err": err, "usage": usage})
v = parse(text) if not err else {"fidelity": None, "err": err[:60]}
v.update(chunk=cid, judge=jm)
out.append(v)
with open(vpath, "a", encoding="utf-8") as f:
f.write(json.dumps(v, ensure_ascii=False) + "\n")
fl = len(v.get("flags", []) or [])
print(f" {cid:<7} {jm:<12} fid={v.get('fidelity')} style={v.get('style')} "
f"cat={v.get('catastrophe')} flags={fl} ${cst:.4f}")
summarize(out, empties)
def parse(text):
m = re.search(r"\{.*\}", text, re.S)
if not m:
return {"fidelity": None, "parse": text[:80]}
try:
d = json.loads(m.group(0))
return {k: d.get(k) for k in ("fidelity", "style", "n_mistranslation", "n_omission",
"n_edit_inversion", "catastrophe", "flags")}
except Exception:
f = re.search(r'"fidelity"\s*:\s*(\d+)', m.group(0))
cat = '"catastrophe": true' in m.group(0).lower() or '"catastrophe":true' in m.group(0).lower()
return {"fidelity": int(f.group(1)) if f else None, "catastrophe": cat, "parse_fallback": True}
def summarize(out, empties):
from collections import defaultdict
import statistics
byj = defaultdict(list); cats = []; invs = []
for v in out:
if v.get("fidelity") is not None:
byj[v["judge"]].append(v["fidelity"])
if v.get("catastrophe"):
cats.append((v["chunk"], v["judge"]))
for fl in (v.get("flags") or []):
if fl.get("type") == "inversion":
invs.append((v["chunk"], v["judge"], fl.get("quote_final", "")[:60]))
print("\n=== FIDELITY RE-GATE свод ===")
for j, xs in byj.items():
print(f" {j}: mean fidelity {statistics.mean(xs):.1f} (n={len(xs)}, min={min(xs)})")
if len(byj) == 2: # leave-one-out
js = list(byj)
print(f" leave-one-out: без {js[0]}{statistics.mean(byj[js[1]]):.1f}; без {js[1]}{statistics.mean(byj[js[0]]):.1f}")
print(f" КАТАСТРОФ-флаги: {len(cats)}{cats[:20]}")
print(f" ИНВЕРСИИ РЕДАКТУРЫ: {len(invs)}")
for c in invs[:15]:
print(f" {c[0]} [{c[1]}]: {c[2]}")
print(f" экспорт-баг пустые финалы (вне скора): {empties}")
if __name__ == "__main__":
run()

122
eval/exp14_sizecurve.py Normal file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""exp14 §кривая — размер-чанка ↔ качество ↔ биллинг ↔ ретраи.
Пере-нарезка главы при разных бюджетах (жадная упаковка целых абзацев-строк до char-порога)
+ whole-chapter. Пайплайн draft(flash thinking-ON)edit(glm-5 дискурс). Снимаем РЕАЛЬНЫЕ
in/out/reasoning-токены, латентность, ошибки-по-причинам, output-token-cost, KPI-абзацы.
Chapter-level инъекция едина для всех сегментов (изолируем РАЗМЕР, не глоссарий).
Бюджет чанка отчитывается в ВЫХОДНЫХ токенах (реальный токенайзер glm-4.6). Оптимизация
retry-adjusted output-cost. Оговорка: у этой вебновеллы плоская структура (1 предл./строка,
мало сцен-маркеров) политика границ «сцена vs жадная» слабо различима; меряем жадную + whole.
Использование: exp14_sizecurve.py [--chapters 17,13] [--dry]
"""
from __future__ import annotations
import argparse, json, re, statistics, sys, time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import exp14_common as C
import exp14_prompts as P
RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun")
RECS = {(r["chapter"], r["chunk_idx"]): r for r in json.load(open(RERUN / "records.json"))}
INJ = json.load(open(C.DIAG / "injection_blocks.json"))
SIZES = [800, 1600, 3200, 99999] # zh-chars; 99999 = whole-chapter
OUT = C.DIAG / "sizecurve"; OUT.mkdir(exist_ok=True)
CAPS = {"deepseek-v4-flash": 0.03, "glm-5": 0.08}
def chapter_source(ch):
cks = sorted(c for (cc, c) in RECS if cc == ch)
return "\n".join(RECS[(ch, c)]["source"] for c in cks)
def rechunk(src, budget):
"""Жадная упаковка целых строк (абзацев) до budget zh-символов; строку не рвём."""
lines = [l for l in src.split("\n") if l.strip()]
segs, cur, n = [], [], 0
for l in lines:
if cur and n + len(l) > budget:
segs.append("\n".join(cur)); cur, n = [], 0
cur.append(l); n += len(l)
if cur:
segs.append("\n".join(cur))
return segs
def paras(t): return [p for p in re.split(r"\n\s*\n", t.strip()) if p.strip()]
def sents(p): return [s for s in re.split(r"(?<=[.!?…])\s+", p.strip()) if s.strip()]
def kpi(t):
narr = [p for p in paras(t) if not p.strip().startswith("")]
sp = [len(sents(p)) for p in narr] or [0]
return round(statistics.mean(sp), 2), round(sum(1 for x in sp if x == 1)/len(sp), 3)
def run():
ap = argparse.ArgumentParser(); ap.add_argument("--chapters", default="17,13")
ap.add_argument("--dry", action="store_true"); a = ap.parse_args()
chapters = [int(x) for x in a.chapters.split(",")]
sp = C.Spender(C.DIAG / "sizecurve_costs.jsonl", CAPS)
tsys = P.translator_system(); esys = P.editor_system("discourse")
rows = []
for ch in chapters:
src = chapter_source(ch)
einj = INJ.get(f"{ch}/ALL", {}).get("editor_constraint", "")
ginj = INJ.get(f"{ch}/ALL", {}).get("bilingual_glossary", "")
for budget in SIZES:
segs = rechunk(src, budget)
label = "whole" if budget == 99999 else f"{budget}c"
tag = f"{ch}-{label}"
outp = OUT / f"{tag}.txt"
if outp.exists():
print(f" [skip] {tag}"); continue
print(f"\n=== ch{ch} size={label}: {len(segs)} сегментов ===")
if a.dry:
for s in segs:
print(f" seg {len(s)}zh-chars")
continue
final_parts, in_tot, out_tot, reason_tot, seg_costs, errs, lat = [], 0, 0, 0, 0.0, [], 0.0
for i, seg in enumerate(segs):
# DRAFT (flash, thinking ON)
ds = C.spec("deepseek-v4-flash", temp=0.3, max_tokens=8000)
dmsgs = C.messages_with_injection(tsys, ginj, P.translator_user(seg))
t0 = time.time()
draft, derr, dusage = C.call(ds, dmsgs, timeout=300)
sp.record({"model": "deepseek-v4-flash", "keyenv": ds["keyenv"], "tag": tag,
"seg": i, "stage": "draft", "err": derr, "usage": dusage})
if derr:
errs.append(f"draft:{derr[:30]}"); draft = seg # фолбэк — исходник (эхо-гард поймает)
# EDIT (glm-5 discourse)
es = C.spec("glm-5", temp=0.4, max_tokens=8000)
emsgs = C.messages_with_injection(esys, einj, P.editor_user(seg, draft))
final, eerr, eusage = C.call(es, emsgs, timeout=360)
c = sp.record({"model": "glm-5", "keyenv": es["keyenv"], "tag": tag, "seg": i,
"stage": "edit", "err": eerr, "usage": eusage})
lat += round(time.time() - t0, 1)
if eerr:
errs.append(f"edit:{eerr[:30]}"); final = draft
final_parts.append(final)
in_tot += (dusage.get("prompt_tokens", 0) or 0) + (eusage.get("prompt_tokens", 0) or 0)
out_tot += (dusage.get("completion_tokens", 0) or 0) + (eusage.get("completion_tokens", 0) or 0)
full = "\n\n".join(final_parts)
outp.write_text(full, encoding="utf-8")
ru_out_tok = C.count_tokens(full, "glm")
mean_spp, share1 = kpi(full)
han = sum(1 for ch_ in full if "" <= ch_ <= "鿿")
row = dict(chapter=ch, size=label, n_seg=len(segs), in_tok=in_tot, out_tok=out_tot,
ru_out_tok=ru_out_tok, cost=round(sp.by_key.get("ZAI_API_KEY", 0)+sp.by_key.get("DEEPSEEK_API_KEY", 0), 4),
mean_spp=mean_spp, share_1sent=share1, han=han, errs=errs, latency_s=round(lat, 1))
rows.append(row)
print(f" segs={len(segs)} in={in_tot} out={out_tot} ru_out_tok={ru_out_tok} "
f"mean_spp={mean_spp} share1={share1} CJK={han} errs={len(errs)} lat={lat:.0f}s")
(OUT / "sizecurve.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
if rows:
print("\n=== КРИВАЯ (per chapter×size) ===")
print(f"{'ch-size':<12}{'segs':>5}{'out_tok':>9}{'mean_spp':>9}{'share1':>8}{'CJK':>5}{'errs':>5}")
for r in rows:
print(f"{r['chapter']}-{r['size']:<8}{r['n_seg']:>5}{r['out_tok']:>9}{r['mean_spp']:>9}{r['share_1sent']:>8}{r['han']:>5}{len(r['errs']):>5}")
if __name__ == "__main__":
run()