zizi 069ae73e97 feat(aigc/worker): U3 trace 扩展键 + U4 缓存命中落账(B5/B9)
6c6g 后端 Tier0 计划 U3+U4(docs/plans/2026-06-17-001-...)。execution §5.8 trace+cost 落值。
- U3 extractTraceQuietly(Java)+ _extract_trace(worker)additive 抽 modelTier/escalationEvents/giveupDumpPath;
  仅真升档态(stage2/escalationEvents 非空)落 modelTier + 形态校验(非空 str/非空 list)→ 字节兼容(无扩展键键集与扩前一致)、Java/Python 两路同口径
- U4 cost.py 缓存命中折¥:两套字段取 max(DeepSeek prompt_cache_hit_tokens / MiniMax prompt_tokens_details.cached_tokens)
  + cache_ratio 折真实 quota;worker usage 捕获(_safe_int 兜脏不抛);pricing 不可达 tokens-only+costFallback 标记;不阻断仅观测
- 诚实边界:SAA Java 路无 cache 字段源→省略不伪造(follow-up);AgentScope 丢 DeepSeek 顶层字段(待真跑对账);worker 生产路完整落账

验证:mini-desktop Java 12+(SaaGraphDispatcherTraceTest)+ Python 73(test_cost 56/wg1_groupb 17)全绿;
codex 评审 NO-MERGE→2 P0(字节兼容/usage 阻断)+2 P1(pricing 口径/两路一致)全修后绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:26:27 +00:00

203 lines
9.5 KiB
Python
Raw 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.

