AI Agent 评估与优化:从基准测试到生产环境的质量守护实战

系列文章

本篇是 AI Agent 系列的第 11 篇。前 10 篇我们从零搭建了 Agent 的核心能力——推理、记忆、工具、规划、协作,现在是时候回答一个关键问题:怎么知道 Agent 干得好不好?

  1. AI Agent 评估与优化:从基准测试到生产环境的质量守护实战(本文)
  2. 理解 AI Agent 的大脑:ReAct 模式从入门到实战
  3. AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程
  4. AI Agent 的规划大脑:从任务分解到自适应执行策略
  5. 让 AI 学会”说人话”——Spring AI 结构化输出实战
  6. AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
  7. AI Agent 的记忆力是怎么实现的——LangChain4j Memory 机制深度解析
  8. MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
  9. AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
  10. 从零理解 RAG:检索增强生成完整指南
  11. AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战

为什么 Agent 评估这么难?

你在公司上线了一个 AI 客服 Agent。Demo 阶段一切完美,老板拍板上线。三天后,客服投诉量暴增——Agent 开始”一本正经地胡说八道”,把退款政策编得头头是道,用户信以为真,结果退款流程根本对不上。

这不是段子,这是 2026 年大量企业部署 Agent 后的真实场景。

传统软件测试的思路是”给定输入,验证输出是否等于预期”。但 Agent 不一样:

第一,输出不确定。 同一个问题问两遍,Agent 可能给出措辞不同但都正确的回答。你没法写 assertEquals("xxx", agent.answer())

第二,评估维度多。 一个回答好不好,要同时看准确性、相关性、完整性、安全性、延迟、成本——这些维度之间甚至可能矛盾。追求准确性可能需要多次检索和推理,延迟和成本就上去了。

第三,链式错误传播。 Agent 不是单次 LLM 调用,而是 ReAct 循环 → 工具调用 → 记忆检索 → 规划 → 生成的长链路。任何一个环节出错,最终输出都可能跑偏,而且你很难定位到底是哪一步出了问题。

第四,幻觉无处不在。 LLM 的幻觉不是 bug,是特性——它本质上是一个概率模型,总有可能生成看起来合理但实际错误的内容。Agent 比纯聊天更危险,因为它会调用工具、执行操作,幻觉可能导致真实世界的副作用。

所以,Agent 评估不是”上线前跑一遍测试”这么简单,它需要一套贯穿开发、测试、上线、运营全生命周期的质量守护体系。


评估维度金字塔:从单轮到端到端

我把 Agent 评估分成四层,像金字塔一样从底到顶:

1
2
3
4
5
6
7
8
9
    ┌─────────────┐
│ 业务价值 │ ← ROI、用户满意度
┌┴─────────────┴┐
│ 端到端质量 │ ← 完整任务成功率
┌┴───────────────┴┐
│ 链路环节质量 │ ← 推理、工具、记忆、规划
┌┴─────────────────┴┐
│ 基础模型质量 │ ← 准确性、幻觉率、安全性
└───────────────────┘

第一层:基础模型质量

这是最底层,评估的是 LLM 本身的回答质量。

准确性(Accuracy): 回答是否正确。可以用精确匹配(Exact Match)或模糊匹配(F1 Score)。

幻觉率(Hallucination Rate): 回答中包含无中生有信息的比例。这是 Agent 场景最致命的问题。

安全性(Safety): 是否泄露了敏感信息、是否生成了有害内容。

指令遵循度(Instruction Following): 是否按照 System Prompt 的要求行事,比如”只用中文回答”、”不确定时说不知道”。

第二层:链路环节质量

Agent 的核心链路包括推理(ReAct)、工具调用(Tool Use)、记忆检索(Memory/RAG)、规划(Planning)。每个环节都需要独立评估:

环节 评估指标 说明
推理 步骤正确率 ReAct 循环是否选择了正确的思考路径
工具调用 工具选择准确率、参数正确率 是否调用了正确的工具、参数是否正确
记忆/RAG 检索召回率、排序准确率 检索到的文档是否相关、排序是否合理
规划 任务分解合理性 子任务划分是否覆盖了完整需求

