From a636b989e5bb9b1aba488141be225e47c0f06dcf Mon Sep 17 00:00:00 2001 From: tzt <14718231+flying-travel@user.noreply.gitee.com> Date: Sat, 5 Sep 2026 15:06:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(sense):=20T-G7=20=E5=AE=A1=E8=AE=A1+?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=88live=20=E5=88=86=E6=B5=81=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=20+=20tier=20=E6=8C=87=E6=A0=87=E5=8D=A1=20+=20Settin?= =?UTF-8?q?gs=20=E7=81=B0=E5=BA=A6=E5=8C=BA=E5=9D=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- gateway/api.py | 24 +++++++ gateway/proxy/routes.py | 50 +++++++++++++- ...ac6-43f2-b2f3-8833b3150465-4843032ba9.json | 6 +- webapp/src/types/index.ts | 9 +++ webapp/src/views/MetricsView.vue | 17 +++++ webapp/src/views/SettingsView.vue | 69 ++++++++++++++++++- 任务拆解与执行计划.md | 2 +- 7 files changed, 171 insertions(+), 6 deletions(-) diff --git a/gateway/api.py b/gateway/api.py index 57cdfc4..51d4f50 100644 --- a/gateway/api.py +++ b/gateway/api.py @@ -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: diff --git a/gateway/proxy/routes.py b/gateway/proxy/routes.py index 08144de..e97c864 100644 --- a/gateway/proxy/routes.py +++ b/gateway/proxy/routes.py @@ -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 "" diff --git a/webapp/.mimosa/hook-status/sess_e50d4f25-3ac6-43f2-b2f3-8833b3150465-4843032ba9.json b/webapp/.mimosa/hook-status/sess_e50d4f25-3ac6-43f2-b2f3-8833b3150465-4843032ba9.json index ebfb729..90bcc1e 100644 --- a/webapp/.mimosa/hook-status/sess_e50d4f25-3ac6-43f2-b2f3-8833b3150465-4843032ba9.json +++ b/webapp/.mimosa/hook-status/sess_e50d4f25-3ac6-43f2-b2f3-8833b3150465-4843032ba9.json @@ -1,14 +1,14 @@ { "schemaVersion": "mimosa-hook-status/v1", - "recordedAt": "2026-09-01T14:21:11.564Z", + "recordedAt": "2026-09-05T07:03:44.339Z", "sessionId": "sess_e50d4f25-3ac6-43f2-b2f3-8833b3150465", "event": "PostToolUse", "toolName": "Edit", - "file": "src/views/AgentView.vue", + "file": "src/views/MetricsView.vue", "outcome": "clear", "coverage": "complete", "findingCount": 0, - "durationMs": 7, + "durationMs": 2, "hostState": "hook_complete", "reportHint": ".mimosa/reports/" } diff --git a/webapp/src/types/index.ts b/webapp/src/types/index.ts index 21f3453..22dff05 100644 --- a/webapp/src/types/index.ts +++ b/webapp/src/types/index.ts @@ -114,4 +114,13 @@ export interface Metrics { cache: Record v2?: Record review?: { pending: number; total: number } + sense?: { + enabled: boolean + mode: string + observations: number + labeled: number + by_decided_tier?: Record + agreement?: number + min_labels?: number + } } diff --git a/webapp/src/views/MetricsView.vue b/webapp/src/views/MetricsView.vue index f93376b..e807294 100644 --- a/webapp/src/views/MetricsView.vue +++ b/webapp/src/views/MetricsView.vue @@ -58,6 +58,21 @@

单价来自模型池条目($/1M tokens);经典设置下的模型成本不计入。

+
+

语义分级(sense · {{ sense.mode }})

+
+ 观察数{{ sense.observations }} + 已标签{{ sense.labeled }} / {{ sense.min_labels }} + 一致率{{ ((sense.agreement ?? 0) * 100).toFixed(1) }}% + +
+

+ 晋升门:一致率 ≥85% + 标签 ≥{{ sense.min_labels }} + 审计无误判(collect 攒满前不开 live) +

+
+

人工检验

@@ -115,6 +130,8 @@ const byModel = computed(() => { return (v2?.by_model as Record) || null }) +const sense = computed(() => data.value?.sense || null) + async function load() { loading.value = true error.value = '' diff --git a/webapp/src/views/SettingsView.vue b/webapp/src/views/SettingsView.vue index 1176cad..8f419d2 100644 --- a/webapp/src/views/SettingsView.vue +++ b/webapp/src/views/SettingsView.vue @@ -389,6 +389,45 @@
+ +
+
+ 🧠 语义分级(Sense · 三级任务分级) +
+ + {{ senseCfg.enabled ? `● ${senseCfg.mode}` : '○ 未启用' }} + +
+
+
+

+ 灰度纪律:collect(只记不决策)攒满 {{ senseCfg.min_labels }} 条标签并过晋升门 + (一致率 ≥85% + conformal 覆盖 ≤α+2% + 审计无误判)后才可开 live。 +

+
+
+ +
+
+ + +
+
+ + +
+
+
+
+