Java 智能体开发实战:基于 Spring AI 构建从对话到任务执行的完整应用

发布时间:2026/10/3 15:12:48
Java 智能体开发实战:基于 Spring AI 构建从对话到任务执行的完整应用
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(RedisTemplateString, 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 groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0-M5.1/version /dependency /dependencies配置文件里把模型连接信息填好。这里要注意API Key 不要硬编码在代码里用环境变量或者配置中心。spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.74.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 FluxString 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用 userIdsessionId 组合任务卡死工具超时无兜底检查外部调用超时设置设超时返回结构化错误工具参数错误模型理解偏差打印实际入参加参数校验和格式说明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 做知识库问答、用多智能体协作处理复杂流程、把任务执行结果做成可视化报表。但不管怎么扩展核心还是那套对话接口管输入输出编排层管决策工具层管执行状态层管持久化。把这四层搭稳了上面加什么功能都不会乱。