☰
Java 智能体开发实战:基于 Spring AI 构建从对话到任务执行的完整应用
2026/10/3 15:12:29 网站建设 项目流程

1. 为什么 Java 开发者现在必须关注智能体开发

过去两年,我身边不少做 Java 后端的同行都有一个共同的焦虑:大模型的能力越来越强,但自己每天写的还是 Controller、Service、DAO 那套东西,感觉跟 AI 隔着一层。直到 Spring AI 和 Spring AI Alibaba 这类框架成熟起来,情况才真正发生变化。现在你可以用一套标准的 Spring Boot 工程,把对话接口、工具调用、任务编排、状态管理全部串起来,做出一个能真正干活的智能体,而不是只会聊天的玩具。

这个项目的核心目标很明确:用 Java 技术栈构建一个从对话接口到任务执行的完整智能体。它解决的是 Java 后端工程师在 AI 时代如何平滑迁移的问题——你不需要放弃现有的 Spring Boot 生态,不需要重学 Python,只需要在熟悉的工程结构里引入 Spring AI 的依赖,就能让服务具备理解意图、调用工具、执行多步任务的能力。适合有一定 Java 和 Spring Boot 基础,想快速把智能体能力落地到实际业务中的开发者,也适合正在准备智能体相关面试、需要理解工程化实现细节的同学。

我实测下来,一个最小可用的 Java 智能体,从零到跑通对话加工具调用,熟练的话半天就能搞定。但要从“能跑”到“能稳定执行复杂任务”,中间有不少坑需要提前知道。下面我按实际开发顺序,把整体设计、核心细节、实操过程、问题排查这几个部分拆开讲,尽量把每个决策背后的原因说清楚。

2. 整体架构设计与技术选型思路

2.1 为什么选 Spring AI 而不是直接调 HTTP 接口

很多团队一开始图省事,直接在 Java 里用 RestTemplate 或 WebClient 调大模型的 HTTP API。这样做不是不行,但很快就会遇到几个问题:第一,不同厂商的请求格式和返回结构不一样,换一个模型就要改一遍代码;第二,对话上下文的管理、流式输出的处理、工具调用的解析,这些都要自己写,重复劳动;第三,没有统一的抽象层,测试和替换模型成本很高。

Spring AI 的价值就在于它提供了一套统一的抽象。ChatClient 接口屏蔽了底层模型的差异,你可以在配置文件里切换不同的模型提供方,代码基本不用动。它内置了对话记忆、工具调用、结构化输出、RAG 等常用能力,这些都是智能体开发的基础设施。Spring AI Alibaba 则是在此基础上补充了国内常用模型的适配,比如通义千问系列,连接和配置都比较顺手。

注意:Spring AI 的版本迭代比较快,不同小版本之间 API 有变化。建议锁定一个稳定版本,不要盲目追最新。我用的组合是 Spring Boot 3.2.x 加 Spring AI 1.0.x 的稳定版,Spring AI Alibaba 选对应兼容版本。

2.2 智能体的分层结构怎么设计

一个能执行任务的智能体,不能把所有逻辑塞在一个 Service 里。我习惯把它分成四层:接入层、编排层、能力层、基础设施层。接入层负责对外暴露接口,可以是 REST 接口,也可以是 WebSocket 或 SSE 流式接口;编排层是核心,负责理解用户意图、决定调用哪些工具、管理多步任务的执行顺序;能力层就是一个个具体的工具,比如查数据库、调第三方接口、发消息、生成文件;基础设施层包括模型客户端、对话记忆存储、日志监控等。

这样分的好处是,编排逻辑和能力实现解耦。今天你用规则加模型来决定调用哪个工具,明天想换成更复杂的规划算法,只需要改编排层,工具不用动。能力层里的每个工具都是独立的 Spring Bean,可以单独测试,也可以被不同的智能体复用。

2.3 对话接口的形态选择:同步、流式还是异步

