Ohhnews

分类导航

$ cd ..
DZone Java原文

第4部分:多智能体编排——将Goose与Quarkus Flow状态机配对

#多智能体编排#goose#quarkus#a2a协议#状态机

核心问题

在第 1 部分中,我们构建了一个 Quarkus MCP 工具服务器。在第 2 部分中,我们使用 agentgateway 对其进行了安全加固。在第 3 部分中,我们添加了分布式追踪。该架构可以很好地处理单次请求的工具调用——但企业流程很少是单步的。

试想当开发者告诉 Goose:“将 users 表迁移以添加 email 列,然后将更新后的服务部署到生产环境。” 这会触发一连串操作:

  1. Goose 调用 migrate-schema 修改生产数据库
  2. Goose 调用 deploy-production 推送新代码
  3. 两项操作都成功——却没有任何人审查模式变更或批准生产部署

像 Goose 这样的本地开发者智能体擅长交互式推理,但长时间运行的业务流程需要:

  • 严格的状态机,通过定义清晰的阶段来跟踪多步骤工作流
  • 最大迭代次数上限,防止智能体无限循环、长期占用资源
  • 正式的人类参与(HITL)审批,在数据库写入、模式迁移或金融交易等破坏性操作之前进行
  • 标准化的智能体到智能体委托,使客户端智能体(Goose)和后端智能体(Quarkus)通过共享协议通信,而不是临时 HTTP 调用

没有这些护栏,自主智能体就在治理真空中运行——正是这一缺口把有用的自动化变成了未经审计的风险。

解决方案:A2A 协议 + Quarkus Flow + AGENTS.md

A2A 将业务任务委托给智能体;第 6 部分的模型路由为推理选择模型后端。模型回退不能取代本章的工作流策略或人工审批关口。

该方案将以下标准与机制组合成一个受治理的多智能体架构:

  • A2A(Agent2Agent) —— Linux 基金会面向多智能体互操作性的标准。Goose 通过 Agent Card 发现 Quarkus 后端,并使用基于 HTTP 的 JSON-RPC 委托任务。A2A Java SDK(@PublicAgentCard + AgentExecutor)自动处理协议合规性。
  • Quarkus Flow —— 一个轻量级状态机,实现 CNCF Serverless Workflow 概念:状态、转换和动作处理器。每个被委托的任务都会经过治理验证、可选的 HITL 审批和执行。
  • MCP 工具委托 —— analyze-logs、health-check 和 generate-report 等自动批准的操作会委托给第 1 部分的 MCP 服务器进行实时工具执行。quarkus-langchain4j-mcp 扩展提供一个受管理的 McpClient,自动处理 Streamable HTTP 传输和 MCP 会话。
  • AGENTS.md —— 一个声明式治理文件,定义哪些操作自动批准、哪些需要人工审批、哪些永久阻止。Quarkus 服务器在启动时解析此文件,并对每个传入的 A2A 任务强制执行。

A2A 任务生命周期驱动整个流程:

submitted → working → input-required → working → completed → failed (rejected) → completed (auto-approved) → failed (blocked by governance)

先决条件

需要第 1–3 部分的全部内容,外加 A2A Java SDK。第 4 部分添加 a2a-java-sdk-reference-jsonrpc 依赖以支持 A2A 协议,并连接第 1 部分的 MCP 服务器进行工具执行。

验证环境:

$ bash
java -version    # Java 25+
mvn --version    # Maven 3.9+

第 1 步:使用 A2A Agent Card 进行智能体发现

A2A 协议从发现开始。每个符合 A2A 的智能体都会在一个众所周知的 URL 上发布 Agent Card,描述其能力、技能和端点:

