Java 转 Agent 开发之路(十五):部署边界——单体、分布式、A2A 与 Nacos 注册
当交易 Agent 需要由支付团队维护、搜索 Agent 需要由搜索团队维护,当某个 Agent 的流量增长曲线和其他 Agent 完全不同,当 Python 侧的实验性 Agent 需要和 Java 侧的事务性系统协作时,“所有 Agent 在一个进程里”就不再是一个中性的技术选择,而是一个组织矛盾和可用性风险的藏身之处。Spring AI Alibaba 的文档把单进程多 Agent 的代价说得很直接:组织协同困难(两个团队维护同一份代码)、可用性难保证(一个 Agent 崩溃拖垮全部)、存在安全风险(内存与上下文共享导致权限边界模糊)。
这一篇要回答的是:什么时候该从单体走到进程边界,A2A 协议和 Nacos 注册各自解决哪一段问题,以及跨语言双栈的边界应该画在哪里。核心判断先放在前面:进程边界的意义不在于“多了几个 Agent”,而在于把故障域、发布节奏和权限边界从“代码约定”变成“物理隔离” 。如果拆完之后所有 Agent 仍然共享同一个数据库、同一个安全上下文、同一条发布流水线,拆分只是增加了网络跳数。
一、单体阶段:模块化不是为了好看,是为了让替换半径可控
在讨论拆进程之前,先确认单体阶段应该做什么。一个没有被模块化的单体,拆进程时付出的代价不是“搬家”,是“考古” ——依赖纠缠在一起,你无法判断哪个类属于哪个边界。
第六篇已经给出了一个参考的包结构:config(模型和向量库配置)、client(ChatClient 封装)、advisor(记忆和上下文策略)、rag(文档 ETL 与检索)、tool(业务工具)、mcp(外部工具适配)、workflow(任务编排)、eval(评测用例)、tenant(多租户过滤)。这个拆分的核心目的不是“看起来整洁”,而是让每一个模块的替换和迁移有一个明确的接缝:把 Redis 记忆换成数据库,只动 advisor 和 config;把本地检索换成远程 MCP 检索服务,只动 tool 和 mcp;把 Planning 从进程内循环改成远程 A2A 调用,只动 workflow。
单体阶段的合理目标不是“永远不拆”,而是“当拆分到来时,被拆出去的那个模块已经是一个边界清晰的单元” 。如果一个 ReactAgent 的构造、工具注册、Prompt 拼装和业务校验全部散落在 Controller 和 Service 里,那么它被拆成独立 Agent 服务的那一天,等于要重新理解一遍整条链路。模块化的价值在那一刻兑现。
二、什么时候该拆进程:三个判断条件,以及“一个都不该拆”的反面
拆分决策和十年前“单体要不要拆微服务”是同一类问题,但 Agent 的拆分比微服务更复杂,因为 Agent 的行为不是确定的,拆分之后的路由错误和上下文损耗更难定位。一个可操作的判断框架是三个条件:
条件一:Prompt 是否已经膨胀到难以维护。 一个 Agent 要处理知识检索、工单查询、转人工三个场景,System Prompt 里就要定义三套规则。每加一个新场景,Prompt 就长一段,改一句担心影响全局。当 Prompt 的场景数量和条件分支超过了单个人能在头脑中完整维护的规模,拆分就是合理的信号。
条件二:不同能力对模型的要求是否已经分化。 知识检索需要大模型的理解能力,意图分类只需要小模型的分类能力。如果所有能力被绑在一个 Agent 上,你只能用同一个模型服务所有场景——大模型做简单分类是浪费,小模型做复杂检索是能力不足。拆分让每个 Agent 独立选模型、独立定温度、独立配置工具集,这是单 Agent 做不到的。
条件三:不同场景的流量是否需要独立扩容。 大促期间,物流查询的咨询量可能翻几倍,但复杂退换货咨询的占比并不高。单 Agent 只能整条链路一起扩容;拆开之后,物流 Agent 单独扩容,知识 Agent 保持不动。当流量曲线在场景之间显著不均衡时,拆分是成本效率问题,不是架构洁癖。
反过来,什么时候不该拆? 当一个 Agent 的表现未达预期时,先尝试让它可靠运行的最小改动,而不是直接拆。如果请求被路由到了错误的功能,先检查工具描述和 System Prompt 是否可以把它们区分得更清楚;如果 Agent 需要新能力,考虑把它作为工具或知识源加到同一个 Agent 里。单独部署一个 Agent 属于较大的改动,应该留到较小的改动无法解决问题时再考虑。微软的经验判断也值得参考:当主 Agent 拥有超过 30–40 个可选动作(工具、主题、其他 Agent)时,路由准确率可能开始下降;在此之前,优化描述比拆分更划算。
最关键的一条判断是:拆分之后,每个子 Agent 是否真的只需要局部上下文。 如果拆分后每个子 Agent 仍然需要完整的全局对话历史、完整的用户画像、完整的工具执行记录,那么拆分只是把一个大 Prompt 拆成了若干个小 Prompt 再加一层通信开销,上下文膨胀的乘法效应(第八篇、第十四篇讨论过)不会消失。进程边界应该对应上下文边界,而不是为了“多 Agent”而多 Agent。
三、A2A 协议:进程之间的“接口契约”,而不是又一个工具调用层
当拆分发生之后,第一个问题不是“怎么调用”,而是“怎么让对方知道我能做什么、我该找谁”。
A2A(Agent-to-Agent)协议解决的就是这件事。Google 推出的 A2A 协议面向“智能体与其他使用不同框架、部署在不同机器、不同公司的智能体进行有效通信和协作”的场景,它定义的核心不是消息格式,而是 Agent Card——一个描述 Agent 身份、能力、端点和认证方式的元数据契约。Agent Card 通常暴露在 /.well-known/agent.json 路径下,包含名称、描述、版本、提供者、支持的能力、默认输入输出模式和技能列表。
A2A 和 MCP 解决的是不同层面的问题,这一点必须分清。 MCP(第十一篇)解决的是工具的标准化发现和调用:模型看到工具 Schema,请求执行,运行时把结果回灌。A2A 解决的是 Agent 与 Agent 之间的协作:一个 Agent 需要把任务委派给另一个独立的 Agent 进程,对方有自己的规划、自己的工具、自己的模型。MCP 的调用方是模型,A2A 的调用方是 Agent。一个 Agent 内部可以使用 MCP 工具,同时通过 A2A 与其他 Agent 协作——两者是叠加关系,不是替代关系。
Spring AI Alibaba 对 A2A 的封装把上述流程简化为三个组件:A2A Server 将本地 ReactAgent 暴露为 A2A 服务,A2A Registry 负责 AgentCard 的注册(支持 Nacos),A2A Discovery 负责远程 Agent 的发现。一个本地 ReactAgent 只要配置了 A2A Server,就会自动生成 AgentCard 并暴露两个端点:GET /.well-known/agent.json(元数据)和 POST /a2a(JSON-RPC 消息端点)。
这里有一个容易踩的配置坑:registry.enabled 和 discovery.enabled 是两个独立的开关。registry.enabled: true 才会把本地 Agent 的 AgentCard 注册到 Nacos,discovery.enabled: true 才能通过 AgentCardProvider 发现其他 Agent。只开 registry 不开 discovery,你的 Agent 能被别人找到,但它找不到别人;反过来则相反。
四、Nacos 注册中心:服务发现的职责边界在哪里
Nacos 3.1.0 引入 A2A 注册中心,提供了轻量化的 Agent 服务注册与发现能力:支持发布与发现 AgentCard 及 Agent 服务端点,当前支持基于 Agent 名称的精准发现和模糊搜索,后续会扩展基于技能标签的多维度检索。Spring AI Alibaba 与 Nacos 的集成让这套能力变成“开箱即用”:Agent 注册、发现、负载均衡都在框架层完成。
但注册中心解决的是“找到谁”,不是“能不能调用”和“调用是否安全” 。Nacos 提供的是地址簿和元数据管理,它不替代以下三件事:
第一,认证与授权不在注册中心里。 AgentCard 的发现端点通常是公开的(/.well-known/agent.json),这是有意的设计——发现应该是公开的,但调用必须鉴权。A2A 的 Java 实现中,Client 和 Server 都支持可选的 shared-secret 认证(X-API-Key header),Server 在 /tasks/send 上校验密钥,而 AgentCard 端点保持开放。更完整的 JWT/OIDC 认证在企业场景中需要在 Server 侧或网关层自己补,不能指望注册中心替你做。
第二,租户隔离需要在 A2A Server 的边界上完成。 一个 Nacos 注册中心可能同时承载多个租户的 Agent。A2A Java SDK 的 AgentCardRouter 提供了按租户解析不同 AgentCard 的机制,但这要求应用层显式实现租户路由,而不是“注册了就能被正确隔离”。如果没有租户维度的 AgentCard 路由和调用鉴权,Nacos 中的 Agent 列表本身就是一个信息泄露面——知道有哪些 Agent 存在,和能够调用它们是两件事,但前者已经不应该对无关租户可见。
第三,超时、重试和幂等是调用方必须处理的问题。 注册中心告诉你“服务在哪个地址”,但网络分区、Agent 进程重启、A2A 调用超时,这些故障模式不会因为有了服务发现就消失。A2A 的远程调用和第十一篇 MCP 的远程工具调用一样,需要在调用方实现超时、重试和幂等键,而不是假设“发现了就一定能调通”。
一个务实的部署判断是:Nacos 适合作为 A2A 和 MCP 的统一注册中心,把 Agent 和工具的服务发现收敛到同一个基础设施上。但注册中心是控制面,不是数据面——它管发现和元数据,不管权限强制和调用可靠性。这两件事需要在 Agent 服务自身和网关层分别处理。
五、Java + Python 双栈:进程隔离是主流方案,不是“混搭”的妥协
第十五篇必须面对一个系列前十四篇刻意没有展开的现实:企业级 Agent 系统很少是纯 Java 的。
纯 Java 做 Agent 的优势和短板都很清楚:与 Spring Cloud 生态无缝集成、强类型、事务安全、分布式成熟,但 AI 开源框架迭代慢,多模态、复杂多 Agent 协作和向量检索生态相对薄弱,复杂 Agent 流程的开发成本高、实验调参效率低。纯 Python 的镜像正好相反:AI 工具链完善、RAG 和智能体编排开箱即用,但不适合高并发交易、分布式事务和复杂权限管控,大规模集群的稳定性和链路追踪成本高。
主流的落地架构是“Java 全栈做业务底座 + Python 智能体做 AI 大脑” :Java 承载交易、权限、分布式、高可用核心链路,Python 依托 LangChain、LangGraph 等框架搭建具备规划、记忆、工具调用能力的智能体,通过 REST/gRPC、消息队列或 MCP 完成跨语言协同。
这个分工在部署边界上的含义是明确的:进程隔离是跨语言协作的主流方案,不是“退而求其次”的妥协。Spring AI 和 LangChain4j 是两套独立的抽象,如果直接把两套框架混在同一个 JVM 的同一个 Service 里——一会儿 ChatClient、一会儿 ChatModel——抽象不互通、工具和向量存储互不识别,后期维护会变成地狱。正确的做法是:Java 侧管业务网关、鉴权、限流、计费和简单工具调用,复杂 Agent 任务转发给 Python 服务;Python 侧只负责 Agent 编排、复杂 RAG 和状态机,输出标准化 JSON,不返回 Prompt 内部细节。
跨语言通信的方式按耦合程度递增有三种选择:普通 HTTP 最简单,但会话状态要自己维护;MCP 更标准化,Python 服务把能力暴露为 MCP Server,Java 侧作为 MCP Client 调用,Spring AI 原生支持这条路径,模型不需要关心底层是 LangChain 还是别的实现;消息队列适合长耗时异步 Agent,Java 侧发任务到 RocketMQ/Kafka,Python 消费执行后回写结果。三种方式的选择标准不是“哪个更先进”,而是调用的实时性要求和故障容忍度——同步对话用 HTTP 或 MCP,长任务用消息队列。
六、收束:部署边界的终点,是让每一个故障域和权限域都有物理承载
这一篇从单体模块化讲到进程拆分时机,再到 A2A 协议、Nacos 注册和 Java/Python 双栈,核心可以收束为三个判断。
第一,拆进程的前提是模块化已经就位,而拆进程的触发条件是三个具体的信号:Prompt 膨胀到难以维护、不同能力需要不同模型、不同场景的流量曲线不均衡。 在信号出现之前,优化描述和工具划分比拆分更划算;在信号出现之后,拆分是为了让故障域和发布节奏获得物理隔离,而不是为了“多 Agent”这个标签。
第二,A2A 解决“Agent 之间的契约”,Nacos 解决“找到谁”,但两者都不解决“能不能安全地调用”。 AgentCard 是公开的发现元数据,调用鉴权、租户隔离、超时幂等必须在 A2A Server 和调用方分别实现。注册中心是控制面,不是安全边界。
第三,Java + Python 双栈的部署边界应该画在进程之间,而不是同一个 JVM 里混用两套框架。 Java 管事务、权限、高可用,Python 管复杂编排和实验迭代,通过 HTTP/MCP/消息队列协作。进程隔离不是妥协,是让两种技术栈各自发挥长处的唯一可靠方式。
- 点赞
- 收藏
- 关注作者
评论(0)