playbook_lift v4 metric: warm-top-1 re-judge — quality lift +24%/-14%
The rank-based "lift" metric (warm-top-1 == cold-judge-best) doesn't distinguish "Shape B surfaced a strictly-better answer" from "Shape B shuffled ranks but quality is unchanged" from "Shape B replaced a good answer with a wrong one." This commit adds Pass 4: judge warm top-1 with the same prompt as cold ratings, then bucket the comparison. Implementation: - New --with-rejudge driver flag (default off). - New WITH_REJUDGE harness env (default 1, on for prod runs). - queryRun gains WarmTop1Metadata (cached during Pass 2 for the rejudge call) + WarmTop1Rating *int (nil-distinguishable; nil = no rejudge, 0..5 = rating). - summary gains RejudgeAttempted, QualityLifted, QualityNeutral, QualityRegressed (counts of warm-rating > / == / < cold-rating). - Markdown headline gains a Quality block when rejudge ran. - ~21 extra judge calls (~30s on qwen2.5). Run #005 result (split inject threshold 0.20 + paraphrase + rejudge): Quality lifted 5 / 21 (24%) — 3× +2 rating, 2× +1 rating Quality neutral 13 / 21 (62%) — includes OOD queries holding 1 Quality regressed 3 / 21 (14%) Net rating delta +3 across 21 queries (+0.14 average) The 5 lifts were all rating-2 cold replaced with rating-3 or rating-4 warm — Shape B took mediocre matches and substituted substantively better ones. The 3 regressions were small (-1, -1, -3). Q11 is the cautionary tale: cold top-1 "production line worker" (rating 4) got replaced by Q1's recorded "forklift OSHA-30 operator" e-5729 (rating 1). Adjacent-domain cross-pollination — production worker and forklift operator embed within 0.20 cosine because both are warehouse-adjacent staffing queries, even though the judge correctly distinguishes them. The split-threshold defense (0.5 boost / 0.20 inject) catches OOD cross-pollination (Q19/Q20/Q21 all stayed neutral at rating 1) but not adjacent-domain cross-pollination. Net product verdict: working, net-positive on quality, but the worst case (Q11 4→1) is customer-visible and warrants a tighter inject threshold OR an additional gate beyond cosine distance. Filed in STATE_OF_PLAY OPEN as a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,12 +75,19 @@ type queryRun struct {
|
||||
PlaybookRecorded bool `json:"playbook_recorded"`
|
||||
PlaybookID string `json:"playbook_target_id,omitempty"`
|
||||
|
||||
WarmTop1ID string `json:"warm_top1_id"`
|
||||
WarmTop1Distance float32 `json:"warm_top1_distance"`
|
||||
WarmBoostedCount int `json:"warm_boosted_count"`
|
||||
WarmJudgeBestRank int `json:"warm_judge_best_rank"`
|
||||
WarmTop1ID string `json:"warm_top1_id"`
|
||||
WarmTop1Distance float32 `json:"warm_top1_distance"`
|
||||
WarmBoostedCount int `json:"warm_boosted_count"`
|
||||
WarmJudgeBestRank int `json:"warm_judge_best_rank"` // rank of cold judge-best in warm — NOT the warm pass's own judge-best
|
||||
WarmTop1Metadata json.RawMessage `json:"-"` // cached for Pass 4 rejudge; not emitted
|
||||
|
||||
Lift bool `json:"lift"` // judge-best was below top-1 cold, but top-1 warm
|
||||
// WarmTop1Rating: only populated when --with-rejudge. Compare to
|
||||
// ColdRatings[0] (== cold top-1 rating) to measure quality lift.
|
||||
// *int so absence (no rejudge pass) and a 0-rating verdict are
|
||||
// distinguishable.
|
||||
WarmTop1Rating *int `json:"warm_top1_rating,omitempty"`
|
||||
|
||||
Lift bool `json:"lift"` // judge-best was below top-1 cold, but top-1 warm
|
||||
|
||||
// Paraphrase pass — only populated when --with-paraphrase. Tests
|
||||
// the playbook's actual learning property: does a recorded entry
|
||||
@@ -114,6 +121,17 @@ type summary struct {
|
||||
ParaphraseTop1Lifts int `json:"paraphrase_top1_lifts,omitempty"` // recorded answer surfaced at rank 0
|
||||
ParaphraseAnyRankHits int `json:"paraphrase_any_rank_hits,omitempty"` // recorded answer surfaced at any rank in top-K
|
||||
|
||||
// Re-judge pass aggregates — only populated when --with-rejudge.
|
||||
// Measures QUALITY lift (warm top-1 rating vs cold top-1 rating)
|
||||
// rather than rank-of-cold-judge-best lift. The latter conflates
|
||||
// "warm surfaced a different but equally-good result" with "warm
|
||||
// shuffled ranks but the answer was the same"; quality lift
|
||||
// disambiguates them.
|
||||
RejudgeAttempted int `json:"rejudge_attempted,omitempty"` // queries that ran the rejudge pass
|
||||
QualityLifted int `json:"quality_lifted,omitempty"` // warm-top-1 rating > cold-top-1 rating
|
||||
QualityNeutral int `json:"quality_neutral,omitempty"` // ratings equal (could be same or different item)
|
||||
QualityRegressed int `json:"quality_regressed,omitempty"` // warm-top-1 rating < cold-top-1 rating
|
||||
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
@@ -128,6 +146,7 @@ func main() {
|
||||
k := flag.Int("k", 10, "top-k from matrix.search per pass")
|
||||
out := flag.String("out", "reports/reality-tests/playbook_lift_001.json", "output JSONL path")
|
||||
withParaphrase := flag.Bool("with-paraphrase", false, "after warm pass, generate a paraphrase via the judge model and re-query with playbook=true to test the learning property")
|
||||
withRejudge := flag.Bool("with-rejudge", false, "after warm pass, judge warm top-1 to measure QUALITY lift (vs cold top-1 rating), not just rank-of-cold-judge-best")
|
||||
flag.Parse()
|
||||
|
||||
// Judge resolution priority: explicit flag > $JUDGE_MODEL env >
|
||||
@@ -225,6 +244,7 @@ func main() {
|
||||
}
|
||||
runs[i].WarmTop1ID = resp.Results[0].ID
|
||||
runs[i].WarmTop1Distance = resp.Results[0].Distance
|
||||
runs[i].WarmTop1Metadata = resp.Results[0].Metadata // cache for Pass 4 rejudge
|
||||
runs[i].WarmBoostedCount = resp.PlaybookBoosted
|
||||
playbookBoostedTotal += resp.PlaybookBoosted
|
||||
|
||||
@@ -304,6 +324,47 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 4 (warm-rejudge) — opt-in via --with-rejudge. Judge warm
|
||||
// top-1 against the same prompt as cold ratings, then compare to
|
||||
// cold top-1 rating. This measures QUALITY lift (did the playbook
|
||||
// produce a better candidate?) rather than just rank-of-cold-judge-
|
||||
// best lift (did the recorded answer move to top-1, even if cold's
|
||||
// top-1 was already good?). See STATE_OF_PLAY OPEN — added because
|
||||
// run #003's verbatim 2/6 didn't tell us whether Shape B was
|
||||
// surfacing better OR same-quality alternatives.
|
||||
rejudgeAttempted := 0
|
||||
qualityLifted := 0
|
||||
qualityNeutral := 0
|
||||
qualityRegressed := 0
|
||||
if *withRejudge {
|
||||
log.Printf("[lift] warm-rejudge pass: measuring quality lift (warm top-1 rating vs cold top-1 rating)")
|
||||
for i := range runs {
|
||||
if runs[i].WarmTop1ID == "" || len(runs[i].WarmTop1Metadata) == 0 {
|
||||
continue // warm pass didn't complete for this query
|
||||
}
|
||||
rejudgeAttempted++
|
||||
result := matrixResult{
|
||||
ID: runs[i].WarmTop1ID,
|
||||
Distance: runs[i].WarmTop1Distance,
|
||||
Metadata: runs[i].WarmTop1Metadata,
|
||||
}
|
||||
warmRating := judgeRate(hc, *ollama, *judge, runs[i].Query, result)
|
||||
runs[i].WarmTop1Rating = &warmRating
|
||||
coldRating := 0
|
||||
if len(runs[i].ColdRatings) > 0 {
|
||||
coldRating = runs[i].ColdRatings[0]
|
||||
}
|
||||
switch {
|
||||
case warmRating > coldRating:
|
||||
qualityLifted++
|
||||
case warmRating < coldRating:
|
||||
qualityRegressed++
|
||||
default:
|
||||
qualityNeutral++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sum := summary{
|
||||
Total: len(runs),
|
||||
WithDiscovery: withDiscovery,
|
||||
@@ -314,6 +375,10 @@ func main() {
|
||||
ParaphraseAttempted: paraphraseAttempted,
|
||||
ParaphraseTop1Lifts: paraphraseTop1Lifts,
|
||||
ParaphraseAnyRankHits: paraphraseAnyRankHits,
|
||||
RejudgeAttempted: rejudgeAttempted,
|
||||
QualityLifted: qualityLifted,
|
||||
QualityNeutral: qualityNeutral,
|
||||
QualityRegressed: qualityRegressed,
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
}
|
||||
if len(runs) > 0 {
|
||||
@@ -323,11 +388,11 @@ func main() {
|
||||
if err := writeJSON(*out, runs, sum); err != nil {
|
||||
log.Fatalf("write %s: %v", *out, err)
|
||||
}
|
||||
if *withParaphrase {
|
||||
log.Printf("[lift] DONE — %d queries · discovery=%d · lift=%d · boosted=%d · meanΔdist=%.4f · paraphrase=%d/%d→top1, %d/%d→anyrank",
|
||||
if *withParaphrase || *withRejudge {
|
||||
log.Printf("[lift] DONE — %d queries · discovery=%d · lift=%d · boosted=%d · meanΔdist=%.4f · paraphrase=%d/%d→top1 · quality=lifted%d/neutral%d/regressed%d",
|
||||
sum.Total, sum.WithDiscovery, sum.LiftCount, sum.PlaybookBoostedTotal, sum.MeanTop1DeltaDistance,
|
||||
sum.ParaphraseTop1Lifts, sum.ParaphraseAttempted,
|
||||
sum.ParaphraseAnyRankHits, sum.ParaphraseAttempted)
|
||||
sum.QualityLifted, sum.QualityNeutral, sum.QualityRegressed)
|
||||
} else {
|
||||
log.Printf("[lift] DONE — %d queries · discovery=%d · lift=%d · boosted=%d · meanΔdist=%.4f",
|
||||
sum.Total, sum.WithDiscovery, sum.LiftCount, sum.PlaybookBoostedTotal, sum.MeanTop1DeltaDistance)
|
||||
|
||||
Reference in New Issue
Block a user