ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java智能体开发实战:基于Spring Boot与Spring AI的任务编排与工具调用

Java智能体开发实战:基于Spring Boot与Spring AI的任务编排与工具调用 1. 从对话到执行Java 智能体到底在解决什么问题很多人第一次听到“Java 智能体开发”脑子里浮现的还是聊天窗口里那个一问一答的机器人。但真正做过落地项目的人都知道对话只是最表层的一层皮智能体的核心价值在于把自然语言意图翻译成可执行的任务并且真的把任务跑完。这两件事之间的鸿沟才是 Java 后端工程师真正要填的坑。我接触过不少团队前期用 Python 快速搭了个 Demo演示的时候效果惊艳一旦要接入公司现有的订单系统、工单系统、权限体系立刻就卡住了。原因很简单企业里跑着的核心业务八成以上是 Java 写的Spring Boot 服务、Dubbo 接口、各种内部 RPCPython 那套生态要对接起来中间得加一层又一层的胶水。这时候用 Java 来做智能体反而成了顺理成章的选择——不是因为它写 AI 算法更方便而是因为它离业务系统更近。所以这篇文章想聊的不是“怎么调一个大模型接口”这种入门话题而是一个 Java 智能体从接收用户对话到拆解任务、调用工具、执行动作、返回结果的完整链路。我会围绕 Spring Boot 和 Spring AI 这套技术栈展开因为这是目前 Java 圈子里最主流、也最容易被团队接受的方案。适合谁看如果你是有 Java 基础、写过 Spring Boot 服务现在想把手里的业务系统接上大模型能力那这篇基本就是给你写的。如果你还在纠结 Java 和 Python 选哪个我也会在选型部分给出我的实际判断。先把结论摆前面Java 智能体开发的关键难点不在模型调用而在任务编排、工具注册、状态管理和安全边界。把这四件事想清楚代码写起来其实很快。下面我按实际项目的推进顺序一层层拆开讲。2. 技术选型为什么是 Spring Boot 加 Spring AI2.1 Java 做智能体的真实优势与边界先泼一盆冷水。如果你的目标是训练模型、做微调、搞复杂的 RAG 算法实验Java 确实不是首选Python 的生态成熟太多。但智能体开发里模型训练占比其实很小大头是工程化接口设计、并发控制、事务一致性、权限校验、日志审计、服务监控。这些恰恰是 Java 的主场。我总结下来Java 做智能体有三个实打实的优势。第一是业务系统对接成本低你直接就能在同一个 Spring 容器里注入现有的 Service不用跨语言通信。第二是类型安全和可维护性智能体的工具调用参数、返回值用 Java 的强类型定义出来编译期就能挡掉一批错误团队协作时接口契约清晰。第三是运维体系现成Spring Boot Admin 做监控、Actuator 做健康检查、Micrometer 做指标采集这些你团队本来就在用不用重新搭一套。边界也要说清楚。涉及大量向量检索、复杂 Prompt 实验、快速迭代算法逻辑的场景我一般会建议把算法部分单独拆成 Python 服务Java 这边通过 HTTP 调用。不要为了技术栈统一而硬扛混合架构在智能体项目里非常常见也很合理。2.2 Spring AI 的定位与版本选择Spring AI 本质上是把大模型调用抽象成了一套 Spring 风格的 API让你用ChatClient、EmbeddingModel、VectorStore这些接口去操作模型底层换供应商的时候改动很小。这个设计思路和 Spring 一贯的风格一致——面向接口编程屏蔽实现差异。版本上要特别注意。Spring AI 迭代很快1.0 之前的版本 API 变动频繁我踩过好几次升级后方法签名全变的坑。现在如果新起项目建议直接用 1.0 以上的稳定版本或者至少锁定一个明确的版本号别用LATEST。另外 Spring Boot 的版本要和 Spring AI 对齐Spring Boot 3.x 是硬性要求因为 Spring AI 用到了 Java 17 的特性和 Spring 6 的新 API。如果你手上还有 Spring Boot 2.3.x 或 2.6.x 的老项目想接智能体能力我的建议是新起一个服务别在老项目上硬改升级成本可能比想象中高。至于国内常用的模型接入Spring AI 提供了 OpenAI 兼容的适配层很多国产模型都支持 OpenAI 协议配置上改一下base-url和api-key就能用。这块后面实操部分我会给具体配置。2.3 对话接口的两种设计思路对话接口看起来简单其实设计上有讲究。第一种是同步阻塞式用户发一条消息服务端调模型等结果返回。这种方式实现简单但模型响应慢的时候HTTP 连接会一直挂着用户体验差还容易触发网关超时。第二种是流式响应用 SSEServer-Sent Events把模型吐出来的 token 一个个推给前端。这是我现在默认的选择因为智能体场景下响应往往很长流式能让用户第一时间看到反馈感知延迟大幅降低。Spring AI 的ChatClient原生支持流式返回FluxString配合 Spring WebFlux 或者 Spring MVC 的SseEmitter都能实现。提示如果你的智能体要执行耗时任务比如查数据库、调外部接口流式响应要分两段设计——先流式输出“思考过程”任务执行完再推最终结果。别让用户对着空白页面等十几秒。3. 核心架构拆解一个智能体的四层结构3.1 对话层意图识别与上下文管理对话层负责接收用户输入维护多轮对话的上下文。这里最容易出问题的是上下文窗口管理。大模型有 token 上限对话轮次多了历史消息会撑爆窗口。我的做法是保留最近 N 轮完整对话更早的做摘要压缩把关键信息用户身份、已确认的参数、任务状态提取成结构化数据存起来而不是把原始对话全塞进去。上下文存储我一般用 Rediskey 用会话 IDvalue 存消息列表设置合理的过期时间。这里有个细节消息的角色要严格区分system、user、assistant工具调用的结果要用专门的 tool 角色混用会导致模型理解错乱。Spring AI 的Message体系已经把这些角色封装好了直接用就行。意图识别这块早期我用过单独的意图分类模型后来发现没必要。现在主流做法是把意图识别和任务规划合并到一次模型调用里通过精心设计的 system prompt让模型直接输出结构化的任务计划。这样少一次调用延迟更低而且意图和计划的一致性更好。3.2 规划层任务拆解与工具选择规划层是智能体的“大脑”。用户说“帮我查一下上个月的销售数据然后生成一份报告发给张总”模型需要拆成查数据、生成报告、发邮件三个子任务并且知道每个子任务该调哪个工具。这里的关键是工具描述的质量。模型选不选得对工具几乎完全取决于你给工具写的描述。我见过太多人工具描述写得含糊比如“查询数据”模型根本不知道查什么数据、参数是什么。好的描述应该包含工具做什么、什么场景用、参数含义、返回什么。举个例子Tool(description 根据时间范围查询销售订单数据。参数 startDate 和 endDate 格式为 yyyy-MM-dd返回订单列表包含订单号、金额、客户名称) public ListOrder querySalesOrders(String startDate, String endDate) { // 实际查询逻辑 }Spring AI 的Tool注解会把方法签名和描述一起发给模型模型据此决定是否调用。描述写得越清楚模型选错的概率越低。3.3 执行层工具调用与结果回传执行层负责真正把工具跑起来。这里有几个工程上的坑。第一是参数校验模型生成的参数不一定合法日期格式错了、ID 不存在都要在执行前挡掉返回明确的错误信息给模型让它重新生成。第二是超时控制外部接口调用必须设超时不能让一个卡住的工具拖垮整个智能体。第三是幂等性涉及写操作的工具下单、发消息要考虑重复调用的问题最好带一个幂等键。工具执行完结果要回传给模型让模型决定下一步。这个循环可能跑好几轮直到模型认为任务完成。Spring AI 的ChatClient支持自动的工具调用循环但我在生产环境更倾向于手动控制循环次数设一个上限比如 10 轮防止模型陷入死循环烧 token。3.4 状态层任务持久化与断点续跑状态层是最容易被忽视、但生产环境最不能少的一层。智能体执行一个复杂任务可能耗时几分钟中间服务重启了怎么办用户关掉页面再回来任务还在跑吗我的方案是把任务状态持久化到数据库每个子任务的执行状态、输入输出都记下来。任务执行器做成异步的提交任务后返回一个任务 ID前端轮询或者通过 WebSocket 拿进度。这样服务重启后扫描未完成的任务继续跑用户也能随时查看进度。这块用 Spring 的Async配合线程池就能实现复杂一点可以用消息队列解耦。4. 实操落地从零搭一个能跑任务的智能体4.1 环境准备与依赖配置先把工程骨架搭起来。用 Spring Initializr 建一个 Spring Boot 3.x 项目Java 17 起步。核心依赖就三个spring-boot-starter-web、spring-ai-openai-spring-boot-starter或者对应的国产模型 starter、spring-boot-starter-data-redis。dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency配置文件里配好模型连接信息。如果用 OpenAI 兼容协议的国产模型把base-url指向对应的地址即可spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://your-model-endpoint/v1 chat: options: model: your-model-name temperature: 0.7注意api-key 千万别硬编码在配置文件里提交到代码仓库用环境变量或者配置中心注入。我见过不止一次密钥泄露的事故。4.2 定义工具集让模型知道能做什么工具定义是整个智能体能力的边界。我一般按业务域分组每个工具类用Component注册方法上加Tool注解。下面是一个订单查询工具的示例Component public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService orderService; } Tool(description 根据订单号查询订单详情。参数 orderId 为订单编号返回订单的完整信息) public OrderDetail getOrderDetail(String orderId) { return orderService.findById(orderId); } Tool(description 查询指定时间范围内的订单列表。startDate 和 endDate 格式为 yyyy-MM-dd) public ListOrderSummary listOrders(String startDate, String endDate) { LocalDate start LocalDate.parse(startDate); LocalDate end LocalDate.parse(endDate); return orderService.listBetween(start, end); } }工具方法里要做参数校验别指望模型每次都传对。日期解析失败就抛一个带明确信息的异常Spring AI 会把异常信息回传给模型模型通常会修正参数重试。4.3 对话接口实现流式响应加工具调用对话接口我用ChatClient来写流式输出配合工具调用。核心代码如下RestController public class AgentController { private final ChatClient chatClient; public AgentController(ChatClient.Builder builder, OrderTools orderTools) { this.chatClient builder .defaultSystem(你是一个订单助手可以帮用户查询订单信息。回答要简洁准确。) .defaultTools(orderTools) .build(); } GetMapping(value /chat, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chat(RequestParam String message, RequestParam String sessionId) { return chatClient.prompt() .user(message) .advisors(a - a.param(chat_memory_conversation_id, sessionId)) .stream() .content(); } }这里用了chat_memory_conversation_id参数来绑定会话记忆Spring AI 会自动从配置的ChatMemory里读写历史消息。会话记忆我一般配 Redis 实现重启不丢。4.4 任务执行链路从意图到动作的完整闭环把上面几层串起来一个完整的任务执行流程是这样的。用户发消息“查一下订单 A123 的详情”对话层接收后连同历史上下文一起发给模型。模型判断需要调用getOrderDetail工具生成调用请求。执行层校验参数、调用工具、拿到订单详情回传给模型。模型根据结果组织自然语言回复流式推给前端。如果任务复杂一点比如“查一下 A123 和 B456 两个订单对比一下金额”模型会连续调用两次工具然后综合两个结果给出对比。这个多轮工具调用的循环Spring AI 会自动处理但你要在配置里设好最大轮次防止失控。spring: ai: openai: chat: options: tool-call-limit: 10实操心得工具调用的日志一定要打全包括模型生成的参数、工具返回的结果、每一轮的耗时。排查问题时这些日志就是救命稻草。我一般用 MDC 把 sessionId 打进日志方便串联一次会话的所有调用。5. 踩坑实录那些文档里不会写的问题5.1 模型不调用工具怎么办这是新手最常遇到的问题。模型明明有能力调工具但它就是直接编一个答案给你。原因通常有三个。第一工具描述太模糊模型不确定该不该用。第二system prompt 里没明确要求“涉及数据查询必须调用工具不要凭记忆回答”。第三模型本身能力不足小参数模型对工具调用的支持确实差。我的解决顺序是先改工具描述把使用场景写清楚再改 system prompt明确禁止编造数据最后才考虑换模型。实测下来前两步能解决八成问题。5.2 参数格式错误与重试策略模型生成的参数格式错误非常常见尤其是日期、枚举值这类。我的做法是在工具方法里做严格校验校验失败抛出带明确提示的异常。Spring AI 会把异常信息作为工具结果回传模型看到“日期格式错误应为 yyyy-MM-dd”之后通常下一次就能改对。但要设重试上限。我遇到过模型反复生成同一个错误参数的情况无限重试会烧掉大量 token。工具调用轮次上限就是干这个用的超过就返回兜底话术让用户手动确认。5.3 上下文膨胀与 token 成本控制多轮对话跑久了上下文会越来越长token 成本直线上升响应也变慢。我的控制策略是保留最近 10 轮完整对话更早的做摘要。摘要用一个便宜的小模型来生成把关键信息压缩成几句话。另外工具返回的结果如果很长比如一个几百条的列表不要原样塞回上下文只回传模型决策需要的字段比如总数、前几条样例。5.4 并发场景下的会话隔离多个用户同时用会话必须隔离。ChatMemory的 key 一定要用 sessionId别用固定的。我见过有人图省事用单例的 memory结果两个用户的对话串在一起A 用户看到了 B 用户的订单信息这是严重的安全事故。会话 ID 建议用 UUID服务端生成后返回给前端后续请求带上。5.5 常见问题速查表问题现象可能原因排查方向模型不调用工具描述模糊、prompt 未约束检查工具描述和 system prompt参数格式错误模型生成不稳定加参数校验返回明确错误提示响应超时工具执行慢、轮次过多设工具超时和轮次上限会话串号memory key 不唯一检查 sessionId 生成和传递token 消耗过快上下文膨胀加摘要压缩精简工具返回服务重启任务丢失状态未持久化任务状态落库支持续跑6. 安全与可观测性生产环境不能省的两件事6.1 工具权限与行为审计智能体能调工具就意味着它能改数据、发消息、动钱。权限必须卡死。我的做法是工具方法上再加一层权限注解执行前校验当前用户有没有权限调这个工具。比如普通用户只能查自己的订单管理员才能查全部。这个校验不能交给模型判断必须在 Java 代码里硬校验。行为审计同样重要。每一次工具调用都要记审计日志谁、什么时候、调了什么工具、参数是什么、结果如何。这既是安全要求也是排查问题的依据。日志里敏感字段手机号、身份证要脱敏。6.2 监控指标与告警配置智能体的监控我关注几个核心指标单次对话的模型调用次数、工具调用成功率、平均响应延迟、token 消耗量。这些用 Micrometer 埋点接到 Prometheus 里配 Grafana 面板。工具调用失败率超过阈值就告警通常是外部依赖出问题了。Spring Boot Actuator 的健康检查也要加上把模型连接状态纳入检查项。模型服务不可用的时候健康检查要能反映出来别等用户反馈才发现。提示智能体的监控和普通服务有个区别——要监控“模型是否在胡说”。可以定期抽样对话记录用另一个模型做质量评估发现异常回答及时介入。这块成本不低但涉及资金、医疗等敏感场景时值得做。7. 我个人的一些实际体会做了一段时间 Java 智能体最大的感受是这东西的难点从来不在 AI而在工程。模型能力是现成的调接口谁都会但怎么让它在你的业务系统里稳定、安全、可控地跑起来才是真功夫。我见过太多 Demo 惊艳、上线就崩的项目问题几乎都出在状态管理、权限控制、异常处理这些“传统”工程问题上。另一个体会是别追求一步到位。先做一个只能查数据的只读智能体跑通了、稳定了再逐步开放写操作。每加一个工具都要想清楚它的失败模式是什么、最坏情况会造成什么影响。智能体的能力边界应该是你主动设计的而不是模型自己探索出来的。最后分享一个小技巧工具的描述和 system prompt建议做成可配置的放在数据库或者配置中心改完不用重启服务。因为这两个东西的调优是个持续过程上线后根据实际对话记录不断打磨效果会越来越好。硬编码在代码里每次改都要走发布流程迭代速度根本跟不上。
返回列表