1. 互通层:为什么单机跑通的 Agent,一上线就"失联"
先说个背景。上一期我们把单个 Agent 的构建、记忆管理和工具调用都盘了一遍,很多朋友照着做完之后,本地测试一切正常,结果一放到多进程、多服务的环境里就出问题:A 服务的 Agent 要调用 B 服务的 Agent,两边明明都在跑,却互相找不到,或者说找到了但调不通。这个问题几乎出现在每一个从单体走向分布式的 Agent 项目里,本质就是互通层没做。
AgentScope Java 在这块的设计思路很直接:用 A2A 协议把 Agent 的能力"暴露"成标准服务,再用 Nacos 做这些服务的注册与发现,让 Agent 之间像调用普通微服务一样互相协作。很多刚开始接触这个概念的同学会问:我已经有 HTTP 接口了,为什么还要搞 A2A?直接调接口不就行了?
答案是:如果你只是调用别人的写死接口,那叫系统集成,不叫 Agent 协作。A2A 解决的是"动态发现能力、动态编排任务、跨实现框架通信"这几件事。你的 Agent 可能用的是 AgentScope Java,对方的 Agent 可能跑在别的语言框架上,你们之间没有约定好 AgentCard、Task 状态机、Message 结构,就只能靠"人肉对齐参数",改一版崩一版。
这篇文章就围绕互通层展开,重点讲三件事:A2A 协议的核心机制、Nacos 接线的完整链路、以及我在实测中踩过的坑。内容是基于 AgentScope Java 当前主线的 A2A 模块经验,部分示例代码做了简化,类名和包路径以你实际引入的版本为准,但思路和排查路径是通用的。
2. 把 Agent "说出去":A2A 协议与 AgentCard 的核心机制
2.1 A2A 不是消息队列,是一套卡口明确的远程调用协议
很多人第一次看 A2A 文档会误以为它是类似 MQ 的异步消息系统,其实不对。A2A(Agent2Agent)是一个基于 JSON-RPC 的同步/异步混合协议,核心特征是:
- 每个 Agent 暴露一组标准端点,如
message/send、task/get、task/cancel,所有请求响应都走 HTTP POST + JSON。 - 一次任务(Task)有完整的生命周期状态:
submitted、working、input-required、completed、canceled、failed。 - 消息内容被拆成 Part,也就是多模态内容片段,可以同时包含文本、URL、结构化数据。
举个例子。假设你有一个"订单售后处理 Agent",另一个团队用别的框架写了一个"仓储库存 Agent"。两边要协作,A2A 的做法是:售后 Agent 收到用户诉求后,需要查库存,于是它构造一个 Task,把用户的诉求描述、相关订单 ID、需要查询的商品清单作为 Message 发出去,通过 A2A 端点发给库存 Agent。库存 Agent 处理完成后,返回一个completed状态的任务,带上库存结果 Part。整个过程是标准化的,两边都不需要关心对方的内部实现。
这种设计的好处在于:协议卡在"边界"上,边界以内的实现随便你折腾。这和微服务之间走 REST 是同一个逻辑,只不过 A2A 把"Agent 之间的对话"抽象成了可以编排、可恢复、带状态机的任务,而不是简单的请求响应。
2.2 AgentCard 里该放什么,不该放什么
A2A 协议里有一个经常被一笔带过但实际上很关键的组件:AgentCard。它本质是一个 JSON 描述文件,通常放在服务的/.well-known/agent.json路径下,用于告诉调用方"这个 Agent 是谁、能干什么、怎么调"。
我在项目里维护过三个 Agent 的卡片,第一版把"能干什么"写得特别详细,几乎把每个工具函数的参数都列进去了,结果维护成本极高,改一个字段就要同步更新卡片。后来我把卡片收敛成下面这个结构:
{ "name": "order-after-sale-agent", "description": "处理订单售后诉求,可查询订单状态、发起退款、生成工单", "url": "https://agent-gateway.example.com/a2b", "version": "1.0.0", "capabilities": { "functionCalling": true, "streaming": false, "multiModal": false }, "skills": [ { "name": "query_order", "description": "根据订单号查询订单当前状态", "parameters": { "type": "object", "properties": { "orderId": { "type": "string" } }, "required": ["orderId"] } } ] }一个核心教训是:AgentCard 是"发现"用的,不是"文档"用的。它要让远程 Agent 在几毫秒内判断"你能不能满足我的诉求、我该不该把任务发给你",所以description和skills里的摘要信息一定要写清楚边界。比如你的 Agent 只能处理国内订单,就在 description 里直接写"prompt:仅支持国内订单,海外订单请转人工",省得调用方把任务打过来再被打回去,来回浪费一次远程调用。
另一个容易忽略的点是url字段要和实际部署环境对齐。本地联调时可以是 localhost 地址,一旦接到 Nacos 上,这个 url 就要改成网关或服务实例的对外地址。否则就会出现:Nacos 里能看到服务,Agent 也选择了实例,但发请求时发现 AgentCard 里的 url 是别人旧环境的地址,直接 404。
3. Nacos 接线:Agent 服务如何被动态发现
3.1 服务注册与发现的完整链路
Nacos 在整套接线里扮演的角色是服务注册中心 + 配置中心。Agent 服务启动后,会把自身的 IP、端口、健康状态、元数据注册到 Nacos;调用方不再需要写死对端地址,而是通过服务名去 Nacos 拉取实例列表,从中选一个健康实例发起 A2A 调用。
接线链路可以拆成五步:
- Agent 服务启动,读取配置,向 Nacos 注册自身实例信息。
- Nacos 注册中心维护服务名到实例列表的映射,并通过心跳机制感知实例存活状态。
- 调用方从 Nacos 查询目标服务名,拿到一个或多个健康实例。
- 调用方构造 A2A 请求,向目标实例的 A2A 端点发送 HTTP POST。
- 目标 Agent 处理请求,返回结果,任务状态流转更新。
这个链路看着简单,但实际接线时容易被三个细节卡住:服务名命名规范、实例元数据设计、健康检查配置。
先讲服务名。我建议按照"业务域-角色-用途"的格式命名,比如agent-order-service、agent-logistics-service,不要用"agent1"、"agent2"这种没有任何语义的名字。因为在多 Agent 协作场景里,服务名就是 Agent 的"电话号码",别人通过名字找到你,名字起得清晰能省很多沟通成本。
再讲元数据。Nacos 注册实例时可以携带自定义 metadata,这是一个很容易被浪费掉的字段。你可以把 AgentCard 的关键信息直接塞进 metadata,比如agentName、version、capabilities。这样调用方在拉取实例列表时,不需要先发一次 HTTP 请求去读 AgentCard,直接通过元数据就能做简单过滤。这在高频调用场景下对性能和稳定性都有帮助。
健康检查配置同样值得上心。默认的心跳机制依赖服务实例主动上报,如果 Agent 的 A2A 端点所在端口和 Nacos 健康检查端口配置不一致,会出现服务"注册成功但始终不健康"的诡异情况。我在调试中就遇到过这个问题,客户端拉到的实例一直标记为 unhealthy,排查了半天才发现是spring.cloud.nacos.discovery.port和实际暴露 A2A 端点的端口没对齐。
3.2 metadata 设计:让调用方一眼看懂 Agent 能力
接线的难点往往不是"注册上去",而是"让对方快速知道该不该调用你"。我推荐在 Nacos 实例元数据里固定维护以下几项:
| metadata 字段 | 示例值 | 作用 |
|---|---|---|
| agentName | order-after-sale-agent | Agent 的唯一标识,服务名前缀 |
| version | 1.0.0 | 能力版本,用于灰度兼容 |
| owner | aftersale-team | 团队负责人,方便排查 |
| capabilities | functionCalling,streaming | 能力标签,用于快速过滤 |
| heartbeatInterval | 5000 | 心跳间隔,帮助调用方预判故障 |
这里有一个经验:metadata 里的能力和 AgentCard 里的能力必须保持同步,否则调用方根据 metadata 判断你支持 streaming,结果发来流式请求发现不支持,任务直接失败。我目前的处理方式是做一个启动时的配置校验,把 metadata 和本地 AgentCard 做一次比对,不一致就打印告警日志,宁可启动失败也不要带病上线。
这个设计还有一个额外收益:当你想做 Agent 分组或灰度时,metadata 直接作为路由维度。比如新版本 Agent 上线只注册 10% 的实例,metadata 标version=2.0.0-canary,调用方通过 Nacos 的权重策略就能把部分任务灰度到新版本上,不需要改动任何 A2A 调用代码。
4. 端到端接线实操:AgentScope Java + Spring Boot + Nacos
4.1 环境准备与依赖配置
这一部分我直接给一份可以照抄的依赖清单。项目的基本框架是 Spring Boot 3.x + AgentScope Java 当前主线版本 + Nacos 服务端 2.x。
<dependency> <groupId>com.alibaba.agentscope</groupId> <artifactId>agentscope-agent</artifactId> <version>${agentscope.version}</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>${alibaba.cloud.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>Nacos 服务端我建议直接用 Docker 起一个做开发验证:
docker run --name nacos-server -p 8848:8848 -p 9848:9848 \ -e MODE=standalone \ nacos/nacos-server:v2.3.0然后配置 application.yml,注意namespace和group一定要提前定好。不同环境的 Nacos 命名空间必须隔离,不然测试环境的 Agent 和生产环境的 Agent 互相"串线",整个协作就乱了。
spring: application: name: order-after-sale-agent cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: dev group: AGENT_GROUP register-enabled: true4.2 实现一个业务 Agent 并暴露为 A2A 端点
这里用一个"订单查询 Agent"做例子。在 AgentScope Java 里,Agent 核心逻辑是重写消息处理方法,把输入 Message 解析成业务需求,执行对应的工具函数,再封装成输出 Message 返回。
@Component public class OrderQueryAgent extends AgentBase { private final OrderService orderService; public OrderQueryAgent(OrderService orderService) { this.orderService = orderService; } @Override protected Message invoke(Message input) { // 解析入参,这里简化成只处理文本文本内容 String text = input.getTextContent(); if (text == null || text.isBlank()) { return Message.textMessage( "参数不完整,需要提供订单号", "order-query-agent" ); } // 调用业务服务查询订单 OrderInfo info = orderService.queryByOrderId(text.trim()); if (info == null) { return Message.textMessage("订单不存在:" + text, "order-query-agent"); } // 把查询结果封装为结构化Part返回 return Message.textMessage( "订单状态:" + info.getStatus() + ",金额:" + info.getAmount(), "order-query-agent" ); } }有了 AgentBean之后,最核心的一步是把它暴露成 A2A 远程端点。AgentScope Java 默认提供的服务端适配模块可以帮我们省去大量协议样板代码,你只需要把 Agent 实例挂载到路由上,框架会自动完成协议转换。
@RestController @RequestMapping("/a2b") public class A2AController { private final A2AServerManager a2aServerManager; public A2AController(A2AServerManager a2aServerManager) { this.a2aServerManager = a2aServerManager; } @PostMapping("/message/send") public Task sendMessage(@RequestBody IncomingMessage message) { return a2aServerManager.sendMessage(message); } @PostMapping("/task/get") public Task getTask(@RequestBody TaskQuery query) { return a2aServerManager.getTask(query.getTaskId()); } @GetMapping("/.well-known/agent.json") public AgentCard getAgentCard() { return a2aServerManager.generateAgentCard(); } }需要注意,不同版本的 AgentScope Java 在端点路径上可能略微不同,但/.well-known/agent.json和message/send是协议层的约定,尽量保持一致,降低对接成本。如果你是自己手写 Controller,建议先别急着做复杂逻辑,直接把 AgentCard 和消息收发跑通再逐步加功能。
4.3 编写客户端:通过 Nacos 找到 Agent 并发起任务
服务端暴露好之后,客户端的关键是从 Nacos 拿实例列表,然后选中一个实例发起 A2A 调用。这一步的"选实例"逻辑很要命。最简单可靠的策略是:先过滤健康实例,再按权重随机选择一个。
@Component public class AgentServiceInvoker { private final NamingService namingService; public AgentServiceInvoker(NamingService namingService) { this.namingService = namingService; } public String invokeAgent(String serviceName, String messageText) throws Exception { // 1. 从 Nacos 拉取健康实例 List<Instance> instances = namingService.selectInstances(serviceName, true); if (instances == null || instances.isEmpty()) { throw new RuntimeException("没有可用Agent实例:" + serviceName); } // 2. 简单的随机选择,生产环境可以换成带权负载 Instance instance = instances.get(ThreadLocalRandom.current().nextInt(instances.size())); String url = "http://" + instance.getIp() + ":" + instance.getPort() + "/a2b"; // 3. 构造 A2A 请求体 Map<String, Object> body = new HashMap<>(); body.put("taskId", UUID.randomUUID().toString()); body.put("localAgentId", "user-service"); body.put("remoteAgentId", serviceName); body.put("message", Map.of( "role", "user", "content", messageText )); // 4. 发起 JSON-RPC 调用 RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Map> resp = restTemplate.postForEntity(url + "/message/send", body, Map.class); return resp.getBody().toString(); } }这段代码最核心的价值不在逻辑,而在于"它揭示了 Agent 协作和普通 RPC 调用的关系"。A2A 调用本质上就是一次 HTTP 请求,不要把它想得过于魔幻。只是它的请求体和响应体要符合协议规范,任务状态要按协议状态机走。
4.4 运行验证与调用链分析
接线完成后别急着上线,先把整条链路的日志打全。我习惯在三个位置打日志:Nacos 注册成功、收到入站消息、发送出站消息。每个日志都要带上taskId和serviceName,这样出问题的时候能通过同一个 taskId 把一条调用链串起来。
一个简单的压测验证:
# 先看 AgentCard 是否可访问 curl http://localhost:8080/a2b/.well-known/agent.json # 再发一条消息 curl -X POST http://localhost:8080/a2b/message/send \ -H "Content-Type: application/json" \ -d '{"taskId":"demo-test-001","localAgentId":"caller","remoteAgentId":"order-after-sale-agent","message":{"role":"user","content":"查询订单 OG123456"}}'如果返回的内容里包含正常的订单状态信息,且 Nacos 控制台上能看到order-after-sale-agent的实例处于健康状态,那么这条接线就基本跑通了。
5. 踩坑实录:接线过程中最折磨人的五个问题
5.1 AgentCard 404 或地址错乱
症状:调用方在 Nacos 中拿到了 Agent 的实例地址,但访问/.well-known/agent.json时返回 404,或者拿到的是一个过期环境的地址。
根因:我把 AgentCard 的url字段和 Nacos 元数据里的uri字段都配成了内网地址,而调用方在外网环境。内网地址它当然访问不到。
处理方式:统一用一个网关域名作为 Agent 的对外地址。比如如果你的 Agent 服务在网关后面,AgentCard 的url就配成网关对外的域名加路径,Nacos 注册的 ip/port 则保留服务实例实际的内网地址。两者各司其职:Nacos 里的地址用于 VPC 内服务发现,AgentCard 里的 url 用于跨网或跨域标识。
5.2 Nacos 心跳丢失,服务列表时有时无
症状:服务启动后,Nacos 控制台能看到实例,但过几分钟就变成了不健康,再过一会儿直接消失,然后又重新出现。
根因:这个坑大概率出在端口配置和网络隔离上。Nacos 2.x 的 gRPC 端口是主端口加 1000 偏移(8848 对应 9848),如果防火墙只开了 8848 而没开 9848,心跳上报会间歇性失败。
处理方式:把 8848 和 9848 都放通,确认服务器安全组规则别漏。还有一个容易忽略的点:Agent 服务所在的容器如果做了端口映射,spring.cloud.nacos.discovery.ip和port要显式配置成宿主机可达的地址,否则注册的是容器内网 IP,别的服务根本路由不过去。
5.3 Message 序列化不一致,中文乱码或结构丢失
症状:调用方发了个结构化的消息,对端 Agent 收到的content是奇怪的乱码,或者嵌套的 Map 结构丢失了。
根因:两边用的 HTTP 客户端/服务端对 JSON 编解码的默认行为不一致。比如 Jackson 的默认配置在某些版本会对未知字段报错,或者对Map<String, Object>的 value 类型推断错误。
处理方式:给 HTTP 调用统一设置produces和consumes为application/json;charset=UTF-8,同时避免在 Message 里塞过于复杂的嵌套泛型。A2A 协议的 Message 本质是 Part 列表,尽量用扁平结构,如content字符串 +metadata简单键值对,复杂对象放到artifact里用文件或内容地址引用。
5.4 Task 状态机没同步好
症状:调用方发出任务后一直轮询task/get,返回的状态始终是working,但 Agent 那边其实已经处理完了。
根因:实现 A2A 服务端时,Task 状态没有同步更新。
处理方式:任务处理结束后,不要只返回业务结果,一定要把 Task 状态显式置为completed或failed。这尤其容易发生在异步处理场景里。如果是异步任务,建议单独维护一个 Task 存储,处理线程完成后更新状态,再开放查询接口。不要试图用线程内的局部变量去驱动状态查询,跨请求的临时状态丢失是埋雷高发区。
5.5 安全与频控
症状:Agent 端点暴露到公网后,被扫描器刷了一波请求,Nacos 服务列表里出现一堆陌生的服务名。
处理方式:A2A 端点不要裸奔。至少加一层简单的鉴权逻辑,比如Authorization头校验或agent-id白名单。同时配合 Nacos 的鉴权能力控制注册权限。频控方面,可以引入 sentinel 结合 Nacos 做限流配置动态下发,这个我在后面的扩展章节细说。
6. 互通层的延伸:配置中心、动态刷新与多环境路由
6.1 Nacos 配置中心承载 Agent 配置的动态更新
Agent 的很多状态性配置,比如系统提示词、工具启停开关、模型参数,都适合放在 Nacos 配置中心里做动态下发。典型场景:某个 Agent 的系统提示词写得不够好,想调整策略,不用重新发布服务,直接在 Nacos 里改配置,Agent 通过监听配置变更自动加载新提示词。
实现思路是引入 Nacos 配置监听器。AgentScope Java 的 Agent 对象在运行时读取配置,我们需要把"配置读取"这一步做成可以动态刷新的模式。
@Component public class DynamicPromptUpdater { private final AgentBase agent; private final ConfigService configService; public DynamicPromptUpdater(AgentBase agent, ConfigService configService) { this.agent = agent; this.configService = configService; } @PostConstruct public void registerListener() throws NacosException { configService.addListener("agent-prompt", "AGENT_GROUP", new Listener() { @Override public Executor getExecutor() { return Executors.newSingleThreadExecutor(); } @Override public void receiveConfigInfo(String configInfo) { // 动态更新Agent的系统提示词 agent.updateSystemPrompt(configInfo); } }); } }需要注意的是,动态更新虽然方便,但对 Agent 这种"会话状态敏感"的组件要谨慎。如果一个长任务正在执行中,你在中途改了系统提示词,可能导致任务行为不一致。我的建议是只对非关键状态做热更新,比如模型温度、超时时间、工具开关,而系统提示词尽量走灰度发布。
6.2 多环境路由与灰度发布
前面提到 metadata 里的 version 字段,配上 Nacos 后可以做很灵活的路由。比如你有 5 个订单查询 Agent 实例,其中 1 个是 v2.0 新版本,你想让 10% 请求打到新版,其他打到 v1.0。Nacos 支持设置实例权重,按权重分配流量。
spring: cloud: nacos: discovery: metadata: version: v2.0 weight: 1另一个思路是根据 Agent 卡片的 capabilities 做路由。例如某实例声明自己支持多模态,调用方拿到实例列表后先按capabilities过滤,只保留带multiModal的实例,再做二次分发。这种"先过滤再选择"的模式,比单纯随机选择能显著降低无效调用。
6.3 限流配置的下发与熔断
最后提一下限流。Agent 端点暴露后,最怕的不是业务问题,而是被大量无效请求打挂。我在前面的项目里尝试过把 sentinel 和 Nacos 结合起来:限流规则统一存在 Nacos 配置中心,sentinel 客户端监听配置变更,动态调整每个 Agent 端点的 QPS 阈值。
这个组合解决了一个很实际的痛点:你的 Agent 是供多个业务方调用的,每个业务方的优先级不同。给内部核心业务配额高一些,给外部试用的配额低一些,这个配额比不是一锤定音,而是能通过 Nacos 实时调整。限流规则有一个routeId的概念,可以按调用方维度做精细化控制。
在写这套配置的时候,启动时的限流规则不要写在代码里,直接写在 Nacos 上,否则改一个阈值又得重新部署。我自己遇到过的坑是:限流规则没放在 Nacos 里,结果压测时 QPS 超了,只能临时改代码重新上线,耽误了半小时。从那以后我所有的流控规则都走配置中心。
7. 写在最后:互通层的核心理念
做了这么多期的实战,走到互通层这一期,我心里最有感触的一点是:不要让 Agent 之间的协作像"人与人之间的私聊",而要让它们像"服务与服务之间的 API 调用"。私聊没有标准、没有契约、没有状态回溯,而 A2A + Nacos 的组合恰恰把这些补上了。
如果你也正在做多 Agent 系统,我的建议是:先把 AgentCard 写好,写清楚边界;再把 Nacos 的 metadata 设计好,别浪费这个免费的能力透传入口;最后把任务状态机跑顺,日志打全。这三件事做到位,互通层的基本盘就稳了。