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 的异步消息系统其实不对。A2AAgent2Agent是一个基于 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 字段示例值作用agentNameorder-after-sale-agentAgent 的唯一标识服务名前缀version1.0.0能力版本用于灰度兼容owneraftersale-team团队负责人方便排查capabilitiesfunctionCalling,streaming能力标签用于快速过滤heartbeatInterval5000心跳间隔帮助调用方预判故障这里有一个经验metadata 里的能力和 AgentCard 里的能力必须保持同步否则调用方根据 metadata 判断你支持 streaming结果发来流式请求发现不支持任务直接失败。我目前的处理方式是做一个启动时的配置校验把 metadata 和本地 AgentCard 做一次比对不一致就打印告警日志宁可启动失败也不要带病上线。这个设计还有一个额外收益当你想做 Agent 分组或灰度时metadata 直接作为路由维度。比如新版本 Agent 上线只注册 10% 的实例metadata 标version2.0.0-canary调用方通过 Nacos 的权重策略就能把部分任务灰度到新版本上不需要改动任何 A2A 调用代码。4. 端到端接线实操AgentScope Java Spring Boot Nacos4.1 环境准备与依赖配置这一部分我直接给一份可以照抄的依赖清单。项目的基本框架是 Spring Boot 3.x AgentScope Java 当前主线版本 Nacos 服务端 2.x。dependency groupIdcom.alibaba.agentscope/groupId artifactIdagentscope-agent/artifactId version${agentscope.version}/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId version${alibaba.cloud.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencyNacos 服务端我建议直接用 Docker 起一个做开发验证docker run --name nacos-server -p 8848:8848 -p 9848:9848 \ -e MODEstandalone \ 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 拉取健康实例 ListInstance 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 请求体 MapString, 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(); ResponseEntityMap 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 的默认配置在某些版本会对未知字段报错或者对MapString, Object的 value 类型推断错误。处理方式给 HTTP 调用统一设置produces和consumes为application/json;charsetUTF-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 设计好别浪费这个免费的能力透传入口最后把任务状态机跑顺日志打全。这三件事做到位互通层的基本盘就稳了。