170 lines
9.1 KiB
Python
170 lines
9.1 KiB
Python
#!/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: B−A = does the deterministic soft-glossary baseline help; C−B = 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()
|