112 lines
6.5 KiB
Python
112 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
||
"""ПЕРЕКРЁСТНОЕ судейство: чужая модель-судья + ЗЕРКАЛЬНАЯ раскладка А/Б.
|
||
|
||
Повод (владелец 05.08): «какой смысл давать 24 агентам одно и то же, если это одна модель?
|
||
От спама их количества суть не поменяется». Довод верный. Пакеты были разные (24 сравнения,
|
||
а не 24 повтора), но судья один — Claude с одной рубрикой, значит систематическая ошибка у всех
|
||
общая, и добавление агентов её не уменьшает. Симптом уже вылез: позиция «А» победила 18 раз из 24,
|
||
а редактура — 12/12 в позиции А против 6/12 в позиции Б.
|
||
|
||
Здесь снимаются обе проблемы разом, дизайном 2×2:
|
||
· судья ДРУГОЙ МОДЕЛИ (через тот же провод, что и сам эксперимент),
|
||
· каждый пакет судится в ДВУХ раскладках — прямой и зеркальной (А и Б переставлены).
|
||
|
||
Тогда:
|
||
вердикт совпал в обеих раскладках → это про текст;
|
||
вердикт следует за ПОЗИЦИЕЙ → это предвзятость судьи, а не качество;
|
||
чужая модель расходится с Claude → расхождение локализуется в судье, а не в данных.
|
||
|
||
Формат ответа судьи намеренно жёсткий и куцый: разбор нам от него не нужен (он уже есть от Claude),
|
||
нужен голос, который можно сложить.
|
||
"""
|
||
from __future__ import annotations
|
||
import json
|
||
import re
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||
HERE = Path(__file__).resolve().parent
|
||
sys.path.insert(0, str(REPO / "eval" / "editor_contract"))
|
||
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||
sys.path.insert(0, str(HERE))
|
||
|
||
import probe as Q # noqa: E402
|
||
|
||
PACK = Path.home() / "books" / "gu-zhenren" / "editor-harness" / ("judge-reflow" if "--reflow" in sys.argv else "judge-direct")
|
||
OUT = PACK.parent / ("judge-cross-reflow" if "--reflow" in sys.argv else "judge-cross")
|
||
AXES = ["верность", "глоссарий", "русский", "полнота", "чистота", "общий"]
|
||
|
||
RUBRIC = """Ты — эксперт по художественному переводу с китайского на русский. Ниже исходник,
|
||
утверждённый глоссарий и две версии перевода. Сравни их.
|
||
|
||
Оси:
|
||
1 верность — искажения смысла, полярность, числа, потерянные или добавленные смысловые атомы
|
||
2 глоссарий — канонические формы терминов соблюдены дословно (склонение допустимо)
|
||
3 русский — живость, отсутствие калек и канцелярита, абзацная вёрстка, оформление прямой речи
|
||
4 полнота — все предложения исходника переданы, ничего не выброшено и не дописано
|
||
5 чистота — нет непереведённых фрагментов, иероглифов, латиницы, преамбул, комментариев
|
||
6 общий — что лучше как издательский перевод
|
||
|
||
⚠ Слияние коротких построчных абзацев исходника в связные русские абзацы — ТРЕБОВАНИЕ, а не
|
||
вольность. Само по себе оно не дефект.
|
||
|
||
ФОРМАТ ОТВЕТА — ровно шесть строк, без единого слова пояснений:
|
||
верность: А|Б|ничья
|
||
глоссарий: А|Б|ничья
|
||
русский: А|Б|ничья
|
||
полнота: А|Б|ничья
|
||
чистота: А|Б|ничья
|
||
общий: А|Б|ничья
|
||
"""
|
||
|
||
|
||
def mirror(packet: str) -> str:
|
||
"""Меняет местами разделы «Версия А» и «Версия Б», не трогая исходник и глоссарий."""
|
||
m = re.search(r"(## Версия А\n\n)(.*?)(\n## Версия Б\n\n)(.*)$", packet, re.S)
|
||
if not m:
|
||
raise ValueError("не нашёл секции версий")
|
||
head = packet[:m.start()]
|
||
return f"{head}## Версия А\n\n{m.group(4).rstrip()}\n\n## Версия Б\n\n{m.group(2).rstrip()}\n"
|
||
|
||
|
||
def parse(reply: str) -> dict:
|
||
out = {}
|
||
for ln in reply.splitlines():
|
||
mm = re.match(r"\s*(\w+)\s*:\s*(А|Б|ничья)\s*$", ln.strip())
|
||
if mm and mm.group(1).lower() in AXES:
|
||
out[mm.group(1).lower()] = mm.group(2)
|
||
return out
|
||
|
||
|
||
def main(model: str, models_short: list[str]) -> None:
|
||
OUT.mkdir(parents=True, exist_ok=True)
|
||
nothink = Q.MODELS[model][5] in ("disable", "effort_none")
|
||
total = 0.0
|
||
rows = []
|
||
for ms in models_short:
|
||
for k in range(6):
|
||
p = PACK / f"packet-{ms}-w{k}.md"
|
||
if not p.exists():
|
||
continue
|
||
body = p.read_text(encoding="utf-8")
|
||
for orient, txt in (("прямая", body), ("зеркальная", mirror(body))):
|
||
pre = "jr" if "--reflow" in sys.argv else "jx"
|
||
tag = f"{pre}-{ms}-w{k}-{'d' if orient == 'прямая' else 'm'}"
|
||
msgs = [{"role": "system", "content": RUBRIC},
|
||
{"role": "user", "content": txt}]
|
||
rec = Q.call(model, msgs, tag, nothink)
|
||
total += rec["cost_usd"]
|
||
v = parse(rec["content"])
|
||
rows.append(dict(model_short=ms, window=k, orient=orient, verdict=v))
|
||
time.sleep(0.2)
|
||
(OUT / f"votes-{model}.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1),
|
||
encoding="utf-8")
|
||
print(f"голосов: {len(rows)}; потрачено ${total:.6f}; → votes-{model}.json")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||
mdl = args[0] if args else "deepseek-v4-pro"
|
||
main(mdl, args[1:] or (["dp", "gl", "lu"] if "--reflow" in sys.argv else ["dp", "gl", "gr", "lu"]))
|