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 协议"
维度MCPA2A
连接对象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 系统的完整技术版图。


Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