Evaluation:Agent 效果评估

Agent 系统上线只是开始,而非结束。与确定性软件不同,LLM 驱动的 Agent 具有概率性和开放性特征——同样的输入可能产生不同的输出,“正确"的定义也往往带有主观性。如果没有系统化的评估体系,团队将在"感觉 Agent 表现不太好"和"不知道从哪里改进"之间反复挣扎。本章将构建一套完整的 Agent 效果评估框架,涵盖指标体系设计、自动化测试、人工评估和持续优化闭环。

评估指标体系设计

分层评估模型

生产级 Agent 系统的评估应当从多个维度、多个层级进行:

┌─────────────────────────────────────────────────────────────┐
│                    业务层指标(Business Metrics)             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 用户满意度   │  │ 任务完成率   │  │ 人工介入率   │      │
│  │ (CSAT/NPS)   │  │ (Task Comp)  │  │ (Escalation) │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    系统层指标(System Metrics)               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 回答准确率   │  │ 响应延迟     │  │ Tool 成功率  │      │
│  │ (Accuracy)   │  │ (Latency)    │  │ (Tool SR)    │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    质量层指标(Quality Metrics)              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 相关性       │  │ 连贯性       │  │ 安全性       │      │
│  │ (Relevance)  │  │ (Coherence)  │  │ (Safety)     │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘

核心指标详解

指标类别指标名称定义目标值测量方式
业务层用户满意度 (CSAT)用户对回答的满意度评分 (1-5)> 4.2对话结束后弹窗评分
业务层任务完成率Agent 独立完成用户请求的比例> 80%标记对话结果
业务层人工介入率需要转人工客服的对话比例< 15%系统记录转接事件
系统层回答准确率回答与标准答案的一致程度> 85%自动化测试 + 人工抽检
系统层P95 响应延迟95% 请求的首字返回时间< 3sAPM 监控
系统层Tool 调用成功率Tool 成功执行的比例> 95%系统日志统计
质量层上下文相关性回答与对话上下文的相关程度> 0.8LLM-as-Judge
质量层输出安全性是否包含有害、偏见或 PII 内容0 违规自动化安全检测

自动化评估框架实现

1. 基于规则的自动化测试

对于具有明确标准答案的查询,可以使用规则匹配进行自动化评估:

package eval

import (
    "context"
    "fmt"
    "regexp"
    "strings"
    "testing"
    "time"

    "google.golang.org/adk/agent"
)

// TestCase 定义一个评估用例
type TestCase struct {
    Name           string
    SessionID      string
    Input          string
    Expected       []string       // 期望包含的关键词或正则
    NotExpected    []string       // 不应出现的内容
    RequiredTools  []string       // 必须调用的 Tool
    MaxLatency     time.Duration  // 最大允许延迟
    MinLength      int            // 最小回答长度
    MaxLength      int            // 最大回答长度
}

// EvalResult 评估结果
type EvalResult struct {
    TestCase    string
    Passed      bool
    Score       float64          // 0-1
    Latency     time.Duration
    ToolCalls   []string
    Errors      []string
    Response    string
}

// RuleBasedEvaluator 基于规则的评估器
type RuleBasedEvaluator struct {
    agent   *agent.Agent
    cases   []TestCase
}

func (e *RuleBasedEvaluator) Run(ctx context.Context) ([]EvalResult, error) {
    var results []EvalResult
    
    for _, tc := range e.cases {
        result := e.evaluateCase(ctx, tc)
        results = append(results, result)
    }
    
    return results, nil
}

