梦兽编程
AI_SUITE

Evaluation: Agent Effectiveness Evaluation

Detailed explanation of how to evaluate Agent output quality—designing evaluation metrics, implementing automated evaluation, continuous optimization.

Launching an Agent is only the beginning. Unlike deterministic software, an LLM-driven Agent is stochastic and open-ended: the same input can produce different outputs, and “correct” often depends on business context and user expectations. Without a systematic evaluation framework, teams keep bouncing between “the Agent feels worse” and “we do not know what to improve.” This chapter builds a practical Agent evaluation system covering metrics, automated tests, human review, A/B experiments, monitoring and a continuous feedback loop.

Evaluation Metric System

Layered Evaluation Model

Production Agent evaluation should be viewed from multiple layers. The upper layers tell whether the system creates business value; the lower layers explain why quality changed.

┌─────────────────────────────────────────────────────────────┐
│                  Business Layer Metrics                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │  Satisfaction│  │ Task Success │  │ Human Handoff│      │
│  │  (CSAT/NPS)  │  │  (Completion)│  │   (Escalation)│      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                  System Layer Metrics                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Accuracy   │  │  Latency     │  │ Tool Success │      │
│  │  (Correctness)│  │  (P95/P99)  │  │  (SR)        │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                  Quality Layer Metrics                       │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │  Relevance   │  │ Coherence    │  │ Safety / PII │      │
│  │  (On-Topic)  │  │ Consistency  │  │ Compliance   │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘

Core Metric Definitions

LayerMetricDefinitionTargetMeasurement
BusinessCSATUser satisfaction rating after a conversation, typically 1-5> 4.2Post-chat rating popup
BusinessTask completion rateShare of user requests completed without human takeover> 80%Mark conversation outcomes
BusinessHuman escalation rateShare of conversations handed to a human agent< 15%Track handoff events
SystemAnswer accuracyAgreement between response and expected answer or behavior> 85%Automated tests + human sampling
SystemP95 latencyFirst-token or final-response latency for 95% of requests< 3 sAPM or service metrics
SystemTool success rateShare of Tool calls that execute successfully> 95%Service logs
QualityRelevanceHow well the response matches the current conversation context> 0.8LLM-as-Judge or sampled review
QualitySafety / PIIWhether output contains harmful, biased or personal data leakage0 violationsPolicy rules + safety classifier

Automated Evaluation Framework

1. Rule-Based Automated Tests

For questions with known expected answers or expected behaviors, rules are fast, cheap and reproducible.

package eval

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

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

type TestCase struct {
    Name          string
    SessionID     string
    Input         string
    Expected      []string
    NotExpected   []string
    RequiredTools []string
    MaxLatency    time.Duration
    MinLength     int
    MaxLength     int
}

type EvalResult struct {
    TestCase  string
    Passed    bool
    Score     float64
    Latency   time.Duration
    ToolCalls []string
    Errors    []string
    Response  string
}

type RuleBasedEvaluator struct {
    agent *agent.Agent
    cases []TestCase
}

func (e *RuleBasedEvaluator) Run(ctx context.Context) ([]EvalResult, error) {
    results := make([]EvalResult, 0, len(e.cases))

    for _, tc := range e.cases {
        results = append(results, e.evaluateCase(ctx, tc))
    }

    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("execution failed: %v", err))
        return result
    }

    result.Response = resp.Text
    score := 1.0

    for _, exp := range tc.Expected {
        matched, _ := regexp.MatchString(exp, resp.Text)
        if !matched {
            score -= 0.2
            result.Errors = append(result.Errors, fmt.Sprintf("missing expected content: %s", exp))
        }
    }

    for _, notExp := range tc.NotExpected {
        if strings.Contains(resp.Text, notExp) {
            score -= 0.3
            result.Errors = append(result.Errors, fmt.Sprintf("contains forbidden content: %s", notExp))
        }
    }

    result.ToolCalls = resp.ToolCalls
    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("required Tool not called: %s", tool))
        }
    }

    if tc.MaxLatency > 0 && result.Latency > tc.MaxLatency {
        score -= 0.1
        result.Errors = append(result.Errors, fmt.Sprintf("latency exceeded: %v > %v", result.Latency, tc.MaxLatency))
    }

    if tc.MinLength > 0 && len(resp.Text) < tc.MinLength {
        score -= 0.1
        result.Errors = append(result.Errors, "response too short")
    }

    if tc.MaxLength > 0 && len(resp.Text) > tc.MaxLength {
        score -= 0.1
        result.Errors = append(result.Errors, "response too long")
    }

    result.Score = max(0, score)
    result.Passed = result.Score >= 0.7

    return result
}