第三层:端到端质量

把所有环节串起来,评估完整任务的完成情况:

  • 任务成功率(Task Success Rate): 给定一个任务描述,Agent 最终是否完成了任务
  • 平均轮次(Average Turns): 完成任务用了多少轮对话/工具调用
  • 错误恢复率(Error Recovery Rate): 遇到工具调用失败后,Agent 是否能自主恢复

第四层:业务价值

最终要落到业务指标上:

  • 用户满意度(CSAT): 用户对 Agent 回答的评分
  • 人工介入率(Human Handoff Rate): 多少比例的对话需要转人工
  • 成本效率(Cost per Resolution): 解决一个问题的平均 Token 成本

幻觉检测:Agent 的头号敌人

幻觉(Hallucination)是 Agent 质量的最大威胁。和纯聊天不同,Agent 的幻觉会导致真实操作——错误地调用工具、错误地执行命令、错误地查询数据。

幻觉的三种类型

事实幻觉(Factual Hallucination): 编造不存在的事实。比如 Agent 告诉用户”您的订单号是 123456”,但系统里根本没有这个订单。

忠实性幻觉(Faithfulness Hallucination): 检索到了正确的文档,但回答时歪曲了文档内容。比如 RAG 场景下,文档说”退款需要 7 个工作日”,Agent 回答”退款 3 天内到账”。

推理幻觉(Reasoning Hallucination): 推理过程中的逻辑跳跃。比如 Agent 看到用户问”能不能退货”,直接跳到”退货流程如下”,跳过了”订单是否在退货期内”的判断。

幻觉检测的三种策略

策略一:基于证据的忠实性检测

核心思想:如果 Agent 的回答能被检索到的上下文(RAG 文档、工具返回结果)所支撑,就不是幻觉。

实现方式很简单——让另一个 LLM 做”裁判”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 基于证据的忠实性检测
* 核心思路:把 Agent 的回答拆成独立的声明,逐条检查是否有上下文支撑
*/
@Component
public class FaithfulnessChecker {

private final ChatClient chatClient;

public FaithfulnessChecker(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

/**
* 检测回答是否忠实于提供的上下文
* @param context 检索到的文档/工具返回结果
* @param answer Agent 的回答
* @return 每条声明的忠实性评分
*/
public FaithfulnessResult check(String context, String answer) {
String prompt = """
你是一个忠实性检查器。你的任务是判断"回答"中的每条声明是否被"上下文"所支撑。

上下文:
%s

回答:
%s

请将回答拆分为独立的声明,对每条声明判断:
- SUPPORTED: 上下文中明确支撑该声明
- NOT_SUPPORTED: 上下文中找不到支撑
- CONTRADICTED: 上下文明确反驳该声明

以 JSON 数组格式返回,每条包含 claim 和 verdict 字段。
""".formatted(context, answer);

String result = chatClient.prompt()
.user(prompt)
.call()
.content();

return parseResult(result);
}

/**
* 计算忠实性分数:支撑的声明数 / 总声明数
*/
public double score(String context, String answer) {
FaithfulnessResult result = check(context, answer);
long supported = result.claims().stream()
.filter(c -> "SUPPORTED".equals(c.verdict()))
.count();
return (double) supported / result.claims().size();
}
}

这个方法的关键在于:让 LLM 做结构化的、可验证的任务——拆分声明、逐条匹配,比让它直接判断”这个回答对不对”可靠得多。

策略二:自一致性检测(Self-Consistency)

核心思想:同一个问题问 N 次,如果 Agent 每次回答都不一样,说明它在”猜”而不是”知道”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 自一致性检测
* 核心思路:多次采样,检查回答的一致性
*/
@Component
public class SelfConsistencyChecker {

private final ChatClient chatClient;

public SelfConsistencyChecker(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

/**
* 采样多次回答,计算一致性分数
* @param query 用户问题
* @param context 上下文(可选)
* @param sampleCount 采样次数,建议 3-5 次
* @return 一致性分数 0.0-1.0
*/
public double checkConsistency(String query, String context, int sampleCount) {
List<String> answers = new ArrayList<>();

for (int i = 0; i < sampleCount; i++) {
String prompt = context != null
? "上下文:%s\n\n问题:%s\n\n请回答。".formatted(context, query)
: "问题:%s\n\n请回答。".formatted(query);

String answer = chatClient.prompt()
.user(prompt)
.options(ChatOptions.builder()
.temperature(0.7) // 适度随机性以暴露不确定性
.build())
.call()
.content();
answers.add(answer);
}

// 让 LLM 判断这些回答是否一致
String judgePrompt = """
以下是对同一个问题的 %d 次回答,请判断它们在核心事实上是否一致:

%s

只返回一个 0.0 到 1.0 的数字,1.0 表示完全一致,0.0 表示完全矛盾。
""".formatted(sampleCount, formatAnswers(answers));

String scoreStr = chatClient.prompt()
.user(judgePrompt)
.call()
.content();

return Double.parseDouble(scoreStr.trim());
}

private String formatAnswers(List<String> answers) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < answers.size(); i++) {
sb.append("回答 %d: %s\n\n".formatted(i + 1, answers.get(i)));
}
return sb.toString();
}
}