$ bash
curl -s http://localhost:8082/.well-known/agent-card.json | jq .
$ cat
{
  "name": "enterprise-workflow-agent",
  "description": "Multi-step enterprise workflow orchestration with HITL approval gates...",
  "supportedInterfaces": [{
    "protocol": "JSONRPC",
    "url": "http://localhost:8082"
  }],
  "version": "1.0.0",
  "capabilities": {
    "streaming": false,
    "pushNotifications": false
  },
  "skills": [
    {
      "id": "analyze-logs",
      "name": "Log Analysis",
      "description": "Analyze logs via MCP delegation"
    },
    {
      "id": "health-check",
      "name": "Health Check",
      "description": "Verify service health via MCP"
    },
    {
      "id": "migrate-schema",
      "name": "Schema Migration",
      "description": "Requires HITL approval"
    },
    {
      "id": "deploy-production",
      "name": "Production Deployment",
      "description": "Requires HITL approval"
    }
  ]
}

Agent Card 告诉 Goose(或任何 A2A 客户端)三件关键事情:

字段用途
supportedInterfacesJSON-RPC 端点 URL 和传输协议
skills智能体可执行的操作——Goose 使用它们决定要委托哪些任务
capabilities智能体是否支持流式传输、推送通知等

使用 A2A Java SDK 时,Agent Card 通过在生产者方法上使用 @PublicAgentCard CDI 限定符来声明——SDK 会自动在 /.well-known/agent-card.json 提供它,并注册 JSON-RPC 传输处理器:

$ java
@ApplicationScoped
public class AgentCardProducer {

    @ConfigProperty(name = "agent.url", defaultValue = "http://localhost:8082")
    String agentUrl;

    @Produces
    @PublicAgentCard
    public AgentCard agentCard() {
        return AgentCard.builder()
                .name("enterprise-workflow-agent")
                .description("Multi-step enterprise workflow orchestration with HITL approval gates...")
                .supportedInterfaces(Collections.singletonList(
                        new AgentInterface("JSONRPC", agentUrl)))
                .version("1.0.0")
                .capabilities(AgentCapabilities.builder()
                        .streaming(false).pushNotifications(false).build())
                .defaultInputModes(Collections.singletonList("text"))
                .defaultOutputModes(Collections.singletonList("text"))
                .skills(List.of(
                        AgentSkill.builder().id("analyze-logs").name("Log Analysis")
                                .description("Analyze logs via MCP tool delegation (auto-approved)")
                                .tags(List.of("observability", "auto-approved")).build(),
                        AgentSkill.builder().id("migrate-schema").name("Schema Migration")
                                .description("Execute database schema migrations (requires HITL approval)")
                                .tags(List.of("database", "hitl-required")).build()
                        // ... additional skills
                ))
                .build();
    }
}

无需 JAX-RS 端点——SDK 会自动处理 Agent Card 服务和 JSON-RPC 路由。

第 2 步:使用 AGENTS.md 定义治理规则

AGENTS.md 是一个声明式治理文件,与应用程序代码放在一起。Quarkus 服务器在启动时读取它,并对每个传入的 A2A 任务强制执行其规则。

创建 src/main/resources/AGENTS.md:

$ md
# AGENTS.md - Governance Rules for Enterprise Workflow Agent

## Execution Bounds
max-iterations: 10

## Operations

### Auto-Approved
- analyze-logs: Read-only log analysis across services
- health-check: Service health and resource utilization verification
- generate-report: Compliance and metrics report generation from existing data

### Requires Human Approval
- migrate-schema: Database schema migrations on production tables
- process-refund: Customer refund processing (auto-approved below $1000 threshold)
- scale-infrastructure: Infrastructure replica scaling operations
- deploy-production: Production environment deployments

### Blocked
- drop-database: Destructive operation — never allowed via agent delegation
- truncate-table: Destructive operation — never allowed via agent delegation
- delete-all-data: Destructive operation — never allowed via agent delegation

三个类别,三种结果:

类别会发生什么示例
自动批准任务立即执行,无需人工介入analyze-logs、health-check
需要人工审批任务在 input-required 状态暂停,直到人工批准或拒绝migrate-schema、deploy-production
被阻止任务立即失败,并报告治理违规drop-database、truncate-table

