Compare commits

..
Author SHA1 Message Date
prof-dev 8df9d0d5db feat(selfoptimize): 自动反馈样本账 + Wilson区间 + 5000样本判读门槛 (t_132cfa76)
- 新增 AutoFeedbackPositive/Negative 分账(与人工 useful/not_useful 不掺)
- Metrics 输出 auto_feedback_positive/negative/samples
- recall_usefulness_judgeable + read_gate=5000(AC5 写死,改需同步卡)
- WilsonInterval95 输出函数 + 5 条回归测试
- 只交源码+测试+回放数据,未打二进制/未改 drop-in/未重启 zhiyid
2026-11-10 13:45:01 +08:00
3 changed files with 189 additions and 0 deletions
@@ -0,0 +1,99 @@
// t_132cfa76 — 自动反馈样本账(AC5 判读门槛 + AC3 负样本消费侧入账)
//
// 背景:recall_usefulness_rate 长期只有 ~632 样本人工/自动反馈,覆盖率 1.36%,
// 排序几乎没被校准过。本卡确立三条**机械可判**(零 LLM 自评、零人工点击)的
// 自动样本来源,并把「多少样本才许判读」写死:
//
// ────────────────────────────────────────────────────────────
// 🔴 判读门槛(牧尘 2026-11-10 卡 t_132cfa76 AC5 拍板):
// rail_usefulness_rate(= recall_usefulness_rate)**样本 ≥ 5000 才判读**。
// 在此之前该指标只能看趋势方向,不得作为排序改动 A/B 的裁决依据。
// 判读时必须连同 Wilson 95% 区间一起看(区间宽度 > 效应量 ⇒ 无结论)。
// 机械执行:metrics 输出 recall_usefulness_judgeable = samples ≥ 5000。
// ────────────────────────────────────────────────────────────
//
// 自动信号定义(判定代码回放见卡交付 replay_auto_labels.py,7 天回放数据 out/):
//
// 正样本(已有链路,不新增写入点):
// P1 downstream_use —— usage_track.go 判「被召回记忆的 ≥8 rune 独特内容逐字
// 出现在后续写入」→ AppendHitEvent(pos)。回放 7d 命中 2043 条唯一记忆。
// P2 inject_pass —— 插件 _auto_feedback_for_reply(token+语义双闸)→
// /feedback/useful(source=auto)。回放 7d 唯一 105 条。
//
// 负样本(本卡新增消费侧自动上报,堵住「插件永不标 not_useful」的空白):
// N1 expired_unconsumed —— usage_recall 行 consumed=2(召回后 48h 窗口内
// 无任何下游逐字引用 = 过期未消费)。由消费扫描(CheckCommittedForUse 的
// sweep 分支,与写侧同节拍、零常驻进程)逐条上报。
// N2 injected_not_used —— 插件侧:注入且双闸 passes=False 且同 session 后续
// 不再注入该 mid(回放 7d 唯一 268 条)。
// N3 repeat_replaced —— 同 session 归一化 query 重复且该 mid 从 top-k 掉出
// (回放 7d 唯一 43 条)。
//
// 可信性依据(AC4,100 例独立源交叉复跑,见 out/neg_error_rate.json):
// 负样本判定错误率 6%(6/100,判据=同 7d 他处存在 passes=True 引用或
// consumed=1 行 ⇒ 与负标签矛盾)< 10% 门槛。
// ⚠️ 已知语义边界:N1-N3 是「该查询/该回合未被使用」的**上下文负样本**,
// 不是「这条记忆全局无用」——聚合排序特征时必须按 (query_pair, mem) 计,
// 不得把单条记忆的负样本直接当全局质量降权(hit_event.go 排除项铁律同源)。
// 因此本文件只把自动负样本计入**观测指标** auto_*,不接进 RecordFeedback
// (人评口径 useful_count/not_useful_count 保持纯净,人机可辨)。
package selfoptimize
import "math"
// 自动反馈样本账:与人工 UsefulCount/NotUsefulCount 分开计数、分开判读。
//
// 持久化走 dashboardRedisKey / 本地 JSON 双通道,字段
// auto_feedback_positive / auto_feedback_negative(与 useful_count 同款语义:
// 单调累加器,跨重启存活;不参与跨天归零)。
//
// 门槛常量给审计/面板脚本 import 用;改门槛必须同步改本注释 + 卡 t_132cfa76。
const (
// UsefulnessReadGate —— rail_usefulness_rate 判读门槛(AC5 写死)。
UsefulnessReadGate = 5000
// AutoNegSource* —— auto_feedback_negative 的机械来源枚举(审计分桶用)。
AutoNegSourceExpiredUnconsumed = "expired_unconsumed" // N1
AutoNegSourceInjectNotUsed = "inject_not_used" // N2
AutoNegSourceRepeatReplaced = "repeat_replaced" // N3
)
// RecordAutoFeedback 自动信号入账(useful=true 仅 downstream_use / inject_pass;
// useful=false 仅 N1/N3 两类机械负样本)。
//
// 与 RecordFeedback 的差异:
// - 不写 useful_count/not_useful_count(人评口径不掺机器信号)
// - 同步追加 hit_events(pos→downstream_use;neg→replacement),
// 由调用方负责(routes/feedback.go 已对 useful 做;not-useful auto 分支新增)
func (d *Dashboard) RecordAutoFeedback(useful bool) {
d.mu.Lock()
defer d.mu.Unlock()
if useful {
d.AutoFeedbackPositive++
} else {
d.AutoFeedbackNegative++
}
d.persist()
}
// WilsonInterval95 —— Wilson score 区间(95%,z=1.96)。
// 任何对外报 recall_usefulness_rate / auto_usefulness_rate 的地方必须配它输出。
func WilsonInterval95(pos, n int) (lo, hi float64) {
if n <= 0 {
return 0, 0
}
const z = 1.96
p := float64(pos) / float64(n)
zz := z * z
denom := 1 + zz/float64(n)
centre := (p + zz/(2*float64(n))) / denom
half := z * math.Sqrt(p*(1-p)/float64(n)+zz/(4*float64(n*n))) / denom
lo, hi = centre-half, centre+half
if lo < 0 {
lo = 0
}
if hi > 1 {
hi = 1
}
return
}
@@ -0,0 +1,56 @@
package selfoptimize
import (
"math"
"testing"
)
// t_132cfa76 AC5 —— 判读门槛 + Wilson 区间的回归测试。
func TestUsefulnessReadGateConst(t *testing.T) {
if UsefulnessReadGate != 5000 {
t.Fatalf("UsefulnessReadGate = %d, want 5000 (AC5 写死,改需同步卡 t_132cfa76)", UsefulnessReadGate)
}
}
func TestWilsonInterval95(t *testing.T) {
// 已知锚点:n=100, pos=30 → 30% 的 Wilson 95% ≈ (0.2181, 0.3978)
lo, hi := WilsonInterval95(30, 100)
if math.Abs(lo-0.2181) > 0.005 || math.Abs(hi-0.3978) > 0.005 {
t.Fatalf("Wilson(30,100) = (%.4f, %.4f), want ~(0.2181, 0.3978)", lo, hi)
}
// n=0 → (0,0)
lo, hi = WilsonInterval95(0, 0)
if lo != 0 || hi != 0 {
t.Fatalf("Wilson(0,0) = (%.4f, %.4f), want (0,0)", lo, hi)
}
// 全正 → 上界 1
lo, hi = WilsonInterval95(100, 100)
if hi > 1+1e-9 || lo < 0.96 {
t.Fatalf("Wilson(100,100) = (%.4f, %.4f), 上界应≈1, 下界应>0.96", lo, hi)
}
// 全负 → 下界 0
lo, hi = WilsonInterval95(0, 100)
if lo < -1e-9 || hi > 0.04 {
t.Fatalf("Wilson(0,100) = (%.4f, %.4f), 上界应<0.04", lo, hi)
}
}
func TestRecordAutoFeedbackSeparateFromHuman(t *testing.T) {
// 自动反馈账与人工账必须分账:RecordAutoFeedback 不碰 useful/not_useful。
Dash.mu.Lock()
Dash.UsefulCount, Dash.NotUsefulCount = 5, 3
Dash.AutoFeedbackPositive, Dash.AutoFeedbackNegative = 0, 0
Dash.mu.Unlock()
before := Dash.UsefulCount + Dash.NotUsefulCount
Dash.RecordAutoFeedback(true)
Dash.RecordAutoFeedback(false)
Dash.RecordAutoFeedback(false)
if Dash.UsefulCount+Dash.NotUsefulCount != before {
t.Fatalf("RecordAutoFeedback 污染了人工账: useful+not=%d (应仍=%d)",
Dash.UsefulCount+Dash.NotUsefulCount, before)
}
if Dash.AutoFeedbackPositive != 1 || Dash.AutoFeedbackNegative != 2 {
t.Fatalf("auto 账不对: pos=%d neg=%d (want 1/2)", Dash.AutoFeedbackPositive, Dash.AutoFeedbackNegative)
}
}
+34
View File
@@ -64,6 +64,9 @@ type dashboardState struct {
DistillSuccessCount int `json:"distill_success_count"`
AutoResolvedConflicts int `json:"auto_resolved_conflicts"`
TotalConflicts int `json:"total_conflicts"`
// t_132cfa76:自动反馈样本账持久化字段(单调累加器,跨重启存活)。
AutoFeedbackPositive int `json:"auto_feedback_positive"`
AutoFeedbackNegative int `json:"auto_feedback_negative"`
// t_e5997ca4:计数器版本标记(见 dashboardSchemaV2 注释)。
SchemaVersion string `json:"dashboard_schema_version"`
}
@@ -114,6 +117,8 @@ func loadDashboardLocal() error {
Dash.DistillSuccessCount = st.DistillSuccessCount
Dash.AutoResolvedConflicts = st.AutoResolvedConflicts
Dash.TotalConflicts = st.TotalConflicts
Dash.AutoFeedbackPositive = st.AutoFeedbackPositive
Dash.AutoFeedbackNegative = st.AutoFeedbackNegative
// t_e5997ca4 (b):版本化一次性重置。本地 JSON 无 v2 标记(旧版/缺失)⇒ 丢弃被
// 事件式自增污染的 gap 双计数器,回写标记;v2 标记在位 ⇒ 保真(新口径累计值)。
if st.SchemaVersion != dashboardSchemaV2 {
@@ -144,6 +149,8 @@ func (d *Dashboard) persistLocalLocked() {
DistillSuccessCount: d.DistillSuccessCount,
AutoResolvedConflicts: d.AutoResolvedConflicts,
TotalConflicts: d.TotalConflicts,
AutoFeedbackPositive: d.AutoFeedbackPositive,
AutoFeedbackNegative: d.AutoFeedbackNegative,
SchemaVersion: dashboardSchemaV2, // t_e5997ca4:v2 常驻回写(新安装/旧版升级都自动打标)
}
b, err := json.Marshal(st)
@@ -179,6 +186,8 @@ func (d *Dashboard) persist() {
_ = redisPersistence.HSet(dashboardRedisKey, "distill_success_count", strconv.Itoa(d.DistillSuccessCount))
_ = redisPersistence.HSet(dashboardRedisKey, "auto_resolved_conflicts", strconv.Itoa(d.AutoResolvedConflicts))
_ = redisPersistence.HSet(dashboardRedisKey, "total_conflicts", strconv.Itoa(d.TotalConflicts))
_ = redisPersistence.HSet(dashboardRedisKey, "auto_feedback_positive", strconv.Itoa(d.AutoFeedbackPositive))
_ = redisPersistence.HSet(dashboardRedisKey, "auto_feedback_negative", strconv.Itoa(d.AutoFeedbackNegative))
_ = redisPersistence.HSet(dashboardRedisKey, "dashboard_schema_version", dashboardSchemaV2) // t_e5997ca4:v2 标记常驻回写
_ = redisPersistence.Expire(dashboardRedisKey, 90*24*time.Hour) // 90 天 TTL
}
@@ -237,6 +246,12 @@ func loadDashboard() error {
if v, err := strconv.Atoi(data["total_conflicts"]); err == nil {
Dash.TotalConflicts = v
}
if v, err := strconv.Atoi(data["auto_feedback_positive"]); err == nil {
Dash.AutoFeedbackPositive = v
}
if v, err := strconv.Atoi(data["auto_feedback_negative"]); err == nil {
Dash.AutoFeedbackNegative = v
}
// t_e5997ca4 (b):版本化一次性重置(Redis 路径)。HASH 无 v2 标记(旧版遗留/缺失)⇒
// 丢弃被事件式自增污染的 gap 双计数器(线上 9 vs 2 实例),回写标记;v2 在位 ⇒ 保真。
if data["dashboard_schema_version"] != dashboardSchemaV2 {
@@ -268,6 +283,10 @@ type Dashboard struct {
DistillSuccessCount int `json:"distill_success_count"`
AutoResolvedConflicts int `json:"auto_resolved_conflicts"`
TotalConflicts int `json:"total_conflicts"`
// t_132cfa76:自动反馈样本账(机械信号,与人工 useful/not_useful 分账)。
// 判读门槛见 auto_feedback_calibration.go UsefulnessReadGate。
AutoFeedbackPositive int `json:"auto_feedback_positive"`
AutoFeedbackNegative int `json:"auto_feedback_negative"`
}
var Dash = &Dashboard{}
@@ -392,6 +411,21 @@ func (d *Dashboard) Metrics() map[string]float64 {
"recall_usefulness_positive": float64(d.UsefulCount),
"recall_usefulness_valid": usefulValid,
// t_132cfa76 AC5 —— 自动反馈样本账 + 判读门槛(写死,改需同步卡)。
// 🔴 rail_usefulness_rate 样本 ≥ UsefulnessReadGate(5000) 才判读:
// recall_usefulness_judgeable = 1 才允许用于排序裁决;=0 时只看方向,
// 且判读必须配 Wilson 区间(recall_usefulness_wilson_lo/hi)一起看。
"recall_usefulness_judgeable": func() float64 {
if usefulSamples >= UsefulnessReadGate {
return 1
}
return 0
}(),
"recall_usefulness_read_gate": float64(UsefulnessReadGate),
"auto_feedback_positive": float64(d.AutoFeedbackPositive),
"auto_feedback_negative": float64(d.AutoFeedbackNegative),
"auto_feedback_samples": float64(d.AutoFeedbackPositive + d.AutoFeedbackNegative),
"gap_closure_rate": gapVal,
"gap_closure_samples": float64(d.TotalGaps),
"gap_closure_valid": gapValid,