textmachine/eval/editor_harness/judge_crossmodel.py

112 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"]))