5fb6e3ffa8
诊断:旧评测 LLM judge 恒给 0.94/1.00、从不判 poor → 低分自动纠偏闭环几乎从没 启动。根因两层: 1. 提示词软:只说"严格"但无评分基准、不强制挑毛病 → 模型恒锚定 4–5。 2. 数学更致命:归一 score/5 下限 0.2,叠加无来源 Overall=0.4*rule+0.6*s 的 0.4*rule≈0.4 底 → Overall 恒 ≥0.52、poor(<0.5)对"流畅但跑题/错误"永不可达。 修复: - judge 提示词改对抗性+rubric:默认怀疑、先点缺陷再打分、给死 1–5 评分基准、 要求用满区间;grounded judge 忠实度按编造说法递减(一处编造≤2)。 - 归一 score/5 → normJudge=(v-1)/4(1→0),让低质能压到 poor 触发纠偏。 - 测试:normJudge 区间 + 流畅跑题(judge=1)可达 poor + 好坏区分度;更新两处旧 断言(4→0.75, 2→0.25)。 live 验证(deepseek):跑题→0.40 poor→自动纠偏(0.40→0.55);截断→0.55 warn; 好答案→1.00 ok。校准前三者全 1.00。评测+纠偏的投入由"摆设"变"在用"。 DEPTH_ROADMAP T0.1 ✅。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
6.6 KiB
Go
171 lines
6.6 KiB
Go
package harness
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestRuleScore(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
output string
|
||
wantMax float64 // 期望分 ≤ 此值
|
||
wantMin float64 // 期望分 ≥ 此值
|
||
wantFlag string // 期望命中的标签(空=不校验)
|
||
}{
|
||
{"正常", "杭州是浙江省会,历史悠久,有西湖等名胜,是著名的旅游与电商之城。", 1.0, 1.0, ""},
|
||
{"空", " ", 0, 0, "空输出"},
|
||
{"过短", "好的", 0.7, 0, "输出过短"},
|
||
{"拒答", "抱歉,我无法回答这个问题,因为信息不足,请谅解理解支持。", 0.8, 0, "疑似拒答/错误"},
|
||
}
|
||
for _, c := range cases {
|
||
s, flags := ruleScore(c.output)
|
||
if s > c.wantMax+1e-9 || s < c.wantMin-1e-9 {
|
||
t.Errorf("%s: 分 %.2f 不在 [%.2f,%.2f]", c.name, s, c.wantMin, c.wantMax)
|
||
}
|
||
if c.wantFlag != "" && !contains(flags, c.wantFlag) {
|
||
t.Errorf("%s: 期望命中标签 %q, got %v", c.name, c.wantFlag, flags)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRuleScore_HeavyRepeat(t *testing.T) {
|
||
out := strings.Repeat("这是一句会重复很多次的废话\n", 4)
|
||
_, flags := ruleScore(out)
|
||
if !contains(flags, "重复啰嗦") {
|
||
t.Errorf("应命中重复啰嗦, got %v", flags)
|
||
}
|
||
}
|
||
|
||
func TestScore_RuleOnly(t *testing.T) {
|
||
e := NewEvaluator(nil, nil) // 无 LLM → 仅规则
|
||
r := e.Score(context.Background(), "问题", "一段质量不错的较完整回答内容,长度足够,没有任何问题。", nil)
|
||
if r.LLM != 0 || r.Overall != r.Rule {
|
||
t.Errorf("无 LLM 时 Overall 应等于规则分, got overall=%.2f rule=%.2f llm=%.2f", r.Overall, r.Rule, r.LLM)
|
||
}
|
||
}
|
||
|
||
func TestScore_WithLLMJudge(t *testing.T) {
|
||
// 注入假评审:返回带围栏的 JSON,验证解析 + 归一化 + 综合权重。
|
||
e := NewEvaluator(
|
||
func() bool { return true },
|
||
func(ctx context.Context, sys, user string) (string, error) {
|
||
return "```json\n{\"score\":4,\"reason\":\"相关且较完整\"}\n```", nil
|
||
},
|
||
)
|
||
r := e.Score(context.Background(), "介绍杭州", "杭州是浙江省会,西湖闻名,历史与现代交融,电商发达。", nil)
|
||
if r.LLM <= 0 {
|
||
t.Fatalf("应有 LLM 分, got %.2f", r.LLM)
|
||
}
|
||
if r.LLM < 0.74 || r.LLM > 0.76 { // 校准版归一 normJudge(4)=(4-1)/4=0.75
|
||
t.Errorf("LLM 分应归一化为 0.75, got %.2f", r.LLM)
|
||
}
|
||
if r.Reason != "相关且较完整" {
|
||
t.Errorf("应解析出 reason, got %q", r.Reason)
|
||
}
|
||
want := 0.4*r.Rule + 0.6*r.LLM
|
||
if r.Overall < want-1e-9 || r.Overall > want+1e-9 {
|
||
t.Errorf("综合分应为 0.4*规则+0.6*LLM=%.3f, got %.3f", want, r.Overall)
|
||
}
|
||
}
|
||
|
||
// TestNormJudge 钉死校准归一:1→0(最低,让低质能压到 poor)、3→0.5、5→1.0,用满区间。
|
||
func TestNormJudge(t *testing.T) {
|
||
cases := []struct {
|
||
v, want float64
|
||
}{{1, 0}, {2, 0.25}, {3, 0.5}, {4, 0.75}, {5, 1.0}, {0, 0}, {6, 1.0}}
|
||
for _, c := range cases {
|
||
if got := normJudge(c.v); got < c.want-1e-9 || got > c.want+1e-9 {
|
||
t.Errorf("normJudge(%.0f)=%.3f, want %.3f", c.v, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestScore_PoorReachable 钉死校准核心:流畅但跑题的回答(judge 给 1)也能压到 poor 区间(<0.5)。
|
||
// 旧归一(score/5 下限 0.2 + 0.4*rule 底)会让 Overall 恒 ≥0.52、poor 永不可达 → 纠偏闭环空转。
|
||
func TestScore_PoorReachable(t *testing.T) {
|
||
judge := func(score int) *Evaluator {
|
||
return NewEvaluator(func() bool { return true },
|
||
func(_ context.Context, _, _ string) (string, error) {
|
||
return fmt.Sprintf(`{"score":%d,"reason":"x"}`, score), nil
|
||
})
|
||
}
|
||
fluentOffTopic := "这是一段流畅、完整、看起来很认真的回答,但其实完全没有回答用户的问题。"
|
||
// judge 判 1(跑题)→ Overall 应落入 poor 区间(<0.5),纠偏闭环才会触发。
|
||
if r := judge(1).Score(context.Background(), "问题", fluentOffTopic, nil); r.Overall >= 0.5 {
|
||
t.Fatalf("流畅但跑题(judge=1)应可达 poor(<0.5),got Overall=%.3f(恒温器空转的根因)", r.Overall)
|
||
}
|
||
// 区分度:好答案(judge=5)应明显高于差答案(judge=1)。
|
||
good := judge(5).Score(context.Background(), "问题", fluentOffTopic, nil).Overall
|
||
bad := judge(1).Score(context.Background(), "问题", fluentOffTopic, nil).Overall
|
||
if good <= bad+0.3 {
|
||
t.Fatalf("好坏答案区分度不足: good=%.3f bad=%.3f", good, bad)
|
||
}
|
||
}
|
||
|
||
func TestScore_LLMJudgeBadJSONFallsBack(t *testing.T) {
|
||
e := NewEvaluator(
|
||
func() bool { return true },
|
||
func(ctx context.Context, sys, user string) (string, error) { return "我觉得还行吧", nil },
|
||
)
|
||
r := e.Score(context.Background(), "q", "一段足够长且正常的回答内容用于评测。", nil)
|
||
if r.LLM != 0 || r.Overall != r.Rule {
|
||
t.Errorf("LLM 返回非 JSON 应回退到规则分, got overall=%.2f llm=%.2f", r.Overall, r.LLM)
|
||
}
|
||
}
|
||
|
||
func TestScore_GroundedFaithfulness(t *testing.T) {
|
||
// 有检索来源 → 走忠实度评测:judge 返回 quality/faithfulness/unsupported。
|
||
var gotPrompt string
|
||
e := NewEvaluator(
|
||
func() bool { return true },
|
||
func(ctx context.Context, sys, user string) (string, error) {
|
||
gotPrompt = user
|
||
return `{"quality":4,"faithfulness":2,"unsupported":["该产品支持离线模式"],"reason":"部分说法无资料支撑"}`, nil
|
||
},
|
||
)
|
||
r := e.Score(context.Background(), "这产品支持什么", "它支持在线与离线模式。",
|
||
[]string{"产品支持在线协作。", "产品提供云端存储。"})
|
||
|
||
if !strings.Contains(gotPrompt, "检索资料") || !strings.Contains(gotPrompt, "产品支持在线协作") {
|
||
t.Fatalf("judge 提示词应包含检索资料, got: %q", gotPrompt)
|
||
}
|
||
if r.LLM < 0.74 || r.LLM > 0.76 { // 校准归一 quality 4 → normJudge(4)=0.75
|
||
t.Errorf("quality 应为 0.75, got %.2f", r.LLM)
|
||
}
|
||
if r.Faithful < 0.24 || r.Faithful > 0.26 { // 校准归一 faithfulness 2 → normJudge(2)=0.25
|
||
t.Errorf("忠实度应为 0.25, got %.2f", r.Faithful)
|
||
}
|
||
if !contains(r.Flags, "未被来源支持:该产品支持离线模式") {
|
||
t.Errorf("未支持说法应进 Flags, got %v", r.Flags)
|
||
}
|
||
want := 0.3*r.Rule + 0.35*r.LLM + 0.35*r.Faithful
|
||
if r.Overall < want-1e-9 || r.Overall > want+1e-9 {
|
||
t.Errorf("综合分应为 0.3规则+0.35质量+0.35忠实=%.3f, got %.3f", want, r.Overall)
|
||
}
|
||
}
|
||
|
||
func TestScore_NoSourcesSkipsFaithfulness(t *testing.T) {
|
||
e := NewEvaluator(
|
||
func() bool { return true },
|
||
func(ctx context.Context, sys, user string) (string, error) {
|
||
return `{"score":5,"reason":"好"}`, nil
|
||
},
|
||
)
|
||
r := e.Score(context.Background(), "q", "一段足够长且正常的回答内容用于评测。", nil)
|
||
if r.Faithful != 0 {
|
||
t.Errorf("无来源不应评忠实度, got %.2f", r.Faithful)
|
||
}
|
||
}
|
||
|
||
func contains(ss []string, want string) bool {
|
||
for _, s := range ss {
|
||
if s == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|