feat(sense): T-X4 可解释决策留痕三元结构 + 修复 sense live 分流潜伏 bug(采纳 ai-model-router 决策模型)

- store:新增 sense_decisions 表(CREATE IF NOT EXISTS 幂等迁移),
  insert_decision / list_decisions(JSON 字段反序列化),q_hash 不落原文(D-G6)
- grader:全模式落库决策三元——reasons(§8 判定顺序最小完备集)、
  candidate_scores(线性头 probs)、rejected(落选档位+原因);
  留痕失败静默不影响决策主链路
- 修复:chat_completions 的 sense 分流引用未导入的 settings_store,NameError
  被 except 吞掉导致 D-G7 live 分流从未生效;改为 build_proxy_router 显式注入
  settings_provider(api.py 传 settings_store),缺省 None 行为安全
- 新增 /proxy/admin/sense-decisions 管理面查询端点(sense 未启用返回空集)
- 回归测试:注入 settings_provider 后 x-campus-tier 头出现且 T1 决策经
  管理面可查(修复前该头永远缺失)

pytest 466 passed(T-X6 后 461 + 5)
This commit is contained in:
tzt
2026-09-18 22:55:49 +08:00
parent 5078721df4
commit 7e01dc9b16
6 changed files with 371 additions and 25 deletions
+47
View File
@@ -7,6 +7,7 @@ WAL;全部访问经全局锁 + 每操作新连接(ReviewQueue 模式),
from __future__ import annotations
import sqlite3
import json
import threading
import time
from pathlib import Path
@@ -29,6 +30,17 @@ CREATE TABLE IF NOT EXISTS sense_artifacts(
version TEXT PRIMARY KEY, kind TEXT NOT NULL,
path TEXT NOT NULL, metrics TEXT NOT NULL, created_ts INTEGER NOT NULL,
active INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS sense_decisions(
id INTEGER PRIMARY KEY, ts INTEGER NOT NULL,
request_id TEXT NOT NULL, consumer TEXT NOT NULL,
decided_tier TEXT NOT NULL, executed_tier TEXT NOT NULL DEFAULT '',
mode TEXT NOT NULL DEFAULT 'collect', fallback INTEGER NOT NULL DEFAULT 0,
policy_version TEXT DEFAULT '', q_hash TEXT DEFAULT '',
reasons TEXT NOT NULL DEFAULT '[]',
candidate_scores TEXT NOT NULL DEFAULT '{}',
rejected TEXT NOT NULL DEFAULT '[]');
CREATE INDEX IF NOT EXISTS idx_dec_ts ON sense_decisions(ts);
"""
@@ -131,6 +143,41 @@ class SenseStore:
"DELETE FROM tier_observations WHERE ts < ?", (int(ts),))
return cur.rowcount
# ---------- 决策留痕(T-X4:可解释路由三元结构) ----------
def insert_decision(self, rec: Dict[str, Any]) -> None:
"""写入一条路由决策记录(reasons/candidate_scores/rejected 为 JSON 串)。"""
with self._lock, self._connect() as conn:
conn.execute(
"""INSERT INTO sense_decisions
(ts, request_id, consumer, decided_tier, executed_tier, mode,
fallback, policy_version, q_hash, reasons, candidate_scores,
rejected)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
(int(rec.get("ts", time.time())), rec["request_id"],
rec["consumer"], rec["decided_tier"],
rec.get("executed_tier", ""), rec.get("mode", "collect"),
1 if rec.get("fallback") else 0,
rec.get("policy_version", ""), rec.get("q_hash", ""),
rec.get("reasons", "[]"), rec.get("candidate_scores", "{}"),
rec.get("rejected", "[]")))
def list_decisions(self, limit: int = 50) -> List[Dict[str, Any]]:
"""最近 N 条决策(新在前;JSON 字段反序列化为对象)。"""
with self._lock, self._connect() as conn:
rows = conn.execute(
"SELECT * FROM sense_decisions ORDER BY id DESC LIMIT ?",
(max(1, int(limit)),)).fetchall()
out: List[Dict[str, Any]] = []
for r in rows:
d = dict(r)
for f in ("reasons", "candidate_scores", "rejected"):
try:
d[f] = json.loads(d.get(f) or ("{}" if f == "candidate_scores" else "[]"))
except json.JSONDecodeError:
pass
out.append(d)
return out
# ---------- 工件 ----------
def register_artifact(self, version: str, kind: str, path: str,
metrics: Dict[str, Any], active: bool = False,