对话接口有三种常见形态,各有适用场景。同步接口最简单,用户发一条消息,服务端等模型生成完整回复后一次性返回。适合后台任务触发、内部工具调用这类不需要即时反馈的场景。流式接口通过 SSE 或 WebSocket 把模型生成的内容逐字推给前端,用户体验好,适合面向用户的聊天界面。异步接口则是用户提交任务后立即返回一个任务 ID,服务端在后台处理,用户通过轮询或回调获取结果,适合耗时较长的任务执行。

我的建议是:面向用户的对话入口用流式,任务执行类接口用异步。流式接口在 Spring AI 里通过 ChatClient 的 stream 方法配合 Flux 就能实现,前端用 EventSource 接收。异步任务则可以用 Spring 的 @Async 或者更可控的线程池加任务状态表来实现。

3. 核心细节解析与实操要点

3.1 对话记忆的存储与窗口控制

智能体要能进行多轮对话,就必须记住上下文。Spring AI 提供了 ChatMemory 抽象,默认有基于内存的实现,但生产环境肯定不能用内存,重启就丢了。常见的做法是存到 Redis 或者数据库。我一般用 Redis,因为对话上下文的读写频率高,Redis 的响应速度合适,而且可以设置过期时间,自动清理旧会话。

窗口控制是个容易被忽略的点。模型的上下文长度有限,不能把历史对话无限拼接。你需要设定一个策略,比如保留最近 N 轮对话,或者按 token 数量截断。我的经验是,保留最近 10 轮加上系统提示词,对大多数客服和助手场景够用了。如果任务需要更长的记忆,就要考虑做摘要,把早期对话压缩成一段摘要存起来,而不是原样保留。

// 配置基于 Redis 的对话记忆 @Bean public ChatMemory chatMemory(RedisTemplate<String, Object> redisTemplate) { return new RedisChatMemory(redisTemplate, Duration.ofHours(2)); } // 在 ChatClient 中启用记忆 ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();

提示:Redis 里存的对话消息建议用 JSON 序列化,不要用 JDK 序列化,方便排查问题,也避免版本升级时的兼容问题。

3.2 工具调用的定义与参数校验

工具调用是智能体从“会说”到“会做”的关键。在 Spring AI 里,你可以用 @Tool 注解把一个方法暴露成工具,模型会根据方法描述和参数说明来决定是否调用。这里有几个实操要点:第一,方法的描述要写清楚,用自然语言说明这个工具做什么、什么时候用,模型靠这个来判断;第二,参数要用 @ToolParam 注解说明含义,尤其是枚举值和格式要求;第三,工具方法内部一定要做参数校验,不能假设模型传进来的参数一定合法。

我踩过的一个坑是,工具方法抛异常后,模型收到的是一个错误信息,它可能会反复重试同一个工具,导致死循环。后来我在工具方法里做了兜底,捕获异常后返回一个结构化的错误结果,告诉模型“这个操作失败了,原因是某某,请尝试其他方式”,这样模型就能调整策略。