GovernanceEngine 在启动时解析此文件,并验证每个操作:

$ java
public GovernanceResult validate(String operation, Map args) {
    if (blocked.containsKey(operation)) {
        return new GovernanceResult(Decision.BLOCKED,
                "Operation '" + operation + "' is blocked by governance policy",
                "critical");
    }

    if (requiresApproval.containsKey(operation)) {
        // Conditional: process-refund auto-approves below $1000
        if (operation.equals("process-refund")) {
            int amount = Integer.parseInt(args.getOrDefault("amount", "0"));
            if (amount <= 1000) {
                return new GovernanceResult(Decision.AUTO_APPROVED, "...", "low");
            }
        }
        return new GovernanceResult(Decision.REQUIRES_APPROVAL, reason, "high");
    }

    if (autoApproved.containsKey(operation)) {
        return new GovernanceResult(Decision.AUTO_APPROVED, "...", "low");
    }

    // Unknown operations default to requiring approval
    return new GovernanceResult(Decision.REQUIRES_APPROVAL, "...", "medium");
}

请注意 process-refund 的条件逻辑——治理规则可以包含阈值,而不只是二元的允许/拒绝。500 美元的退款会自动通过;2,500 美元的退款会暂停等待人工审查。

第 3 步:实现工作流状态机

每个 A2A 任务都会流经一个包含六种可能状态的状态机:

WorkflowEngine 管理此生命周期:

$ java
public Map submitTask(String taskId, String messageText) {
    TaskInstance task = tasks.computeIfAbsent(taskId, TaskInstance::new);
    task.incrementIteration();

    // Safety bound: prevent runaway agent loops
    if (task.getIteration() > governance.getMaxIterations()) {
        task.setState(TaskState.FAILED);
        task.addMessage("agent", "Max iterations exceeded. Task terminated.");
        return task.toA2AResponse();
    }

    String operation = parseOperation(messageText);
    Map args = parseArguments(messageText);

    task.setState(TaskState.WORKING);

    // Governance gate
    GovernanceResult gov = governance.validate(operation, args);

    return switch (gov.decision()) {
        case BLOCKED -> {
            task.setState(TaskState.FAILED);
            task.addMessage("agent", gov.reason());
            yield task.toA2AResponse();
        }
        case REQUIRES_APPROVAL -> {
            task.setState(TaskState.INPUT_REQUIRED);
            task.addMessage("agent", "HITL approval required: " + gov.reason());
            yield task.toA2AResponse();
        }
        case AUTO_APPROVED -> {
            String result = execute(operation, args);
            task.setState(TaskState.COMPLETED);
            task.addMessage("agent", result);
            yield task.toA2AResponse();
        }
    };
}

A2A Java SDK 自动处理 JSON-RPC 路由。你实现 AgentExecutor 接口——SDK 会为每个传入的 SendMessage 请求调用你的 execute() 方法:

$ java
@ApplicationScoped
public class AgentExecutorProducer {

    @Inject
    WorkflowEngine engine;

    @Produces
    public AgentExecutor agentExecutor() {
        return new AgentExecutor() {
            @Override
            public void execute(RequestContext context, AgentEmitter emitter) throws A2AError {
                String messageText = extractText(context.getMessage());
                String taskId = context.getTaskId();

                // HITL follow-up: task already exists in INPUT_REQUIRED state
                if (context.getTask() != null &&
                        context.getTask().status().state() == TaskState.TASK_STATE_INPUT_REQUIRED) {
                    handleHitlFollowUp(taskId, messageText, emitter);
                    return;
                }

                // New task: submit to workflow engine for governance check
                TaskInstance task = engine.submitTask(taskId, messageText);

                switch (task.getState()) {
                    case FAILED ->
                            emitter.fail(agentMessage(emitter, task.getLastAgentMessage()));
                    case INPUT_REQUIRED -> {
                        emitter.startWork();
                        emitter.requiresInput(agentMessage(emitter, task.getLastAgentMessage()));
                    }
                    case COMPLETED -> {
                        emitter.startWork();
                        emitter.addArtifact(List.of(new TextPart(task.getLastAgentMessage())));
                        emitter.complete();
                    }
                }
            }
            // ...
        };
    }
}

