Add the adaptive-memory research verdict (doc 14) with four executable probes: flagship logprob access, in-context demos vs glossary, lemma post-check, and the deterministic lever curve

This commit is contained in:
Claude (backend session) 2026-07-05 01:34:18 +03:00
parent 1878c3ea95
commit e4ca3a57fd
5 changed files with 617 additions and 1 deletions

View file

@ -79,8 +79,10 @@
| D. глоссарий + дистрактор-демонстрации | **0.875** | near-miss не укусил на этой метрике/масштабе |
| E. глоссарий с ОДНОЙ отравленной строкой (赵太爷→«Ли») | **0.833**, отравление принято **0/3** | сильная модель **сопротивлялась** отраве на известном имени |
**Поправка (диагностика `eval/adaptive_reask.py`):** строгий accept-регэксп **занизил** глоссарий. Персистентный промах 王胡 — **артефакт метрики, не провал adherence**: модель рендерила канон Рогова «Бородатого Вана» (склонённое), а `Ван\b` не ловит суффикс. Склонение-толерантный матч → **глоссарий = 8/8 = 1.000** (оба репа, ноль реальных провалов adherence). То есть **на этом чанке детерминированный глоссарий УЖЕ насыщает консистентность** — demos не бьют его **потому что бить нечего** (потолок). Это (а) усиливает вердикт §2, (б) — **живая инстанция несущего невалидированного риска research/13 §2**: post-check регэкспом в русской морфологии undercount'ит И даёт ложные флаги → **post-check обязан лемматизировать (pymorphy2/Natasha), а не регэкспить.**
Три индикативных вывода (малый N, hand-built accept-регэксп → риск само-подтверждения, одна книга/модель — **не** финальный eval):
1. **Детерминированный soft-глоссарий сильно работает на zh→ru** (0.458→0.875) — впервые индикативно подтверждено на нашем направлении (валидация банка это на zh→ru не мерила). Хорошая новость для ядра.
1. **Детерминированный soft-глоссарий сильно работает на zh→ru** (без памяти 0.458 → глоссарий **~1.0** после поправки на склонение) — впервые индикативно подтверждено на нашем направлении (валидация банка это на zh→ru не мерила). Хорошая новость для ядра. Оговорка: чанк «лёгкий» (глоссарий насыщается) → реальную ценность demos и re-ask надо мерить на **трудных** чанках (длинные/плотные, где adherence падает на 1736%, 2310.05824).
2. **Демонстрации НЕ бьют глоссарий по консистентности** (0.833 vs 0.875) — ровно как предсказывает Li (выигрыш демо — 65% стиль, а не термин-матч, чего эта метрика не видит). Значит **выигрыш демонстраций, если он есть, — стилевой, а не консистентность/fidelity**, а БАР-(в) меряет именно консистентность/fidelity.
3. **Отравленную строку сильная модель отвергла** на ИЗВЕСТНОМ имени (阿Q-канон в претрейне) — мягкое свидетельство против слепого over-trust, но **НЕ** тест на НОВОМ термине без прайора (там over-trust реален — 2510.00829).
@ -174,6 +176,18 @@
---
## §9. Максимизация коэффициента канон-консистентности — детерминированные рычаги (по запросу владельца)
Раз гибрид отклонён — правильный ход не «обучать ранкер», а **добивать детерминированную сторону**. Замер `eval/adaptive_levers.py` (трудный чанк 阿Q, 11 сущностей, 1600 знаков, deepseek+grok) даёт кривую рычагов и порядок внедрения бэкенду:
- **L1 — post-check ЛЕММАТИЗИРУЮЩИЙ (pymorphy3), не регэксп. Внедрять ПЕРВЫМ.** Наивный `\b`-регэксп на трудном чанке даёт **1836% ложных флагов** (deepseek 2/11, grok 4/11 — склонённые формы, отрендеренные верно). pymorphy3 их снимает и **сворачивает OOV-транслит** (Вана→ван, Дэна→дэн, Вэйчжуане→вэйчжуан). Это количественная валидация несущего невалидированного риска research/13 §2 (регэксп-шум → редакторы не верят флагам → safety рушится). Дешевле всего, чинит больше всего.
- **L2 — RECALL матчера: нормализация (полная, тестируемая) + alias-граф + sticky.** Точный матч на trad/alias формах: recall **0.00 → +норм 0.50 → +alias 0.75**. Живой баг: неполная trad-карта (爺→爷) молча промахнула 赵太爷 → **A4 подтверждён: нормализацию покрыть и тестировать бит-в-бит** (OpenCC, не мини-карта). Крупнейший рычаг рекола (retrieval_bench recall 0.571 — 43% упоминаний не инъектится).
- **L3 — post-check + точечный re-ask** (verifier-gated correction, M2). На трудном чанке добивает реальные промахи adherence (deepseek 0.82→0.91). Не чинит гомограф/односимвольный ключ (钱) — и не должен.
- **L4 — таблица Палладия/Поливанова (B6): потолок ПРАВИЛЬНОСТИ.** L1L3 поднимают «использует ли модель канон»; L4 — «правильный ли канон». Локаль zh 0.26, облако-топ 0.75 — только детерминированная таблица закрывает разрыв.
- **Побочно подтверждено:** 钱 (односимвольный ключ, гомограф Цянь/деньги) упорно промахивается даже после re-ask → **правило `min_key_len`/запрет одиночных ключей (A3) обязательно** — 钱 из глоссария выкинуть или заменить целой сущностью (钱府→«дом Цянь»).
**Где ваш инстинкт «тюнить ранкер» легитимен (честно):** ТОЛЬКО во втором эшелоне (fuzzy/семантика, кандидаты для человека) — у bge-m3 нет разделяющего порога (retrieval_bench), и research/13 §5 оставил лазейку: другое представление / sparse-голова / маленький cross-encoder reranker может разделять лучше. Это Фаза-2, low-trust, коэффициент горячего пути (§9) не двигает. Инстинкт верен, цель — второй эшелон, не костяк.
## Честные слабые места рекомендаций (по этим пунктам пишут решение)
1. **Ни одна цифра литературы не на zh/ja→ru — тотально.** Все 24 верификации: DelTA, Moslem, Li, Power-of-Noise, self-correction-скептики, kNN-MT, LoRA, ModernMT/Lilt — англо-центричны или ru только как источник. Ближайшее к нам — **WMT24 Discourse-Level Literary (2412.11732) с добавленным zh→ru document-level треком** и **DITING (2510.09116)** zh-en вебновелла — но это **бенчмарки-оценки**, не методы адаптации, и ja→ru литературной адаптации/бенчмарка **не существует вовсе** (2505.05423 держит ru только как источник). Любая рекомендация по ja→ru — по аналогии, не по данным.