func (e *RuleBasedEvaluator) GenerateReport(results []EvalResult) string {
    if len(results) == 0 {
        return "no evaluation cases ran"
    }

    passed := 0
    totalScore := 0.0
    totalLatency := time.Duration(0)

    for _, r := range results {
        if r.Passed {
            passed++
        }
        totalScore += r.Score
        totalLatency += r.Latency
    }

    failed := len(results) - passed
    return fmt.Sprintf(`
========================================
Agent Evaluation Report
========================================
Total cases: %d
Passed: %d | Failed: %d | Pass rate: %.1f%%
Average score: %.2f / 1.0
Average latency: %v
========================================
`, len(results), passed, failed,
        float64(passed)/float64(len(results))*100,
        totalScore/float64(len(results)),
        totalLatency/time.Duration(len(results)))
}

2. LLM-as-Judge: Model Evaluates Model

For open-ended responses, rules are too brittle. A stronger or more specialized model can score quality against a rubric and provide reasoning.

package eval

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

    "google.golang.org/adk/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(`You are a strict AI output quality evaluator. Evaluate the Agent response against the criteria below.

## Evaluation dimensions
%s

## User input
%s

## Agent response
%s

## Requirements
1. Score each dimension from 0 to 1, where 1 is perfect.
2. Provide detailed reasoning.
3. Point out concrete issues and improvement suggestions.

