feat(sense): T-G3 标签+校准(labeler 四规则推导 + split-conformal + last-good)
- labeler.py:derive_true_tier 纯函数(规则0 人工优先 / plan_multi->T3 / 失败->下一档(T3保持) / 成功->executed / 空结果跳过)+ derive_true_tiers 批量回填 + 180d 留存清理 - calibrate.py:compute_thresholds(τ1/τ3 分组扫描——并列分数整组判定, 精度 >= 1-α 的最大覆盖阈值;<min_labels -> ok=False 沿用 last-good); save_thresholds 工件落盘+登记;load_active 回退链 active->同kind扫描->last-good->保守值 - store:all_observations/list_artifacts 支撑方法 - 测试 +9(四规则/批量清理/覆盖达标/标签不足/工件往返与回退),全量 395 passed
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""split-conformal 阈值校准(T-G3)+ last-good 回退 + 工件读写(D-G3)。
|
||||
|
||||
目标(§5 policy.alpha):P(true>T1 | 判 T1) ≤ α(τ3 同理对 T3)。
|
||||
实现:calib 集按分数降序扫描,取**满足精度的最低阈值**(覆盖率最大);
|
||||
无任何满足点 -> 阈值取最高分 + ε(全拒,保守)。
|
||||
标签数 < min_labels -> 沿用 last-good(D-G3)-> 再无 -> 内置保守值。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from gateway.sense.store import json_dumps
|
||||
|
||||
CONSERVATIVE = {"t1": 0.90, "t3": 0.90}
|
||||
THRESHOLD_KIND = "thresholds"
|
||||
|
||||
|
||||
def _threshold_for(samples: List[Tuple[float, bool]], alpha: float) -> Optional[float]:
|
||||
"""样本 [(score, true==tier)] -> 满足精度 >= 1-α 的最低阈值(并列分数整组判定)。
|
||||
|
||||
按唯一分数降序累积计数;每组边界处检查精度;取满足精度的最大覆盖(最低边界)。
|
||||
无任何满足点 -> None(调用方全拒,保守)。
|
||||
"""
|
||||
if not samples:
|
||||
return None
|
||||
groups: Dict[float, List[int]] = {}
|
||||
for score, ok in samples:
|
||||
g = groups.setdefault(score, [0, 0])
|
||||
g[0] += 1
|
||||
g[1] += 1 if ok else 0
|
||||
total = 0
|
||||
correct = 0
|
||||
best: Optional[float] = None
|
||||
for score in sorted(groups, reverse=True):
|
||||
cnt, ok_cnt = groups[score]
|
||||
total += cnt
|
||||
correct += ok_cnt
|
||||
if correct / total >= 1.0 - alpha:
|
||||
best = score
|
||||
return best
|
||||
|
||||
|
||||
def compute_thresholds(rows: List[Dict[str, Any]], alpha: float = 0.05,
|
||||
min_labels: int = 500) -> Dict[str, Any]:
|
||||
"""rows: [{probs(JSON 串或 dict), true_tier}] -> {t1, t3, coverage, n, ok}。
|
||||
|
||||
ok=False 表示标签不足(沿用 last-good 的信号)。
|
||||
coverage = 通过阈值的 calib 样本占比(实测覆盖率,§6 晋升门比对 α+2%)。
|
||||
"""
|
||||
labeled = []
|
||||
for r in rows:
|
||||
probs = r.get("probs")
|
||||
if isinstance(probs, str):
|
||||
try:
|
||||
probs = json.loads(probs)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if not isinstance(probs, dict):
|
||||
continue
|
||||
true_tier = r.get("true_tier")
|
||||
if true_tier not in ("T1", "T2", "T3"):
|
||||
continue
|
||||
labeled.append((float(probs.get("t1", 0)), float(probs.get("t3", 0)),
|
||||
true_tier))
|
||||
n = len(labeled)
|
||||
if n < min_labels:
|
||||
return {"t1": None, "t3": None, "coverage": 0.0, "n": n, "ok": False}
|
||||
|
||||
s1 = [(p1, t == "T1") for p1, _p3, t in labeled]
|
||||
s3 = [(p3, t == "T3") for _p1, p3, t in labeled]
|
||||
t1 = _threshold_for(s1, alpha)
|
||||
t3 = _threshold_for(s3, alpha)
|
||||
if t1 is None or t3 is None:
|
||||
return {"t1": t1, "t3": t3, "coverage": 0.0, "n": n, "ok": False}
|
||||
coverage = (sum(1 for p1, _p3, _t in labeled if p1 >= t1)
|
||||
+ sum(1 for _p1, p3, _t in labeled if p3 >= t3)) / (2 * n)
|
||||
return {"t1": round(t1, 4), "t3": round(t3, 4),
|
||||
"coverage": round(coverage, 4), "n": n, "ok": True}
|
||||
|
||||
|
||||
def save_thresholds(store, models_dir: str | Path, version: str,
|
||||
th: Dict[str, Any], activate: bool = False) -> str:
|
||||
"""阈值工件落盘 thresholds.json + 登记工件表。返回文件路径。"""
|
||||
d = Path(models_dir) / version
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
path = d / "thresholds.json"
|
||||
path.write_text(json_dumps(th), encoding="utf-8")
|
||||
store.register_artifact(version, THRESHOLD_KIND, str(path),
|
||||
{k: th.get(k) for k in ("t1", "t3", "coverage", "n")},
|
||||
active=activate)
|
||||
return str(path)
|
||||
|
||||
|
||||
def load_active(store, models_dir: str | Path) -> Dict[str, Any]:
|
||||
"""active 阈值工件 -> 旧工件扫描回退(last-good)-> 内置保守值(D-G3 回退链)。"""
|
||||
art = store.active_artifact(THRESHOLD_KIND)
|
||||
if art:
|
||||
th = _read_threshold(art)
|
||||
if th is not None:
|
||||
th.setdefault("version", art["version"])
|
||||
return th
|
||||
# active 文件缺失/损坏:按时间倒序扫描同 kind 工件(last-good 语义)
|
||||
for cand in store.list_artifacts(THRESHOLD_KIND):
|
||||
if cand["version"] == art["version"]:
|
||||
continue
|
||||
th = _read_threshold(cand)
|
||||
if th is not None:
|
||||
th.setdefault("version", cand["version"])
|
||||
return th
|
||||
last_good = Path(models_dir) / "last-good.json"
|
||||
if last_good.exists():
|
||||
try:
|
||||
th = json.loads(last_good.read_text(encoding="utf-8"))
|
||||
if th.get("t1") is not None and th.get("t3") is not None:
|
||||
th.setdefault("version", "last-good")
|
||||
return th
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return {**CONSERVATIVE, "version": "conservative"}
|
||||
|
||||
|
||||
def _read_threshold(art: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
th = json.loads(Path(art["path"]).read_text(encoding="utf-8"))
|
||||
if isinstance(th, dict) and th.get("t1") is not None and th.get("t3") is not None:
|
||||
return th
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return None
|
||||
@@ -0,0 +1,60 @@
|
||||
"""夜间 true_tier 推导(T-G3,§4 四条规则,确定性)+ 留存清理。
|
||||
|
||||
规则(对每条 true_tier 为空的观察,按序判定):
|
||||
0. human_override 非空 -> 以人工为准(规则 4,优先级最高)。
|
||||
1. features.plan_multi(consumer=pipeline 发出 brief 的多步信号)-> true=T3。
|
||||
2. outcome∈{escalated,failed,user_retry,timeout} -> true = executed 的下一档
|
||||
(T1→T2→T3,T3 保持 T3)。
|
||||
3. outcome∈{ok,verified} -> true = executed(T1 成功即 T1;T2/T3 同理)。
|
||||
4. 其余(outcome 为空等)-> 跳过,等下一轮。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
NEXT_TIER = {"T1": "T2", "T2": "T3", "T3": "T3"}
|
||||
SUCCESS = ("ok", "verified")
|
||||
FAILURE = ("escalated", "failed", "user_retry", "timeout")
|
||||
|
||||
|
||||
def _features(row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
try:
|
||||
obj = json.loads(row.get("features") or "{}")
|
||||
return obj if isinstance(obj, dict) else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def derive_true_tier(row: Dict[str, Any]) -> Optional[str]:
|
||||
"""单行推导(纯函数,测试友好)。返回 None = 暂不判定。"""
|
||||
if row.get("human_override"):
|
||||
return str(row["human_override"])
|
||||
outcome = row.get("outcome") or ""
|
||||
executed = row.get("executed_tier") or "T2"
|
||||
feats = _features(row)
|
||||
if feats.get("plan_multi"):
|
||||
return "T3" # 规则 1(pipeline brief 信号)
|
||||
if outcome in FAILURE: # 规则 2
|
||||
return NEXT_TIER.get(executed, "T3")
|
||||
if outcome in SUCCESS: # 规则 3
|
||||
return executed
|
||||
return None # 规则 4:等待结果
|
||||
|
||||
|
||||
def derive_true_tiers(store, now: Optional[float] = None,
|
||||
retention_days: int = 180) -> int:
|
||||
"""批量推导(§7 签名):返回回填条数;顺带 180d 留存清理。"""
|
||||
now = time.time() if now is None else now
|
||||
rows = store.all_observations()
|
||||
count = 0
|
||||
for row in rows:
|
||||
if row.get("true_tier"):
|
||||
continue
|
||||
true_tier = derive_true_tier(row)
|
||||
if true_tier:
|
||||
store.set_true_tier(row["request_id"], true_tier)
|
||||
count += 1
|
||||
store.purge_older_than(now - retention_days * 86400)
|
||||
return count
|
||||
@@ -116,6 +116,14 @@ class SenseStore:
|
||||
" WHERE true_tier != ''").fetchone()
|
||||
return int(row["c"])
|
||||
|
||||
def all_observations(self, limit: int = 200000) -> List[Dict[str, Any]]:
|
||||
"""全量遍历(labeler 夜间推导输入;量大时可分页)。"""
|
||||
with self._lock, self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM tier_observations ORDER BY id LIMIT ?",
|
||||
(limit,)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def purge_older_than(self, ts: float) -> int:
|
||||
"""留存清理(D-G6:默认 180 天,夜间任务顺带)。"""
|
||||
with self._lock, self._connect() as conn:
|
||||
@@ -154,6 +162,14 @@ class SenseStore:
|
||||
" ORDER BY created_ts DESC LIMIT 1", (kind,)).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def list_artifacts(self, kind: str) -> List[Dict[str, Any]]:
|
||||
"""同 kind 工件按时间倒序(load_active 的 last-good 扫描用)。"""
|
||||
with self._lock, self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM sense_artifacts WHERE kind = ?"
|
||||
" ORDER BY created_ts DESC", (kind,)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
# ---------- 自省 ----------
|
||||
def table_names(self) -> List[str]:
|
||||
with self._lock, self._connect() as conn:
|
||||
|
||||
Reference in New Issue
Block a user