170
eval/adaptive_incontext.py Normal file
View file

@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""INDICATIVE in-context experiment for the adaptive-memory verdict (docs/research/14).
Tests THE central BAR question of the "free win" (in-context demonstrations) strand:
does adding the book's own DEMONSTRATIONS (prior translated passages) beat the
deterministic baseline the bank ALREADY ships soft-glossary injection for keeping a
recurring entity's rendering canonical and consistent on a REAL zh→ru literary chunk;
and do the learning layer's failure modes actively HARM (Power-of-Noise near-miss
distractor, 2401.14887; over-trust of a poisoned entry, 2510.00829)?
Testbed: 魯迅阿Q正传ch5-9 (real PD zh) a dense chunk where А-Q fantasizes the
revolution and names 8 recurring characters at once. Canonical dst = В. Рогов's published
translation (eval/data/samples/ru/luxun-ah-q-ru.txt); demonstrations are AUTHENTIC Rogov
sentences. Metric = the bank's OWN deterministic post-check (canonical stem present in
output), scored only over entities whose zh key is in the source chunk.
Arms:
A_bare no memory (raw model)
B_glossary soft-glossary "src → dst" block <- THE deterministic baseline
C_demos glossary + 3 authentic prior passages <- the learning layer (free win)
D_noise glossary + 3 IRRELEVANT prior passages <- Power-of-Noise near-miss arm
E_poison glossary with ONE wrong dst (赵太爷"Ли") <- over-trust / silent-injection arm
Model: deepseek-v4-flash (the real draft model; thinking ON by default avoids the zh
echo). N reps (temp 0.3) for stability. INDICATIVE: small N, hand-built accept-regexes
(self-confirmation risk), one model, one chunk. Not the Phase-2.5 in-house eval a probe.
Usage: eval/.venv/bin/python eval/adaptive_incontext.py [--reps 3] [--provider deepseek]
Output: stdout table + eval/data/adaptive_incontext.json
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import refusal_bench as rb # reuse call_provider + providers quirks + env load
ROOT = Path(__file__).resolve().parent
SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt"
OUT = ROOT / "data" / "adaptive_incontext.json"
# Dense multi-entity window (offset 6360, 650 chars): А-Q's revolution fantasy.
CHUNK_OFF, CHUNK_LEN = 6360, 650
# Recurring entities: zh key -> (canonical Rogov dst shown in glossary, accept-regex on
# the canonical STEM allowing Russian declension). Stem match = the bank's post-check (E1).
ENTITIES = {
"阿Q": ("А-кью", r"А[-\s]?кью"),
"未庄": ("Вэйчжуан (деревня)", r"Вэйчжуан"),
"赵太爷": ("почтенный Чжао", r"Чжао"),
"王胡": ("Бородатый Ван", r"Ван\b"),
"小D": ("Маленький Дэн", r"Дэн"),
"假洋鬼子": ("Поддельный заморский чёрт", r"заморск\w+ чёрт|поддельн"),
"吴妈": ("У-ма", r"У[-\s]?ма"),
"邹七嫂": ("тётушка Цзоу Седьмая", r"Цзоу"),
}
# Arm C: AUTHENTIC prior translated passages (Rogov), each carrying a recurring entity in
# natural inflected Russian — the "demonstration" the learning layer would retrieve.
DEMOS_RELEVANT = [
"Он знал, что вместо него нанимают Маленького Дэна, этого худого и слабосильного бедняка.",
"Сын почтенного Чжао добился, наконец, учёной степени сюцая, и об этом оповестили всю деревню.",
"Жители деревни Вэйчжуан требовали от него только подённой работы да насмехались над ним.",
]
# Arm D: IRRELEVANT prior passages (near-miss distractors) — plausible book prose, but
# about material NOT in this chunk. Tests "better nothing than garbage" on the real wire.
DEMOS_NOISE = [
"Стены храма Тудигун почернели от времени, и ветер свистел в щелях по ночам.",
"На реке скрипели вёсла лодочников, а торговцы раскладывали на пристани сушёную рыбу.",
"В тот год урожай риса выдался скудным, и цены на масло поднялись по всей округе.",
]
SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент художественного "
"произведения на русский язык. Это перевод существующего текста, предоставленного "
"правообладателем. Сохрани все реплики и детали без пропусков; стиль — живой "
"литературный русский. Выведи ТОЛЬКО перевод, без комментариев.")
def glossary_block(poison=False):
lines = []
for zh, (dst, _) in ENTITIES.items():
d = "господин Ли (важный чиновник)" if (poison and zh == "赵太爷") else dst
lines.append(f"{zh}{d}")
return "ГЛОССАРИЙ (используй эти утверждённые переводы имён/терминов последовательно):\n" + "\n".join(lines)
def demos_block(demos):
return ("РАНЕЕ ПЕРЕВЕДЁННЫЕ ФРАГМЕНТЫ ЭТОЙ КНИГИ (держи ту же терминологию и стиль):\n"
+ "\n".join(f"{d}" for d in demos))
def build_user(chunk, arm):
parts = []
if arm in ("B_glossary", "C_demos", "D_noise"):
parts.append(glossary_block())
if arm == "E_poison":
parts.append(glossary_block(poison=True))
if arm == "C_demos":
parts.append(demos_block(DEMOS_RELEVANT))
if arm == "D_noise":
parts.append(demos_block(DEMOS_NOISE))
parts.append("ФРАГМЕНТ ДЛЯ ПЕРЕВОДА:\n" + chunk)
return "\n\n".join(parts)
def cjk_ratio(s):
cjk = len(re.findall(r"[㐀-鿿]", s))
return round(cjk / max(1, len(s)), 3)
def score(output, present):
"""Bank post-check: fraction of source-present entities whose canonical stem is in output."""
matched = [zh for zh in present if re.search(ENTITIES[zh][1], output)]
return round(len(matched) / max(1, len(present)), 3), matched
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--reps", type=int, default=3)
ap.add_argument("--provider", default="deepseek")
ap.add_argument("--providers", default=str(ROOT / "providers.json"))
args = ap.parse_args()
prov = next(p for p in json.load(open(args.providers))["providers"] if p["name"] == args.provider)
chunk = SRC.read_text(encoding="utf-8")[CHUNK_OFF:CHUNK_OFF + CHUNK_LEN]
present = [zh for zh in ENTITIES if zh in chunk]
print(f"provider={prov['model']} chunk={len(chunk)}ch present entities ({len(present)}): {present}\n")
arms = ["A_bare", "B_glossary", "C_demos", "D_noise", "E_poison"]
results = {"provider": prov["model"], "present": present, "reps": args.reps, "arms": {}}
print(f"{'arm':<12} {'canon-consist':<14} {'cjk-echo':<9} {'poison-followed':<16} note")
print("-" * 78)
for arm in arms:
user = build_user(chunk, arm)
recs = []
for _ in range(args.reps):
text, err, usage = rb.call_provider(prov, SYSTEM, user, timeout=180)
if not text:
recs.append({"err": err}); continue
cons, matched = score(text, present)
rec = {"consist": cons, "matched": matched, "cjk": cjk_ratio(text),
"poison_followed": bool(re.search(r"господин Ли|\bЛи\b", text)) and not re.search(r"Чжао", text),
"len": len(text), "sample": text[:0]}
recs.append(rec)
ok = [r for r in recs if "err" not in r]
if not ok:
print(f"{arm:<12} {'ALL-FAILED':<14} {'':<9} {'':<16} {[r['err'] for r in recs][:1]}")
results["arms"][arm] = {"records": recs}; continue
mc = round(sum(r["consist"] for r in ok) / len(ok), 3)
me = round(sum(r["cjk"] for r in ok) / len(ok), 3)
pf = sum(r["poison_followed"] for r in ok)
echo = " ECHO!" if me > 0.15 else ""
results["arms"][arm] = {"mean_consist": mc, "mean_cjk": me, "poison_followed": pf,
"n_ok": len(ok), "records": recs}
print(f"{arm:<12} {mc:<14} {me:<9} {str(pf)+'/'+str(len(ok)):<16} n={len(ok)}{echo}")
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\nsaved -> {OUT}")
print("Reading: BA = does the deterministic soft-glossary baseline help; CB = does the\n"
"learning layer (demonstrations) beat that baseline; D vs C = Power-of-Noise harm;\n"
"E poison-followed>0 = over-trust (why the learning layer must NOT touch approved names).")
if __name__ == "__main__":
main()

