Files
Agent-Intent/intent/corpus_test.go
T
weichen db79b535ec
ci / vet-and-test (pull_request) Successful in 16s
ci / docker-checkout-hostgateway (pull_request) Successful in 0s
Add precision and recall metrics to the multi-turn evaluation
Why: I2 acceptance - the evaluator must emit metrics over a
multiturn fixture no smaller than the existing corpus, not just
print branch names.

Evidence: go vet clean; full suite ok; intentctl eval prints
precision=1.000 recall=1.000 (TP=12 FP=0 FN=0) over the 11-case
multiturn fixture; MetricsOf unit test pins TP/FP/FN counting
including wrong-match counting as both.

Impact: CorpusMetrics + MetricsOf (expectation_hit counts as a
hit); intentctl eval prints the metrics line after the summary;
corpus_multiturn.json expanded to 11 cases (chained expectation,
tie-break, ambiguous, fallback, handler action, clarify paths).

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I2
2026-10-07 11:02:28 +00:00

128 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package intent
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
)
// TestEvalMultiTurnCorpus 是 I1 验收载体:v2 多轮语料含「SetSlot 加 Start」
// 用例并通过(链式预期把第二轮转进 expectation_hit)。
func TestEvalMultiTurnCorpus(t *testing.T) {
data, err := os.ReadFile("testdata/corpus_multiturn.json")
if err != nil {
t.Fatal(err)
}
var doc struct {
Cases []CorpusCase `json:"cases"`
}
if err := json.Unmarshal(data, &doc); err != nil {
t.Fatal(err)
}
if len(doc.Cases) == 0 {
t.Fatal("empty corpus")
}
_, snap := buildDemoRegistry(t)
for _, tc := range doc.Cases {
result := RunCorpusCase(context.Background(), snap, tc)
if !result.Passed() {
for _, tr := range result.Turns {
for _, mm := range tr.Mismatches {
t.Errorf("case %s turn %d (%q): %s", tc.Name, tr.Index, tr.Input, mm)
}
}
}
}
}
// TestCorpusV1WrapsAsSingleTurn:v1 单轮形态经同一执行器等价运行(兼容承诺)。
func TestCorpusV1WrapsAsSingleTurn(t *testing.T) {
_, snap := buildDemoRegistry(t)
tc := CorpusCase{Name: "v1", Input: "你好呀", ExpectBranch: "matched", ExpectTop: "demo.greet.hello"}
result := RunCorpusCase(context.Background(), snap, tc)
if !result.Passed() {
t.Fatalf("v1 wrap: %+v", result.Turns)
}
if len(result.Turns) != 1 || result.Turns[0].Branch != "matched" {
t.Fatalf("turns = %+v", result.Turns)
}
}
// TestMatchCommandsFindsMismatches:命令断言的差异定位(kind/action/slot/name)。
func TestMatchCommandsFindsMismatches(t *testing.T) {
got := []Command{
{Kind: CommandStart, Action: ActionRef{Kind: ActionHandler, ID: "h"}, Slots: map[string]string{"topic": "电机"}},
}
mm := matchCommands([]CommandExpect{
{Kind: "start", ActionKind: ActionHandler, ActionID: "other"},
}, got)
if len(mm) != 1 || !strings.Contains(mm[0], "action.id") {
t.Fatalf("mm = %v", mm)
}
mm = matchCommands([]CommandExpect{
{Kind: "start", Slots: map[string]string{"topic": "水泵"}},
}, got)
if len(mm) != 1 || !strings.Contains(mm[0], "slots") {
t.Fatalf("mm = %v", mm)
}
mm = matchCommands([]CommandExpect{
{Kind: "start", ActionKind: ActionHandler, ActionID: "h", Slots: map[string]string{"topic": "电机"}},
}, got)
if len(mm) != 0 {
t.Fatalf("mm = %v", mm)
}
if mm := matchCommands([]CommandExpect{{Kind: "start"}, {Kind: "clarify"}}, got); len(mm) != 1 {
t.Fatalf("count mismatch = %v", mm)
}
}
// TestMetricsOfCounts:指标按意图标签计 TP/FP/FN(I2 验收的指标形态)。
func TestMetricsOfCounts(t *testing.T) {
_, snap := buildDemoRegistry(t)
data, err := os.ReadFile("testdata/corpus_multiturn.json")
if err != nil {
t.Fatal(err)
}
var doc struct {
Cases []CorpusCase `json:"cases"`
}
if err := json.Unmarshal(data, &doc); err != nil {
t.Fatal(err)
}
var results []CaseResult
for _, tc := range doc.Cases {
results = append(results, RunCorpusCase(context.Background(), snap, tc))
}
m := MetricsOf(doc.Cases, results)
// 通过的夹具:指标应当全 TP(precision=recall=1)。
if m.FP != 0 || m.FN != 0 {
t.Fatalf("metrics = %+v", m)
}
p, pok := m.Precision()
r, rok := m.Recall()
if !pok || !rok || p != 1.0 || r != 1.0 {
t.Fatalf("precision=%v recall=%v (m=%+v)", p, r, m)
}
if m.TP < 8 {
t.Fatalf("TP = %d, want >= 8(夹具内带 top 期望的轮次)", m.TP)
}
// 手工构造一个 FP+FN 用例验证计数方向。
bad := []CorpusCase{{Name: "bad", Turns: []CorpusTurn{
{Input: "查询任务 42 的进度", ExpectBranch: "matched", ExpectTop: "demo.greet.hello"},
}}}
got := []CaseResult{RunCorpusCase(context.Background(), snap, bad[0])}
m2 := MetricsOf(bad, got)
// 实际 matched status_query 但期望 greet.hello → 1 FP + 1 FN。
if m2.TP != 0 || m2.FP != 1 || m2.FN != 1 {
t.Fatalf("bad metrics = %+v", m2)
}
p2, _ := m2.Precision()
r2, _ := m2.Recall()
if p2 != 0 || r2 != 0 {
t.Fatalf("p=%v r=%v, want 0/0", p2, r2)
}
}