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
This commit is contained in:
tzt
2026-09-05 14:17:44 +08:00
parent 2a4cb529de
commit 4575025056
6 changed files with 376 additions and 1 deletions
+34
View File
@@ -37,6 +37,40 @@ def build_sense_routers(cfg: SenseConfig) -> List[APIRouter]:
return {"enabled": cfg.enabled, "mode": cfg.mode,
"embedder": cfg.embedder.base_url}
@v1.post("/v1/route", tags=["sense"])
async def route_v1(request: Request):
"""分级决策:{query|messages, consumer, domain?} -> TierDecision 视图。
D-G6:不落 query 原文(观察表只存哈希可关联 id + 特征 + int8 向量)。
mode=collect/shadowtier 为决策值,执行仍走消费方现状;
mode=live:消费方按 tier 分流(D-G7)。
"""
from gateway.sense.grader import Grader
try:
body = await request.json()
except Exception:
body = {}
query = (body or {}).get("query")
messages = (body or {}).get("messages")
consumer = str((body or {}).get("consumer") or "proxy")
domain = str((body or {}).get("domain") or "")
payload = messages if messages else (query or "")
grader = Grader(cfg, store,
get_observer(store) if cfg.mode != "collect" else None)
d = await grader.decide(payload, consumer, domain)
probs_total = sum(d.probs.values()) or 1.0
return {
"tier": d.tier if cfg.mode == "live" else d.tier,
"probs": {k: round(v / probs_total, 4) for k, v in d.probs.items()},
"confidence": round(max(d.probs.values()) / probs_total, 4)
if d.probs else 0.0,
"thresholds_version": d.thresholds_version,
"head_version": d.head_version,
"mode": d.mode,
"fallback": d.fallback,
"hard_gates": d.hard_gates,
}
@v1.post("/v1/embeddings", tags=["sense"])
async def embeddings(request: Request):
"""OpenAI 兼容透传 embedder(客户端/代理共用)。"""