@Component public class OrderTools { @Tool(description = "根据订单号查询订单状态,订单号格式为 ORD 开头的 12 位字符串") public OrderStatus queryOrder( @ToolParam(description = "订单号,例如 ORD20250101001") String orderId) { if (orderId == null || !orderId.matches("^ORD\\d{9}$")) { return OrderStatus.invalid("订单号格式不正确"); } // 实际查询逻辑 return orderService.findStatus(orderId); } }

3.3 任务执行的编排与状态管理

当用户的需求需要多步操作时,比如“帮我查一下上个月的销售数据,生成报表并发给张经理”,这就涉及任务编排。简单的做法是让模型一次性规划出所有步骤,然后按顺序执行。但实际中模型可能会漏步骤或者顺序不对,所以更稳妥的方式是引入一个轻量的状态机。

我的做法是定义一个 Task 对象,包含任务 ID、当前步骤、已完成步骤、中间结果、状态等字段。编排层每执行一步,就更新 Task 状态并持久化。这样即使服务重启,也能从上次中断的地方继续。状态存储用数据库表就行,字段不用太复杂,关键是可追溯。

注意:任务执行过程中,如果某一步调用了外部接口超时,要有重试和降级策略。不要让整个任务卡死在一个工具调用上。

4. 完整实操过程与核心环节实现

4.1 工程初始化与依赖配置

先创建一个标准的 Spring Boot 3.2.x 工程,JDK 用 17 或 21。在 pom.xml 里引入 Spring AI 的 BOM 和 starter,以及 Spring AI Alibaba 的依赖。如果你用的是通义千问,还需要配置对应的 API Key 和模型名称。

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> <version>1.0.0-M5.1</version> </dependency> </dependencies>

配置文件里把模型连接信息填好。这里要注意,API Key 不要硬编码在代码里,用环境变量或者配置中心。

spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7

4.2 对话接口的实现与流式输出

对话接口我一般放在一个独立的 Controller 里,路径用 /api/agent/chat。同步接口返回完整回复,流式接口返回 Flux 。流式接口的关键是设置正确的 Content-Type 为 text/event-stream,并且处理好异常和完成信号。

@RestController @RequestMapping("/api/agent") public class AgentChatController { private final ChatClient chatClient; public AgentChatController(ChatClient chatClient) { this.chatClient = chatClient; } @PostMapping("/chat") public ChatResponse chat(@RequestBody ChatRequest request) { String reply = chatClient.prompt() .user(request.message()) .advisors(a -> a.param("chatId", request.sessionId())) .call() .content(); return new ChatResponse(reply); } @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chatStream(@RequestBody ChatRequest request) { return chatClient.prompt() .user(request.message()) .advisors(a -> a.param("chatId", request.sessionId())) .stream() .content(); } }

实测下来,流式接口在浏览器端用 EventSource 接收时,要注意跨域和超时设置。如果前端用的是 fetch 的 ReadableStream,处理起来会更灵活一些。

4.3 工具注册与任务执行链路打通

工具注册很简单,把带有 @Tool 注解的 Bean 交给 ChatClient 就行。Spring AI 会自动扫描并注册。任务执行链路则是把对话接口和工具调用串起来:用户发消息,模型判断需要调用工具,框架执行工具方法,把结果返回给模型,模型生成最终回复。

@Configuration public class AgentConfig { @Bean public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory, OrderTools orderTools) { return ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .defaultTools(orderTools) .build(); } }

这里有个细节:defaultTools 注册的工具是全局可用的。如果工具很多,建议按场景分组,不同的 ChatClient 注册不同的工具集,避免模型在无关场景下误调用。

4.4 任务状态持久化与恢复

对于多步任务,我建了一张 agent_task 表,字段包括 task_id、session_id、status、current_step、steps_json、result、created_at、updated_at。每执行一步就更新一次。服务启动时,可以扫描 status 为 RUNNING 的任务,根据 steps_json 里的进度决定是否继续执行。

CREATE TABLE agent_task ( task_id VARCHAR(64) PRIMARY KEY, session_id VARCHAR(64) NOT NULL, status VARCHAR(20) NOT NULL, current_step INT DEFAULT 0, steps_json TEXT, result TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

提示:steps_json 里存的是步骤定义和中间结果,不要存太大的对象,避免单行数据过大。中间结果如果很大,可以存到对象存储,表里只存引用。

5. 常见问题与排查技巧实录

5.1 模型不调用工具或调用错误工具

这是最常见的问题。原因通常有三个:工具描述不清楚、参数说明不明确、系统提示词没有引导。排查时先把工具描述打印出来,站在模型的角度看能不能理解。我一般会在系统提示词里加一句“当用户需求涉及订单查询时,必须调用 queryOrder 工具,不要自己编造订单状态”。另外,工具名称尽量用英文动词加名词,避免歧义。

5.2 流式输出中断或乱码

流式输出中断多半是网络问题或者服务端异常没有正确捕获。检查点包括:Controller 是否返回了 Flux 而不是阻塞式结果、异常处理器是否把异常转成了错误事件、前端是否正确处理了 event 的 data 字段。乱码问题通常是编码没设置对,确保响应头里 charset 是 UTF-8。

5.3 对话记忆串会话

如果多个用户共用了同一个 chatId,对话就会串。排查时检查前端传的 sessionId 是否唯一,后端存储 key 是否带了用户标识。我习惯用 userId + sessionId 组合作为 Redis 的 key,避免不同用户之间的会话冲突。

5.4 工具调用超时导致任务卡死

外部接口超时是常态,必须在工具方法里设置超时时间。可以用 Resilience4j 或者简单的 Future.get(timeout) 来实现。超时后返回一个明确的错误信息给模型,让它决定是重试还是换方案。不要让线程无限等待。

问题现象可能原因排查方向解决建议
模型不调用工具描述不清、提示词缺失检查工具注解和系统提示补充描述,加引导语
流式输出中断异常未捕获、网络问题查看服务端日志和前端事件加全局异常处理,转错误事件
对话串会话sessionId 不唯一检查前端传参和存储 key用 userId+sessionId 组合
任务卡死工具超时无兜底检查外部调用超时设置设超时,返回结构化错误
工具参数错误模型理解偏差打印实际入参加参数校验和格式说明

5.5 版本兼容与依赖冲突

Spring AI 和 Spring AI Alibaba 的版本要匹配,Spring Boot 的版本也要在支持范围内。我遇到过引入 Alibaba starter 后和现有 WebFlux 依赖冲突的情况,排查方法是 mvn dependency:tree 看冲突,然后排除掉重复的依赖。建议新项目直接用 Spring Boot 3.2.x 加 Spring AI 1.0.x 的稳定组合,不要混用快照版。

6. 智能体行为审计与上线前检查

智能体上线前,行为审计这块不能省。你需要记录每一次对话的输入输出、调用了哪些工具、参数是什么、结果如何、耗时多少。这些日志一方面用于排查问题,另一方面也是合规要求。我一般用 AOP 在工具方法上加切面,统一记录调用日志,写到独立的审计表或者日志文件里。

另外,要设置好限流和熔断。智能体调用模型和工具都会消耗资源,没有限流的话,一个异常流量就可能把服务打挂。用 Spring Cloud Gateway 或者 Resilience4j 做限流都可以,关键是阈值要根据实际压测结果来定,不要拍脑袋。

注意:审计日志里不要记录敏感信息,比如用户的完整手机号、身份证号。如果业务需要记录,要做脱敏处理。

7. 我踩过的坑和最后分享几个实用技巧

第一个坑是对话记忆的序列化。一开始我用 JDK 序列化存 Redis,后来升级 Spring AI 版本,消息类的字段变了,反序列化直接报错。换成 JSON 序列化后就没这个问题了,而且排查问题时能直接看懂存的内容。

第二个坑是工具方法的返回值。如果返回一个复杂的嵌套对象,模型有时候解析不了。后来我统一把工具返回值转成扁平的 JSON 字符串,字段名用英文,模型理解起来准确多了。

第三个技巧是关于系统提示词的。不要写得太长,把最关键的规则放在前面,用编号列出来。我试过写一大段自然语言描述,模型反而抓不住重点。改成“1. 你是订单助手。2. 查询订单必须调用工具。3. 不要编造数据。”这种形式后,行为稳定了很多。

还有一个实用技巧是给模型加一个“思考”步骤。在系统提示词里让它先输出一段简短的推理,再决定调用哪个工具。实测下来,这样能明显减少误调用,尤其是工具数量多的时候。代价是多消耗一些 token,但换来的准确性提升是值得的。

这个方向后续还可以扩展的地方很多,比如接入 RAG 做知识库问答、用多智能体协作处理复杂流程、把任务执行结果做成可视化报表。但不管怎么扩展,核心还是那套:对话接口管输入输出,编排层管决策,工具层管执行,状态层管持久化。把这四层搭稳了,上面加什么功能都不会乱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询