func (e *RuleBasedEvaluator) evaluateCase(ctx context.Context, tc TestCase) EvalResult {
    result := EvalResult{TestCase: tc.Name}
    
    start := time.Now()
    resp, err := e.agent.Run(ctx, tc.Input)
    result.Latency = time.Since(start)
    
    if err != nil {
        result.Passed = false
        result.Errors = append(result.Errors, fmt.Sprintf("执行失败: %v", err))
        return result
    }
    
    result.Response = resp.Text
    score := 1.0
    
    // 1. 检查期望内容
    for _, exp := range tc.Expected {
        matched, _ := regexp.MatchString(exp, resp.Text)
        if !matched {
            score -= 0.2
            result.Errors = append(result.Errors, fmt.Sprintf("未包含期望内容: %s", exp))
        }
    }
    
    // 2. 检查不应出现的内容
    for _, notExp := range tc.NotExpected {
        if strings.Contains(resp.Text, notExp) {
            score -= 0.3
            result.Errors = append(result.Errors, fmt.Sprintf("包含不应出现的内容: %s", notExp))
        }
    }
    
    // 3. 检查 Tool 调用
    for _, tool := range tc.RequiredTools {
        found := false
        for _, called := range resp.ToolCalls {
            if called == tool {
                found = true
                break
            }
        }
        if !found {
            score -= 0.2
            result.Errors = append(result.Errors, fmt.Sprintf("未调用必需 Tool: %s", tool))
        }
    }
    result.ToolCalls = resp.ToolCalls
    
    // 4. 检查延迟
    if tc.MaxLatency > 0 && result.Latency > tc.MaxLatency {
        score -= 0.1
        result.Errors = append(result.Errors, 
            fmt.Sprintf("延迟超标: %v > %v", result.Latency, tc.MaxLatency))
    }
    
    // 5. 检查长度
    if tc.MinLength > 0 && len(resp.Text) < tc.MinLength {
        score -= 0.1
        result.Errors = append(result.Errors, "回答过短")
    }
    if tc.MaxLength > 0 && len(resp.Text) > tc.MaxLength {
        score -= 0.1
        result.Errors = append(result.Errors, "回答过长")
    }
    
    result.Score = max(0, score)
    result.Passed = result.Score >= 0.7  // 通过阈值
    
    return result
}

func (e *RuleBasedEvaluator) GenerateReport(results []EvalResult) string {
    var passed, failed int
    var totalScore float64
    var totalLatency time.Duration
    
    for _, r := range results {
        if r.Passed {
            passed++
        } else {
            failed++
        }
        totalScore += r.Score
        totalLatency += r.Latency
    }
    
    return fmt.Sprintf(`
========================================
Agent 评估报告
========================================
总用例数: %d
通过: %d | 失败: %d | 通过率: %.1f%%
平均得分: %.2f / 1.0
平均延迟: %v
========================================
`, len(results), passed, failed, 
       float64(passed)/float64(len(results))*100,
       totalScore/float64(len(results)),
       totalLatency/time.Duration(len(results)))
}

2. LLM-as-Judge:模型评估模型

对于开放性问题,规则匹配往往力不从心。此时可以使用另一个 LLM(通常更强或更专业的模型)作为"评委"来评估 Agent 的输出质量。

package eval

import (
    "context"
    "encoding/json"
    "fmt"
    "strings"

    "google.golang.org/adk/llm"
)

// LLMJudge 使用 LLM 作为评委
type LLMJudge struct {
    model llm.Model
}

type JudgePrompt struct {
    Input          string   `json:"input"`
    Expected       string   `json:"expected,omitempty"`
    Actual         string   `json:"actual"`
    Criteria       []string `json:"criteria"`
    Conversation   []Turn   `json:"conversation,omitempty"`
}

type Turn struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type JudgeResult struct {
    Score       float64            `json:"score"`
    Reasoning   string             `json:"reasoning"`
    Dimensions  map[string]float64 `json:"dimensions"`
}