什么时候用这个方法? 在关键场景(比如退款政策、法律条款)下,如果一致性分数低于阈值(比如 0.8),就应该触发人工审核,而不是直接返回给用户。

策略三:工具结果验证

Agent 和纯聊天的最大区别是——它会调用工具。幻觉的一个常见来源是 Agent “编造”了工具返回的结果,或者错误地解读了结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 工具调用结果验证
* 检查 Agent 是否正确解读了工具返回的数据
*/
@Component
public class ToolResultVerifier {

private final ChatClient chatClient;

public ToolResultVerifier(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

/**
* 验证 Agent 对工具结果的解读是否正确
* @param toolCall Agent 发起的工具调用(函数名 + 参数)
* @param toolResult 工具实际返回的结果
* @param agentClaim Agent 基于工具结果做出的陈述
* @return 验证结果
*/
public VerificationResult verify(
String toolCall, String toolResult, String agentClaim) {

String prompt = """
你是一个工具调用结果验证器。

工具调用:%s
工具返回:%s
Agent 陈述:%s

请判断:
1. Agent 的陈述是否被工具返回的数据支撑?
2. Agent 是否遗漏了工具返回中的重要信息?
3. Agent 是否编造了工具返回中不存在的数据?

返回 JSON:{"supported": true/false, "missing_info": [...], "fabricated_info": [...]}
""".formatted(toolCall, toolResult, agentClaim);

String result = chatClient.prompt()
.user(prompt)
.call()
.content();

return parseVerification(result);
}
}

成本优化:Agent 的第二大挑战

上线 Agent 后你会发现,成本增长曲线远超预期。一个客服对话可能涉及 5-10 次 LLM 调用(ReAct 循环、工具调用、总结),每次调用都带着 System Prompt + 历史消息 + 检索结果。单次对话的 Token 消耗轻松突破 10K。

成本的四大来源

1
2
3
4
5
单次对话成本 ≈ Prompt Tokens × $单价 + Completion Tokens × $单价

其中:
- Prompt Tokens = System Prompt + 历史消息 + RAG 上下文 + 工具描述
- Completion Tokens = 推理步骤 + 工具调用 + 最终回答

优化策略一:分层模型调度

不是所有任务都需要最强的模型。一个成熟的 Agent 系统应该有模型分层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* 分层模型路由器
* 根据任务复杂度选择合适的模型
*/
@Component
public class ModelRouter {

private final ChatClient fastModel; // 轻量模型:简单问答、格式化
private final ChatClient smartModel; // 标准模型:一般推理、工具调用
private final ChatClient proModel; // 旗舰模型:复杂推理、代码生成

public ModelRouter(
@Qualifier("fast") ChatClient.Builder fastBuilder,
@Qualifier("smart") ChatClient.Builder smartBuilder,
@Qualifier("pro") ChatClient.Builder proBuilder) {
this.fastModel = fastBuilder.build();
this.smartModel = smartBuilder.build();
this.proModel = proBuilder.build();
}

/**
* 根据任务复杂度路由到合适的模型
*/
public ChatClient route(String userMessage, List<ToolDefinition> availableTools) {
TaskComplexity complexity = classifyComplexity(userMessage);

return switch (complexity) {
case SIMPLE -> {
// 简单问答、格式转换、闲聊 → 轻量模型
// 例:"今天天气怎么样"、"帮我翻译这句话"
yield fastModel;
}
case MODERATE -> {
// 需要工具调用、多步推理 → 标准模型
// 例:"帮我查一下最近的订单"、"总结这篇文章"
yield smartModel;
}
case COMPLEX -> {
// 复杂推理、代码生成、多轮规划 → 旗舰模型
// 例:"帮我设计一个微服务架构"、"分析这段代码的性能问题"
yield proModel;
}
};
}

/**
* 任务复杂度分类
* 实际生产中可以用规则 + 轻量模型结合判断
*/
private TaskComplexity classifyComplexity(String message) {
// 规则层:基于关键词快速判断
if (message.length() < 20 && !message.contains("代码") && !message.contains("分析")) {
return TaskComplexity.SIMPLE;
}

// 复杂度信号
int complexityScore = 0;
if (message.contains("代码") || message.contains("实现")) complexityScore += 2;
if (message.contains("架构") || message.contains("设计")) complexityScore += 2;
if (message.contains("分析") || message.contains("对比")) complexityScore += 1;
if (message.length() > 200) complexityScore += 1;

if (complexityScore >= 3) return TaskComplexity.COMPLEX;
if (complexityScore >= 1) return TaskComplexity.MODERATE;
return TaskComplexity.SIMPLE;
}

enum TaskComplexity { SIMPLE, MODERATE, COMPLEX }
}

实际效果参考: 在一个客服场景中,70% 的查询是简单问答(退换货政策、营业时间),用轻量模型处理,成本下降约 60%,响应速度提升 3 倍,用户满意度几乎不受影响。

优化策略二:Prompt 压缩

System Prompt 和历史消息是 Token 消耗的大头。优化手段包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* Prompt 压缩器
* 减少每次请求的 Token 数量
*/
@Component
public class PromptCompressor {

private final ChatClient chatClient;

public PromptCompressor(ChatClient.Builder builder) {
this.chatClient = builder.build();
}

/**
* 历史消息压缩:保留关键信息,去除冗余
* 当消息超过阈值时,将早期消息压缩为摘要
*/
public List<Message> compressHistory(List<Message> history, int maxTokens) {
int currentTokens = estimateTokens(history);
if (currentTokens <= maxTokens) {
return history; // 未超限,不压缩
}

// 保留最近 N 轮(用户+助手)完整对话
int keepRecent = 4;
List<Message> recent = history.subList(
Math.max(0, history.size() - keepRecent), history.size());
List<Message> older = history.subList(
0, Math.max(0, history.size() - keepRecent));

// 将早期对话压缩为摘要
String olderText = older.stream()
.map(m -> m.getMessageType() + ": " + m.getText())
.collect(Collectors.joining("\n"));

String summary = chatClient.prompt()
.user("请用 3-5 句话概括以下对话的关键信息:\n" + olderText)
.call()
.content();

// 用摘要替换早期消息
List<Message> compressed = new ArrayList<>();
compressed.add(new SystemMessage("之前的对话摘要:" + summary));
compressed.addAll(recent);
return compressed;
}

/**
* RAG 上下文压缩:只保留与问题最相关的片段
*/
public String compressContext(String query, String fullContext, int maxChars) {
if (fullContext.length() <= maxChars) {
return fullContext;
}

// 分段,保留与 query 最相关的段落
String[] paragraphs = fullContext.split("\n\n");
return chatClient.prompt()
.user("""
从以下文档中,提取与问题最相关的关键信息,不超过 %d 字符:

问题:%s

文档:
%s
""".formatted(maxChars, query, fullContext))
.call()
.content();
}

private int estimateTokens(List<Message> messages) {
// 粗略估算:中文 1 字 ≈ 2 tokens,英文 1 词 ≈ 1.3 tokens
return messages.stream()
.mapToInt(m -> m.getText().length() * 2)
.sum();
}
}

优化策略三:缓存与语义去重

很多用户的问题本质上是一样的,只是措辞不同。用语义缓存可以避免重复调用 LLM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 语义缓存
* 基于向量相似度的缓存,语义相近的问题命中同一缓存
*/
@Component
public class SemanticCache {

private final EmbeddingModel embeddingModel;
private final CacheEntryStore store; // 可以是 Redis、本地 Map 等
private final double similarityThreshold;

public SemanticCache(
EmbeddingModel embeddingModel,
CacheEntryStore store,
@Value("${agent.cache.similarity-threshold:0.92}") double threshold) {
this.embeddingModel = embeddingModel;
this.store = store;
this.similarityThreshold = threshold;
}

/**
* 尝试从缓存中获取语义匹配的回答
* @return 缓存命中则返回回答,否则返回 null
*/
public String get(String query) {
float[] queryEmbedding = embeddingModel.embed(query).getContent();

// 在缓存中查找最相似的条目
CacheEntry best = store.findMostSimilar(queryEmbedding);

if (best != null && best.similarity() >= similarityThreshold) {
return best.answer();
}
return null;
}

/**
* 将新的问答对加入缓存
*/
public void put(String query, String answer) {
float[] embedding = embeddingModel.embed(query).getContent();
store.save(new CacheEntry(query, answer, embedding));
}

record CacheEntry(String query, String answer, float[] embedding, double similarity) {
CacheEntry(String query, String answer, float[] embedding) {
this(query, answer, embedding, 0.0);
}
}
}

阈值选择的经验法则:

  • 0.95+:非常保守,只缓存几乎一模一样的问题,命中率低但安全
  • 0.90-0.95:平衡模式,推荐大多数场景
  • 0.85-0.90:激进模式,命中率高但可能返回不完全匹配的缓存

Benchmark 框架:系统化评估 Agent

手动测试几个 case 不够——你需要系统化的基准测试框架。

评估数据集设计

一个好的评估数据集应该包含:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Agent 评估用例
*/
public record EvalCase(
String id,
String category, // 分类:qa / tool_use / multi_turn / edge_case
String query, // 用户输入
String expectedAnswer, // 期望的回答(或关键信息)
List<String> expectedTools, // 期望调用的工具列表
EvalRubric rubric // 评分标准
) {}

/**
* 评分标准
* 定义不同维度的评分权重
*/
public record EvalRubric(
double accuracyWeight, // 准确性权重
double completenessWeight, // 完整性权重
double safetyWeight, // 安全性权重
double efficiencyWeight // 效率权重(轮次、Token 消耗)
) {
// 默认权重
public static EvalRubric defaultRubric() {
return new EvalRubric(0.4, 0.3, 0.2, 0.1);
}
}

自动化评估流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* Agent 评估引擎
* 自动运行评估用例、打分、生成报告
*/
@Service
public class AgentEvalEngine {

private final ChatClient judgeModel;
private final AgentService agentService;

public AgentEvalEngine(
@Qualifier("smart") ChatClient.Builder builder,
AgentService agentService) {
this.judgeModel = builder.build();
this.agentService = agentService;
}

/**
* 运行完整的评估套件
*/
public EvalReport runSuite(List<EvalCase> cases) {
List<EvalResult> results = new ArrayList<>();

for (EvalCase evalCase : cases) {
// 1. 运行 Agent
long startTime = System.currentTimeMillis();
AgentResponse response = agentService.chat(evalCase.query());
long latency = System.currentTimeMillis() - startTime;

// 2. 用 LLM 裁判评分
ScoredResult scored = score(evalCase, response, latency);
results.add(new EvalResult(evalCase, response, scored));
}

// 3. 汇总报告
return generateReport(results);
}

/**
* LLM 裁判评分
*/
private ScoredResult score(EvalCase evalCase, AgentResponse response, long latency) {
String prompt = """
你是一个 AI Agent 评估专家。请对以下回答进行多维度评分。

【问题】%s
【期望回答】%s
【Agent 实际回答】%s
【Agent 调用的工具】%s
【期望调用的工具】%s
【响应延迟】%d ms

请分别对以下维度打分(0-10 分):
1. 准确性:回答的事实是否正确
2. 完整性:是否覆盖了所有关键信息
3. 安全性:是否有有害/误导性内容
4. 工具使用:是否正确调用了合适的工具

以 JSON 格式返回:{"accuracy": N, "completeness": N, "safety": N, "tool_use": N, "reasoning": "..."}
""".formatted(
evalCase.query(),
evalCase.expectedAnswer(),
response.answer(),
String.join(", ", response.toolsUsed()),
String.join(", ", evalCase.expectedTools()),
latency
);

String result = judgeModel.prompt().user(prompt).call().content();
return parseScoredResult(result, latency);
}

/**
* 生成评估报告
*/
private EvalReport generateReport(List<EvalResult> results) {
double avgAccuracy = results.stream()
.mapToDouble(r -> r.scored().accuracy())
.average().orElse(0);

double avgCompleteness = results.stream()
.mapToDouble(r -> r.scored().completeness())
.average().orElse(0);

double avgSafety = results.stream()
.mapToDouble(r -> r.scored().safety())
.average().orElse(0);

double avgLatency = results.stream()
.mapToDouble(EvalResult::latencyMs)
.average().orElse(0);

double toolAccuracy = results.stream()
.filter(r -> !r.evalCase().expectedTools().isEmpty())
.mapToDouble(r -> r.scored().toolUse())
.average().orElse(0);

// 按分类统计
Map<String, List<EvalResult>> byCategory = results.stream()
.collect(Collectors.groupingBy(r -> r.evalCase().category()));

return new EvalReport(
results.size(),
avgAccuracy, avgCompleteness, avgSafety, toolAccuracy,
avgLatency,
byCategory.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> calculateCategoryStats(e.getValue())))
);
}
}

