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