首页 / 资讯中心 / 文章详情

Java开发者转型AI Agent实战:用Spring AI构建智能应用

Java开发者转型AI Agent实战:用Spring AI构建智能应用 ★ FEATURED ARTICLE
1. 一个Java老兵的转型困局为什么90%的人卡在同一个地方干了十三年Java从SSH三大框架一路写到Spring Boot微服务中间经历过无数次技术迭代自认为学习能力还算过关。但去年开始接触AI Agent这个方向的时候我承认自己一度陷入了非常典型的焦虑——打开任何一个技术社区满屏都是Python代码LangChain、AutoGPT、CrewAI所有教程默认你是一个Python开发者。我身边好几个同样做Java后端的兄弟第一反应都是“那我是不是得先把Python学一遍”。这个想法非常危险。不是说Python不值得学而是你把“学Python”当成了转型AI Agent的前置条件这个假设本身就是错的。我花了大概两个月时间用纯Java技术栈落地了三个可用的Agent项目踩了无数的坑回头再看发现90%的Java开发者在转型路上挂掉根本不是因为技术难度而是因为一开始的方向就偏了。这篇文章想做的事情很明确把“Java开发者如何用自己已有的技术栈切入AI Agent开发”这件事讲透。我会从架构思路、技术选型、实操步骤、踩坑记录几个维度展开把每一步的决策逻辑和操作细节都摊开来讲。适合有Java后端经验、想往AI Agent方向靠但不知道从哪里下手的开发者也适合已经在做但总觉得别扭、想看看别人怎么绕开弯路的同行。核心关键词就几个Java AI Agent、Spring AI、工具调用、Agent架构、Java转型AI。这些词后面会反复出现但我尽量不堆砌而是融进实际的决策场景里讲。2. 转型思路的整体拆解为什么不用先学Python2.1 先搞清楚AI Agent到底在做什么很多人对AI Agent的理解停留在“会自己思考的AI”这个说法太玄了。从工程角度看一个Agent的本质就是大语言模型 工具调用 记忆管理 任务编排。大模型负责理解和生成工具调用让它能跟外部世界交互记忆管理让它有上下文任务编排让它能处理复杂流程。你仔细看这四个部分除了大模型本身是外部服务通过API调用剩下的工具调用、记忆管理、任务编排全都是你做了十几年的后端工程问题。工具调用本质上就是API网关和适配器模式记忆管理就是状态管理和缓存设计任务编排就是工作流引擎。这些东西用Java做你比Python开发者更有优势因为Java的工程化生态在这些领域积累了太多成熟的方案。我一开始也想过要不要学Python后来问了自己一个问题我是要去做大模型训练和算法研究还是要做Agent的应用层工程答案很明确我要做的是后者。应用层工程的核心能力是系统设计、接口抽象、状态管理、异常处理这些恰好是Java开发者最擅长的。2.2 Java生态在AI Agent领域的现状去年年初的时候Java在AI Agent这块确实比较空白但到了现在情况已经完全不一样了。Spring AI已经迭代了好几个版本LangChain4j的成熟度也上来了还有像Semantic Kernel的Java版本也在跟进。这些框架把大模型调用、Prompt模板、向量存储、工具调用这些基础能力都封装好了你不需要从零造轮子。我实际用下来Spring AI的抽象层次设计得比较合理它把ChatClient、EmbeddingClient、VectorStore这些核心概念抽象得很干净跟Spring Boot的集成也非常自然。LangChain4j在Agent编排和工具调用方面更灵活一些适合做复杂流程。两个框架可以混用也可以根据项目需求选一个。这里有个关键认知框架只是工具核心是你对Agent架构的理解。不要因为框架更新快就焦虑把底层逻辑搞清楚了换框架就是几天的事。2.3 转型路径的决策逻辑我给自己的转型路径定了三个原则。第一不脱离Java技术栈所有代码用Java写构建工具用Maven或Gradle部署用现有的CI/CD流程。第二从最小可用Agent开始不要一上来就搞多Agent协作、自主规划这些复杂的东西先做一个能调用一两个工具、能完成单一任务的Agent。第三以项目驱动学习不要先系统学一遍理论再动手而是定一个具体的小需求边做边查。这三个原则帮我省了大量时间。我见过太多人卡在“准备阶段”——买了好几本Python书看了几十个小时的视频教程结果半年过去了连一个能跑的Agent都没做出来。动手做比什么都重要。3. 核心细节解析Java AI Agent的技术栈怎么搭3.1 大模型接入层的选型与配置大模型接入是整个Agent的基础。Java这边目前主流的方式是通过HTTP客户端调用大模型服务商的APISpring AI和LangChain4j都提供了封装好的Client。我选的是Spring AI的ChatClient原因是它跟Spring Boot的自动配置集成得最好配置项清晰切换模型服务商也方便。配置的核心参数有几个需要特别注意。temperature控制输出的随机性做Agent的时候一般设低一点0.1到0.3之间比较合适因为Agent需要稳定的决策而不是创意写作。maxTokens要根据你的任务复杂度来设太小会导致输出被截断太大会增加成本和延迟。topP一般保持默认就行不用太纠结。spring: ai: openai: api-key: ${API_KEY} chat: options: model: gpt-4o temperature: 0.2 max-tokens: 2048这段配置看起来简单但里面有个坑API Key的管理。千万不要硬编码在配置文件里用环境变量或者配置中心。我在本地开发的时候用环境变量部署到服务器上用配置中心这样既安全又方便切换环境。3.2 工具调用的实现机制工具调用是Agent区别于普通聊天机器人的核心能力。它的原理其实不复杂你把可用的工具定义名称、描述、参数schema一起发给大模型大模型根据用户输入判断是否需要调用工具、调用哪个工具、传什么参数然后返回一个结构化的调用请求你的代码执行这个调用把结果再发给大模型大模型根据结果生成最终回复。在Spring AI里工具调用通过Tool注解来实现。你定义一个普通的Java方法加上注解Spring AI会自动把它注册为可调用的工具。Component public class WeatherTools { Tool(description 查询指定城市的当前天气) public String getWeather( ToolParam(description 城市名称) String city) { // 实际调用天气API return weatherService.query(city); } }这里有几个实操要点。工具描述要写清楚大模型是根据描述来判断什么时候调用这个工具的描述模糊会导致调用错误。参数类型要明确尽量用String、Integer这些基础类型复杂对象容易出问题。工具方法要幂等因为大模型可能会重复调用同一个工具你的实现要能处理这种情况。3.3 记忆管理的设计思路Agent的记忆分两种短期记忆和长期记忆。短期记忆就是当前对话的上下文这个直接用消息列表维护就行。长期记忆需要把历史信息存储起来需要的时候检索出来这就涉及到向量数据库。我一开始觉得记忆管理很复杂后来发现用Java做这件事其实很顺手。短期记忆就是一个ListMessage每次对话把用户消息和助手回复加进去发给大模型的时候带上最近N条。长期记忆用向量存储把文本转成embedding存进去检索的时候算相似度。public class ConversationMemory { private final ListMessage messages new ArrayList(); private final VectorStore vectorStore; public void addMessage(Message message) { messages.add(message); if (messages.size() MAX_SIZE) { // 超出上限时把旧消息存入长期记忆 Message old messages.remove(0); vectorStore.add(old.getContent()); } } public ListMessage getRecentMessages(int count) { int size messages.size(); return messages.subList(Math.max(0, size - count), size); } }记忆管理最容易踩的坑是上下文长度爆炸。大模型的上下文窗口是有限的你把所有历史消息都塞进去很快就会超限。我的做法是保留最近10轮对话作为短期记忆更早的存入向量库需要的时候检索相关片段。3.4 任务编排的工程化实现任务编排是Agent处理复杂任务的关键。简单任务一步就完成了复杂任务需要拆解成多个步骤每个步骤可能调用不同的工具步骤之间可能有依赖关系。这部分用Java做简直是降维打击因为Java有太多工作流引擎和状态机的成熟方案。我自己的做法是用一个轻量的状态机来管理任务流程。每个任务定义为一个状态图节点是步骤边是转移条件。Agent在执行过程中根据当前状态和工具返回结果决定下一步走向。public class TaskOrchestrator { private final MapString, State states; private final ChatClient chatClient; public TaskResult execute(Task task) { State current states.get(START); while (!current.isTerminal()) { StepResult result current.execute(task, chatClient); current current.next(result); } return current.getResult(); } }这种设计的好处是流程清晰、可追踪、可中断恢复。每个步骤的执行结果都有记录出问题的时候能快速定位是哪个环节出了错。4. 实操过程从零搭建一个可用的Java AI Agent4.1 项目初始化的关键步骤我用Spring Boot 3.x作为基础框架Maven作为构建工具。项目结构跟普通的Spring Boot项目没有本质区别只是多了一些AI相关的依赖和配置。dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0-M4/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-core/artifactId version1.0.0-M4/version /dependency版本选择上有个建议不要追最新版。Spring AI的版本迭代很快新版本可能引入不兼容的改动。我选的是当时比较稳定的M4版本用下来没什么大问题。如果你要用LangChain4j也是同样的原则选一个社区反馈比较多的稳定版本。项目初始化之后先写一个最简单的ChatClient调用确认大模型能通。这一步很重要不要急着写复杂的Agent逻辑先把基础链路跑通。RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder.build(); } GetMapping(/chat) public String chat(RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }启动项目访问这个接口如果能看到大模型的回复说明基础环境没问题。这一步看起来简单但我见过不少人卡在这里各种依赖冲突、版本不匹配、网络问题。遇到问题不要慌先看日志大部分问题日志里都有答案。4.2 工具调用的完整实现流程基础链路通了之后开始加工具调用。我以“查询订单状态”这个场景为例完整走一遍流程。第一步定义工具方法。这个方法需要能根据订单号查询订单状态实际项目中可能是查数据库或者调内部API。Component public class OrderTools { Tool(description 根据订单号查询订单的当前状态返回状态描述) public String queryOrderStatus( ToolParam(description 订单号格式为ORD开头加数字) String orderId) { // 模拟查询 Order order orderRepository.findByOrderId(orderId); if (order null) { return 未找到该订单; } return String.format(订单%s当前状态%s, orderId, order.getStatus()); } }第二步把工具注册到ChatClient。Spring AI会自动扫描Tool注解的方法你只需要在构建ChatClient的时候把工具对象传进去。Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder, OrderTools orderTools) { return builder .defaultTools(orderTools) .build(); } }第三步测试工具调用。发一条“帮我查一下订单ORD12345的状态”观察大模型是否正确地调用了工具以及返回结果是否符合预期。这里有个细节需要注意大模型调用工具是一个多轮交互过程。第一轮大模型返回的是工具调用请求你的代码执行工具把结果发回去大模型再生成最终回复。Spring AI把这个过程封装好了你不需要手动处理但理解这个机制对排查问题很有帮助。4.3 记忆管理的落地实现记忆管理我分了两层来实现。第一层是内存中的对话历史用ConcurrentHashMap按会话ID存储每个会话维护一个消息列表。第二层是向量存储用Redis或者PgVector都行我选的是PgVector因为项目里本来就有PostgreSQL不用额外引入组件。Service public class MemoryService { private final MapString, ListMessage sessionMemory new ConcurrentHashMap(); private final VectorStore vectorStore; public void addMessage(String sessionId, Message message) { sessionMemory.computeIfAbsent(sessionId, k - new ArrayList()) .add(message); } public ListMessage getContext(String sessionId, String query) { ListMessage recent sessionMemory.getOrDefault(sessionId, List.of()); // 从向量库检索相关历史 ListDocument relevant vectorStore.similaritySearch(query); // 合并最近对话和相关历史 return merge(recent, relevant); } }实操中有一个容易忽略的点消息的裁剪策略。不能简单地按条数裁剪因为有些消息很长有些很短。我后来改成按token数估算设置一个上限超出就裁剪最旧的消息。估算token可以用简单的字符数除以4来近似中文的话除以2更准一些。4.4 完整Agent的组装与调试把大模型接入、工具调用、记忆管理三块拼起来一个基本的Agent就成型了。但组装的过程中会遇到各种问题我挑几个典型的讲。第一个问题是工具调用的循环。有时候大模型会反复调用同一个工具陷入死循环。我的解决办法是设置一个最大调用次数比如5次超过就强制结束并返回当前结果。第二个问题是工具返回结果的格式。大模型对工具返回的内容有格式偏好太长的文本、太复杂的结构都容易导致解析失败。我的经验是工具返回尽量用简洁的自然语言不要返回JSON除非大模型明确要求。第三个问题是异常处理。工具调用可能失败网络可能超时大模型可能返回不合预期的内容。这些异常都要有兜底处理不能让整个Agent崩溃。public String executeWithRetry(String userInput, int maxRetries) { for (int i 0; i maxRetries; i) { try { return chatClient.prompt() .user(userInput) .call() .content(); } catch (Exception e) { log.warn(第{}次调用失败: {}, i 1, e.getMessage()); if (i maxRetries - 1) { return 抱歉服务暂时不可用请稍后重试; } } } return null; }5. 常见问题与排查技巧实录5.1 依赖冲突与版本问题速查Java项目最烦人的就是依赖冲突AI框架引入的依赖又多冲突概率更高。我整理了一个速查表覆盖最常见的几类问题。问题现象可能原因排查方法解决方案启动报NoSuchMethodError依赖版本不一致mvn dependency:tree排除冲突依赖统一版本工具调用不生效注解未被扫描检查Component扫描路径确保工具类在扫描范围内大模型返回乱码编码配置问题检查HTTP客户端编码设置UTF-8编码响应超时网络或模型负载查看日志中的耗时增加超时时间加重试内存溢出上下文过长监控JVM内存限制上下文长度及时清理依赖冲突这块我的经验是尽量用Spring AI的BOM来管理版本不要自己一个个指定版本号。BOM会把兼容的版本组合好省去很多麻烦。5.2 工具调用失败的典型场景工具调用失败是最常见的问题我遇到过好几种情况。一种是大模型不调用工具直接用自己的知识回答。这通常是因为工具描述不够清晰或者用户输入跟工具的相关性不强。解决办法是优化工具描述在系统提示词里明确告诉大模型什么时候该用工具。另一种是调用参数错误。大模型传的参数格式不对或者缺少必填参数。这个要在工具方法的参数校验上下功夫参数不合法就返回明确的错误信息让大模型知道哪里错了它下一轮会修正。还有一种是工具执行超时。外部API响应慢导致整个Agent卡住。我的做法是给每个工具调用设置独立的超时时间超时了就返回一个默认值或者错误提示不要让整个流程挂起。5.3 性能优化的实操经验Agent的性能瓶颈通常在两个地方大模型调用和工具调用。大模型调用受限于服务商的响应速度你能做的优化有限主要是减少不必要的调用。比如简单的意图识别可以用本地的小模型或者规则引擎来做不用每次都调大模型。工具调用的优化空间更大。并行调用是一个有效的手段如果多个工具之间没有依赖关系可以并发执行。Java的CompletableFuture做这件事很顺手。public MapString, Object parallelToolCall(ListToolCall calls) { ListCompletableFutureToolResult futures calls.stream() .map(call - CompletableFuture.supplyAsync( () - executeTool(call), executorService)) .toList(); return futures.stream() .map(CompletableFuture::join) .collect(Collectors.toMap(ToolResult::getName, ToolResult::getResult)); }另外缓存也很重要。有些工具调用的结果在一定时间内是稳定的比如查询配置信息、查询用户基本信息这些可以缓存起来避免重复调用。5.4 独家避坑技巧汇总踩了这么多坑我总结了几条别人不太会告诉你的经验。第一条不要迷信框架的封装。Spring AI和LangChain4j都做了很多封装用起来很方便但出问题的时候你如果不懂底层原理排查起来会很痛苦。我的建议是至少把大模型API的原始调用方式搞清楚知道框架在背后做了什么。第二条日志要打全。Agent的执行过程涉及多次大模型调用和工具调用每个环节的输入输出都要打日志。我一开始日志打得不全出了问题根本不知道是哪一步错了。后来加了详细的日志排查效率提升了好几倍。第三条先做单Agent再做多Agent。多Agent协作听起来很酷但复杂度是指数级上升的。我见过不少人一上来就搞多Agent结果连一个Agent都没跑通。先把单Agent做扎实理解清楚Agent的基本运作机制再考虑多Agent。第四条测试要覆盖边界情况。大模型的输出是不确定的同样的输入可能得到不同的输出。测试的时候不能只测正常路径要覆盖各种边界情况空输入、超长输入、特殊字符、工具调用失败、大模型返回异常格式等等。第五条成本要提前算。大模型调用是按token计费的Agent因为有多轮交互token消耗比普通聊天大得多。上线之前一定要估算成本设置好预算上限和告警。我见过一个项目因为没做成本控制一个月烧掉了几万块。6. 从单Agent到多Agent的扩展思路6.1 什么时候需要多Agent单Agent能处理大部分场景但有些复杂任务确实需要多个Agent协作。判断标准很简单如果一个任务需要多种截然不同的专业能力且这些能力之间需要频繁交互那就考虑多Agent。比如一个客服场景可能需要一个负责理解用户意图的Agent、一个负责查询订单的Agent、一个负责处理退换货的Agent。这三个Agent各司其职通过一个协调者来调度。但我要强调的是多Agent不是必须的。很多场景用单Agent加多个工具就能解决没必要引入多Agent的复杂度。我自己的项目里只有一个是真正的多Agent架构其他都是单Agent。6.2 多Agent通信的Java实现多Agent之间的通信本质上就是消息传递。Java里可以用消息队列、事件总线、或者简单的内存队列来实现。我选的是Spring的ApplicationEvent机制轻量且跟Spring生态集成好。public class AgentCoordinator { private final MapString, Agent agents; private final ApplicationEventPublisher eventPublisher; public void dispatch(Task task) { String agentName route(task); Agent agent agents.get(agentName); AgentResult result agent.execute(task); eventPublisher.publishEvent(new AgentCompletedEvent(agentName, result)); } }这种设计的好处是解耦每个Agent只需要关注自己的任务协调者负责路由和结果汇总。扩展新Agent的时候只需要注册进去不用改现有代码。6.3 多Agent协作的注意事项多Agent协作最容易出的问题是死循环和责任不清。死循环就是Agent A等Agent B的结果Agent B等Agent A的结果互相等待。解决办法是设置超时和最大轮次超时了就强制结束。责任不清是指一个任务多个Agent都觉得自己该处理或者都觉得不该自己处理。这个要在协调者层面把路由规则定义清楚每个Agent的职责边界要明确。我的经验是多Agent系统的调试难度比单Agent高一个数量级。如果没有明确的必要优先考虑单Agent方案。如果确实需要多Agent先把每个单Agent单独调通再做集成。7. 一些掏心窝子的实操体会写了这么多最后分享几个我在实际项目中体会最深的点。Java做AI Agent最大的优势是工程化能力。你可能觉得Java在AI领域生态不如Python但在Agent的应用层工程上Java的成熟度是碾压的。依赖注入、AOP、事务管理、连接池、熔断限流这些在Python里要自己搭的东西Java里都是现成的。我做一个Agent的异常处理和重试机制用Spring Retry几行代码就搞定了Python里可能要写一堆装饰器。不要被“AI”这个词吓到。Agent开发的核心不是AI算法而是系统设计。你不需要懂Transformer的原理不需要会调参你需要的是把大模型当成一个能力很强但不太稳定的外部服务围绕它设计一套健壮的系统。这个思路转变过来之后你会发现一切都很熟悉。持续迭代比一步到位重要。我的第一个Agent版本非常粗糙工具调用经常出错记忆管理也很简陋。但就是这么一个粗糙的东西让我把整个链路跑通了后面所有的优化都是在这个基础上做的。如果你一直在准备永远不会有准备好的那一天。先做一个能跑的版本哪怕很烂然后在用的过程中不断改进。社区和文档是最好的老师。Spring AI和LangChain4j的文档质量都不错遇到问题先查文档再去GitHub的issue里搜大部分问题别人都遇到过。我遇到过一个工具调用的bug在issue里搜到了解决方案省了我至少两天时间。这个方向还在快速演进新的框架、新的模式层出不穷。但底层的东西是不变的理解Agent的基本原理掌握工具调用和记忆管理的实现方式用你熟悉的Java技术栈去构建。把这些搞定了不管上层怎么变你都能快速跟上。
阅读完成 · 觉得有帮助?
咨询建站