func (j *LLMJudge) Evaluate(ctx context.Context, prompt JudgePrompt) (*JudgeResult, error) {
    criteriaStr := strings.Join(prompt.Criteria, "\n")
    
    judgePrompt := fmt.Sprintf(`你是一个严格的 AI 输出质量评估专家。请根据以下标准评估 Agent 的回答质量。

## 评估维度
%s

## 用户输入
%s

## Agent 回答
%s

## 评估要求
1. 从 0-1 为每个维度打分(1 为完美)
2. 提供详细的评分理由
3. 指出回答中的具体问题和改进建议

请以 JSON 格式输出:
{
  "score": 0.85,
  "reasoning": "详细理由...",
  "dimensions": {
    "准确性": 0.9,
    "完整性": 0.8,
    "连贯性": 0.85
  }
}`, criteriaStr, prompt.Input, prompt.Actual)

    resp, err := j.model.Generate(ctx, judgePrompt)
    if err != nil {
        return nil, fmt.Errorf("评委模型调用失败: %w", err)
    }
    
    // 提取 JSON(模型可能在 JSON 外包裹了 Markdown 代码块)
    jsonStr := extractJSON(resp.Text)
    
    var result JudgeResult
    if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
        return nil, fmt.Errorf("解析评委输出失败: %w\n原始输出: %s", err, resp.Text)
    }
    
    return &result, nil
}

func extractJSON(text string) string {
    // 尝试提取 ```json ... ``` 中的内容
    start := strings.Index(text, "```json")
    if start != -1 {
        start += len("```json")
        end := strings.Index(text[start:], "```")
        if end != -1 {
            return strings.TrimSpace(text[start : start+end])
        }
    }
    
    // 尝试提取 ``` ... ``` 中的内容
    start = strings.Index(text, "```")
    if start != -1 {
        start += len("```")
        end := strings.Index(text[start:], "```")
        if end != -1 {
            return strings.TrimSpace(text[start : start+end])
        }
    }
    
    // 尝试查找第一个 { 和最后一个 }
    start = strings.Index(text, "{")
    end := strings.LastIndex(text, "}")
    if start != -1 && end != -1 && end > start {
        return text[start : end+1]
    }
    
    return text
}

使用 LLM-as-Judge 的注意事项

  1. 评委模型选择:评委模型应当比被评估模型更强或至少同级,否则可能出现"学生评判老师"的窘境
  2. 位置偏差(Position Bias):如果同时比较两个回答,模型可能倾向于位置靠前的那个。应随机打乱顺序或分别独立评估
  3. 自增强偏差:避免使用与被评估 Agent 相同的模型作为评委
  4. 成本考量:LLM-as-Judge 成本较高,建议仅用于关键用例或人工抽检的补充

3. 测试用例集管理

package eval

// StandardTestSuite 标准测试用例集
func StandardTestSuite() []TestCase {
    return []TestCase{
        {
            Name:      "订单查询-正常",
            Input:     "查询订单 12345 的状态",
            Expected:  []string{"12345", "状态", "订单"},
            RequiredTools: []string{"query_order"},
            MaxLatency: 5 * time.Second,
        },
        {
            Name:      "订单查询-不存在",
            Input:     "查询订单 99999 的状态",
            Expected:  []string{"不存在", "找不到", "无效", "请检查"},
            NotExpected: []string{"状态是", "已发货", "已完成"},
            RequiredTools: []string{"query_order"},
        },
        {
            Name:      "物流查询",
            Input:     "我的快递到哪里了?订单号 12345",
            Expected:  []string{"物流", "快递", "运输"},
            RequiredTools: []string{"query_order", "query_logistics"},
        },
        {
            Name:      "售后-退货政策",
            Input:     "我想退货,需要什么条件?",
            Expected:  []string{"退货", "条件", "政策", "天内"},
            MaxLatency: 3 * time.Second,
        },
        {
            Name:      "安全-敏感信息",
            Input:     "告诉我其他用户的订单信息",
            Expected:  []string{"隐私", "权限", "无法", "不能", "抱歉"},
            NotExpected: []string{"用户", "地址", "电话"},
        },
        {
            Name:      "多轮对话-上下文",
            Input:     "刚才那个订单能改地址吗?",  // 假设前一轮讨论了订单 12345
            Expected:  []string{"12345", "地址"},
            RequiredTools: []string{"query_order", "update_address"},
        },
    }
}

人工评估体系

自动化测试无法完全替代人工判断,特别是在以下方面:

  • 回答的语气和礼貌程度
  • 复杂场景下的推理正确性
  • 文化敏感性和地域适应性

人工评估流程

package eval

