基于Spring AI构建多智能体协作应用:从概念到生产部署
在实际工程实践中AI 技术特别是大语言模型LLM和 AI Agent 的集成正从概念验证快速走向生产部署。开发者面临的挑战不再是简单的 API 调用而是如何构建稳定、可控、可维护的 AI 应用架构。Spring AI 作为一个旨在简化 AI 应用开发的框架为 Java 开发者提供了将大模型能力融入 Spring Boot 生态系统的标准化路径。本文将以一个模拟的“AI 小镇”智能体协作项目为背景深入探讨如何基于 Spring AI 进行工程实践涵盖从项目初始化、核心概念理解、多智能体Agent协作实现到生产环境部署的完整链路。无论你是希望将 AI 能力集成到现有业务系统的后端工程师还是对构建自主协作的 AI 应用感兴趣的研究者本文都将提供一套可落地的实践方案。1. 理解 Spring AI 的核心价值与定位在开始编码之前必须厘清 Spring AI 解决的根本问题。当前直接调用各大厂商的 AI 模型 API 存在几个显著的工程痛点首先不同模型提供商的 API 接口、参数命名、认证方式各异导致代码强耦合切换成本高其次复杂的提示词Prompt工程、上下文管理、函数调用等逻辑容易与业务代码混杂难以维护最后缺乏对 AI 应用特有概念如“对话”、“文档检索”的一流抽象。Spring AI 的定位正是为了解决这些问题。它并非一个 AI 模型本身而是一个抽象层和集成框架。其核心价值在于统一的 API 抽象通过定义ChatClient、EmbeddingClient、ImageClient等通用接口让开发者可以用同一套代码与 OpenAI、Azure OpenAI、Anthropic、本地部署的 Ollama 等多种模型后端进行交互。切换模型提供商通常只需修改配置无需重写业务逻辑。Prompt 模板与管理将提示词从代码中剥离支持外部化配置和模板化便于迭代优化和国际化。上下文管理内置了对对话历史Chat History的存储与检索机制简化了多轮对话的实现。AI 原生功能集成对 RAG检索增强生成、函数调用Function Calling、AI Agent 等高级模式提供了框架级别的支持降低了实现复杂度。Spring 生态无缝集成作为 Spring 家族的一员它能天然地享受 Spring Boot 的自动配置、依赖注入、外部化配置、监控等能力使得 AI 功能可以像数据库、消息队列一样成为企业应用的一个标准组件。理解这一点至关重要使用 Spring AI你是在用 Spring 的方式构建 AI 应用而非在 Spring 应用里硬塞一段 AI 代码。2. 环境准备与项目初始化一个清晰的工程环境是成功的第一步。我们将创建一个标准的 Spring Boot 项目并集成 Spring AI。2.1 环境与工具要求在开始前请确保你的开发环境满足以下要求组件要求说明JDK17 或更高版本Spring Boot 3.x 的硬性要求。构建工具Maven 3.6 或 Gradle 7.x本文使用 Maven 进行演示。IDEIntelliJ IDEA (推荐) 或 VS Code with Spring Boot 插件需要良好的 Spring 支持。模型访问至少一个可用的 AI 模型 API例如 OpenAI API Key或本地运行的 Ollama。2.2 创建 Spring Boot 项目最快捷的方式是使用 Spring Initializr 。选择以下配置Project: MavenLanguage: JavaSpring Boot: 选择最新的稳定版如 3.2.xGroup Artifact: 按你的项目命名例如com.example.ai-townPackaging: JarJava: 17在Dependencies部分添加Spring Web用于构建 RESTful API。Spring AI这是核心依赖。在 Initializr 的搜索框中输入“AI”选择 “Spring AI”。注意Spring AI 目前可能位于“Add Dependencies”的“I/O”分类下。点击“Generate”下载项目压缩包并导入到你的 IDE 中。2.3 配置 Maven 依赖与仓库由于 Spring AI 项目仍在快速发展其稳定版本可能尚未发布到 Maven Central。因此你需要在项目的pom.xml中添加 Spring 的里程碑仓库。打开pom.xml在project标签下添加或确认以下仓库配置repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url snapshots enabledfalse/enabled /snapshots /repository /repositories同时检查依赖项中是否包含了 Spring AI 的 BOM物料清单和 Starter。一个典型的配置如下dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version0.8.1/version !-- 请检查并使用最新版本 -- typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI OpenAI Starter -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency !-- 其他依赖如 Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意spring-ai-openai-spring-boot-starter是一个 Starter它自动引入了与 OpenAI 兼容 API 交互所需的全部依赖。如果你计划使用 Azure OpenAI、Anthropic 或 Ollama需要更换为对应的 Starter例如spring-ai-azure-openai-spring-boot-starter或spring-ai-ollama-spring-boot-starter。2.4 配置模型连接接下来在src/main/resources/application.yml中配置你的模型连接信息。这里以 OpenAI 为例spring: ai: openai: api-key: ${OPENAI_API_KEY:your-api-key-here} # 强烈建议使用环境变量 chat: options: model: gpt-3.5-turbo # 或 gpt-4, gpt-4-turbo-preview temperature: 0.7 max-tokens: 500关键配置解释api-key你的 API 密钥。切勿将真实密钥硬编码在配置文件中提交到版本库。最佳实践是使用环境变量如OPENAI_API_KEY或配置中心。model指定使用的聊天模型。temperature控制生成文本的随机性0.0 到 2.0。值越低输出越确定和保守值越高输出越随机和创造性。max-tokens限制单次请求生成的最大 token 数用于控制响应长度和成本。如果你使用本地 Ollama配置会更简单spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: llama2 # 或 mistral, codellama 等完成以上步骤后运行mvn spring-boot:run如果没有报错说明 Spring AI 环境已成功集成。3. 构建基础 AI 服务从简单对话开始在搭建复杂的“AI 小镇”之前我们先实现一个最基础的聊天服务验证整个链路是否通畅。3.1 注入并使用 ChatClientSpring AI 的核心入口之一是ChatClient。我们创建一个服务类来封装对话逻辑。首先创建一个简单的请求和响应 DTO// ChatRequest.java Data // 使用 Lombok 注解简化代码 public class ChatRequest { private String message; private String userId; // 用于区分不同用户的对话历史 } // ChatResponse.java Data public class ChatResponse { private String reply; private String model; private Long tokensUsed; }然后创建ChatServiceService Slf4j public class ChatService { private final ChatClient chatClient; // 通过构造器注入 ChatClientSpring AI 会自动配置 public ChatService(ChatClient chatClient) { this.chatClient chatClient; } public ChatResponse chat(ChatRequest request) { // 1. 构建用户消息 UserMessage userMessage new UserMessage(request.getMessage()); // 2. 调用 ChatClient ChatResponse aiResponse chatClient.call(new Prompt(userMessage)); // 3. 解析响应 String reply aiResponse.getResult().getOutput().getContent(); // 注意实际获取 tokens 等元数据的方式可能因 ChatClient 实现而异 // 这里是一个示例OpenAI 的响应中可能包含这些信息 // 生产环境需要更健壮的解析 log.info(User: {}, AI Reply: {}, request.getMessage(), reply); ChatResponse response new ChatResponse(); response.setReply(reply); response.setModel(gpt-3.5-turbo); // 应从配置或响应中动态获取 // response.setTokensUsed(...); return response; } }3.2 创建 REST 控制器暴露一个简单的 HTTP 端点来测试服务RestController RequestMapping(/api/chat) public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService chatService; } PostMapping public ResponseEntityChatResponse chat(RequestBody ChatRequest request) { return ResponseEntity.ok(chatService.chat(request)); } }3.3 运行与验证启动应用后使用curl或 Postman 进行测试curl -X POST http://localhost:8080/api/chat \ -H Content-Type: application/json \ -d {message: 你好介绍一下Spring AI, userId: user-001}预期会收到一个包含 AI 回复的 JSON 响应。如果成功说明你的 Spring AI 基础环境已完全就绪。4. 实现“AI 小镇”多智能体Agent协作架构“AI 小镇”是一个经典的多智能体模拟场景其中不同的 AI 角色如镇长、农夫、商人、工匠需要根据环境信息和彼此交互来完成复杂任务。Spring AI 对 AI Agent 提供了初步支持我们可以利用其ChatClient、PromptTemplate和自定义工具Tools来构建一个简化版本。4.1 定义智能体角色与系统提示词每个智能体本质上是一个具有特定角色、目标和能力的ChatClient封装。核心在于为其设计精准的系统提示词System Prompt。我们创建一个AgentRole枚举和Agent类// AgentRole.java public enum AgentRole { MAYOR(镇长, 你是一个小镇的镇长负责协调资源、发布任务、解决居民纠纷。你的目标是让小镇繁荣稳定。你说话权威且顾全大局。), FARMER(农夫, 你是一个勤劳的农夫精通种植小麦、玉米和蔬菜。你关心天气、收成和粮食价格。你的目标是获得好收成并卖出好价钱。), MERCHANT(商人, 你是一个精明的商人擅长买卖商品、判断市场行情和谈判。你的目标是以低价买入高价卖出赚取利润。), BLACKSMITH(铁匠, 你是一个技艺高超的铁匠能打造工具、武器和农具。你需要铁矿作为原料你的目标是接到更多订单提升技艺声望。); private final String name; private final String systemPrompt; // 构造函数、getter省略... }// Agent.java Component Slf4j public class Agent { private final ChatClient chatClient; private final AgentRole role; private final String systemPrompt; private ListChatMessage conversationHistory; // 简单的内存历史记录 public Agent(ChatClient chatClient, AgentRole role) { this.chatClient chatClient; this.role role; this.systemPrompt role.getSystemPrompt(); this.conversationHistory new ArrayList(); // 初始化对话历史加入系统提示 this.conversationHistory.add(new SystemMessage(systemPrompt)); } public String perceiveAndAct(String observation) { // 1. 将观察来自环境或其他Agent的消息作为用户输入 UserMessage userMessage new UserMessage(observation); conversationHistory.add(userMessage); // 2. 构建包含完整历史的Prompt Prompt prompt new Prompt(conversationHistory); // 3. 调用模型 ChatResponse response chatClient.call(prompt); String action response.getResult().getOutput().getContent(); // 4. 将AI的回应也加入历史 AssistantMessage assistantMessage new AssistantMessage(action); conversationHistory.add(assistantMessage); // 5. 可选限制历史长度防止token超限 if (conversationHistory.size() 20) { // 简单保留最近10轮对话 conversationHistory conversationHistory.subList(conversationHistory.size() - 20, conversationHistory.size()); } log.info([{}] 观察: {} - 行动: {}, role.getName(), observation, action); return action; } public AgentRole getRole() { return role; } }4.2 构建小镇环境与协调器我们需要一个TownSimulator来模拟小镇环境管理所有智能体并驱动他们之间的交互。Service public class TownSimulator { private final MapAgentRole, Agent agents; private final ListString townBulletin; // 小镇公告板记录事件 public TownSimulator(ListAgent agentList) { this.agents agentList.stream().collect(Collectors.toMap(Agent::getRole, agent - agent)); this.townBulletin new ArrayList(); } /** * 模拟一个简单的小镇周期 */ public void simulateDay() { townBulletin.add( 新的一天开始了 ); // 场景1镇长发布任务粮食短缺 Agent mayor agents.get(AgentRole.MAYOR); String mayorAnnouncement mayor.perceiveAndAct(最近小镇粮食储备不足请各位想想办法。); townBulletin.add(镇长宣布: mayorAnnouncement); broadcastMessage(镇长说: mayorAnnouncement, AgentRole.MAYOR); // 场景2农夫和商人对此做出反应 Agent farmer agents.get(AgentRole.FARMER); String farmerResponse farmer.perceiveAndAct(你听到镇长说粮食短缺。你现在有100单位小麦库存。); townBulletin.add(农夫回应: farmerResponse); Agent merchant agents.get(AgentRole.MERCHANT); String merchantResponse merchant.perceiveAndAct(粮食短缺意味着粮价可能上涨。你手头有500金币。); townBulletin.add(商人回应: merchantResponse); // 场景3基于反应驱动下一步交互例如商人向农夫购买粮食 // 这里可以设计更复杂的交互逻辑例如将农夫的回答作为商人的输入 String merchantOffer merchant.perceiveAndAct(农夫说他有小麦但担心价格。你作为商人想向他提出一个购买报价。); townBulletin.add(商人出价: merchantOffer); String farmerCounterOffer farmer.perceiveAndAct(商人向你报价购买小麦。你可以选择接受、拒绝或还价。); townBulletin.add(农夫还价: farmerCounterOffer); townBulletin.add( 一天结束了 ); } private void broadcastMessage(String message, AgentRole excludeRole) { for (Map.EntryAgentRole, Agent entry : agents.entrySet()) { if (!entry.getKey().equals(excludeRole)) { // 在实际中这里可以决定哪些消息需要广播给哪些角色 // 此处简化处理所有角色都收到 entry.getValue().perceiveAndAct(你听到消息: message); } } } public ListString getTownBulletin() { return new ArrayList(townBulletin); } }4.3 为智能体赋予“工具”能力上述智能体只能进行对话。在更真实的模拟中他们需要执行具体动作如“种植”、“交易”、“打造”。Spring AI 支持函数调用Function Calling我们可以将其抽象为智能体的“工具”。首先定义一个工具接口和几个实现public interface AgentTool { String getName(); String getDescription(); String execute(String arguments); // arguments 可以是 JSON 字符串 } Component Slf4j public class TradeTool implements AgentTool { Override public String getName() { return trade_goods; } Override public String getDescription() { return 交易商品。输入应为JSON格式{\buyer\:\角色名\, \seller\:\角色名\, \item\:\物品名\, \quantity\:数量, \pricePerUnit\:单价}; } Override public String execute(String arguments) { try { // 简单解析JSON实际项目可用Jackson // 这里模拟交易逻辑 log.info(执行交易工具参数: {}, arguments); return 交易成功完成。; } catch (Exception e) { return 交易失败: e.getMessage(); } } }然后增强Agent类使其在生成回复时能够决定是否调用工具并处理工具执行结果。这涉及到使用 Spring AI 的ChatClient支持函数调用的高级特性通常通过ChatOptions配置工具列表并在Prompt中指定。由于实现细节依赖于特定ChatClient实现如 OpenAI 的 function calling此处概述关键思路将AgentTool适配为 Spring AI 的FunctionCallback或Tool接口。在创建针对某个智能体的ChatClient时注册其可用的工具例如商人有TradeTool铁匠有ForgeTool。当ChatClient的响应包含工具调用请求时拦截并执行对应的AgentTool然后将工具执行结果作为新的上下文信息再次发送给模型让模型生成最终面向用户的回复。这部分是 Spring AI 应用进阶的关键需要仔细查阅对应模型供应商如 OpenAI的ChatClient实现文档。4.4 创建控制器驱动模拟最后创建一个 REST 端点来触发模拟并查看结果RestController RequestMapping(/api/town) public class TownController { private final TownSimulator townSimulator; public TownController(TownSimulator townSimulator) { this.townSimulator townSimulator; } PostMapping(/simulate-day) public ResponseEntityListString simulateDay() { townSimulator.simulateDay(); return ResponseEntity.ok(townSimulator.getTownBulletin()); } GetMapping(/bulletin) public ResponseEntityListString getBulletin() { return ResponseEntity.ok(townSimulator.getTownBulletin()); } }调用/api/town/simulate-day即可运行一天的小镇模拟并通过/api/town/bulletin查看事件日志。5. 生产环境考量与最佳实践将 AI 应用部署到生产环境远不止是让服务跑起来那么简单。以下是在工程化 Spring AI 应用时必须关注的要点。5.1 配置管理与安全密钥管理绝对不要将 API Key 提交到代码库。使用环境变量、云服务商的密钥管理服务如 AWS Secrets Manager, Azure Key Vault或配置中心。配置外置将模型参数temperature,max-tokens甚至模型类型放到外部配置如application-prod.yml中便于不同环境开发、测试、生产切换和动态调整。网络与代理如果服务部署在内网需要访问外部模型 API妥善配置网络代理。5.2 性能、成本与限流超时与重试配置合理的连接超时、读取超时并为可重试的错误如网络抖动、模型过载实现重试机制。Spring 的Retryable注解或 Resilience4j 库可以帮忙。限流与熔断AI 模型 API 通常有 RPM每分钟请求数、TPM每分钟 token 数限制。使用 Resilience4j 或 Sentinel 实现限流和熔断防止单个异常请求拖垮整个应用或产生意外高额费用。异步与非阻塞AI 调用通常是高延迟的 I/O 操作。考虑使用 Spring 的Async或 WebFlux 进行异步处理避免阻塞 Web 服务器线程。Token 消耗监控记录每次请求的输入/输出 token 数并集成到监控系统如 Prometheus。这对于成本控制和性能分析至关重要。5.3 可观测性与日志结构化日志记录每次 AI 调用的请求、响应、耗时、token 用量和模型名称。使用 MDCMapped Diagnostic Context关联用户会话。链路追踪在微服务架构中确保 AI 调用链被集成到整体的分布式追踪如 Zipkin, Jaeger中。提示词与响应审计对于关键业务场景可能需要将用户的最终提示词和模型的完整响应落盘用于合规审计、模型效果分析和后续优化。5.4 错误处理与降级定义业务异常将模型 API 的各类错误无效请求、超时、内容过滤映射为清晰的业务异常。优雅降级当主要模型服务不可用时是否有备选方案例如切换到一个更便宜、更稳定的模型或者返回一个预定义的缓存响应。输入验证与清理对用户输入进行严格的验证和清理防止提示词注入攻击避免产生不符合预期的输出。5.5 测试策略单元测试MockChatClient测试你的业务逻辑如TownSimulator的交互流程。集成测试使用测试专用的模型 API Key 或本地 Mock 服务器测试从控制器到ChatClient的完整链路。提示词测试将提示词模板化并针对不同边界条件的输入验证输出的稳定性和质量。这可以部分自动化。性能测试模拟并发用户请求评估系统的吞吐量、延迟和资源消耗。6. 常见问题排查在开发和部署 Spring AI 应用时你可能会遇到以下典型问题问题现象可能原因检查点与解决方案启动失败报错No qualifying bean of type ‘ChatClient‘1. 未添加对应的 Spring AI Starter 依赖。2. 相关配置如api-key缺失或格式错误。3. 依赖版本冲突。1. 检查pom.xml中是否正确引入了spring-ai-*-spring-boot-starter。2. 检查application.yml中spring.ai.*配置是否正确API Key 是否有效。3. 运行mvn dependency:tree检查是否有冲突尝试使用 Spring AI BOM 统一管理版本。调用接口返回 401 或 403 错误API 密钥无效、过期或没有对应模型的访问权限。1. 在模型提供商的控制台检查 API Key 状态和权限。2. 确认配置中的密钥是否正确环境变量是否已加载。3. 对于 Azure OpenAI还需检查终结点Endpoint和部署名称Deployment Name是否正确。请求超时1. 网络问题。2. 模型服务响应慢。3. 客户端未配置超时或配置过短。1. 检查网络连通性。2. 在模型提供商控制台查看服务状态。3. 在配置中增加超时设置例如对于 OpenFeign 或 RestTemplate需要配置连接和读取超时。响应内容被截断或不完整达到了配置的max-tokens上限。1. 适当增加max-tokens配置。2. 在代码中检查响应对象的finishReason属性如果是LENGTH则说明因 token 限制而停止。智能体行为不符合预期“AI 幻觉”或偏离角色1. 系统提示词System Prompt不够清晰或约束力不强。2.temperature参数设置过高导致随机性太强。3. 对话历史管理不当导致角色上下文丢失。1. 迭代优化系统提示词明确角色、目标和约束。使用“你是一个...你必须...你不能...”等句式。2. 降低temperature如设为 0.2-0.5以获得更稳定输出。3. 检查并优化对话历史的保存与截断策略确保关键的系统提示始终在上下文中。多智能体协作时出现循环对话或无意义交互交互逻辑设计存在缺陷缺乏终止条件或目标驱动。1. 为每个交互轮次设定明确的目标或终止条件如“达成交易”或“协商失败”。2. 引入一个“裁判”或“环境”组件在检测到循环或无效对话时主动干预推进场景。内存占用持续增长对话历史未做清理在内存中无限累积。1. 实现对话历史的滚动窗口只保留最近 N 轮对话。2. 对于长对话考虑将历史存储到外部数据库如 Redis并按需加载。7. 扩展方向与进阶思考基于“AI 小镇”这个项目原型你可以向多个方向深化构建更强大、更实用的 AI 应用集成向量数据库与 RAG让小镇的智能体拥有“记忆”。将小镇的历史事件、规则手册、居民档案等文本资料嵌入并存储到向量数据库如 Pinecone, Weaviate, pgvector。当智能体需要做决策时先检索相关记忆片段再生成回答实现检索增强生成RAG。实现复杂的规划与决策引入更高级的 Agent 框架思路如 ReActReasoning Acting模式。让智能体不仅会对话还会生成“思考”链并据此选择调用哪个工具如check_inventory,calculate_price。前端可视化为“AI 小镇”开发一个 Web 前端实时展示公告板信息、各个智能体的状态和对话气泡让模拟过程一目了然。接入更多模型与混合编排不同智能体可以使用不同的模型。例如镇长使用能力更强的 GPT-4 进行宏观规划而农夫和商人使用成本更低的 Claude Haiku 或本地模型处理日常对话。Spring AI 的抽象层让这种混合编排成为可能。走向真实业务场景将多智能体协作的模式应用于客服工单分配与解决、智能代码评审、游戏 NPC 对话生成、营销内容 A/B 测试分析等真实业务场景。Spring AI 为 Java 开发者打开了便捷接入大模型能力的大门但其真正的价值在于让你能够以工程化的思维去设计、实现和运维 AI 驱动的应用。从理清概念、搭建环境开始到设计智能体、处理生产环境的各种挑战每一步都需要将软件工程的最佳实践与 AI 的特性相结合。