AgentEmitter 管理 A2A 任务生命周期——startWork()、requiresInput()、complete()、fail()——而 WorkflowEngine 负责治理验证和 MCP 工具执行。

第 4 步:人类参与(HITL)审批关口

当 Goose 提交高风险操作时,工作流会在 input-required 暂停。任务会保持冻结,直到人工明确批准或拒绝。

触发 HITL

提交模式迁移——这是一个需要审批的高风险操作:

$ bash
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{
    "message":{"messageId":"migration-msg-1","role":"ROLE_USER","parts":[{"text":"migrate-schema --database production --table users --changes add-column-email"}]},
    "configuration":{"returnImmediately":true}
  }}' | jq .result.task.status
$ cat
{
  "state": "TASK_STATE_INPUT_REQUIRED",
  "message": {
    "role": "ROLE_AGENT",
    "parts": [{
      "text": "HITL approval required. Reason: Database schema migrations on production tables. Workflow paused at state INPUT_REQUIRED. Awaiting human decision."
    }]
  }
}

轮询状态

Goose 轮询 GetTask,看到任务已暂停。它可以提醒开发者:

$ bash
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":2,"method":"GetTask","params":{"id":"migration-1"}}' \
  | jq .result.status.state
$ cat
"TASK_STATE_INPUT_REQUIRED"

批准任务

人工审查该操作,并通过管理 API(或 SPA)批准它:

$ bash
curl -s -X POST http://localhost:8082/api/tasks/migration-1/approve | jq .status
$ cat
{
  "state": "COMPLETED",
  "message": {
    "role": "agent",
    "parts": [{
      "text": "Schema migration completed successfully. Database: production. Table: users. Changes: add-column-email..."
    }]
  }
}

拒绝任务

如果人工判断该迁移不安全:

$ bash
curl -s -X POST http://localhost:8082/api/tasks/migration-2/reject \
  -H "Content-Type: application/json" \
  -d '{"reason":"Schema change not reviewed by DBA team"}' | jq .status.state
$ cat
"failed"

拒绝原因会记录在任务历史中——Goose 在下一次 GetTask 轮询时会收到它,并可以转告开发者。

对比:自动批准与被阻止的操作

并非每个操作都会触发 HITL。比较以下场景:

$ bash
# Auto-approved: executes immediately
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":3,"method":"SendMessage","params":{
    "message":{"messageId":"logs-msg-1","role":"ROLE_USER","parts":[{"text":"analyze-logs --service api-gateway --timeframe 24h"}]}
  }}' | jq .result.task.status.state
# → "TASK_STATE_COMPLETED"

# Blocked: fails immediately
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":4,"method":"SendMessage","params":{
    "message":{"messageId":"drop-msg-1","role":"ROLE_USER","parts":[{"text":"drop-database --target production"}]},
    "configuration":{"returnImmediately":true}
  }}' | jq .result.task.status.state
# → "TASK_STATE_FAILED"

第 5 步:运行完整演示

使用一键脚本启动所有服务:

$ bash
cd part4-multi-agent
./start-all.sh

该脚本会构建第 1 部分的 MCP 服务器和第 4 部分的 A2A Flow 服务器,启动两者(MCP 在 :8080,A2A 在 :8082),并启动交互式演示 SPA。像 analyze-logs 这样的自动批准任务会委托给第 1 部分的 MCP 工具以获取实时数据。

在 http://localhost:8889/index.html 打开 A2A Multi-Agent Console。如果发现过程报告 Failed to fetch,请用 Ctrl+C 停止启动器,更新代码,再次运行 ./start-all.sh,然后重新加载页面。控制台需要后端的 quarkus.http.cors.enabled=true 设置才能跨端口读取响应。

