fix: address Copilot review round 2

- Fix F1/HitRate weighted aggregation: track ValidF1Count separately so
  computeModeAgg weights F1 by valid scores only, not TotalQuestions
- No-context retrieval failure uses 0.0 (genuine bad score) instead of
  -1.0 sentinel (reserved for API/parse failures)
- Validate --timeout > 0 to prevent disabling HTTP timeouts
This commit is contained in:
BeaconCat 2026-04-13 10:35:46 +08:00
parent 90a57642b0
commit 8eca3139db
3 changed files with 23 additions and 7 deletions

View file

@ -36,6 +36,7 @@ type AggMetrics struct {
OverallHitRate float64 `json:"overallHitRate"` OverallHitRate float64 `json:"overallHitRate"`
ByCategory map[int]*CatMetrics `json:"byCategory"` ByCategory map[int]*CatMetrics `json:"byCategory"`
TotalQuestions int `json:"totalQuestions"` TotalQuestions int `json:"totalQuestions"`
ValidF1Count int `json:"validF1Count"`
} }
// CatMetrics holds metrics for a single category. // CatMetrics holds metrics for a single category.
@ -43,6 +44,7 @@ type CatMetrics struct {
F1 float64 `json:"f1"` F1 float64 `json:"f1"`
HitRate float64 `json:"hitRate"` HitRate float64 `json:"hitRate"`
QuestionCount int `json:"questionCount"` QuestionCount int `json:"questionCount"`
ValidF1Count int `json:"validF1Count"`
} }
// EvalLegacy evaluates using legacy session store (raw history + budget truncation). // EvalLegacy evaluates using legacy session store (raw history + budget truncation).
@ -239,7 +241,10 @@ func aggregateMetrics(qaResults []QAResult) AggMetrics {
} }
byCat := map[int]*CatMetrics{} byCat := map[int]*CatMetrics{}
for cat, acc := range byCatAcc { for cat, acc := range byCatAcc {
cm := &CatMetrics{QuestionCount: acc.hitRateCount} cm := &CatMetrics{
QuestionCount: acc.hitRateCount,
ValidF1Count: acc.f1Count,
}
if acc.f1Count > 0 { if acc.f1Count > 0 {
cm.F1 = acc.f1Sum / float64(acc.f1Count) cm.F1 = acc.f1Sum / float64(acc.f1Count)
} }
@ -253,6 +258,7 @@ func aggregateMetrics(qaResults []QAResult) AggMetrics {
OverallHitRate: totalHitRate / float64(nHit), OverallHitRate: totalHitRate / float64(nHit),
ByCategory: byCat, ByCategory: byCat,
TotalQuestions: len(qaResults), TotalQuestions: len(qaResults),
ValidF1Count: validF1Count,
} }
} }
@ -298,27 +304,33 @@ func SaveAggregated(results []EvalResult, outDir string) error {
func computeModeAgg(results []EvalResult) AggMetrics { func computeModeAgg(results []EvalResult) AggMetrics {
agg := AggMetrics{ByCategory: map[int]*CatMetrics{}} agg := AggMetrics{ByCategory: map[int]*CatMetrics{}}
for _, r := range results { for _, r := range results {
agg.OverallF1 += r.Agg.OverallF1 * float64(r.Agg.TotalQuestions) agg.OverallF1 += r.Agg.OverallF1 * float64(r.Agg.ValidF1Count)
agg.OverallHitRate += r.Agg.OverallHitRate * float64(r.Agg.TotalQuestions) agg.OverallHitRate += r.Agg.OverallHitRate * float64(r.Agg.TotalQuestions)
agg.TotalQuestions += r.Agg.TotalQuestions agg.TotalQuestions += r.Agg.TotalQuestions
agg.ValidF1Count += r.Agg.ValidF1Count
for cat, cm := range r.Agg.ByCategory { for cat, cm := range r.Agg.ByCategory {
existing, ok := agg.ByCategory[cat] existing, ok := agg.ByCategory[cat]
if !ok { if !ok {
existing = &CatMetrics{} existing = &CatMetrics{}
agg.ByCategory[cat] = existing agg.ByCategory[cat] = existing
} }
existing.F1 += cm.F1 * float64(cm.QuestionCount) existing.F1 += cm.F1 * float64(cm.ValidF1Count)
existing.HitRate += cm.HitRate * float64(cm.QuestionCount) existing.HitRate += cm.HitRate * float64(cm.QuestionCount)
existing.QuestionCount += cm.QuestionCount existing.QuestionCount += cm.QuestionCount
existing.ValidF1Count += cm.ValidF1Count
} }
} }
if agg.ValidF1Count > 0 {
agg.OverallF1 /= float64(agg.ValidF1Count)
}
if agg.TotalQuestions > 0 { if agg.TotalQuestions > 0 {
agg.OverallF1 /= float64(agg.TotalQuestions)
agg.OverallHitRate /= float64(agg.TotalQuestions) agg.OverallHitRate /= float64(agg.TotalQuestions)
} }
for _, cat := range agg.ByCategory { for _, cat := range agg.ByCategory {
if cat.ValidF1Count > 0 {
cat.F1 /= float64(cat.ValidF1Count)
}
if cat.QuestionCount > 0 { if cat.QuestionCount > 0 {
cat.F1 /= float64(cat.QuestionCount)
cat.HitRate /= float64(cat.QuestionCount) cat.HitRate /= float64(cat.QuestionCount)
} }
} }

View file

@ -211,10 +211,10 @@ func EvalSeahorseLLM(
Question: qa.Question, Question: qa.Question,
Category: qa.Category, Category: qa.Category,
GoldAnswer: qa.AnswerString(), GoldAnswer: qa.AnswerString(),
TokenF1: -1.0, TokenF1: 0.0,
HitRate: 0.0, HitRate: 0.0,
}) })
log.Printf("[seahorse-llm] sample=%s q=%d/%d score=-1.00 answer=(no context)", log.Printf("[seahorse-llm] sample=%s q=%d/%d score=0.00 answer=(no context)",
sample.SampleID, total, totalQA) sample.SampleID, total, totalQA)
continue continue
} }

View file

@ -305,6 +305,10 @@ func buildLLMOptions() (LLMClientOptions, error) {
} }
apiKey := envOrFlag(flagAPIKey, "MEMBENCH_API_KEY") apiKey := envOrFlag(flagAPIKey, "MEMBENCH_API_KEY")
if flagTimeout <= 0 {
return LLMClientOptions{}, fmt.Errorf("--timeout must be > 0, got %d", flagTimeout)
}
return LLMClientOptions{ return LLMClientOptions{
BaseURL: base, BaseURL: base,
Model: model, Model: model,