Score the Russian-to-keywords rewrite end to end (#403)
Same 9 cases as the retrieval eval, so the numbers compare directly: hand-written keywords hit 8 of 8, this is what the model reaches on its own. Reports the hand-written query next to the model's for every case, because where the phrasing differs is the useful part. Opt-in on MAVEN_KIWIX_URL + MAVEN_LLM_URL, like the other evals. Result on Qwen3.5-0.8B: 3 of 8, identical on all three runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
package kiwix
|
||||||
|
|
||||||
|
// End-to-end score: Russian question -> model rewrite -> Kiwix search -> did a
|
||||||
|
// wanted article come back. Same 9 cases as the retrieval eval, so the two
|
||||||
|
// numbers are directly comparable: retrieval with hand-written keywords is the
|
||||||
|
// ceiling, this is what the model actually reaches.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RewriteOutcome — one case, end to end.
|
||||||
|
type RewriteOutcome struct {
|
||||||
|
Outcome
|
||||||
|
ModelQuery string // what the model asked for ("" if it failed)
|
||||||
|
RewriteErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunRewriteEval rewrites every question with the model, then searches.
|
||||||
|
func RunRewriteEval(ctx context.Context, c *Client, rw *Rewriter, topN int) (RewriteReport, error) {
|
||||||
|
var f fixture
|
||||||
|
if err := json.Unmarshal(knowledgeFixtureJSON, &f); err != nil {
|
||||||
|
return RewriteReport{}, err
|
||||||
|
}
|
||||||
|
rep := RewriteReport{Report: Report{Name: f.Name + "-rewrite", Book: f.Book, TopN: topN}}
|
||||||
|
for _, cs := range f.Cases {
|
||||||
|
out := RewriteOutcome{Outcome: Outcome{Case: cs}}
|
||||||
|
q, err := rw.Rewrite(ctx, cs.Question)
|
||||||
|
out.ModelQuery, out.RewriteErr = q, err
|
||||||
|
if err == nil {
|
||||||
|
res, serr := c.Search(ctx, q, f.Book, topN)
|
||||||
|
out.Err = serr
|
||||||
|
for i, hit := range res {
|
||||||
|
out.Titles = append(out.Titles, hit.Title)
|
||||||
|
if out.Rank == 0 && matches(cs.WantTitles, hit.Title) {
|
||||||
|
out.Rank = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.RewriteErr != nil || out.Err != nil {
|
||||||
|
rep.Errors++
|
||||||
|
}
|
||||||
|
if !cs.ExpectMiss {
|
||||||
|
rep.Scored++
|
||||||
|
if out.Hit() {
|
||||||
|
rep.Hits++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rep.Cases = append(rep.Cases, out)
|
||||||
|
}
|
||||||
|
return rep, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RewriteReport — the score plus per-case detail.
|
||||||
|
type RewriteReport struct {
|
||||||
|
Report
|
||||||
|
Cases []RewriteOutcome
|
||||||
|
}
|
||||||
|
|
||||||
|
// String — the headline number.
|
||||||
|
func (r RewriteReport) String() string {
|
||||||
|
return fmt.Sprintf("%s: %d/%d answerable questions retrieve a wanted article in top %d (%.1f%%), %d errors\n book: %s\n",
|
||||||
|
r.Name, r.Hits, r.Scored, r.TopN, 100*r.Accuracy(), r.Errors, r.Book)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail — per case: hand-written query next to the model's, and what came back.
|
||||||
|
// The point is seeing WHERE the model's phrasing differs, not just the score.
|
||||||
|
func (r RewriteReport) Detail() string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, o := range r.Cases {
|
||||||
|
mark := "MISS"
|
||||||
|
switch {
|
||||||
|
case o.Case.ExpectMiss:
|
||||||
|
mark = "n/a "
|
||||||
|
case o.Hit():
|
||||||
|
mark = fmt.Sprintf("hit@%d", o.Rank)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, " %-6s %-20s\n", mark, o.Case.ID)
|
||||||
|
fmt.Fprintf(&b, " asked: %s\n", o.Case.Question)
|
||||||
|
fmt.Fprintf(&b, " hand: %q\n", o.Case.Query)
|
||||||
|
fmt.Fprintf(&b, " model: %q\n", o.ModelQuery)
|
||||||
|
if o.RewriteErr != nil {
|
||||||
|
fmt.Fprintf(&b, " rewrite rejected: %v\n", o.RewriteErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if o.Err != nil {
|
||||||
|
fmt.Fprintf(&b, " search error: %v\n", o.Err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, " got: %s\n", strings.Join(o.Titles, " | "))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package kiwix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Opt-in: needs a live Kiwix server AND a live llama-server.
|
||||||
|
// MAVEN_KIWIX_URL=http://127.0.0.1:8034 MAVEN_LLM_URL=http://127.0.0.1:18099 \
|
||||||
|
//
|
||||||
|
// no_proxy=127.0.0.1,localhost go test -run RewriteEval -v ./internal/kiwix/
|
||||||
|
func TestRewriteEval(t *testing.T) {
|
||||||
|
kbase, lbase := os.Getenv("MAVEN_KIWIX_URL"), os.Getenv("MAVEN_LLM_URL")
|
||||||
|
if kbase == "" || lbase == "" {
|
||||||
|
t.Skip("set MAVEN_KIWIX_URL and MAVEN_LLM_URL to run the rewrite eval")
|
||||||
|
}
|
||||||
|
noProxyLoopback(t)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
rw := NewRewriter(llm.New(lbase, 3*time.Minute))
|
||||||
|
rep, err := RunRewriteEval(ctx, New(kbase), rw, 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("eval: %v", err)
|
||||||
|
}
|
||||||
|
// No pass bar on purpose: the number is the finding.
|
||||||
|
t.Log("\n" + rep.String() + rep.Detail())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user