与 CI/CD 集成

评估不应该只在开发阶段做,应该集成到 CI/CD 流程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# .github/workflows/agent-eval.yml
name: Agent Evaluation

on:
pull_request:
paths:
- 'src/main/java/**/agent/**'
- 'src/main/resources/prompts/**'

jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run Agent Eval Suite
run: |
mvn test -Dtest=AgentEvalTest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

- name: Check Quality Gates
run: |
# 准确性必须 >= 8.0
ACCURACY=$(cat eval-report.json | jq '.avgAccuracy')
if (( $(echo "$ACCURACY < 8.0" | bc -l) )); then
echo "❌ Accuracy $ACCURACY below threshold 8.0"
exit 1
fi
echo "✅ All quality gates passed"

生产环境的可观测性

上线不是终点,是起点。你需要持续监控 Agent 的表现。

关键监控指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* Agent 可观测性指标
* 使用 Micrometer 暴露到 Prometheus/Grafana
*/
@Component
public class AgentMetrics {

private final MeterRegistry registry;
private final Counter hallucinationCounter;
private final Counter toolFailureCounter;
private final Timer responseTimer;
private final DistributionSummary tokenUsage;

public AgentMetrics(MeterRegistry registry) {
this.registry = registry;
this.hallucinationCounter = Counter.builder("agent.hallucination.detected")
.description("检测到的幻觉次数")
.register(registry);
this.toolFailureCounter = Counter.builder("agent.tool.failure")
.description("工具调用失败次数")
.tag("tool", "all")
.register(registry);
this.responseTimer = Timer.builder("agent.response.time")
.description("Agent 响应时间")
.register(registry);
this.tokenUsage = DistributionSummary.builder("agent.token.usage")
.description("每次对话的 Token 消耗")
.register(registry);
}

/**
* 记录一次对话的完整指标
*/
public void recordConversation(ConversationMetrics metrics) {
responseTimer.record(metrics.duration());
tokenUsage.record(metrics.totalTokens());

if (metrics.hallucinationDetected()) {
hallucinationCounter.increment();
}

metrics.toolFailures().forEach(toolName -> {
Counter.builder("agent.tool.failure")
.tag("tool", toolName)
.register(registry)
.increment();
});
}
}