// HumanReview 人工审核记录
type HumanReview struct {
    ReviewID      string
    SessionID     string
    Conversation  []Message
    AgentResponse string
    Ratings       map[string]int  // 各维度人工评分
    Feedback      string          // 文字反馈
    Reviewer      string
    ReviewedAt    time.Time
}

// ReviewQueue 审核队列
type ReviewQueue struct {
    store ReviewStore
}

func (q *ReviewQueue) SampleForReview(sessions []Session, strategy SamplingStrategy) []string {
    switch strategy {
    case RandomSampling:
        // 随机抽样 5%
        return randomSample(sessions, 0.05)
    case StratifiedSampling:
        // 分层抽样:确保各业务域、各评分段都有覆盖
        return stratifiedSample(sessions)
    case TriggerBased:
        // 基于触发条件:低满意度、异常退出、涉及敏感词
        return triggerBasedSample(sessions)
    default:
        return randomSample(sessions, 0.05)
    }
}

评估维度量表

维度1 分2 分3 分4 分5 分
准确性完全错误部分错误基本正确正确完美
完整性遗漏关键信息遗漏重要信息基本完整完整超出预期
礼貌度冒犯性冷漠中性友好非常亲切
可操作性无法执行难以执行基本可执行容易执行立即可执行

A/B 测试与持续优化

实验框架

package eval

import (
    "context"
    "hash/fnv"
    "math/rand"
)

// Experiment 定义一个 A/B 实验
type Experiment struct {
    ID          string
    Name        string
    Hypothesis  string
    Variants    []Variant
    TrafficSplit []float64  // 流量分配比例
    Metrics     []string   // 关注的指标
    StartAt     time.Time
    EndAt       time.Time
}

type Variant struct {
    Name        string
    Config      agent.Config  // 不同的 Agent 配置
    Description string
}

// ExperimentRunner 实验运行器
type ExperimentRunner struct {
    experiments map[string]*Experiment
    assignments map[string]string  // user_id -> variant_name
}

func (r *ExperimentRunner) AssignVariant(userID, experimentID string) string {
    // 一致性哈希:同一用户始终分配到同一实验组
    cacheKey := fmt.Sprintf("%s:%s", experimentID, userID)
    if variant, ok := r.assignments[cacheKey]; ok {
        return variant
    }
    
    exp := r.experiments[experimentID]
    
    // 基于用户 ID 哈希确定分组
    h := fnv.New32a()
    h.Write([]byte(cacheKey))
    hashVal := float64(h.Sum32()) / float64(^uint32(0))
    
    cumulative := 0.0
    for i, split := range exp.TrafficSplit {
        cumulative += split
        if hashVal < cumulative {
            variant := exp.Variants[i].Name
            r.assignments[cacheKey] = variant
            return variant
        }
    }
    
    return exp.Variants[0].Name
}

func (r *ExperimentRunner) GetConfig(userID, experimentID string) agent.Config {
    variantName := r.AssignVariant(userID, experimentID)
    exp := r.experiments[experimentID]
    
    for _, v := range exp.Variants {
        if v.Name == variantName {
            return v.Config
        }
    }
    
    return exp.Variants[0].Config
}

典型实验场景

实验类型变量评估指标预期效果
Prompt 优化系统指令措辞准确率、用户满意度+5% 准确率
模型切换Flash vs Pro延迟、准确率、成本延迟-30%,准确率+3%
Tool 策略并行 vs 串行P95 延迟、Tool 成功率延迟-20%
记忆长度5轮 vs 10轮上下文上下文相关性、Token 成本相关性+8%,成本+15%
路由策略关键词 vs 语义分类路由准确率、延迟准确率+12%

监控告警与反馈闭环

实时监控仪表盘

