feat(sense): T-G7 审计+前端(live 分流接线 + tier 指标卡 + Settings 灰度区块)
- 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
This commit is contained in:
@@ -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:
|
||||
|
||||
+49
-1
@@ -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 ""
|
||||
|
||||
+3
-3
@@ -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/"
|
||||
}
|
||||
|
||||
@@ -114,4 +114,13 @@ export interface Metrics {
|
||||
cache: Record<string, unknown>
|
||||
v2?: Record<string, unknown>
|
||||
review?: { pending: number; total: number }
|
||||
sense?: {
|
||||
enabled: boolean
|
||||
mode: string
|
||||
observations: number
|
||||
labeled: number
|
||||
by_decided_tier?: Record<string, number>
|
||||
agreement?: number
|
||||
min_labels?: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,21 @@
|
||||
<p class="hint">单价来自模型池条目($/1M tokens);经典设置下的模型成本不计入。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="sense && sense.observations" class="metric-card">
|
||||
<h3>语义分级(sense · {{ sense.mode }})</h3>
|
||||
<div class="kv-list">
|
||||
<span>观察数</span><b>{{ sense.observations }}</b>
|
||||
<span>已标签</span><b>{{ sense.labeled }} / {{ sense.min_labels }}</b>
|
||||
<span>一致率</span><b>{{ ((sense.agreement ?? 0) * 100).toFixed(1) }}%</b>
|
||||
<template v-for="(v, k) in sense.by_decided_tier" :key="k">
|
||||
<span>判 {{ k }}</span><b>{{ v }}</b>
|
||||
</template>
|
||||
</div>
|
||||
<p class="hint">
|
||||
晋升门:一致率 ≥85% + 标签 ≥{{ sense.min_labels }} + 审计无误判(collect 攒满前不开 live)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="data.review" class="metric-card review-card">
|
||||
<h3>人工检验</h3>
|
||||
<div class="review-stats">
|
||||
@@ -115,6 +130,8 @@ const byModel = computed(() => {
|
||||
return (v2?.by_model as Record<string, ModelBucket>) || null
|
||||
})
|
||||
|
||||
const sense = computed(() => data.value?.sense || null)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
@@ -389,6 +389,45 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 🧠 语义分级(sense)灰度控制 -->
|
||||
<section class="settings-section">
|
||||
<div class="section-title">
|
||||
<span>🧠 语义分级(Sense · 三级任务分级)</span>
|
||||
<div class="title-right">
|
||||
<span :class="['server-badge', senseCfg.enabled ? 'badge-running' : 'badge-stopped']">
|
||||
{{ senseCfg.enabled ? `● ${senseCfg.mode}` : '○ 未启用' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<p class="role-hint">
|
||||
灰度纪律:collect(只记不决策)攒满 {{ senseCfg.min_labels }} 条标签并过晋升门
|
||||
(一致率 ≥85% + conformal 覆盖 ≤α+2% + 审计无误判)后才可开 live。
|
||||
</p>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="senseCfg.enabled" @change="saveSense" />
|
||||
启用语义分析器(总开关,改后保存生效)
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>灰度模式</label>
|
||||
<select v-model="senseCfg.mode" @change="saveSense"
|
||||
:disabled="!senseCfg.enabled">
|
||||
<option value="collect">collect — 只采集观察</option>
|
||||
<option value="shadow">shadow — 决策只比对不执行</option>
|
||||
<option value="live">live — 真分流(需晋升门通过)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>观察/标签数(min_labels={{ senseCfg.min_labels }})</label>
|
||||
<input :value="`${senseCfg.obs} / ${senseCfg.labeled}`" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions">
|
||||
<button class="btn-primary" :disabled="saving || !dirty" @click="save">
|
||||
@@ -412,6 +451,7 @@ import {
|
||||
getLlamaStatus, listLocalModels, startLlama, stopLlama,
|
||||
downloadModel, watchDownload,
|
||||
getPool, upsertPoolEntry, deletePoolEntry, setPoolRoles, testPoolEntry, listPoolModels,
|
||||
getMetrics,
|
||||
} from '@/api'
|
||||
import type {
|
||||
ModelSettings, ModelInfo, LlamaStatus, LocalModel,
|
||||
@@ -961,7 +1001,34 @@ async function reset() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
// 语义分级(sense)灰度
|
||||
const senseCfg = reactive({ enabled: false, mode: 'collect', obs: 0, labeled: 0, min_labels: 500 })
|
||||
|
||||
async function loadSense() {
|
||||
try {
|
||||
const [cfg, m] = await Promise.all([getConfig(), getMetrics()])
|
||||
const s = (cfg as any).sense || {}
|
||||
senseCfg.enabled = !!s.enabled
|
||||
senseCfg.mode = s.mode || 'collect'
|
||||
senseCfg.min_labels = s.policy?.min_labels ?? 500
|
||||
const sm = (m as any).sense || {}
|
||||
senseCfg.obs = sm.observations ?? 0
|
||||
senseCfg.labeled = sm.labeled ?? 0
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
|
||||
async function saveSense() {
|
||||
try {
|
||||
await updateConfig({ sense: { enabled: senseCfg.enabled, mode: senseCfg.mode } } as any)
|
||||
saveMsg.value = { ok: true, msg: `✅ 语义分级已${senseCfg.enabled ? '启用(' + senseCfg.mode + ')' : '关闭'}` }
|
||||
} catch (e: any) {
|
||||
saveMsg.value = { ok: false, msg: '❌ 保存失败:' + (e?.response?.data?.detail || e?.message || e) }
|
||||
await loadSense()
|
||||
}
|
||||
setTimeout(() => (saveMsg.value = null), 3000)
|
||||
}
|
||||
|
||||
onMounted(() => { load(); loadSense() })
|
||||
onUnmounted(() => { _dlSSE?.close() })
|
||||
</script>
|
||||
|
||||
|
||||
+1
-1
@@ -162,5 +162,5 @@ P0 完成后的能力:干净的后端抽象 + 可量化的评测 + 可追溯
|
||||
| T-G4 | 线性头:离线训练脚本 + LinearHead 纯 Python 推理 + 登记 | ✅ 完成 | T-G4 |
|
||||
| T-G5 | Grader:决策组合(特征门×概率×conformal)+ /v1/route + 三态 mode | ✅ 完成 | T-G5 |
|
||||
| T-G6 | live 分流:pipeline tier_fn 三档钩子 + proxy 档位映射 + T2 档升级阶梯 | ✅ 完成 | T-G6 |
|
||||
| T-G7 | 审计+前端:ReviewQueue 抽样 + tier 指标卡 + 客户端来源显示/一键升级 | ⬜ 待办 | |
|
||||
| T-G7 | 审计+前端:ReviewQueue 抽样 + tier 指标卡 + 客户端来源显示/一键升级 | ✅ 完成 | T-G7 |
|
||||
| T-G8 | 实验:E-G1/E-G3 报告;(可选)LoraRemote + E-G2 线性 vs LoRA | ⬜ 待办 | |
|
||||
|
||||
Reference in New Issue
Block a user