红线告警

设置自动告警,当以下指标异常时立即通知:

  • 幻觉率突增:5 分钟窗口内幻觉率 > 5%
  • 延迟飙升:P95 延迟 > 阈值的 2 倍
  • 成本异常:单小时 Token 消耗超过日均的 3 倍
  • 工具失败率:某个工具连续失败 3 次以上

生产环境最佳实践

1. 渐进式灰度发布

不要一次性把新版本推给所有用户。用 A/B 测试验证效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 灰度路由:按比例分流到不同版本的 Agent
*/
@Component
public class GrayReleaseRouter {

@Value("${agent.gray.new-version-ratio:0.1}")
private double newVersionRatio;

public AgentVersion route(String userId) {
// 基于用户 ID 的哈希确保同一用户始终路由到同一版本
int hash = Math.abs(userId.hashCode());
return (hash % 100) < (newVersionRatio * 100)
? AgentVersion.NEW
: AgentVersion.STABLE;
}
}

2. 安全兜底策略

Agent 不确定时,宁可说”我不知道”也不要瞎编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 安全兜底:检测不确定性并触发降级
*/
@Component
public class SafetyNet {

private final FaithfulnessChecker faithfulnessChecker;
private final SelfConsistencyChecker consistencyChecker;
private final double faithfulnessThreshold;
private final double consistencyThreshold;

public SafetyNet(
FaithfulnessChecker faithfulnessChecker,
SelfConsistencyChecker consistencyChecker,
@Value("${agent.safety.faithfulness-threshold:0.7}") double faithThreshold,
@Value("${agent.safety.consistency-threshold:0.75}") double consistencyThreshold) {
this.faithfulnessChecker = faithfulnessChecker;
this.consistencyChecker = consistencyChecker;
this.faithfulnessThreshold = faithThreshold;
this.consistencyThreshold = consistencyThreshold;
}

/**
* 检查回答是否安全可返回
* @return 安全则返回原始回答,否则返回兜底话术
*/
public String checkAndFallback(String context, String answer, String originalQuery) {
// 检查忠实性
double faithfulness = faithfulnessChecker.score(context, answer);
if (faithfulness < faithfulnessThreshold) {
return "抱歉,关于这个问题我目前没有足够的信息来给出准确回答,建议您联系人工客服获取帮助。";
}

return answer; // 通过检查,返回原始回答
}
}

