#!/usr/bin/env python3 """Детерминированные рычаги канон-консистентности — кривая L1→L3 на ТРУДНОМ чанке. Follow-up к adaptive_incontext/adaptive_reask (по запросу владельца: «раз не идём в гибрид — долбить детерминированную сторону; замерить, сколько добивает каждый рычаг»). Лёгкий чанк насыщал глоссарий до 1.0 → рычаги не было видно. Здесь — ТРУДНЫЙ чанк (1600 знаков, ~11 рекуррентных сущностей, плотно) + контролируемый recall-тест, где точный матч промахивается. Три рычага (все ДЕТЕРМИНИРОВАННЫЕ, 0 ML, 0 VRAM): L1 post-check ЛЕММАТИЗИРУЮЩИЙ (pymorphy3), а не регэксп → убирает ложные флаги/undercount (мой промах 王胡→«Бородатого Вана» на регэкспе `Ван\b`). Меряем false-flag rate. L2 RECALL матчера: точный матч → +нормализация(trad→simp/NFKC) → +alias-граф → +sticky. Детерминированный (без LLM): сколько «должных» записей вообще инъектится. L3 post-check + точечный RE-ASK на РЕАЛЬНЫЕ промахи (verifier-gated correction) → прирост adherence на трудном чанке. deepseek-v4-flash + grok-4.3. INDICATIVE (малый N). Usage: eval/.venv/bin/python eval/adaptive_levers.py Output: stdout + eval/data/adaptive_levers.json """ from __future__ import annotations import json, re, sys, unicodedata from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import refusal_bench as rb import pymorphy3 ROOT = Path(__file__).resolve().parent SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt" OUT = ROOT / "data" / "adaptive_levers.json" MORPH = pymorphy3.MorphAnalyzer() # Трудный чанк: 1600 знаков, ~11 рекуррентных сущностей (плотно → adherence проседает). HARD = SRC.read_text(encoding="utf-8")[6450:6450 + 1600] # entity → (canon dst для глоссария, СТРОГИЙ регэксп [наивный post-check], лемма-ключ[и] для L1) ENT = { "阿Q": ("А-кью", r"А-кью\b", ["а-кью"]), # дефис → hyph-режим "未庄": ("Вэйчжуан", r"Вэйчжуан\b", ["вэйчжуан"]), "赵太爷": ("почтенный Чжао", r"Чжао\b", ["чжао"]), "王胡": ("Бородатый Ван", r"Ван\b", ["ван"]), "小D": ("Маленький Дэн", r"Дэн\b", ["дэн"]), "假洋鬼子": ("Поддельный заморский чёрт", r"заморский чёрт\b", ["заморский", "чёрт"]), "尼姑": ("монашка", r"монашка\b", ["монашка"]), "吴妈": ("У-ма", r"У-ма\b", ["у-ма"]), "邹七嫂": ("тётушка Цзоу Седьмая", r"Цзоу\b", ["цзоу"]), "秀才": ("сюцай", r"сюцай\b", ["сюцай"]), "钱": ("Цянь", r"Цянь\b", ["цянь"]), } HYPH = {"а-кью": r"А-?кью", "у-ма": r"У-?ма"} # дефисные translit — регэксп, не лемма SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент на русский язык. " "Сохрани все реплики и детали без пропусков; стиль — живой литературный русский. Выведи ТОЛЬКО перевод.") GLOSS = "ГЛОССАРИЙ (используй утверждённые переводы последовательно):\n" + \ "\n".join(f"{z} → {v[0]}" for z, v in ENT.items()) PRESENT = [z for z in ENT if z in HARD] # ---------- матчеры post-check (L1) ---------- def match_strict(out, z): """Наивный регэксп-post-check (как в adaptive_incontext) — ломается на склонении.""" return bool(re.search(ENT[z][1], out)) def lemmas_of(out): return {MORPH.parse(w)[0].normal_form for w in re.findall(r"[А-Яа-яЁё]+", out)} def match_lemma(out, z, lem_cache): """L1: лемматизирующий post-check. Для дефисных translit — регэксп-фолбэк.""" keys = ENT[z][2] if keys[0] in HYPH: return bool(re.search(HYPH[keys[0]], out)) return all(k in lem_cache for k in keys) # ---------- матчер РЕКОЛА (L2), детерминированный ---------- TRAD2SIMP = {"趙": "赵", "莊": "庄", "錢": "钱", "陳": "陈", "萬": "万", "舊": "旧", "臉": "脸"} def normalize(s): s = unicodedata.normalize("NFKC", s) return "".join(TRAD2SIMP.get(c, c) for c in s) ALIASES = {"王胡": ["王癞胡", "癞胡"], "赵太爷": ["赵老太爷"]} def recall_config(chunks, keys_present, *, norm=False, alias=False, sticky=False): """Сколько записей из keys_present инъектится по цепочке чанков при данной конфигурации.""" active_prev, injected_ever = set(), set() for ch in chunks: text = normalize(ch) if norm else ch hit = set() for z in keys_present: pats = [z] + (ALIASES.get(z, []) if alias else []) pats = [normalize(p) if norm else p for p in pats] if any(p in text for p in pats): hit.add(z) if sticky: hit |= (active_prev & set(keys_present)) # scene-inertia active_prev = hit injected_ever |= hit return injected_ever def translate(model, base, key_env, user): t, _, _ = rb.call_provider({"name": model, "base_url": base, "model": model, "api_key_env": key_env, "max_tokens": 8000, **({"temperature": 0} if "grok" in model else {})}, SYSTEM, user, timeout=200) return t or "" def main(): res = {"present": PRESENT, "hard_chunk_len": len(HARD)} print(f"ТРУДНЫЙ чанк: {len(HARD)} знаков, present entities ({len(PRESENT)}): {PRESENT}\n") # ===== L2: детерминированный RECALL-curve (без LLM) ===== print("=== L2 · RECALL матчера (детерминированный, без LLM) ===") # 3 чанка, где точный матч промахивается: trad-вариант, alias, местоименный (sticky) recall_chunks = [ "趙太爺同錢老爺在未莊很有威。王癞胡也常來。", # trad + alias 王癞胡(=王胡) "他慢慢站起来,看了看四周。", # местоименный — 王胡 из sticky ] keys = ["赵太爷", "钱", "未庄", "王胡"] configs = [("точный", {}), ("+норм", {"norm": True}), ("+alias", {"norm": True, "alias": True}), ("+sticky", {"norm": True, "alias": True, "sticky": True})] l2 = {} for name, kw in configs: got = recall_config(recall_chunks, keys, **kw) l2[name] = sorted(got) print(f" {name:<9} recall {len(got)}/{len(keys)} = {len(got)/len(keys):.2f} инъектировано: {sorted(got)}") res["L2_recall"] = l2 # ===== L1 + L3: adherence + re-ask на трудном чанке (LLM) ===== print("\n=== L1 (лемма-post-check) + L3 (re-ask) на трудном чанке ===") res["models"] = {} for model, base, kenv in [("deepseek-v4-flash", "https://api.deepseek.com/v1", "DEEPSEEK_API_KEY"), ("grok-4.3", "https://api.x.ai/v1", "XAI_API_KEY")]: out = translate(model, base, kenv, GLOSS + "\n\nФРАГМЕНТ:\n" + HARD) if not out: print(f"\n[{model}] пустой ответ — пропуск"); continue lem = lemmas_of(out) strict = [z for z in PRESENT if match_strict(out, z)] lemma = [z for z in PRESENT if match_lemma(out, z, lem)] false_flags = [z for z in lemma if z not in strict] # L1 чинит именно их real_miss = [z for z in PRESENT if z not in lemma] # настоящие провалы adherence print(f"\n[{model}] строгий-регэксп {len(strict)}/{len(PRESENT)}={len(strict)/len(PRESENT):.2f}" f" | L1 лемма-post-check {len(lemma)}/{len(PRESENT)}={len(lemma)/len(PRESENT):.2f}") print(f" L1 убрал ЛОЖНЫХ флагов: {len(false_flags)} {false_flags}") print(f" РЕАЛЬНЫЕ промахи adherence (по лемме): {real_miss}") rec = {"strict": len(strict)/len(PRESENT), "lemma": len(lemma)/len(PRESENT), "false_flags": false_flags, "real_miss": real_miss} # L3: re-ask на реальные промахи if real_miss: terms = "; ".join(f"{z} = {ENT[z][0]}" for z in real_miss) out2 = translate(model, base, kenv, GLOSS + "\n\nФРАГМЕНТ:\n" + HARD + f"\n\nВАЖНО: обязательно используй утверждённые формы: {terms}. Перепиши перевод с ними.") lem2 = lemmas_of(out2) lemma2 = [z for z in PRESENT if match_lemma(out2, z, lem2)] print(f" L3 re-ask по {real_miss}: adherence {len(lemma)/len(PRESENT):.2f} → {len(lemma2)/len(PRESENT):.2f}") rec["after_reask"] = len(lemma2)/len(PRESENT) rec["still_missing"] = [z for z in PRESENT if z not in lemma2] else: print(" L3 re-ask не нужен (реальных промахов нет)") rec["after_reask"] = rec["lemma"] res["models"][model] = rec OUT.write_text(json.dumps(res, ensure_ascii=False, indent=2), encoding="utf-8") print(f"\nsaved → {OUT}") print("\nЧитается: L2 — рекол точного матча растёт с норм/alias/sticky; L1 — сколько ложных\n" "флагов убирает лемматизация (риск research/13 §2); L3 — сколько adherence добивает re-ask.") if __name__ == "__main__": main()