feat(sense): T-X3 路由决策缓存——live 模式 embed+线性头去重(采纳 cortiq 决策哈希缓存)
- gateway/sense/decision_cache.py:DecisionCache(sha256(scope+文本) 键、 LRU + TTL 60s、4096 条上限、时钟可注入),对齐 auth._AuthCache 进程内模式 - Grader:仅 live 模式缓存纯决策负载(probs/head_version/vec); 命中跳过 embed + 线性头预测;tier 仍按当前 conformal 阈值即时重算; 观察落盘(observer.log)不因缓存命中跳过——校准数据完整性不受影响 - collect/shadow 模式不走缓存(校准必须全量);invalidate() 同步清空缓存 (阈值/工件切换即时生效,TTL 仅兜底陈旧) pytest 455 passed(T-X2 后 447 + 8)
This commit is contained in:
+35
-17
@@ -17,6 +17,7 @@ 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.decision_cache import DecisionCache
|
||||
from gateway.sense.embedder import embed
|
||||
from gateway.sense.errors import EmbedderDown
|
||||
from gateway.sense.features import gate
|
||||
@@ -53,6 +54,7 @@ class Grader:
|
||||
self._head: Optional[LinearHead] = None
|
||||
self._head_loaded = False
|
||||
self._thresholds: Optional[Dict[str, Any]] = None
|
||||
self._dcache = DecisionCache() # T-X3:live 模式决策缓存
|
||||
|
||||
def _load_head(self):
|
||||
if not self._head_loaded:
|
||||
@@ -66,10 +68,11 @@ class Grader:
|
||||
return self._head
|
||||
|
||||
def invalidate(self) -> None:
|
||||
"""工件 promote 后调用(重载 active 工件与阈值)。"""
|
||||
"""工件 promote 后调用(重载 active 工件与阈值;同步清空决策缓存)。"""
|
||||
self._head = None
|
||||
self._head_loaded = False
|
||||
self._thresholds = None
|
||||
self._dcache.clear()
|
||||
|
||||
def _thresholds_cached(self) -> Dict[str, Any]:
|
||||
if self._thresholds is None:
|
||||
@@ -79,32 +82,47 @@ class Grader:
|
||||
async def decide(self, query_or_messages, consumer: str,
|
||||
domain: str = "", request_id: str = "",
|
||||
executed_tier: str = "") -> TierDecision:
|
||||
"""分级决策(§8 时序;全模式写观察)。"""
|
||||
"""分级决策(§8 时序;全模式写观察)。
|
||||
|
||||
T-X3:live 模式下对 (consumer, 文本) 的纯决策负载(probs/head_version/vec)
|
||||
做 60s LRU 缓存,命中时跳过 embed + 线性头;观察/审计照常落盘。
|
||||
collect/shadow 模式不走缓存(校准数据必须全量产出)。
|
||||
"""
|
||||
ts = time.time()
|
||||
rid = request_id or ("rt" + uuid.uuid4().hex[:10])
|
||||
text = (query_or_messages if isinstance(query_or_messages, str)
|
||||
else "\n".join(str(m.get("content") or "")
|
||||
for m in query_or_messages))
|
||||
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
|
||||
cached = self._dcache.get(consumer, text) \
|
||||
if self.cfg.mode == "live" else None
|
||||
if cached is not None:
|
||||
probs = dict(cached["probs"])
|
||||
head_version = str(cached["head_version"])
|
||||
vec = cached["vec"]
|
||||
else:
|
||||
try:
|
||||
vec = await embed(text, self.cfg.embedder)
|
||||
except EmbedderDown:
|
||||
fallback = True
|
||||
|
||||
head = None if fallback else self._load_head()
|
||||
if head is None:
|
||||
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
|
||||
if not fallback and vec is not None:
|
||||
probs = head.predict([float(v) for v in vec])
|
||||
head_version = head.version
|
||||
if self.cfg.mode == "live":
|
||||
self._dcache.put(consumer, text,
|
||||
{"probs": dict(probs),
|
||||
"head_version": head_version,
|
||||
"vec": vec})
|
||||
|
||||
th = self._thresholds_cached()
|
||||
th_version = str(th.get("version") or "")
|
||||
|
||||
Reference in New Issue
Block a user