这个单用户实验环境将 A2A 服务器绑定到 localhost,并显式设置 a2a.authorization.required=false。A2A SDK 1.3+ 默认需要 TaskAuthorizationProvider;共享部署需要经过身份验证的用户以及该提供程序。实验环境的 AGENTS.md 操作规则和 HITL 关口仍然有效。

演示流程

SPA 提供四个引导步骤,展示完整的治理范围:

第 1 步:发现智能体

点击 Discover Agent,从 /.well-known/agent-card.json 获取 Agent Card。架构图会动画展示发现流程,并加载 AGENTS.md 治理规则。状态卡片显示智能体在线,且治理规则已加载。

第 2 步:自动批准任务(analyze-logs)

点击 Auto-Approved Task 提交只读日志分析操作。任务会经过 AGENTS.md 验证(自动批准),跳过 HITL 关口,并通过 McpToolClient 委托给第 1 部分的 MCP 工具。状态机转换为:SUBMITTED → WORKING → GOVERNANCE → EXECUTING → COMPLETED。

第 3 步:HITL 任务(migrate-schema)

点击 HITL Task 提交高风险数据库迁移。治理将其标记为需要人工审查后,任务在 INPUT_REQUIRED 暂停。审批面板出现,包含 Approve 和 Reject 按钮,并使用服务器返回的任务 ID。点击 Approve 运行模拟的模式迁移,或点击 Reject 终止任务。状态机:SUBMITTED → WORKING → GOVERNANCE → HITL GATE → (approve) → EXECUTING → COMPLETED。

第 4 步:被阻止任务(drop-database)

点击 Blocked Task 提交破坏性操作。AGENTS.md 治理会立即拒绝它——任务永远不会到达 HITL 关口或 MCP 委托。状态机:SUBMITTED → WORKING → GOVERNANCE → FAILED。

连接 Goose

当 Goose 添加 A2A 支持后,将其配置为发现后端智能体:

$ bash
goose session --a2a-discover http://localhost:8082

Goose 将获取 Agent Card,枚举可用技能,并通过 SendMessage 委托操作。高风险操作会暂停,直到人工通过 SPA 或 CLI 批准。

我们实现了什么

从第 3 部分的可观测架构出发,我们添加了带治理和 HITL 的多智能体编排,而没有更改任何现有 MCP 工具代码:

层级我们添加的内容关键文件
A2A 协议通过 A2A Java SDK 提供 Agent Card + JSON-RPCAgentCardProducer.java、AgentExecutorProducer.java
治理AGENTS.md 规则解析与强制执行GovernanceEngine.java、AGENTS.md
工作流带 HITL 审批关口的状态机WorkflowEngine.java
MCP 集成通过 quarkus-langchain4j-mcp 进行工具委托McpToolClient.java、application.properties
管理用于人工审批的 REST API + SPAAdminEndpoint.java、index.html

生产环境注意事项

关注点本地(本教程)生产环境
任务存储内存中的 ConcurrentHashMapPostgreSQL + Flyway 迁移
HITL 通知浏览器从 SPA 轮询WebSocket 推送或 Slack/Teams 集成
治理规则classpath 上的 AGENTS.md 文件策略引擎(OPA、Cedar 或 Keycloak 策略)
A2A 传输明文 HTTP JSON-RPCmTLS + OAuth2 客户端凭据
工作流引擎自定义状态机SonataFlow(Quarkus 上的 CNCF Serverless Workflow)
审计跟踪内存中的任务历史基于 Kafka 的只追加事件存储

第 5 部分预告

在治理、安全、追踪和多智能体编排就位之后,这套技术栈已具备生产能力——但如何确保它在代码变更过程中始终保持正确?在第 5 部分,我们将添加 自动化智能体评估与回归测试——使用黄金数据集验证 MCP 工具输出、Bean Validation 边界和多步智能体工作流在代码库演进过程中产生正确且一致的结果。

DZone 贡献者表达的观点仅代表其本人。