1. 从对话到执行:Java 智能体到底在解决什么问题
很多人第一次听到“Java 智能体开发”,脑子里浮现的还是聊天窗口里那个一问一答的机器人。但真正做过落地项目的人都知道,对话只是最表层的一层皮,智能体的核心价值在于把自然语言意图翻译成可执行的任务,并且真的把任务跑完。这两件事之间的鸿沟,才是 Java 后端工程师真正要填的坑。
我接触过不少团队,前期用 Python 快速搭了个 Demo,演示的时候效果惊艳,一旦要接入公司现有的订单系统、工单系统、权限体系,立刻就卡住了。原因很简单:企业里跑着的核心业务,八成以上是 Java 写的,Spring Boot 服务、Dubbo 接口、各种内部 RPC,Python 那套生态要对接起来,中间得加一层又一层的胶水。这时候用 Java 来做智能体,反而成了顺理成章的选择——不是因为它写 AI 算法更方便,而是因为它离业务系统更近。
所以这篇文章想聊的,不是“怎么调一个大模型接口”这种入门话题,而是一个 Java 智能体从接收用户对话,到拆解任务、调用工具、执行动作、返回结果的完整链路。我会围绕 Spring Boot 和 Spring AI 这套技术栈展开,因为这是目前 Java 圈子里最主流、也最容易被团队接受的方案。适合谁看?如果你是有 Java 基础、写过 Spring Boot 服务,现在想把手里的业务系统接上大模型能力,那这篇基本就是给你写的。如果你还在纠结 Java 和 Python 选哪个,我也会在选型部分给出我的实际判断。
先把结论摆前面:Java 智能体开发的关键难点不在模型调用,而在任务编排、工具注册、状态管理和安全边界。把这四件事想清楚,代码写起来其实很快。下面我按实际项目的推进顺序,一层层拆开讲。
2. 技术选型:为什么是 Spring Boot 加 Spring AI
2.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 连接会一直挂着,用户体验差,还容易触发网关超时。
第二种是流式响应,用 SSE(Server-Sent Events)把模型吐出来的 token 一个个推给前端。这是我现在默认的选择,因为智能体场景下响应往往很长,流式能让用户第一时间看到反馈,感知延迟大幅降低。Spring AI 的ChatClient原生支持流式,返回Flux<String>,配合 Spring WebFlux 或者 Spring MVC 的SseEmitter都能实现。
提示:如果你的智能体要执行耗时任务(比如查数据库、调外部接口),流式响应要分两段设计——先流式输出“思考过程”,任务执行完再推最终结果。别让用户对着空白页面等十几秒。
3. 核心架构拆解:一个智能体的四层结构
3.1 对话层:意图识别与上下文管理
对话层负责接收用户输入,维护多轮对话的上下文。这里最容易出问题的是上下文窗口管理。大模型有 token 上限,对话轮次多了,历史消息会撑爆窗口。我的做法是保留最近 N 轮完整对话,更早的做摘要压缩,把关键信息(用户身份、已确认的参数、任务状态)提取成结构化数据存起来,而不是把原始对话全塞进去。
上下文存储我一般用 Redis,key 用会话 ID,value 存消息列表,设置合理的过期时间。这里有个细节:消息的角色要严格区分system、user、assistant,工具调用的结果要用专门的 tool 角色,混用会导致模型理解错乱。Spring AI 的Message体系已经把这些角色封装好了,直接用就行。
意图识别这块,早期我用过单独的意图分类模型,后来发现没必要。现在主流做法是把意图识别和任务规划合并到一次模型调用里,通过精心设计的 system prompt,让模型直接输出结构化的任务计划。这样少一次调用,延迟更低,而且意图和计划的一致性更好。
3.2 规划层:任务拆解与工具选择
规划层是智能体的“大脑”。用户说“帮我查一下上个月的销售数据,然后生成一份报告发给张总”,模型需要拆成:查数据、生成报告、发邮件三个子任务,并且知道每个子任务该调哪个工具。
这里的关键是工具描述的质量。模型选不选得对工具,几乎完全取决于你给工具写的描述。我见过太多人工具描述写得含糊,比如“查询数据”,模型根本不知道查什么数据、参数是什么。好的描述应该包含:工具做什么、什么场景用、参数含义、返回什么。举个例子:
@Tool(description = "根据时间范围查询销售订单数据。参数 startDate 和 endDate 格式为 yyyy-MM-dd,返回订单列表,包含订单号、金额、客户名称") public List<Order> 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> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.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 List<OrderSummary> 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 Flux<String> 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,建议做成可配置的,放在数据库或者配置中心,改完不用重启服务。因为这两个东西的调优是个持续过程,上线后根据实际对话记录不断打磨,效果会越来越好。硬编码在代码里,每次改都要走发布流程,迭代速度根本跟不上。