在实际工程实践中,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 环境与工具要求
在开始前,请确保你的开发环境满足以下要求:
| 组件 | 要求 | 说明 |
|---|---|---|
| JDK | 17 或更高版本 | Spring Boot 3.x 的硬性要求。 |
| 构建工具 | Maven 3.6+ 或 Gradle 7.x+ | 本文使用 Maven 进行演示。 |
| IDE | IntelliJ IDEA (推荐) 或 VS Code with Spring Boot 插件 | 需要良好的 Spring 支持。 |
| 模型访问 | 至少一个可用的 AI 模型 API | 例如 OpenAI API Key,或本地运行的 Ollama。 |
2.2 创建 Spring Boot 项目
最快捷的方式是使用 Spring Initializr 。选择以下配置:
- Project: Maven
- Language: Java
- Spring Boot: 选择最新的稳定版(如 3.2.x)
- Group & Artifact: 按你的项目命名,例如
com.example.ai-town - Packaging: Jar
- Java: 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> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>同时,检查依赖项中是否包含了 Spring AI 的 BOM(物料清单)和 Starter。一个典型的配置如下:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>0.8.1</version> <!-- 请检查并使用最新版本 --> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI OpenAI Starter --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> <!-- 其他依赖,如 Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</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 注入并使用 ChatClient
Spring 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; }然后,创建ChatService:
@Service @Slf4j public class ChatService { private final ChatClient chatClient; // 通过构造器注入 ChatClient,Spring 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 ResponseEntity<ChatResponse> 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 List<ChatMessage> 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 Map<AgentRole, Agent> agents; private final List<String> townBulletin; // 小镇公告板,记录事件 public TownSimulator(List<Agent> 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.Entry<AgentRole, Agent> entry : agents.entrySet()) { if (!entry.getKey().equals(excludeRole)) { // 在实际中,这里可以决定哪些消息需要广播给哪些角色 // 此处简化处理,所有角色都收到 entry.getValue().perceiveAndAct("你听到消息: " + message); } } } public List<String> 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 ResponseEntity<List<String>> simulateDay() { townSimulator.simulateDay(); return ResponseEntity.ok(townSimulator.getTownBulletin()); } @GetMapping("/bulletin") public ResponseEntity<List<String>> 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 用量和模型名称。使用 MDC(Mapped Diagnostic Context)关联用户会话。
- 链路追踪:在微服务架构中,确保 AI 调用链被集成到整体的分布式追踪(如 Zipkin, Jaeger)中。
- 提示词与响应审计:对于关键业务场景,可能需要将用户的最终提示词和模型的完整响应落盘,用于合规审计、模型效果分析和后续优化。
5.4 错误处理与降级
- 定义业务异常:将模型 API 的各类错误(无效请求、超时、内容过滤)映射为清晰的业务异常。
- 优雅降级:当主要模型服务不可用时,是否有备选方案?例如,切换到一个更便宜、更稳定的模型,或者返回一个预定义的缓存响应。
- 输入验证与清理:对用户输入进行严格的验证和清理,防止提示词注入攻击,避免产生不符合预期的输出。
5.5 测试策略
- 单元测试:Mock
ChatClient,测试你的业务逻辑(如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 框架思路,如 ReAct(Reasoning + Acting)模式。让智能体不仅会对话,还会生成“思考”链,并据此选择调用哪个工具(如
check_inventory,calculate_price)。 - 前端可视化:为“AI 小镇”开发一个 Web 前端,实时展示公告板信息、各个智能体的状态和对话气泡,让模拟过程一目了然。
- 接入更多模型与混合编排:不同智能体可以使用不同的模型。例如,镇长使用能力更强的 GPT-4 进行宏观规划,而农夫和商人使用成本更低的 Claude Haiku 或本地模型处理日常对话。Spring AI 的抽象层让这种混合编排成为可能。
- 走向真实业务场景:将多智能体协作的模式应用于客服工单分配与解决、智能代码评审、游戏 NPC 对话生成、营销内容 A/B 测试分析等真实业务场景。
Spring AI 为 Java 开发者打开了便捷接入大模型能力的大门,但其真正的价值在于让你能够以工程化的思维去设计、实现和运维 AI 驱动的应用。从理清概念、搭建环境开始,到设计智能体、处理生产环境的各种挑战,每一步都需要将软件工程的最佳实践与 AI 的特性相结合。