Evaluation:Agent 效果评估
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% 请求的首字返回时间 | < 3s | APM 监控 |
| 系统层 | Tool 调用成功率 | Tool 成功执行的比例 | > 95% | 系统日志统计 |
| 质量层 | 上下文相关性 | 回答与对话上下文的相关程度 | > 0.8 | LLM-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 的注意事项:
- 评委模型选择:评委模型应当比被评估模型更强或至少同级,否则可能出现"学生评判老师"的窘境
- 位置偏差(Position Bias):如果同时比较两个回答,模型可能倾向于位置靠前的那个。应随机打乱顺序或分别独立评估
- 自增强偏差:避免使用与被评估 Agent 相同的模型作为评委
- 成本考量: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 效果评估体系:
- 指标体系:业务层、系统层、质量层三层指标,全面衡量 Agent 表现
- 自动化测试:基于规则的快速验证 + LLM-as-Judge 的深度评估
- 人工评估:分层抽样审核,覆盖自动化无法判断的质量维度
- A/B 测试:系统化实验框架,用数据驱动优化决策
- 监控告警:实时指标 + 自动告警,确保线上质量可控
- 反馈闭环:从用户反馈到系统改进的完整链路
Go ADK 实战完全指南——全部完成!
学习路径建议:
- 模块 1-2:入门(环境 + 基础)
- 模块 3-4:核心(工具 + 记忆)
- 模块 5-6:进阶(协作 + 流式)
- 模块 7-10:生产(部署 + A2A + 进阶 + 实战)
想跟着学更多 Go ADK 实战?关注「全栈之巅-梦兽编程」公众号,每周更新 Go / AI 编程实战干货。