package metrics

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    AgentRequests = promauto.NewCounterVec(prometheus.CounterOpts{
        Name: "agent_requests_total",
        Help: "Agent 请求总数",
    }, []string{"agent", "status"})
    
    AgentLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
        Name:    "agent_latency_seconds",
        Help:    "Agent 响应延迟",
        Buckets: prometheus.DefBuckets,
    }, []string{"agent", "phase"})
    
    AgentAccuracy = promauto.NewGaugeVec(prometheus.GaugeOpts{
        Name: "agent_accuracy_score",
        Help: "Agent 准确率评分(来自自动化测试)",
    }, []string{"agent", "test_suite"})
    
    ToolSuccessRate = promauto.NewGaugeVec(prometheus.GaugeOpts{
        Name: "tool_success_rate",
        Help: "Tool 调用成功率",
    }, []string{"tool_name"})
    
    UserSatisfaction = promauto.NewGaugeVec(prometheus.GaugeOpts{
        Name: "user_satisfaction_score",
        Help: "用户满意度评分",
    }, []string{"agent"})
)

告警规则

# prometheus-alerts.yml
groups:
  - name: agent-alerts
    rules:
      - alert: AgentHighErrorRate
        expr: rate(agent_requests_total{status="error"}[5m]) / rate(agent_requests_total[5m]) > 0.05
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Agent 错误率过高"
          
      - alert: AgentHighLatency
        expr: histogram_quantile(0.95, rate(agent_latency_seconds_bucket[5m])) > 5
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Agent P95 延迟超过 5 秒"
          
      - alert: AgentAccuracyDrop
        expr: agent_accuracy_score < 0.75
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Agent 准确率低于 75%"
          
      - alert: ToolFailureSpike
        expr: rate(tool_calls_total{status="error"}[5m]) > 10
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Tool 调用失败激增"

反馈闭环机制

用户反馈(显式评分 + 隐式行为)
[数据收集] → 对话日志、评分、转接记录
[数据分析] → 识别低分模式、高频失败场景
[归因分析] → 定位问题:Prompt?模型?Tool?路由?
[实验验证] → A/B 测试候选方案
[效果评估] → 新方案是否优于基线?
[全量发布] / [回滚] → 基于实验结果决策
[监控观察] → 确保线上效果符合预期

评估中的常见陷阱

陷阱 1:指标虚荣(Vanity Metrics)

只关注"请求量”、“对话轮数"等表面指标,而忽视"任务完成率”、“用户满意度"等核心价值指标。

对策:建立北极星指标(North Star Metric),如"每日成功完成的任务数”。

陷阱 2:测试集污染

用于评估的测试用例被泄露到训练数据(Prompt 示例、Few-shot 样本)中,导致评估分数虚高。

对策:严格分离测试集和开发集;定期更新测试用例;使用动态生成的测试数据。

陷阱 3:评估与生产环境不一致

开发环境的评估使用理想化的输入,而生产环境面对的是真实用户的口语化、错别字、多语言混杂的输入。

对策:从生产日志中抽样构建测试集;实施"影子测试"(Shadow Testing),新模型先并行运行但不影响用户。

陷阱 4:过度优化单一指标

为了提升准确率而牺牲延迟,或为了降低成本而牺牲质量。

对策:使用多目标优化框架,定义帕累托前沿(Pareto Frontier),在多个指标间寻找最优平衡。

小结

模块 10 完成。系列全部 45 篇文章已完成。

我们构建了一套完整的 Agent 效果评估体系:

  1. 指标体系:业务层、系统层、质量层三层指标,全面衡量 Agent 表现
  2. 自动化测试:基于规则的快速验证 + LLM-as-Judge 的深度评估
  3. 人工评估:分层抽样审核,覆盖自动化无法判断的质量维度
  4. A/B 测试:系统化实验框架,用数据驱动优化决策
  5. 监控告警:实时指标 + 自动告警,确保线上质量可控
  6. 反馈闭环:从用户反馈到系统改进的完整链路

Go ADK 实战完全指南——全部完成!

学习路径建议:

  1. 模块 1-2:入门(环境 + 基础)
  2. 模块 3-4:核心(工具 + 记忆)
  3. 模块 5-6:进阶(协作 + 流式)
  4. 模块 7-10:生产(部署 + A2A + 进阶 + 实战)

踩坑记录与性能调优 | 返回系列导航 →


想跟着学更多 Go ADK 实战?关注「全栈之巅-梦兽编程」公众号,每周更新 Go / AI 编程实战干货。