"""agent_loop/calibrate.py —— W-G1 player-panel 校准("好玩 Claude-free 可信"的命门验证)。
目的:在【已知答案】样本上测 player(vision/text) 判定 vs 人类锚的吻合度,
重点 = 反样本(空心游戏)否决率——这是 text player 翻车、整个 W-G1 论点站不站得住的命门。
锚来源(见 labels.json.anchor_note
· hollow 反样本 = 客观锚(真玩 remaining 零变化=空心,无主观成分);
· good 正样本 = opus-proxy 临时锚,创始人亲玩后覆盖 human.fun/verdict 即权威。
复用 studio 的 _judge_vision/_judge_text与生产同一 player prompt/同款人格/同 max_tokens
吃各游戏已存的 evidencefirst-paint.png/after-play.png/verdict.json不重开服务=便宜可复跑。
"""
import argparse
import json
import sys
from collections import defaultdict
from pathlib import Path
# 包内/直跑兼容导入(镜像 studio.py 的导入范式)。
try:
from . import config, studio
from .. import run, cost
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import run, cost # type: ignore
from agent_loop import config, studio # type: ignore
WORKER_ROOT = Path(__file__).resolve().parents[2] # wg1/gen-worker
CALIB_DIR = WORKER_ROOT / "calibration"
GEN_DIR = run.GEN_DIR
BRIEFS_DIR = run.BRIEFS_DIR
# 生产同款人格(与 studio.run_player_panel 内联人格逐字一致,保证校准的是生产 player
PERSONA_VISION = "急性子休闲玩家,凭第一眼观感和反馈下判断"
PERSONA_TEXT = "细致的完整度审查者,只信运行数据、不脑补"
def _load_verdict(game_id):
"""读某游戏的真玩 verdict喂文本 player 的运行数据;缺失返回 None"""
p = GEN_DIR / game_id / "evidence" / "verdict.json"
if p.exists():
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return None
return None
def _load_brief(brief_ref):
"""读题面文本(与生成时同一 brief保证 player 看到的题面口径一致)。"""
p = BRIEFS_DIR / f"{brief_ref}.json"
return json.loads(p.read_text(encoding="utf-8"))["brief"]
def _judge_user_text(brief, verdict):
"""组装喂给 player 的证据文本(与 studio.run_player_panel 同口径)。"""
summary = studio._play_summary(verdict)
return (f"游戏题面:\n{brief}\n\n{summary}\n\n"
"请基于以上证据(及截图,若有)给出你的玩家评判。")
def _num(x):
"""安全取数字player 偶尔把分数写成字符串)。非数字返回 None。"""
if isinstance(x, bool):
return None
if isinstance(x, (int, float)):
return x
try:
return float(x)
except (TypeError, ValueError):
return None
def run_calibration(labels_path, stage):
"""跑校准:逐样本跑 vision+text player → 算每位 player 的吻合指标。返回报告 dict。"""
labels = json.loads(Path(labels_path).read_text(encoding="utf-8"))
samples = labels["samples"]
vis_model = config.model_name_for("player_vision", stage)
txt_model = config.model_name_for("player_text", stage)
per_model = defaultdict(lambda: [0, 0]) # model -> [in, out]
rows = []
for s in samples:
gid = s["game_id"]
brief = _load_brief(s["brief_ref"])
verdict = _load_verdict(gid)
ut = _judge_user_text(brief, verdict)
# 视觉位吃截图。U4/B9 后 _judge_* 返回 (in,out,cached) 三元组——校准只关心 in/outcached 占位忽略
# P2-2校准成本有意取「全价上界」不折缓存——校准是离线评估账走保守上界即可不必对齐生产逐款精账
try:
vj, (vi, vo, _vca) = studio._judge_vision(vis_model, PERSONA_VISION, ut, gid)
per_model[vis_model][0] += vi
per_model[vis_model][1] += vo
except Exception as e:
vj = {"error": str(e)[:180]}
# 文本位(仅运行数据)
try:
tj, (ti, to, _tca) = studio._judge_text(txt_model, PERSONA_TEXT, ut)
per_model[txt_model][0] += ti
per_model[txt_model][1] += to
except Exception as e:
tj = {"error": str(e)[:180]}
rows.append({"game_id": gid, "class": s["class"], "human": s["human"],
"vision": vj, "text": tj})
# ── 指标:逐 player 算 好玩 MAE / verdict 吻合 / 反样本否决·假绿 / 正样本接受 ──
metrics = {}
for who, mname in (("vision", vis_model), ("text", txt_model)):
fun_abs = []
verdict_match = n_verdict = 0
hollow_total = hollow_rejected = hollow_false_green = 0
good_total = good_accepted = 0
for r in rows:
pj, human, cls = r[who], r["human"], r["class"]
pf, pv = _num(pj.get("fun")), str(pj.get("verdict"))
hf, hv = _num(human.get("fun")), str(human.get("verdict"))
if pf is not None and hf is not None:
fun_abs.append(abs(pf - hf))
if pv in ("pass", "fix"):
n_verdict += 1
if pv == hv:
verdict_match += 1
if cls == "hollow":
hollow_total += 1
# 正确否决 = 判 fix 且 好玩≤2既看 verdict 也看分,防"判 fix 却给高分"的矛盾)
if pv == "fix" and pf is not None and pf <= 2:
hollow_rejected += 1
# 危险假绿 = 放过空心(判 pass 或 好玩≥3
if pv == "pass" or (pf is not None and pf >= 3):
hollow_false_green += 1
elif cls == "good":
good_total += 1
if pv == "pass" and pf is not None and pf >= 3:
good_accepted += 1
# 是否够格当 Claude-free 好玩门:必须 100% 否决反样本(零假绿)+ 不误杀正样本
gatekeeper_ok = (hollow_total > 0 and hollow_false_green == 0
and (good_total == 0 or good_accepted == good_total))
metrics[who] = {
"model": mname,
"fun_mae": round(sum(fun_abs) / len(fun_abs), 2) if fun_abs else None,
"verdict_match": f"{verdict_match}/{n_verdict}",
"hollow_rejected": f"{hollow_rejected}/{hollow_total}",
"hollow_false_green": f"{hollow_false_green}/{hollow_total}",
"good_accepted": f"{good_accepted}/{good_total}",
"gatekeeper_ok": gatekeeper_ok,
}
# ── 成本(测而不闸;复用 cost.py 的 new-api quota 口径)──
# P2-2此处 compute 不传 cached_tokens默认 0→ 成本是「全价上界」(不折缓存命中),与生产 run_studio 的
# 精账(折 cache_ratio有意不同——校准为离线评估账走保守上界即可。
try:
pricing = cost.fetch_pricing()
qpu, usd = cost.fetch_status()
rws, total = {}, 0.0
for name, (i, o) in per_model.items():
c = cost.compute(name, i, o, pricing, qpu, usd) # 不传 cached → 全价上界P2-2
rws[name] = {"in": i, "out": o, "rmb": c["rmb"]}
total += c["rmb"]
cost_info = {"by_model": rws, "total_rmb": round(total, 5), "costNote": "fullPriceUpperBound"}
except Exception as e:
cost_info = {"error": str(e)[:180],
"by_model": {k: {"in": v[0], "out": v[1]} for k, v in per_model.items()}}
report = {"stage": stage, "n_samples": len(samples),
"anchor_note": labels.get("anchor_note", ""),
"rows": rows, "metrics": metrics, "cost": cost_info}
CALIB_DIR.mkdir(parents=True, exist_ok=True)
(CALIB_DIR / "report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
return report
def main():
ap = argparse.ArgumentParser(description="W-G1 player-panel 校准")
ap.add_argument("--labels", default=str(CALIB_DIR / "labels.json"))
ap.add_argument("--stage", default=None, help="models.yaml 阶段(默认 default_stage)")
args = ap.parse_args()
_cfg, default_stage = config.load_config()
stage = args.stage or default_stage
print(f"[calib] stage={stage} labels={args.labels}")
rep = run_calibration(args.labels, stage)
print("\n=== player 校准报告(逐样本)===")
for r in rep["rows"]:
h, v, t = r["human"], r["vision"], r["text"]
flag = "🟥反样本" if r["class"] == "hollow" else "🟩正样本"
print(f"\n[{r['game_id']}] {flag} 人类锚 fun={h.get('fun')} verdict={h.get('verdict')} ({h.get('anchor')})")
print(f" vision: fun={v.get('fun')} verdict={v.get('verdict')} note={str(v.get('note') or v.get('error'))[:64]}")
print(f" text : fun={t.get('fun')} verdict={t.get('verdict')} note={str(t.get('note') or t.get('error'))[:64]}")
print("\n--- 指标(命门=反样本假绿必须 0/N---")
for who, m in rep["metrics"].items():
verdict_gate = "✅够格当好玩门" if m["gatekeeper_ok"] else "❌不够格"
print(f" {who}({m['model']}): fun_MAE={m['fun_mae']} verdict吻合={m['verdict_match']} "
f"反样本否决={m['hollow_rejected']} 反样本假绿={m['hollow_false_green']} "
f"正样本接受={m['good_accepted']}{verdict_gate}")
c = rep.get("cost") or {}
print(f"\n[calib] ¥={c.get('total_rmb', '?')} report → wg1/gen-worker/calibration/report.json")
if __name__ == "__main__":
main()