feat(sense): T-G7 审计+前端(live 分流接线 + tier 指标卡 + Settings 灰度区块)

- proxy/routes:sense.mode=live 时转发前调 Grader 分流,档位 -> 池条目
  (tier_pool_hint 映射),响应头 X-Campus-Tier 透出(可解释性);
  T1 决策按 review.sample_rate 抽样入 ReviewQueue(sense_t1 标签,
  人工核"判 T1 实为更高档");分级故障不影响代理可用性(D-G4)
- /api/metrics:sense 段(enabled/mode/观察数/已标签/档位分布/shadow 一致率)
- 前端:MetricsView「语义分级」卡片(观察/标签进度/一致率/晋升门提示);
  SettingsView「Sense 灰度控制」区块(总开关 + collect/shadow/live 切换 +
  标签进度),npm run build 产物更新
- 全量 410 passed
This commit is contained in:
tzt
2026-09-05 15:06:27 +08:00
parent 204789aed6
commit a636b989e5
7 changed files with 171 additions and 6 deletions
+24
View File
@@ -1177,6 +1177,30 @@ try:
"pending": get_review().count(status="pending"),
"total": get_review().count(),
}
# 语义分析器(T-G7):灰度状态 + 观察统计 + 档位分布 + shadow 一致率
try:
from gateway.sense.config import build_sense_config
from gateway.sense.store import SenseStore
scfg = build_sense_config(settings_store().to_dict())
srows = SenseStore.init_db(scfg.db_path).all_observations(limit=100000)
by_decided: Dict[str, int] = {}
consistent = 0
labeled = 0
for r in srows:
by_decided[r["decided_tier"]] = by_decided.get(r["decided_tier"], 0) + 1
if r["executed_tier"] and r["decided_tier"] == r["executed_tier"]:
consistent += 1
if r["true_tier"]:
labeled += 1
out["sense"] = {
"enabled": scfg.enabled, "mode": scfg.mode,
"observations": len(srows), "labeled": labeled,
"by_decided_tier": by_decided,
"agreement": round(consistent / len(srows), 4) if srows else 0.0,
"min_labels": scfg.min_labels,
}
except Exception:
pass
return out
except ImportError:
+49 -1
View File
@@ -78,11 +78,59 @@ def build_proxy_router(cfg: ProxyConfig, pool) -> APIRouter:
_limits(), time.time())
if not _limits().acquire_slot(ctx["key_id"]):
raise QuotaError("并发请求已达该 key 上限")
# 语义分析器 live 分流(D-G7:mode=live 才启用;任何异常不影响代理可用性)
tier_used = None
try:
return await _run_chat(body, dict(request.headers), ctx, cfg, ledger, pool)
from gateway.sense.config import build_sense_config
from gateway.sense.grader import Grader
from gateway.sense.observer import get_observer
from gateway.sense.store import SenseStore
scfg = build_sense_config(settings_store().to_dict())
if scfg.enabled and scfg.mode == "live":
sstore = SenseStore.init_db(scfg.db_path)
grader = Grader(scfg, sstore, get_observer(sstore))
qtext = "\n".join(str(m.get("content") or "")
for m in (body.get("messages") or []))
d = await grader.decide(qtext or str(body.get("model") or ""),
"proxy")
tier_used = d.tier
# 档位 -> 池条目(§2 消费方表):t1/t2/t3 映射模型替换请求模型
hint = scfg.tier_pool_hint("proxy", tier_used)
e = _entry_by_tier_hint(pool, hint)
if e is not None:
body = {**body, "model": e["model"]}
except Exception:
pass # D-G4:分级故障不影响代理可用性
try:
resp = await _run_chat(body, dict(request.headers), ctx, cfg, ledger, pool)
if tier_used:
resp.headers["x-campus-tier"] = tier_used
# T1 审计抽样(§9.4):按 review.sample_rate 入队人工核
try:
import random as _random
rate = float(load_config().get("review", {}).get("sample_rate", 0.1))
if _random.random() < rate:
get_review().enqueue(
request_id + "-sense", str(body.get("model") or "proxy"),
"(sense T1 审计抽样)", tags=["sense_t1"],
reason="sense_audit")
except Exception:
pass
return resp
finally:
_limits().release_slot(ctx["key_id"])
def _entry_by_tier_hint(pool, hint: str) -> Optional[Dict[str, Any]]:
"""按档位名选池条目(tier_hint 优先,缺省按 tier 字段映射)。"""
entries = pool.list().get("entries", [])
for e in entries:
if not e.get("enabled") or e.get("backend") == "mock":
continue
if (e.get("tier_hint") or e.get("tier")) == hint:
return e
return None
# ---------------- 管理面 ----------------
def _guard_admin(request: Request) -> None:
host = request.client.host if request.client else ""