3. 持续评估闭环

评估不是一次性的事,而是一个持续的闭环:

1
生产日志 → 采样 → 人工标注 → 更新评估集 → 重新评估 → 优化 Prompt/模型 → 灰度验证 → 全量发布

每周从生产日志中随机采样 50-100 条对话,人工标注质量分数,持续积累评估数据集。这个数据集的价值会随时间指数增长。


总结:Agent 评估的”不可能三角”

Agent 评估存在一个”不可能三角”——准确性、成本、速度三者很难同时最优:

  • 追求准确性 → 多次 LLM 调用做交叉验证 → 成本高、速度慢
  • 追求成本低 → 用轻量模型、减少验证 → 准确性下降
  • 追求速度快 → 减少推理步骤、跳过验证 → 质量风险

实际落地的关键是分场景制定策略

场景 准确性要求 推荐策略
金融交易 极高 忠实性检测 + 人工审核
客服问答 忠实性检测 + 安全兜底
内容创作 自一致性检测
闲聊娱乐 基本安全过滤即可

最后,回到系列文章的视角——Agent 评估不是独立的能力,而是对前面所有能力的守护

  • ReAct 推理质量好不好?用推理步骤正确率评估
  • Tool Use调得对不对?用工具选择准确率评估
  • Memory/RAG检索准不准?用检索召回率评估
  • Planning分解合不合理?用任务成功率评估
  • Multi-Agent协作顺不顺?用端到端质量评估

没有评估的 Agent 就像没有仪表盘的汽车——你不知道它跑多快、油还剩多少、发动机温度是否正常。也许它看起来跑得很好,但哪天突然抛锚了你都不知道为什么。

给你的 Agent 装上仪表盘,让它在生产环境中安全、可控、可持续地运行。