别再把大模型当成"万能工具人":Spring AI Multi-Agent 架构的工程化落地实战
单 Agent 能跑通 Demo,Multi-Agent 才能承载生产。真正的分水岭不是 Prompt 写得多漂亮,而是能否把智能体能力拆成可治理、可扩展、可观测、可灰度的工程系统。
版本与适用范围
本文面向 Java/Spring 技术栈,重点讨论如何基于 Spring AI、Spring AI Alibaba、A2A、Nacos、Redis、Kafka 与 Kubernetes 构建生产级 Multi-Agent 系统。
示例代码以 Java 21、Spring Boot 3.x、Spring AI 1.x API 风格为主,A2A 与 Nacos 部分参考 Spring AI Alibaba 1.0.0.4+ 及 Spring AI A2A 社区实现。由于 Spring AI 生态迭代较快,生产落地时应通过 BOM 锁定依赖版本,并以官方文档中的当前 API 为准。
一、为什么单 Agent 到生产一定会遇到天花板
很多团队做 AI 应用的第一版,通常是这样的:
一个超级系统提示词 + 一堆工具 + 一个 ChatClient = 一个看起来什么都会的 Agent
这个方案在原型阶段很快,但一旦进入电商客服、金融投顾、企业知识库、售后工单、运维诊断这类真实场景,就会暴露出结构性问题。
1.1 单 Agent 的四个核心问题
第一,认知负载过高。
一个 Agent 同时负责意图识别、订单查询、库存判断、优惠计算、退款审核、物流追踪和人工升级,模型需要在大量工具和规则之间做选择。工具越多,选择越不稳定,Prompt 越长,推理噪声越大。
第二,上下文污染严重。
订单问题、商品问题、售后问题、闲聊内容混在同一个上下文窗口里,模型很容易把历史信息误用于当前任务。例如用户上一轮问过 A 商品库存,下一轮查询 B 商品退款,模型可能错误复用 A 商品上下文。
第三,工程扩展性差。
单 Agent 无法按能力独立扩容。大促期间订单查询暴涨,本应只扩容订单 Agent,却不得不扩容整个 AI 服务,导致商品咨询、售后问答、闲聊能力也被动扩容,成本不可控。
第四,治理边界不清。
在企业里,订单团队、库存团队、售后团队、风控团队通常是不同组织。单 Agent 模式会把所有业务规则、工具定义和 Prompt 堆在一起,既难以审计,也难以灰度发布。
1.2 Multi-Agent 的本质不是"多个机器人聊天"
生产级 Multi-Agent 不是让几个大模型互相对话,而是把智能能力拆成多个具备明确职责、工具权限、上下文边界和治理策略的执行单元。
一个典型智能客服系统可以拆成:
| Agent | 职责 | 可访问工具 | 扩容特征 |
|---|---|---|---|
| Intent Agent | 意图识别、置信度评估 | 意图分类模型、规则库 | CPU 轻、低延迟 |
| Order Agent | 订单查询、取消、退款前置判断 | 订单服务、支付服务 | 大促高峰明显 |
| Product Agent | 商品咨询、库存、推荐 | 商品服务、库存服务、向量检索 | 搜索链路重 |
| AfterSale Agent | 物流、退换货、纠纷处理 | 物流服务、售后系统 | 长链路、强规则 |
| Risk Agent | 风控校验、敏感操作拦截 | 风控规则、黑名单、审计系统 | 强一致、低容错 |
| Human Handoff Agent | 人工升级、工单生成 | CRM、工单系统 | 业务兜底 |
架构视图如下:
Multi-Agent 的工程价值在于:
- 职责单一:每个 Agent 只处理一个业务域,Prompt 更短,工具更少,决策更稳定。
- 独立扩容:订单 Agent、售后 Agent、商品 Agent 可以按流量特征独立伸缩。
- 故障隔离:库存工具异常不应拖垮退款链路,推荐服务超时不应影响订单查询。
- 独立治理:不同团队可以独立维护自己的 Agent、工具、Prompt 和评估集。
- 安全可控:高风险能力可以挂接 Risk Agent 与人工确认,不把关键操作完全交给模型。
二、生产级 Multi-Agent 总体架构
2.1 分层架构
这套架构有一个重要原则:LLM 只负责需要推理的部分,不负责系统治理。
也就是说,权限、限流、熔断、幂等、审计、灰度、租户隔离、预算控制、结果校验,都应该由工程系统兜住,而不是写进 Prompt 后祈祷模型遵守。
2.2 核心组件职责
| 组件 | 职责 | 关键设计点 |
|---|---|---|
| Orchestrator | 接收请求、识别意图、路由 Agent、聚合结果 | 超时预算、并发执行、降级策略 |
| Agent Registry | 管理本地与远程 Agent 元数据 | 能力标签、版本、健康状态、灰度权重 |
| Context Manager | 管理会话、任务、链路上下文 | Redis 持久化、上下文压缩、租户隔离 |
| Tool Registry | 管理工具定义与执行入口 | 动态工具发现、权限控制、审计 |
| Guardrail | 输入输出安全与业务风控 | 敏感词、PII、越权、人工确认 |
| Event Bus | 异步任务和领域事件 | Kafka 解耦、削峰、最终一致 |
| Observability | 可观测与成本治理 | traceId、token、latency、tool error |
2.3 技术选型建议
| 场景 | 推荐方案 |
|---|---|
| 快速原型 | Spring AI + ChatClient + 本地工具 |
| 中小规模生产 | Spring AI + Redis ChatMemory/Context + Resilience4j + Micrometer |
| 多团队协作 | Spring AI Alibaba + Nacos A2A Registry + 独立 Agent 服务 |
| 高并发场景 | Java 21 虚拟线程 + Reactor 流式输出 + Redis 限流 + K8s HPA |
| 企业级治理 | A2A + MCP + Kafka + OpenTelemetry + 评估集 + 灰度平台 |
三、领域建模:先把 Agent 当成工程组件,而不是 Prompt
3.1 Agent 元数据模型
生产环境中,一个 Agent 至少要描述这些信息:
package com.example.agent.core;
import java.time.Duration;
import java.util.Set;
public record AgentDescriptor(
String name,
String version,
String description,
Set<String> capabilities,
Set<String> allowedToolNames,
Duration timeout,
int maxConcurrency,
boolean streamingSupported,
AgentStatus status
) {
public boolean canHandle(String capability) {
return status == AgentStatus.UP && capabilities.contains(capability);
}
}
enum AgentStatus {
UP, DEGRADED, DOWN
}
这里的重点不是字段本身,而是架构思想:Agent 必须像微服务一样被描述、注册、发现、限流、熔断和观测。
3.2 统一执行接口
package com.example.agent.core;
import reactor.core.publisher.Flux;
public interface Agent {
AgentDescriptor descriptor();
AgentResult execute(AgentRequest request);
default Flux<AgentStreamChunk> stream(AgentRequest request) {
return Flux.just(AgentStreamChunk.done(execute(request).answer()));
}
}
package com.example.agent.core;
import java.time.Instant;
import java.util.Map;
public record AgentRequest(
String requestId,
String tenantId,
String userId,
String sessionId,
String input,
AgentContext context,
Map<String, Object> attributes,
Instant deadline
) {
public boolean expired() {
return Instant.now().isAfter(deadline);
}
}
public record AgentResult(
String agentName,
String answer,
double confidence,
boolean requiresHuman,
Map<String, Object> facts
) {
public static AgentResult humanRequired(String agentName, String reason) {
return new AgentResult(agentName, reason, 0.0, true, Map.of("reason", reason));
}
}
public record AgentStreamChunk(String content, boolean done) {
public static AgentStreamChunk chunk(String content) {
return new AgentStreamChunk(content, false);
}
public static AgentStreamChunk done(String content) {
return new AgentStreamChunk(content, true);
}
}
统一接口的好处是,Orchestrator 不需要关心底层是本地 Agent、远程 A2A Agent、Graph 工作流,还是一个普通 Java 服务。
四、Skills 与 Tool Calling:从"能调用"升级为"可治理"
Spring AI 的 Tool Calling 把 Java 方法或函数暴露给模型,让模型在需要时调用业务系统。官方抽象里,工具最终会被建模为 ToolCallback,可以通过 @Tool、函数式 Bean 或自定义 ToolCallback 提供。
但生产环境不能只停留在 @Tool。一个可上线的 Skill 至少要具备:
- 元数据:名称、版本、负责人、风险等级、所属业务域。
- 权限:哪些 Agent、租户、用户角色可以调用。
- 治理:超时、重试、熔断、限流、降级。
- 审计:谁在什么上下文里调用了什么工具,参数和结果是否脱敏。
- 幂等:创建工单、取消订单、发起退款等写操作必须可重复提交。
4.1 Skill 元数据
package com.example.agent.skill;
import java.time.Duration;
import java.util.Set;
public record SkillMetadata(
String name,
String version,
String owner,
String domain,
SkillRiskLevel riskLevel,
Set<String> allowedAgents,
Duration timeout,
int requestsPerSecond,
boolean returnDirect
) {
}
enum SkillRiskLevel {
LOW, MEDIUM, HIGH
}
4.2 生产级库存查询 Skill
下面的代码展示一个更接近生产的工具实现:包含参数校验、限流、超时、指标、审计与异常转换。
package com.example.agent.skill.inventory;
import com.example.agent.skill.SkillMetadata;
import com.example.agent.skill.SkillRiskLevel;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import jakarta.validation.constraints.NotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Set;
@Component
public class InventorySkill {
private static final Logger log = LoggerFactory.getLogger(InventorySkill.class);
private final InventoryClient inventoryClient;
private final MeterRegistry meterRegistry;
public InventorySkill(InventoryClient inventoryClient, MeterRegistry meterRegistry) {
this.inventoryClient = inventoryClient;
this.meterRegistry = meterRegistry;
}
public SkillMetadata metadata() {
return new SkillMetadata(
"inventory_query",
"1.0.0",
"inventory-team",
"product",
SkillRiskLevel.LOW,
Set.of("product-agent", "orchestrator-agent"),
Duration.ofSeconds(2),
300,
false
);
}
@Tool(name = "inventory_query", description = "查询指定 SKU 在指定仓库的可售库存。只用于库存咨询,不用于锁库存。")
@CircuitBreaker(name = "inventoryClient", fallbackMethod = "fallback")
@RateLimiter(name = "inventoryClient")
public InventoryResponse queryInventory(
@ToolParam(description = "商品 SKU 编码,例如 SKU-10086") @NotBlank String sku,
@ToolParam(description = "仓库编码,可为空;为空时查询全国汇总库存") String warehouseCode
) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
InventoryResponse response = inventoryClient.query(sku, warehouseCode);
meterRegistry.counter("agent.skill.success", "skill", "inventory_query").increment();


320

被折叠的 条评论
为什么被折叠?