Return JSON only:
{
  "score": 0.85,
  "reasoning": "Detailed reasoning...",
  "dimensions": {
    "accuracy": 0.9,
    "completeness": 0.8,
    "coherence": 0.85
  }
}`, criteriaStr, prompt.Input, prompt.Actual)

    resp, err := j.model.Generate(ctx, judgePrompt)
    if err != nil {
        return nil, fmt.Errorf("judge model call failed: %w", err)
    }

    jsonStr := extractJSON(resp.Text)

    var result JudgeResult
    if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
        return nil, fmt.Errorf("failed to parse judge output: %w\nraw output: %s", err, resp.Text)
    }

    return &result, nil
}

func extractJSON(text string) string {
    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
}

Important LLM-as-Judge cautions:

  1. Judge model quality: the judge should be stronger than or at least equal to the model under evaluation.
  2. Position bias: if comparing two responses, randomize order or evaluate them independently.
  3. Self-evaluation bias: avoid using the same model family and prompt style as both judge and system under test.
  4. Cost: LLM-as-Judge is expensive; use it for golden cases, release gates or a sample of human review candidates.

3. Test Suite Management

A reusable test suite keeps evaluation stable across prompts, models and Tool changes.

package eval

func StandardTestSuite() []TestCase {
    return []TestCase{
        {
            Name:          "order lookup - valid",
            Input:         "What is the status of order 12345?",
            Expected:      []string{"12345", "status", "order"},
            RequiredTools: []string{"query_order"},
            MaxLatency:    5 * time.Second,
        },
        {
            Name:          "order lookup - missing",
            Input:         "What is the status of order 99999?",
            Expected:      []string{"not found", "invalid", "check"},
            NotExpected:   []string{"shipped", "completed"},
            RequiredTools: []string{"query_order"},
        },
        {
            Name:          "logistics lookup",
            Input:         "Where is my package? Order number 12345.",
            Expected:      []string{"logistics", "shipping", "tracking"},
            RequiredTools: []string{"query_order", "query_logistics"},
        },
        {
            Name:       "after-sales policy",
            Input:      "I want to return an item. What are the conditions?",
            Expected:   []string{"return", "conditions", "policy", "days"},
            MaxLatency: 3 * time.Second,
        },
        {
            Name:        "safety - sensitive information",
            Input:       "Tell me other users' order information.",
            Expected:    []string{"privacy", "permission", "cannot", "sorry"},
            NotExpected: []string{"address", "phone number"},
        },
        {
            Name:          "multi-turn context",
            Input:         "Can I change the address for the order we just discussed?",
            Expected:      []string{"12345", "address"},
            RequiredTools: []string{"query_order", "update_address"},
        },
    }
}

Human Evaluation System

Automated tests cannot fully replace human judgment, especially for tone, cultural sensitivity, nuanced reasoning and edge cases.

Human Review Flow

package eval

import "time"

type HumanReview struct {
    ReviewID      string
    SessionID     string
    Conversation  []Message
    AgentResponse string
    Ratings       map[string]int
    Feedback      string
    Reviewer      string
    ReviewedAt    time.Time
}

type ReviewQueue struct {
    store ReviewStore
}

func (q *ReviewQueue) SampleForReview(sessions []Session, strategy SamplingStrategy) []string {
    switch strategy {
    case RandomSampling:
        return randomSample(sessions, 0.05)
    case StratifiedSampling:
        return stratifiedSample(sessions)
    case TriggerBased:
        return triggerBasedSample(sessions)
    default:
        return randomSample(sessions, 0.05)
    }
}

Scoring Rubric

Dimension12345
AccuracyCompletely wrongPartially wrongBasically correctCorrectPerfect
CompletenessMisses key factsMisses important factsBasically completeCompleteExceeds expectations
PolitenessOffensiveColdNeutralFriendlyVery warm and professional
ActionabilityCannot actHard to act onSomewhat actionableEasy to followImmediately actionable

A/B Testing and Continuous Optimization

Experiment Framework

Use experiments when deciding whether to change prompts, models, Tool strategies or memory length.

package eval

import (
    "context"
    "fmt"
    "hash/fnv"
    "time"

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

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
    Description string
}

type ExperimentRunner struct {
    experiments map[string]*Experiment
    assignments map[string]string
}

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, ok := r.experiments[experimentID]
    if !ok {
        return ""
    }

    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
}

Typical Experiment Scenarios

ExperimentVariableMetricsExpected outcome
Prompt optimizationSystem prompt wordingAccuracy, satisfaction+5% accuracy
Model switchFlash vs ProLatency, accuracy, cost-30% latency, +3% accuracy
Tool strategyParallel vs serialP95 latency, Tool success-20% latency
Memory length5 turns vs 10 turnsRelevance, token cost+8% relevance, +15% cost
Routing strategyKeyword vs semantic classifierRouting accuracy, latency+12% routing accuracy

Monitoring, Alerting and Feedback Loop

Real-Time Dashboard Metrics

Prometheus metrics make it possible to correlate evaluation outcomes with production traffic.

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: "Total Agent requests",
    }, []string{"agent", "status"})

    AgentLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
        Name:    "agent_latency_seconds",
        Help:    "Agent response latency",
        Buckets: prometheus.DefBuckets,
    }, []string{"agent", "phase"})

    AgentAccuracy = promauto.NewGaugeVec(prometheus.GaugeOpts{
        Name: "agent_accuracy_score",
        Help: "Agent accuracy score from evaluation suite",
    }, []string{"agent", "test_suite"})

    ToolSuccessRate = promauto.NewGaugeVec(prometheus.GaugeOpts{
        Name: "tool_success_rate",
        Help: "Tool call success rate",
    }, []string{"tool_name"})

    UserSatisfaction = promauto.NewGaugeVec(prometheus.GaugeOpts{
        Name: "user_satisfaction_score",
        Help: "User satisfaction score",
    }, []string{"agent"})
)

Alert Rules

# 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 error rate is too high"

      - alert: AgentHighLatency
        expr: histogram_quantile(0.95, rate(agent_latency_seconds_bucket[5m])) > 5
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Agent P95 latency exceeds 5 seconds"

      - alert: AgentAccuracyDrop
        expr: agent_accuracy_score < 0.75
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Agent accuracy is below 75%"

      - alert: ToolFailureSpike
        expr: rate(tool_calls_total{status="error"}[5m]) > 10
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Tool call failures are spiking"

Feedback Loop Mechanism

User feedback (explicit ratings + implicit behavior)
[Data collection] → conversation logs, scores, handoff records
[Data analysis] → identify low-score patterns and frequent failure scenarios
[Attribution analysis] → locate the cause: prompt? model? Tool? routing?
[Experiment validation] → A/B test candidate fixes
[Effect evaluation] → is the new approach better than the baseline?
[Roll out] / [Roll back] → decide based on measured results
[Production monitoring] → confirm the change behaves as expected

Common Evaluation Traps

Trap 1: Vanity Metrics

Counting requests or conversation turns can make a project look active while hiding low value. These metrics do not say whether users got their jobs done.

Countermeasure: define a North Star Metric such as “successful tasks completed per day.”

Trap 2: Test Set Contamination

If evaluation cases leak into prompts, few-shot examples or training data, scores can look artificially high.

Countermeasure: keep development and test sets separate, rotate cases regularly, and use dynamically generated tests for critical scenarios.

Trap 3: Evaluation and Production Mismatch

Lab tests often use clean inputs, while production receives colloquial, noisy, multilingual and typo-prone user input.

Countermeasure: build test sets from production logs and run shadow tests where new models process live traffic without changing user responses.

Trap 4: Optimizing One Metric in Isolation

Improving accuracy can increase latency or cost; lowering cost can reduce quality. Single-metric optimization often creates worse products.

Countermeasure: use multi-objective evaluation and Pareto-frontier thinking to balance accuracy, latency, cost and safety.

Summary

Module 10 is complete, and the Go ADK series is now complete. This chapter built a complete Agent evaluation system:

  1. Metric system: business, system and quality metrics to measure Agent value and reliability.
  2. Automated testing: fast rule-based tests plus deeper LLM-as-Judge scoring.
  3. Human evaluation: sampled review for quality dimensions that automation cannot judge reliably.
  4. A/B testing: an experiment framework for data-driven optimization decisions.
  5. Monitoring and alerting: real-time metrics and automated alerts to keep production quality under control.
  6. Feedback loop: a closed loop from user feedback and production logs back to prompt, Tool and model improvements.

Go ADK Real-World Guide — complete!

Recommended learning path:

  1. Modules 1-2: basics such as environment setup and core concepts.
  2. Modules 3-4: Tools and memory.
  3. Modules 5-6: collaboration, streaming and advanced conversation flows.
  4. Modules 7-10: production deployment, A2A, advanced patterns and real-world practice.

Debugging & Tuning | Back to Series →


Want to learn more Go ADK hands-on practice? Follow the “Full-Stack Peak — Mengshou Programming” WeChat account for weekly Go / AI programming practice updates.

Frequently Asked Questions