三:Agent 自动评估体系与 A2A 协议实战:让你的 AI 智能体从“能用“走向“可靠“
Agent 自动评估体系与 A2A 协议实战:让你的 AI 智能体从"能用"走向"可靠"
前言
前两篇文章中,我们从零构建了一个具备工具调用、RAG 和记忆能力的单体 Agent,又用 Multi-Agent + MCP 协议搭建了企业级协作架构。但到了落地阶段,几乎所有团队都会遇到同一个灵魂拷问:
“Agent 的回答准确率到底是多少?怎么衡量?出了问题怎么定位?”
没有评估体系,Agent 就永远停留在 Demo 阶段。与此同时,Google 提出的 A2A(Agent-to-Agent)协议 正在补全 Agent 生态的最后一块拼图——跨系统、跨组织的 Agent 通信。
本文将从这两个方向入手,帮你把 Agent 从"能用"推向"可靠"。
一、Agent 评估:为什么它比你想的难得多?
1.1 传统测试为什么不够用?
在传统软件开发中,我们习惯于确定性的单元测试:输入 1+1,期望输出 2。但 Agent 系统有三个根本性的不同:
差异一:输出不确定性
同样的问题,Agent 可能给出措辞完全不同但含义等价的回答。"A 栋今日能耗 3856 kWh,超出均值 84%“和"A 栋办公楼当前用电 3856 度,较历史均值偏高约八成”——这两句话表达的信息一致,但传统的字符串断言会直接判定失败。
差异二:执行路径不确定性
Agent 可能通过不同的工具调用序列达到相同的结果。"查 A 栋能耗"这个需求,Agent 可能先查实时数据再查趋势,也可能反过来。两条路径都是合理的。
差异三:多步推理的链式误差
Agent 的 ReAct 循环可能包含 3~5 步推理,每一步都有出错的可能。一个看似简单的失败,根因可能在推理链的第二步。
1.2 Agent 评估的四维模型
要系统性地评估 Agent,我们需要从四个维度建立度量标准:
┌────────────────────────────────────────────────────┐
│ Agent 评估四维模型 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 任务完成 │ │ 工具调用 │ │ 回答质量 │ │
│ │ 准确率 │ │ 正确性 │ │ │ │
│ │ │ │ │ │ │ │
│ │ Agent 是 │ │ 调用了正 │ │ 回答是否 │ │
│ │ 否完成了 │ │ 确的工具?│ │ 准确、完 │ │
│ │ 用户意图?│ │ 参数对吗?│ │ 整、无幻 │ │
│ │ │ │ 顺序合理?│ │ 觉? │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 执行效率 │ │
│ │ │ │
│ │ Token 消耗量、工具调用次数、端到端延迟 │ │
│ └──────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
| 维度 | 含义 | 怎么衡量 |
|---|---|---|
| 任务完成率 | Agent 是否正确理解了用户意图并完成了任务 | 端到端判定:任务成功 / 失败 / 部分成功 |
| 工具调用正确性 | 调了哪些工具?参数对不对?顺序是否合理? | 对比预期工具调用序列 |
| 回答质量 | 回答是否准确、完整、没有幻觉? | LLM-as-Judge(用大模型评判大模型) |
| 执行效率 | 消耗了多少 Token?调了几次工具?耗时多少? | 数值指标统计 |
1.3 LLM-as-Judge:用大模型评判大模型
这是目前 Agent 评估领域最实用的方法论。核心思想是:设计一个专门的"评审 Agent",让它根据预设的评分标准对"被测 Agent"的回答打分。
为什么不用人来评?因为人工评估成本高、速度慢、标准不统一。一个包含 200 条测试用例的评估集,人工评估可能需要一整天,而 LLM-as-Judge 只需几分钟。
当然,LLM-as-Judge 也有局限——它本身也会犯错。但在实践中,LLM 评分与人类专家评分的一致性通常在 85%~92% 之间,已经足够用于持续迭代的参考。
二、构建完整的 Agent 评估框架
2.1 评估数据集设计
评估数据集是整个评估体系的基石。一个好的评估集应该覆盖:
- 正常路径:典型的、预期内的问题
- 边界情况:模糊的、多义的问题
- 异常路径:恶意输入、超出能力范围的请求
- 复合任务:需要多步推理的复杂问题
/**
* 评估数据集定义
*
* 每条测试用例包含:
* - input: 用户问题
* - expectedTools: 预期应该调用的工具列表(验证工具调用正确性)
* - referenceAnswer: 参考答案(用于 LLM-as-Judge 评分对照)
* - category: 测试分类(便于按类别统计通过率)
* - difficulty: 难度等级(便于分层分析)
*/
public record EvalTestCase(
String id,
String input,
List<ExpectedToolCall> expectedTools,
String referenceAnswer,
String category,
Difficulty difficulty
) {
public enum Difficulty { EASY, MEDIUM, HARD }
public record ExpectedToolCall(
String toolName,
Map<String, Object> expectedArgs // null 表示不校验参数
) {}
}
/**
* 评估数据集加载器
*
* 数据来源:
* 1. 历史对话日志中筛选的典型案例
* 2. 领域专家编写的边界测试
* 3. 线上故障复现的异常场景
*/
@Component
public class EvalDatasetLoader {
/**
* 从 JSON 文件加载评估数据集
*/
public List<EvalTestCase> load(String resourcePath) throws IOException {
String json = new String(
getClass().getResourceAsStream(resourcePath).readAllBytes(),
StandardCharsets.UTF_8
);
return JsonUtils.parseList(json, EvalTestCase.class);
}
/**
* 能源管理场景的内置评估集(示例)
*/
public List<EvalTestCase> builtInDataset() {
return List.of(
// ===== 正常路径 =====
new EvalTestCase(
"TC-001",
"A栋办公楼今天用电量是多少?",
List.of(new ExpectedToolCall(
"query_realtime_energy",
Map.of("areaName", "A栋办公楼")
)),
"A栋办公楼今日实时用电量为1523.5 kWh。",
"数据查询",
EvalTestCase.Difficulty.EASY
),
// ===== 多步推理 =====
new EvalTestCase(
"TC-002",
"分析一下C栋数据中心最近一周的能耗趋势,如果偏高就通知张工",
List.of(
new ExpectedToolCall("query_energy_trend",
Map.of("areaName", "C栋数据中心", "days", 7)),
new ExpectedToolCall("send_alert", null) // null = 不校验具体参数
),
"C栋数据中心近7天平均能耗8734.6 kWh,超出历史均值,已通知张工进行排查。",
"复合任务",
EvalTestCase.Difficulty.HARD
),
// ===== 边界情况:模糊意图 =====
new EvalTestCase(
"TC-003",
"最近电费好像有点高",
List.of(), // 预期不调用工具,而是先追问
"请问您想了解哪个区域的用电情况?我可以帮您查询实时数据或分析趋势。",
"模糊意图",
EvalTestCase.Difficulty.MEDIUM
),
// ===== 异常路径:超出能力范围 =====
new EvalTestCase(
"TC-004",
"帮我预订一张明天去北京的机票",
List.of(), // 不应调用任何能源工具
"抱歉,我目前只负责能源管理相关的事务,无法帮您预订机票。",
"越界请求",
EvalTestCase.Difficulty.EASY
),
// ===== RAG 知识检索 =====
new EvalTestCase(
"TC-005",
"根据公司规定,能耗告警的触发条件是什么?",
List.of(), // 应走 RAG 而非工具调用
"根据《能源管理手册》,当某区域单日能耗超过近30天均值的150%时触发黄色告警,超过200%时触发红色告警。",
"知识问答",
EvalTestCase.Difficulty.MEDIUM
)
);
}
}
2.2 评估执行引擎
/**
* Agent 评估执行引擎
*
* 执行流程:
* 1. 逐条加载测试用例
* 2. 将被测 Agent 的对话过程完整录制(包括中间的工具调用)
* 3. 收集四个维度的评估指标
* 4. 输出结构化评估报告
*/
@Service
public class AgentEvalEngine {
private final EnergyAgent agent;
private final EvalJudge judge;
private final AgentCallRecorder recorder;
public AgentEvalEngine(EnergyAgent agent, EvalJudge judge,
AgentCallRecorder recorder) {
this.agent = agent;
this.judge = judge;
this.recorder = recorder;
}
/**
* 执行完整评估
*/
public EvalReport runEvaluation(List<EvalTestCase> testCases) {
List<EvalResult> results = new ArrayList<>();
for (EvalTestCase tc : testCases) {
log.info("执行测试用例: {} - {}", tc.id(), tc.input());
// 1. 开启录制(拦截所有工具调用)
recorder.startRecording();
// 2. 调用被测 Agent
String actualResponse = agent.chat(tc.input());
// 3. 停止录制,获取工具调用链
List<RecordedToolCall> toolCalls = recorder.stopRecording();
// 4. 四维评分
EvalResult result = evaluate(tc, actualResponse, toolCalls);
results.add(result);
log.info("用例 {} 评分: taskComplete={}, toolCorrect={}, " +
"answerQuality={}, tokens={}",
tc.id(),
result.taskCompleted(),
result.toolCallScore(),
result.answerQualityScore(),
result.totalTokens());
}
return generateReport(results, testCases);
}
/**
* 单条用例的四维评分
*/
private EvalResult evaluate(EvalTestCase tc, String actualResponse,
List<RecordedToolCall> toolCalls) {
// 维度1: 任务完成率(二值判定)
boolean taskCompleted = judge.judgeTaskCompletion(
tc.input(), tc.referenceAnswer(), actualResponse);
// 维度2: 工具调用正确性(0.0 ~ 1.0)
double toolCallScore = judge.judgeToolCalls(
tc.expectedTools(), toolCalls);
// 维度3: 回答质量(LLM-as-Judge,0.0 ~ 1.0)
double answerQuality = judge.judgeAnswerQuality(
tc.input(), tc.referenceAnswer(), actualResponse);
// 维度4: 执行效率(Token 统计)
int totalTokens = recorder.getTotalTokens();
int toolCallCount = toolCalls.size();
long latencyMs = recorder.getLatencyMs();
return new EvalResult(
tc.id(), taskCompleted, toolCallScore,
answerQuality, totalTokens, toolCallCount, latencyMs
);
}
}
2.3 LLM-as-Judge 实现
这是评估框架最核心的部分。Judge Agent 是一个独立的、专门用于评分的 Agent:
/**
* 评审 Agent(LLM-as-Judge)
*
* 设计原则:
* 1. Judge 和被测 Agent 必须隔离——不能共享记忆和上下文
* 2. 评分标准必须明确写入 Prompt,避免"自由心证"
* 3. 要求输出结构化 JSON,便于程序解析
*/
@Service
public class EvalJudge {
private final ChatLanguageModel judgeModel;
public EvalJudge() {
// Judge 模型建议使用更强的模型(如 GPT-4o),
// 且 temperature 设为 0 以确保评分稳定
this.judgeModel = OpenAiChatModel.builder()
.apiKey(System.getenv("LLM_API_KEY"))
.modelName("gpt-4o")
.temperature(0.0)
.build();
}
/**
* 维度1: 任务完成度判定
*
* 评判标准:
* - 被测 Agent 是否正确理解了用户意图
* - 最终回答是否实质性地解决了用户的问题
* - 与参考答案的核心信息是否一致
*/
public boolean judgeTaskCompletion(String userQuery,
String referenceAnswer,
String actualAnswer) {
String prompt = String.format("""
你是一个严格的任务完成度评审员。
请判断 Agent 的回答是否正确完成了用户的任务。
## 评分标准
- true: Agent 的回答实质性地解决了用户问题,核心信息正确
- false: Agent 未能理解意图、给出错误信息、或拒绝回答合理请求
允许:措辞差异、信息量差异(只要核心信息覆盖即可)
不允许:事实错误、关键数据缺失、答非所问
## 用户问题
%s
## 参考答案(仅供参考,不是唯一标准)
%s
## Agent 实际回答
%s
只返回 true 或 false,不要返回其他内容。
""", userQuery, referenceAnswer, actualAnswer);
String result = judgeModel.generate(
UserMessage.from(prompt)).content().text().trim();
return Boolean.parseBoolean(result);
}
/**
* 维度2: 工具调用正确性评分
*
* 评分逻辑(规则引擎,无需 LLM):
* - 工具名称匹配率 × 0.4
* - 参数正确率 × 0.3
* - 调用顺序合理性 × 0.3
*/
public double judgeToolCalls(List<ExpectedToolCall> expected,
List<RecordedToolCall> actual) {
if (expected.isEmpty() && actual.isEmpty()) return 1.0;
if (expected.isEmpty() || actual.isEmpty()) return 0.0;
// 工具名称匹配率
Set<String> expectedNames = expected.stream()
.map(ExpectedToolCall::toolName)
.collect(Collectors.toSet());
Set<String> actualNames = actual.stream()
.map(RecordedToolCall::toolName)
.collect(Collectors.toSet());
long matched = expectedNames.stream()
.filter(actualNames::contains).count();
double nameScore = (double) matched / Math.max(
expectedNames.size(), actualNames.size());
// 参数正确率(只校验期望参数非 null 的)
long paramTotal = 0, paramCorrect = 0;
for (ExpectedToolCall exp : expected) {
if (exp.expectedArgs() == null) continue;
RecordedToolCall act = findByName(actual, exp.toolName());
if (act == null) continue;
for (var entry : exp.expectedArgs().entrySet()) {
paramTotal++;
if (entry.getValue().equals(
act.arguments().get(entry.getKey()))) {
paramCorrect++;
}
}
}
double paramScore = paramTotal == 0 ? 1.0
: (double) paramCorrect / paramTotal;
// 调用顺序合理性(第一个期望工具是否在第一个实际工具之前或相等)
double orderScore = 1.0;
if (expected.size() >= 2 && actual.size() >= 2) {
int firstExpIdx = indexOfFirst(actual, expected.get(0).toolName());
int lastExpIdx = indexOfFirst(actual,
expected.get(expected.size() - 1).toolName());
if (firstExpIdx >= 0 && lastExpIdx >= 0
&& firstExpIdx < lastExpIdx) {
orderScore = 1.0;
} else {
orderScore = 0.5;
}
}
return nameScore * 0.4 + paramScore * 0.3 + orderScore * 0.3;
}
/**
* 维度3: 回答质量评分(LLM-as-Judge)
*
* 评分维度:
* - 准确性 (0~1):事实是否正确
* - 完整性 (0~1):是否覆盖了用户关心的要点
* - 幻觉度 (0~1):是否包含捏造的信息(1=无幻觉)
* - 专业性 (0~1):措辞是否专业得体
*/
public double judgeAnswerQuality(String userQuery,
String referenceAnswer,
String actualAnswer) {
String prompt = String.format("""
你是一个专业的回答质量评审员。请从以下四个维度对 Agent 的回答打分。
每个维度 0~1 分,最终返回四个维分的加权平均。
## 评分维度与权重
1. 准确性 (权重0.35): 事实和数据是否正确?有没有编造数据?
2. 完整性 (权重0.25): 是否回答了用户关心的所有要点?
3. 无幻觉 (权重0.25): 是否包含无法验证的或捏造的信息?
4. 专业性 (权重0.15): 措辞是否专业、简洁、得体?
## 用户问题
%s
## 参考答案
%s
## Agent 实际回答
%s
严格按以下 JSON 格式返回,不要返回其他内容:
{
"accuracy": 0.0~1.0,
"completeness": 0.0~1.0,
"noHallucination": 0.0~1.0,
"professionalism": 0.0~1.0,
"weightedScore": 加权总分,
"reason": "一句话说明扣分原因"
}
""", userQuery, referenceAnswer, actualAnswer);
String result = judgeModel.generate(
UserMessage.from(prompt)).content().text().trim();
// 解析 JSON 获取加权分数
JsonNode node = JsonUtils.parse(result);
return node.get("weightedScore").asDouble();
}
}
2.4 工具调用录制器
为了评估工具调用的正确性,我们需要在不侵入 Agent 代码的前提下"录制"所有的工具调用过程。这可以通过 AOP 拦截 实现:
/**
* Agent 工具调用录制器
*
* 实现原理:
* 使用 Spring AOP 拦截所有 @Tool 标注的方法调用,
* 记录调用时间、参数、返回值,存储到 ThreadLocal 中。
*
* 优势:对业务代码零侵入,Agent 完全不知道自己在被"监控"。
*/
@Component
public class AgentCallRecorder {
private final ThreadLocal<List<RecordedToolCall>> callLog =
ThreadLocal.withInitial(ArrayList::new);
private final ThreadLocal<Long> startTime = new ThreadLocal<>();
private final ThreadLocal<Integer> totalTokens =
ThreadLocal.withInitial(() -> 0);
/**
* 开始录制
*/
public void startRecording() {
callLog.get().clear();
startTime.set(System.currentTimeMillis());
totalTokens.set(0);
}
/**
* 停止录制,返回工具调用链
*/
public List<RecordedToolCall> stopRecording() {
return List.copyOf(callLog.get());
}
public long getLatencyMs() {
return System.currentTimeMillis() - startTime.get();
}
public int getTotalTokens() {
return totalTokens.get();
}
/**
* AOP 切面:拦截所有 @Tool 方法调用
*/
@Around("@annotation(dev.langchain4j.agent.tool.Tool)")
public Object recordToolCall(ProceedingJoinPoint pjp) throws Throwable {
long callTime = System.currentTimeMillis();
String toolName = pjp.getSignature().getName();
Map<String, Object> arguments = extractArguments(pjp);
Object result;
try {
result = pjp.proceed();
} catch (Throwable t) {
callLog.get().add(new RecordedToolCall(
toolName, arguments, null, t.getMessage(),
callTime, System.currentTimeMillis()
));
throw t;
}
callLog.get().add(new RecordedToolCall(
toolName, arguments, result, null,
callTime, System.currentTimeMillis()
));
return result;
}
private Map<String, Object> extractArguments(ProceedingJoinPoint pjp) {
String[] paramNames = ((MethodSignature) pjp.getSignature())
.getParameterNames();
Object[] args = pjp.getArgs();
Map<String, Object> map = new LinkedHashMap<>();
for (int i = 0; i < paramNames.length; i++) {
map.put(paramNames[i], args[i]);
}
return map;
}
}
/**
* 录制的单次工具调用
*/
public record RecordedToolCall(
String toolName,
Map<String, Object> arguments,
Object result,
String error,
long startTime,
long endTime
) {}
2.5 评估报告生成
/**
* 评估报告
*/
public record EvalReport(
int totalCases,
double taskCompletionRate, // 任务完成率
double avgToolCallScore, // 平均工具调用正确率
double avgAnswerQuality, // 平均回答质量
double avgTokens, // 平均 Token 消耗
double avgLatencyMs, // 平均延迟
Map<String, CategoryScore> byCategory, // 按类别统计
List<EvalResult> failedCases // 失败用例(便于定向优化)
) {
public record CategoryScore(
int count,
double completionRate,
double avgQuality
) {}
}
/**
* 报告生成逻辑
*/
private EvalReport generateReport(List<EvalResult> results,
List<EvalTestCase> cases) {
int total = results.size();
long completed = results.stream()
.filter(EvalResult::taskCompleted).count();
double completionRate = (double) completed / total;
double avgTool = results.stream()
.mapToDouble(EvalResult::toolCallScore).average().orElse(0);
double avgQuality = results.stream()
.mapToDouble(EvalResult::answerQualityScore).average().orElse(0);
double avgTokens = results.stream()
.mapToInt(EvalResult::totalTokens).average().orElse(0);
double avgLatency = results.stream()
.mapToLong(EvalResult::latencyMs).average().orElse(0);
// 按类别分组统计
Map<String, EvalReport.CategoryScore> byCategory = cases.stream()
.collect(Collectors.groupingBy(EvalTestCase::category))
.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> {
List<EvalResult> catResults = e.getValue().stream()
.map(c -> findById(results, c.id()))
.toList();
long catCompleted = catResults.stream()
.filter(EvalResult::taskCompleted).count();
return new EvalReport.CategoryScore(
catResults.size(),
(double) catCompleted / catResults.size(),
catResults.stream()
.mapToDouble(EvalResult::answerQualityScore)
.average().orElse(0)
);
}
));
// 失败用例
List<EvalResult> failed = results.stream()
.filter(r -> !r.taskCompleted()).toList();
return new EvalReport(total, completionRate, avgTool, avgQuality,
avgTokens, avgLatency, byCategory, failed);
}
2.6 评估报告示例
运行评估后,输出类似以下结构的报告:
╔══════════════════════════════════════════════════════════╗
║ Agent 评估报告 2026-08-27 ║
╠══════════════════════════════════════════════════════════╣
║ 总用例数: 50 ║
║ 任务完成率: 88.0% (44/50) ║
║ 工具调用正确率: 82.3% ║
║ 回答质量均分: 0.81 ║
║ 平均 Token: 2,340 ║
║ 平均延迟: 4.2s ║
╠══════════════════════════════════════════════════════════╣
║ 按类别统计: ║
║ ┌──────────────┬──────┬──────────┬──────────┐ ║
║ │ 类别 │ 数量 │ 完成率 │ 质量均分 │ ║
║ ├──────────────┼──────┼──────────┼──────────┤ ║
║ │ 数据查询 │ 15 │ 100.0% │ 0.91 │ ║
║ │ 复合任务 │ 12 │ 75.0% │ 0.73 │ ║
║ │ 模糊意图 │ 10 │ 80.0% │ 0.78 │ ║
║ │ 知识问答 │ 8 │ 87.5% │ 0.85 │ ║
║ │ 越界请求 │ 5 │ 100.0% │ 0.92 │ ║
║ └──────────────┴──────┴──────────┴──────────┘ ║
╠══════════════════════════════════════════════════════════╣
║ 失败用例 (Top 3): ║
║ 1. TC-023: "对比A栋和B栋上个月的能耗差异并..." ║
║ → 工具调用遗漏:未调用 query_history ║
║ 2. TC-041: "如果能耗正常就不用通知了" ║
║ → 条件推理错误:即使正常也发了通知 ║
║ 3. TC-037: "帮我看看B栋3楼的空调是不是太费电了" ║
║ → 粒度不足:只查了B栋整体,未细化到3楼 ║
╚══════════════════════════════════════════════════════════╝
这份报告的价值在于:它直接告诉你 Agent 的薄弱环节在哪里,下一步该优化什么。 比如上例中,"复合任务"完成率只有 75%,说明 Orchestrator 的路由逻辑需要加强;TC-041 说明 Agent 的条件推理能力不足,需要在 System Prompt 中增加对条件语句的处理规则。
三、A2A 协议:Agent 间通信的"HTTP"
3.1 A2A 解决了什么问题?
MCP 协议解决了 Agent 与工具之间的集成问题,但还有一个更大的问题没有解决:不同系统、不同组织构建的 Agent 之间如何互相通信、互相协作?
想象这样的场景:
- 你公司的 能源管理 Agent 需要向物业公司的 设备维修 Agent 发起维修工单
- 供应链部门的 库存 Agent 需要向采购部门的 采购 Agent 提出备品需求
- 客户的 私人助理 Agent 需要与你公司的 客服 Agent 协商预约时间
这些 Agent 可能基于不同的框架构建(LangChain4j、Spring AI、AutoGen……),部署在不同的服务器上,由不同的团队维护。如果没有统一的通信协议,每对 Agent 之间都需要单独开发对接方案——这是 N² 级别的复杂度。
A2A(Agent-to-Agent Protocol)就是为此而生的。它是 Google 于 2025 年提出的开放协议,到 2026 年已获得 50+ 家企业和组织的支持。
3.2 A2A vs MCP:互补而非竞争
很多人会问:“已经有了 MCP,为什么还需要 A2A?”
答案是:它们解决的问题不在同一层。
┌────────────────────────────────────────────────────┐
│ │
│ Agent A ──── A2A 协议 ────→ Agent B │
│ │ │ │
│ │ │ │
│ MCP 协议 MCP 协议 │
│ │ │ │
│ 工具集 A 工具集 B │
│ │
└────────────────────────────────────────────────────┘
MCP = Agent 与工具之间的"USB 接口"
A2A = Agent 与 Agent 之间的"HTTP 协议"
| 维度 | MCP | A2A |
|---|---|---|
| 连接对象 | Agent ↔ 工具/数据源 | Agent ↔ Agent |
| 关系 | 主从(Agent 调用工具) | 对等(Agent 互相请求) |
| 通信模式 | 请求-响应 | 任务(可异步、可流式) |
| 类比 | USB 接口 | HTTP 协议 |
3.3 A2A 的核心概念
A2A 协议定义了四个核心概念:
Agent Card(名片)
每个 Agent 发布一个 JSON 文件描述自己的身份信息,类似于 OpenAPI 的 swagger.json。其他 Agent 通过读取 Agent Card 来了解"对方能做什么"。
Task(任务)
A2A 通信的基本单元。一个 Agent 向另一个 Agent 发起任务,任务有明确的生命周期状态:
submitted → working → input-required → completed
↑ │
└──── 需要更多 ────┘
信息
↘
failed / canceled
Message(消息)
任务中的通信载体。一条消息包含多个 Part(内容片段),每个 Part 可以是文本、文件、或结构化数据。
Artifact(产物)
任务执行完成后产出的结果,如生成的报告文件、分析数据等。
3.4 实现一个 A2A Agent Server
下面我们把能源管理 Agent 包装成一个标准的 A2A Server,让外部 Agent 可以通过 A2A 协议调用它。
依赖配置
<dependencies>
<!-- A2A Java SDK -->
<dependency>
<groupId>io.github.a2ap</groupId>
<artifactId>a2a-java-sdk</artifactId>
<version>0.5.0</version>
</dependency>
</dependencies>
发布 Agent Card
/**
* Agent Card 端点
*
* 作用:向外界声明"我是谁、我能做什么"
* 路径:/.well-known/agent.json(A2A 协议约定的标准路径)
*
* 其他 Agent 在调用我们之前,会先请求这个地址获取 Agent Card,
* 从而了解我们的能力和通信方式。
*/
@RestController
public class AgentCardController {
@GetMapping("/.well-known/agent.json")
public AgentCard getAgentCard() {
return AgentCard.builder()
.name("Energy Management Agent")
.description("智慧能源管理智能体,提供能耗查询、趋势分析、预警通知等能力")
.url("https://energy.example.com/a2a") // A2A 通信端点
.version("1.0.0")
.protocolVersion("0.2.1")
// 声明支持的能力
.capabilities(AgentCapabilities.builder()
.streaming(true) // 支持流式响应
.pushNotifications(true) // 支持异步推送通知
.build())
// 声明支持的内容类型
.defaultInputModes(List.of("text/plain", "application/json"))
.defaultOutputModes(List.of("text/plain", "application/json"))
// 技能列表(其他 Agent 据此决定是否调用我们)
.skills(List.of(
AgentSkill.builder()
.id("query-energy")
.name("能耗数据查询")
.description("查询指定区域的实时能耗、历史趋势、对比分析")
.tags(List.of("energy", "data", "analytics"))
.build(),
AgentSkill.builder()
.id("energy-alert")
.name("能耗预警通知")
.description("根据能耗异常情况发送预警通知给相关负责人")
.tags(List.of("energy", "alert", "notification"))
.build(),
AgentSkill.builder()
.id("energy-report")
.name("能耗报告生成")
.description("基于能耗数据生成专业的分析报告")
.tags(List.of("energy", "report", "document"))
.build()
))
.build();
}
}
处理 A2A 任务请求
/**
* A2A 任务处理端点
*
* 处理外部 Agent 发来的任务请求
*/
@RestController
@RequestMapping("/a2a")
public class A2ATaskHandler {
private final EnergyAgent energyAgent;
private final TaskRepository taskRepository;
public A2ATaskHandler(EnergyAgent energyAgent,
TaskRepository taskRepository) {
this.energyAgent = energyAgent;
this.taskRepository = taskRepository;
}
/**
* 接收并处理 A2A 任务
*
* 完整流程:
* 1. 解析外部 Agent 的任务请求
* 2. 将任务转化为内部 Agent 调用
* 3. 返回任务状态和结果
*/
@PostMapping("/tasks/send")
public ResponseEntity<TaskResponse> handleTask(
@RequestBody TaskRequest request) {
// 1. 创建任务记录
Task task = Task.builder()
.id(request.taskId())
.status(TaskStatus.WORKING)
.build();
taskRepository.save(task);
// 2. 提取用户消息(从 A2A Message 的 Part 中提取)
String userMessage = extractMessageFromParts(request.message());
// 3. 调用内部 Agent
String response = energyAgent.chat(userMessage);
// 4. 构建 A2A 响应
TaskResponse response = TaskResponse.builder()
.taskId(task.getId())
.status(TaskStatus.COMPLETED)
// 将回答封装为 A2A Artifact
.artifacts(List.of(
Artifact.builder()
.name("analysis-result")
.parts(List.of(
Part.textPart(response)
))
.build()
))
.build();
taskRepository.updateStatus(task.getId(), TaskStatus.COMPLETED);
return ResponseEntity.ok(response);
}
/**
* 任务状态查询(外部 Agent 可轮询此接口获取进度)
*/
@GetMapping("/tasks/{taskId}")
public ResponseEntity<TaskStatus> getTaskStatus(
@PathVariable String taskId) {
Task task = taskRepository.findById(taskId)
.orElseThrow(() -> new TaskNotFoundException(taskId));
return ResponseEntity.ok(task.getStatus());
}
/**
* 流式任务处理(SSE)
*
* 适用于耗时较长的任务,外部 Agent 可以实时获取中间进展
*/
@PostMapping(value = "/tasks/sendSubscribe",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleTaskStream(@RequestBody TaskRequest request) {
SseEmitter emitter = new SseEmitter();
CompletableFuture.runAsync(() -> {
try {
// 发送"开始工作"事件
emitter.send(SseEmitter.event()
.name("task-status")
.data(new TaskStatusEvent(request.taskId(), "working")));
String userMessage = extractMessageFromParts(request.message());
// 调用流式 Agent
TokenStream stream = streamingAgent.chat(userMessage);
stream.onNext(token -> {
try {
emitter.send(SseEmitter.event()
.name("task-artifact")
.data(new ArtifactChunk(token)));
} catch (IOException e) {
emitter.completeWithError(e);
}
});
stream.onComplete(response -> {
try {
emitter.send(SseEmitter.event()
.name("task-status")
.data(new TaskStatusEvent(
request.taskId(), "completed")));
emitter.complete();
} catch (IOException e) {
emitter.completeWithError(e);
}
});
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
private String extractMessageFromParts(List<Part> parts) {
return parts.stream()
.filter(Part::isText)
.map(Part::getText)
.collect(Collectors.joining("\n"));
}
}
3.5 作为 A2A Client 调用外部 Agent
现在让我们反过来——我们的 Agent 作为客户端,去调用物业公司的"设备维修 Agent":
/**
* A2A Client:调用外部 Agent 的服务
*/
@Service
public class A2AClientService {
private final A2AClient a2aClient;
public A2AClientService() {
this.a2aClient = A2AClient.builder()
.build();
}
/**
* 调用外部 Agent 之前,先读取对方的 Agent Card
* 了解它有哪些能力
*/
public AgentCard discoverAgent(String agentUrl) {
return a2aClient.getAgentCard(agentUrl);
}
/**
* 向物业维修 Agent 提交维修工单
*
* @param deviceName 设备名称
* @param issue 问题描述
* @param urgency 紧急程度
* @return 工单编号
*/
public String submitRepairTask(String deviceName,
String issue,
String urgency) {
// 1. 构建 A2A 任务请求
TaskRequest request = TaskRequest.builder()
.taskId(UUID.randomUUID().toString())
.message(Message.builder()
.role("user")
.parts(List.of(
Part.textPart(String.format("""
请为以下设备创建维修工单:
设备名称:%s
问题描述:%s
紧急程度:%s
提交方:能源管理系统
""", deviceName, issue, urgency))
))
.build())
.build();
// 2. 发送到物业维修 Agent 的 A2A 端点
TaskResponse response = a2aClient.sendTask(
"https://property-repair.example.com/a2a",
request
);
// 3. 提取结果
if (response.status() == TaskStatus.COMPLETED) {
return response.artifacts().get(0).parts().get(0).getText();
} else {
throw new RuntimeException(
"维修工单创建失败: " + response.status());
}
}
}
3.6 将 A2A Client 集成到 Agent 的工具集中
最强大的做法是:把"调用外部 Agent"也封装成一个工具,让 Agent 自主决定什么时候需要请求外援。
@Component
public class ExternalAgentTools {
private final A2AClientService a2aClient;
public ExternalAgentTools(A2AClientService a2aClient) {
this.a2aClient = a2aClient;
}
@Tool("向物业维修系统提交设备维修工单。" +
"当发现设备能耗异常、可能存在硬件故障时使用此工具。" +
"返回维修工单编号。")
public String submitRepairOrder(String deviceName,
String issueDescription,
String urgency) {
return a2aClient.submitRepairTask(
deviceName, issueDescription, urgency);
}
@Tool("查询物业维修系统的工单处理进度。" +
"当用户询问'维修进展如何'、'工单处理到哪了'时使用此工具。")
public String queryRepairProgress(String orderId) {
return a2aClient.queryTaskStatus(
"https://property-repair.example.com/a2a", orderId);
}
}
这样,你的 Agent 不仅能管理能耗数据,还能 自主地 发现设备故障 → 调用外部维修 Agent → 跟踪维修进度——形成了一个完整的业务闭环。
四、评估 + A2A 的完整集成测试
4.1 CI/CD 集成:每次代码变更自动评估
/**
* Agent 评估的集成测试
*
* 在 CI/CD 流水线中运行,确保每次代码变更不会导致 Agent 能力退化
*/
@SpringBootTest
public class AgentEvalIntegrationTest {
@Autowired
private AgentEvalEngine evalEngine;
@Autowired
private EvalDatasetLoader datasetLoader;
/**
* 核心回归测试:确保关键能力不退化
*
* 阈值设定依据:
* - 任务完成率 >= 85%(允许少量边界情况失败)
* - 工具调用正确率 >= 80%
* - 回答质量均分 >= 0.75
*/
@Test
public void regressionTest() {
List<EvalTestCase> dataset = datasetLoader.builtInDataset();
EvalReport report = evalEngine.runEvaluation(dataset);
// 输出报告(CI 日志可查)
System.out.println(formatReport(report));
// 关键指标断言
assertThat(report.taskCompletionRate())
.as("任务完成率不应低于 85%%")
.isGreaterThanOrEqualTo(0.85);
assertThat(report.avgToolCallScore())
.as("工具调用正确率不应低于 80%%")
.isGreaterThanOrEqualTo(0.80);
assertThat(report.avgAnswerQuality())
.as("回答质量均分不应低于 0.75")
.isGreaterThanOrEqualTo(0.75);
// 特定类别的断言
assertThat(report.byCategory().get("数据查询").completionRate())
.as("数据查询类任务应 100%% 完成")
.isEqualTo(1.0);
}
/**
* 性能回归测试:确保响应时间不退化
*/
@Test
public void performanceTest() {
List<EvalTestCase> dataset = datasetLoader.builtInDataset();
EvalReport report = evalEngine.runEvaluation(dataset);
assertThat(report.avgLatencyMs())
.as("平均响应时间不应超过 10 秒")
.isLessThanOrEqualTo(10000);
assertThat(report.avgTokens())
.as("平均 Token 消耗不应超过 5000")
.isLessThanOrEqualTo(5000);
}
}
4.2 评估驱动的持续优化闭环
评估不是一次性的动作,而是一个持续迭代的闭环:
代码变更 → CI 自动评估 → 评估报告 → 发现退化?
│
┌───── Yes ───┤
│ │
▼ No → 合并代码
分析失败用例
│
▼
定位根因(Prompt? 工具? RAG?)
│
▼
修复 & 重新评估
│
▼
通过率达标?──→ 合并代码
五、三篇文章的完整知识图谱
让我们回顾一下整个系列的脉络:
┌─────────────────────────────────────────────────────────────┐
│ AI Agent 技术全景图 │
│ │
│ 第一篇:单体 Agent │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Tool Calling + RAG + Memory │ │
│ │ → 掌握 Agent 的基本构建能力 │ │
│ └───────────────────────────┬───────────────────────┘ │
│ │ │
│ 第二篇:Multi-Agent + MCP │ │
│ ┌───────────────────────────▼───────────────────────┐ │
│ │ 多 Agent 协作 + 工具标准化 │ │
│ │ → 掌握企业级的架构设计和协议集成 │ │
│ └───────────────────────────┬───────────────────────┘ │
│ │ │
│ 第三篇:评估 + A2A(本篇) │ │
│ ┌───────────────────────────▼───────────────────────┐ │
│ │ 自动评估体系 + Agent 间通信 │ │
│ │ → 掌握从"能用"到"可靠"的工程化能力 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
六、总结
本文覆盖了两个关键主题:
Agent 自动评估体系:
- 四维评估模型(任务完成率、工具正确性、回答质量、执行效率)
- LLM-as-Judge 方法论的完整实现
- AOP 工具调用录制器,零侵入
- CI/CD 集成,评估驱动持续优化
A2A 协议实战:
- Agent Card 发布(让别人找到你)
- 任务处理(同步 + 流式)
- 作为 Client 调用外部 Agent
- 将 A2A 集成到工具集,形成跨组织协作闭环
A2A + MCP + Multi-Agent + 评估体系——这四者的组合,构成了 2026 年企业级 AI Agent 系统的完整技术版图。
更多推荐
所有评论(0)