170
eval/adaptive_levers.py Normal file
View file

@ -0,0 +1,170 @@
#!/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 матчера: точный матч +нормализация(tradsimp/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()

176
eval/adaptive_probe.py Normal file
View file

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Constraint probe for the ADAPTIVE-MEMORY research (docs/research/14-adaptive-memory.md).
Question it answers empirically (mandate: «проверяй заблокированность методов пробой,
а не по памяти» ADAPTIVE_MEMORY_RESEARCH_PROMPT §Жёсткий контур п.1): what LOGIT-LEVEL
control do our closed flagship providers actually expose in 2026? This bounds which
"learns-during-the-book" mechanisms are runnable on the flagship TRANSLATOR:
logit-level lever needs from the API available on flagship?
------------------------------------------------------------------------------------
kNN-MT (Khandelwal 2021) DECODER HIDDEN STATES per step NO API exposes this
constrained/beam decoding beam access / token masking logit_bias at best
output-distribution reading per-token logprobs <- THIS we probe live
hard steering logit_bias honored <- THIS we probe live
So even the BEST case (logprobs + logit_bias both honored) does NOT unblock kNN-MT: the
datastore key in kNN-MT is the decoder's context representation, which none of these APIs
return. This probe measures the ceiling (logprobs/logit_bias), and the ceiling is still
below what kNN-MT needs a doubly-closed door. Local decoders (llama.cpp/vLLM) DO expose
logprobs/logits by design (n_probs / prompt_logprobs), so local kNN-MT is physically
possible; that half is asserted from the runtimes' documented APIs, not reprobed here (the
stand GPU is held by the polygon's resident model — do not evict, exp03/§coordination).
Cheap by design: ~1 short call per provider. Reuses eval/.env + eval/providers.json quirks.
Usage: eval/.venv/bin/python eval/adaptive_probe.py
Output: stdout table + eval/data/adaptive_probe.json
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent
OUT = ROOT / "data" / "adaptive_probe.json"
def load_env() -> None:
for cand in (ROOT / ".env", Path("~/.textmachine.env").expanduser()):
if not cand.exists():
continue
for line in cand.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
if v.strip():
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
return
load_env()
# The models we actually probe. Includes the REAL translator/editor stack models
# (deepseek-v4-flash draft, grok-4.3 editor, gemini-3.1-pro premium) plus a clean
# non-reasoning path per family to give logprobs its best chance to appear.
PROBES = [
{"name": "deepseek-v4-flash", "base": "https://api.deepseek.com/v1",
"model": "deepseek-v4-flash", "key": "DEEPSEEK_API_KEY", "max": 200},
{"name": "grok-non-reasoning", "base": "https://api.x.ai/v1",
"model": "grok-4.20-0309-non-reasoning", "key": "XAI_API_KEY", "max": 32},
{"name": "grok-4.3(editor)", "base": "https://api.x.ai/v1",
"model": "grok-4.3", "key": "XAI_API_KEY", "max": 200},
{"name": "glm-4.5-air", "base": "https://api.z.ai/api/paas/v4",
"model": "glm-4.5-air", "key": "ZAI_API_KEY", "max": 64,
"extra": {"thinking": {"type": "disabled"}}},
{"name": "gemini-2.5-flash", "base": "https://generativelanguage.googleapis.com/v1beta/openai",
"model": "gemini-2.5-flash", "key": "GEMINI_API_KEY", "max": 64,
"extra": {"extra_body": {"google": {"thinking_config": {"thinking_budget": 0}}}}},
{"name": "gemini-3.1-pro(premium)", "base": "https://generativelanguage.googleapis.com/v1beta/openai",
"model": "gemini-3.1-pro-preview", "key": "GEMINI_API_KEY", "max": 8000},
{"name": "gpt-5-mini", "base": "https://api.openai.com/v1",
"model": "gpt-5-mini", "key": "OPENAI_API_KEY", "max": 8000, "reasoning": True},
]
def one_call(p: dict, with_logprobs: bool, with_logit_bias: bool):
"""Return dict describing what the provider did with logprobs / logit_bias."""
key = os.environ.get(p["key"], "")
if not key:
return {"skip": f"no key {p['key']}"}
body = {
"model": p["model"],
"messages": [{"role": "user", "content": "Reply with exactly the single word: ok"}],
"max_tokens": p["max"],
}
if p.get("reasoning"):
body["max_completion_tokens"] = body.pop("max_tokens")
body["reasoning_effort"] = "minimal"
else:
body["temperature"] = 0.0
body.update(p.get("extra", {}))
if with_logprobs:
body["logprobs"] = True
body["top_logprobs"] = 5
if with_logit_bias:
# bias an arbitrary token id hard; we only check acceptance, not effect.
body["logit_bias"] = {"1734": -100}
req = urllib.request.Request(
p["base"].rstrip("/") + "/chat/completions",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"},
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
data = json.loads(resp.read())
except urllib.error.HTTPError as e:
return {"http_error": e.code, "detail": e.read().decode(errors="replace")[:300].replace("\n", " ")}
except Exception as e:
return {"transport_error": f"{type(e).__name__}: {e}"}
ch = (data.get("choices") or [{}])[0]
lp = ch.get("logprobs")
# logprobs "present" = non-null object carrying per-token content entries
has_lp = bool(lp) and bool((lp or {}).get("content"))
sample = None
if has_lp:
first = lp["content"][0]
sample = {"token": first.get("token"),
"logprob": round(first.get("logprob", 0.0), 3),
"n_top": len(first.get("top_logprobs", []))}
return {
"http": 200,
"content": (ch.get("message", {}).get("content") or "")[:40],
"finish": ch.get("finish_reason"),
"logprobs_present": has_lp,
"logprobs_sample": sample,
"usage": {k: data.get("usage", {}).get(k) for k in ("prompt_tokens", "completion_tokens")},
}
def main():
results = []
print(f"{'provider':<26} {'logprobs?':<11} {'logit_bias':<12} sample / note")
print("-" * 88)
for p in PROBES:
r_lp = one_call(p, with_logprobs=True, with_logit_bias=False)
r_lb = one_call(p, with_logprobs=False, with_logit_bias=True)
rec = {"provider": p["name"], "model": p["model"], "logprobs": r_lp, "logit_bias": r_lb}
results.append(rec)
if "skip" in r_lp:
print(f"{p['name']:<26} {'SKIP':<11} {'':<12} {r_lp['skip']}")
continue
if r_lp.get("http") == 200:
lp_verdict = "YES" if r_lp["logprobs_present"] else "no(200)"
elif "http_error" in r_lp:
lp_verdict = f"HTTP{r_lp['http_error']}"
else:
lp_verdict = "ERR"
# logit_bias: 200 = accepted (not rejected); HTTP4xx = rejected
if r_lb.get("http") == 200:
lb_verdict = "accepted"
elif "http_error" in r_lb:
lb_verdict = f"HTTP{r_lb['http_error']}"
else:
lb_verdict = "ERR"
note = ""
if r_lp.get("logprobs_sample"):
s = r_lp["logprobs_sample"]
note = f"tok={s['token']!r} lp={s['logprob']} top{s['n_top']}"
elif "detail" in r_lp:
note = r_lp["detail"][:60]
elif r_lp.get("http") == 200 and not r_lp["logprobs_present"]:
note = f"finish={r_lp.get('finish')} content={r_lp.get('content')!r}"
print(f"{p['name']:<26} {lp_verdict:<11} {lb_verdict:<12} {note}")
OUT.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\nsaved -> {OUT}")
print("\nReading: 'logprobs YES' = output distribution readable (still NOT decoder hidden\n"
"states → kNN-MT still blocked). 'logit_bias accepted' = crude token steering only.")
if __name__ == "__main__":
main()

86
eval/adaptive_reask.py Normal file
View file

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Диагностика + демонстрация ДЕТЕРМИНИРОВАННОГО рычага коэффициента канон-консистентности
(follow-up к adaptive_incontext.py по запросу владельца: «раз не идём в гибрид долбить
детерминированную сторону»).
Два вопроса:
1. ДИАГНОЗ: промахи в arm B_glossary (напр. 王胡) это РЕАЛЬНЫЙ провал adherence
(модель не использовала утв. форму) или АРТЕФАКТ моего accept-регэкспа (напр. `Ван\b`
не ловит склонённое «Вана/Вану»)? Печатаем реальный вывод + два матча (строгий \b vs
склонение-толерантный стем).
2. РЫЧАГ: post-check находит промах точечный re-ask («используй утв. формы X,Y»)
повторный перевод коэффициент до/после. Это verifier-gated single-pass correction
(research/14 §6, M2) тот самый детерминированный рычаг, а не дообучение.
deepseek-v4-flash, тот же чанк 阿Q. INDICATIVE (малый N). Usage: eval/.venv/bin/python eval/adaptive_reask.py
"""
from __future__ import annotations
import json, re, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import refusal_bench as rb
ROOT = Path(__file__).resolve().parent
SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt"
CHUNK = SRC.read_text(encoding="utf-8")[6360:6360 + 650]
# canonical dst + СТРОГИЙ (\b, как в adaptive_incontext) и СКЛОНЕНИЕ-ТОЛЕРАНТНЫЙ стем-матч
ENT = {
"阿Q": ("А-кью", r"А[-\s]?кью", r"А[-\s]?кью"),
"未庄": ("Вэйчжуан", r"Вэйчжуан", r"Вэйчжуан"),
"赵太爷": ("почтенный Чжао", r"Чжао", r"Чжао"),
"王胡": ("Бородатый Ван", r"Ван\b", r"Ван[аоуые]?\b|Бород"),
"小D": ("Маленький Дэн", r"Дэн", r"Дэн"),
"假洋鬼子": ("Поддельный заморский чёрт", r"заморск\w+ чёрт|поддельн", r"заморск|поддельн|чёрт"),
"吴妈": ("У-ма", r"У[-\s]?ма", r"У[-\s]?ма"),
"邹七嫂": ("тётушка Цзоу Седьмая", r"Цзоу", r"Цзоу"),
}
SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент на русский язык. "
"Сохрани все реплики и детали; стиль — живой литературный русский. Выведи ТОЛЬКО перевод.")
GLOSS = "ГЛОССАРИЙ (используй эти утверждённые переводы последовательно):\n" + \
"\n".join(f"{z}{v[0]}" for z, v in ENT.items())
PRESENT = [z for z in ENT if z in CHUNK]
def score(out, strict):
idx = 1 if strict else 2
return [z for z in PRESENT if re.search(ENT[z][idx], out)]
def translate(user):
t, err, _ = rb.call_provider({"name": "deepseek", "base_url": "https://api.deepseek.com/v1",
"model": "deepseek-v4-flash", "api_key_env": "DEEPSEEK_API_KEY",
"max_tokens": 8000}, SYSTEM, user, timeout=180)
return t or ""
def main():
print(f"present entities ({len(PRESENT)}): {PRESENT}\n")
for rep in range(2):
print(f"================= REP {rep+1} =================")
out = translate(GLOSS + "\n\nФРАГМЕНТ:\n" + CHUNK)
strict, tol = score(out, True), score(out, False)
print(f"[B_glossary] строгий-матч {len(strict)}/8={len(strict)/8:.3f} | склонение-толерантный {len(tol)}/8={len(tol)/8:.3f}")
print(f" промах(строгий): {[z for z in PRESENT if z not in strict]}")
print(f" промах(толерантный, = РЕАЛЬНЫЕ провалы adherence): {[z for z in PRESENT if z not in tol]}")
# показать фактический рендер спорных сущностей
for z in ["王胡", "假洋鬼子", "吴妈"]:
can = ENT[z][0]
frag = re.findall(r".{0,4}(?:Ван|Бород|заморск|поддельн|У-ма|У ма|Ху|Хо)\w*.{0,6}", out)
print(f" {z}→канон «{can}»: фрагменты вывода {frag[:3]}")
# РЫЧАГ: re-ask по реальным промахам
miss = [z for z in PRESENT if z not in tol]
if miss:
terms = "; ".join(f"{z} = {ENT[z][0]}" for z in miss)
reask = (GLOSS + "\n\nФРАГМЕНТ:\n" + CHUNK +
f"\n\nВАЖНО: в переводе ОБЯЗАТЕЛЬНО используй утверждённые формы: {terms}. Перепиши перевод, применив их.")
out2 = translate(reask)
tol2 = score(out2, False)
print(f" → post-check re-ask по {miss}: {len(tol)}/8 → {len(tol2)}/8 (толерантный)")
else:
print(" → промахов (толерантных) нет — re-ask не нужен")
print()
if __name__ == "__main__":
main()