Files
tzt 4575025056 feat(sense): T-G5 Grader 决策组合(特征门×概率×conformal + /v1/route + 三态 mode)
- features.py:gate() 纯函数——轮数/字符估算/意图黑名单/仓库级信号/长度门
  -> t1_hard_ok(任一硬门不过即 False,D-G1)
- grader.py:Grader.decide §8 时序(embed 降级检查 -> 特征门 -> LinearHead 概率
  -> conformal 阈值:p1>=τ1 且 t1_hard_ok->T1,p3>=τ3 或 repo_signals->T3,
  其余 T2 默认;collect/shadow 只写观察 executed=现行为,live 决策即执行;
  全模式 observer.log);工件/阈值缓存 + invalidate;D-G4 规则门退化
- routes:/v1/route 契约(D-G6 不落 query 原文)
- fix(observer):embedding list -> BLOB 转换(修 sqlite 绑定)
- 测试 +6(门矩阵/shadow 不改流/live 决策/降级/保守阈值/写观察),全量 405 passed
2026-09-05 14:17:44 +08:00

156 lines
5.9 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.
"""分级决策组合(T-G5,§8 主时序):
embedder.embed(挂 -> fallback=T2+规则门,D-G4
-> features.gatet1_hard_ok
-> LinearHead.predict -> {p1,p2,p3}
-> conformal 阈值:p1>=τ1 且 t1_hard_ok -> T1p3>=τ3 或 repo_signals -> T3;其余 T2
-> mode 裁剪:collect/shadow 只写观察(decided≠executed),live 返回决策
-> observer.log(全模式必写)
"""
from __future__ import annotations
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from gateway.sense.calibrate import load_active
from gateway.sense.classifier import LinearHead
from gateway.sense.config import SenseConfig
from gateway.sense.embedder import embed
from gateway.sense.errors import EmbedderDown
from gateway.sense.features import gate
@dataclass
class TierDecision:
tier: str # 决策档(live 时即执行档的候选)
probs: Dict[str, float]
hard_gates: Dict[str, Any]
thresholds_version: str = ""
head_version: str = ""
mode: str = "collect"
fallback: bool = False # True = embedder/工件故障,规则门退化
executed_tier: str = "" # 现行为执行的档位(collect/shadow 记录用)
def _rule_tier(feats) -> str:
"""规则门兜底(无模型时的决策):仓库级 -> T3;t1 硬门过 -> T1;否则 T2。"""
if feats.repo_signals:
return "T3"
if feats.t1_hard_ok:
return "T1"
return "T2"
class Grader:
"""决策器(持有 active 工件缓存;工件/阈值切换后调 invalidate)。"""
def __init__(self, cfg: SenseConfig, store, observer=None):
self.cfg = cfg
self.store = store
self.observer = observer
self._head: Optional[LinearHead] = None
self._head_loaded = False
self._thresholds: Optional[Dict[str, Any]] = None
def _load_head(self):
if not self._head_loaded:
art = self.store.active_artifact("head")
if art:
try:
self._head = LinearHead.load(art["path"], version=art["version"])
except Exception: # noqa: BLE001
self._head = None
self._head_loaded = True
return self._head
def invalidate(self) -> None:
"""工件 promote 后调用(重载 active 工件与阈值)。"""
self._head = None
self._head_loaded = False
self._thresholds = None
def _thresholds_cached(self) -> Dict[str, Any]:
if self._thresholds is None:
self._thresholds = load_active(self.store, self.cfg.models_dir)
return self._thresholds
async def decide(self, query_or_messages, consumer: str,
domain: str = "", request_id: str = "",
executed_tier: str = "") -> TierDecision:
"""分级决策(§8 时序;全模式写观察)。"""
ts = time.time()
rid = request_id or ("rt" + uuid.uuid4().hex[:10])
feats = gate(query_or_messages, consumer, self.cfg)
probs: Dict[str, float] = {}
head_version = ""
fallback = False
vec: Optional[List[int]] = None
try:
vec = await embed(feats and (query_or_messages
if isinstance(query_or_messages, str)
else "\n".join(
str(m.get("content") or "")
for m in query_or_messages)),
self.cfg.embedder)
except EmbedderDown:
fallback = True
head = None if fallback else self._load_head()
if head is None:
fallback = True
if not fallback and vec is not None:
probs = head.predict([float(v) for v in vec])
head_version = head.version
th = self._thresholds_cached()
th_version = str(th.get("version") or "")
# ---- 决策 ----
if fallback:
tier = _rule_tier(feats) # D-G4 规则门退化
else:
t1_ok = (probs.get("t1", 0.0) >= float(th.get("t1", 0.9))
and feats.t1_hard_ok)
t3_ok = (probs.get("t3", 0.0) >= float(th.get("t3", 0.9))
or feats.repo_signals)
if t3_ok and not t1_ok:
tier = "T3"
elif t1_ok:
tier = "T1"
else:
tier = "T2"
# ---- mode 裁剪(D-G7----
if self.cfg.mode == "live":
executed = tier # live:决策即执行
else:
# collect/shadow:现行为——规则门等价(T1 门/T3 信号)近似 v2 现状
executed = executed_tier or _rule_tier(feats)
decision = TierDecision(
tier=tier, probs=probs,
hard_gates={"turns": feats.turns, "est_tokens": feats.est_tokens,
"single_turn": feats.single_turn,
"intent_blocked": feats.intent_blocked,
"repo_signals": feats.repo_signals,
"over_length": feats.over_length,
"t1_hard_ok": feats.t1_hard_ok},
thresholds_version=th_version, head_version=head_version,
mode=self.cfg.mode, fallback=fallback,
executed_tier=executed)
# ---- 观察必写(全模式)----
if self.observer is not None:
self.observer.log(__import__("gateway.sense.observer",
fromlist=["Observation"]).Observation(
request_id=rid, consumer=consumer, decided_tier=tier,
executed_tier=executed, probs=probs,
policy_version=f"head:{head_version}|th:{th_version}",
features=decision.hard_gates, embedding=vec,
bucket="default", domain=domain, ts=ts))
return decision