
Java 工程师转型 AI Agent 这件事我从去年下半年开始认真琢磨到现在算是踩完了第一轮坑。身边不少写了五六年 Spring Boot 的朋友都在问同一个问题我 CRUD 写得挺熟但 AI Agent 这东西到底跟我有什么关系我是不是得从头学 Python我的答案很直接——不用。你现有的 Java 工程能力恰恰是很多纯算法背景的人最缺的那块拼图。问题不在于你学不学得会而在于你有没有搞清楚 Agent 的本质是什么、Java 生态里哪些工具已经能用了、以及哪些地方是真正的坑。这篇东西我打算把从原理到落地的整条链路讲透不讲虚的。核心围绕几个关键词展开Java、AI Agent、LangChain4j、Spring AI、ReAct。如果你是一个有 Java 基础、想切入 AI Agent 方向但不知道从哪下手的工程师这篇应该能帮你省掉至少两三个月的瞎摸索时间。1. 先搞清楚 AI Agent 到底比普通接口调用多了什么1.1 从一问一答到自主决策的本质区别很多人第一次接触 AI Agent 的时候脑子里想的还是我调一个 API传个 prompt拿个回复。这个理解不能说错但它只覆盖了 Agent 最表层的东西。普通的 LLM 调用是一问一答你问什么它答什么它不会主动去做任何事。而 Agent 的核心在于它能自己决定下一步该干什么。我举个具体的例子你就明白了。假设你做一个客服系统用户说帮我查一下上个月的订单然后退掉那个蓝色的。普通 LLM 调用能做到的是理解这句话的意思然后告诉你我需要查询订单接口。但 Agent 能做到的是它自己判断需要先调用订单查询工具拿到结果后判断哪个是蓝色的再调用退款工具最后把结果汇总给用户。整个过程不需要你写 if-else 去编排它自己决定调用顺序和参数。这个自己决定的能力在技术实现上靠的是ReAct 模式Reasoning Acting。ReAct 的核心逻辑是一个循环思考Thought→ 行动Action→ 观察Observation→ 再思考。每一轮循环模型根据当前已有的信息决定下一步做什么直到它认为任务完成。1.2 ReAct 循环在 Java 里长什么样我用 LangChain4j 写一个最简化的 ReAct 循环示意你感受一下// 伪代码展示 ReAct 循环的核心逻辑 while (!taskCompleted) { // 1. 把当前上下文历史对话 工具列表 已有观察结果发给 LLM String thought llm.generate(context); // 2. 解析 LLM 的输出判断它是想调用工具还是给出最终答案 if (thought.contains(Action:)) { String toolName extractToolName(thought); String toolInput extractToolInput(thought); // 3. 执行工具调用 String observation toolRegistry.execute(toolName, toolInput); // 4. 把观察结果追加到上下文进入下一轮循环 context.append(Observation: observation); } else { // 5. LLM 认为任务完成输出最终答案 taskCompleted true; return thought; } }这段代码看起来简单但里面有几个关键点决定了 Agent 能不能跑起来。第一LLM 必须能稳定地按照你期望的格式输出比如固定输出 Action: xxx 和 Action Input: xxx否则你的解析逻辑就崩了。第二工具的描述必须足够清晰模型才能正确选择工具。第三循环必须有终止条件不然模型可能陷入死循环。1.3 Java 工程师做 Agent 的天然优势在哪我说句可能得罪人的话很多纯 Python 背景做 Agent 的人工程能力是真的不行。他们能把 Demo 跑通但一上生产就各种问题——并发扛不住、状态管理混乱、错误处理缺失、日志打不全。而这些恰恰是 Java 工程师每天都在解决的问题。Agent 系统本质上是一个分布式状态机它需要管理对话状态、工具调用链路、超时重试、并发控制、可观测性。你写过多线程、用过 Spring 的依赖注入、处理过事务回滚这些经验直接就能迁移过来。LangChain4j 和 Spring AI 这两个框架的设计思路本质上就是把 Agent 的各种组件做成可注入的 Bean让你用熟悉的 Spring 方式去组装。2. LangChain4j 和 Spring AI 到底该选哪个2.1 两个框架的定位差异这是被问得最多的问题。我的结论是如果你已经在用 Spring Boot优先选 Spring AI如果你想要更灵活的 Agent 编排能力选 LangChain4j。但实际情况往往更复杂我展开说。LangChain4j 的定位是Java 版的 LangChain它的核心抽象是 Chain、Tool、Memory、Retriever 这些概念。它的优势在于 Agent 编排能力更强支持多种 Agent 模式ReAct、Tool Calling、Plan-and-Execute而且对 RAG 的支持非常成熟多路召回、重排序这些都有现成的实现。Spring AI 的定位是Spring 生态的 AI 集成层它的核心思路是把 AI 能力做成 Spring 的 Bean让你用Autowired就能注入 ChatClient、EmbeddingModel 这些组件。它的优势在于和 Spring Boot 的无缝集成配置管理、依赖注入、AOP 这些你熟悉的东西全都能用上。我列一个对比表格你对照自己的场景看维度LangChain4jSpring AIAgent 编排能力强支持多种 Agent 模式中等主要靠 Tool CallingRAG 支持非常成熟多路召回、重排序都有基础 RAG 够用高级特性在补齐Spring 集成度一般需要手动配置原生集成开箱即用学习曲线稍陡概念较多平缓Spring 开发者上手快社区活跃度高更新频繁高背靠 Spring 官方生产稳定性成熟成熟2.0 之后更稳2.2 我的实际选型经验我自己的项目里两个框架都用过。最开始用 LangChain4j 做 RAG因为它的多路召回和重排序确实好用。后来做企业内部的 Agent 平台换成了 Spring AI因为团队里其他人都是 Spring 背景用 Spring AI 他们能快速上手不需要额外学一套新概念。这里有个坑我要提醒你不要在两个框架之间反复横跳。我见过一个团队一开始用 LangChain4j后来觉得 Spring AI 更香迁移到一半发现 Spring AI 的某个 Agent 特性还不支持又想迁回去结果两套代码混在一起维护成本爆炸。选型之前先把你最核心的需求列出来对着表格打分选定了就别轻易换。2.3 一个容易被忽略的点版本兼容性Spring AI 2.0 之后 API 有不小的变化如果你看的教程是 1.x 版本的很多代码直接跑不起来。LangChain4j 也是0.x 到 1.x 的 API 变动很大。我的建议是直接看官方文档的最新版本不要看博客里的老代码。博客里的代码可能写的时候是对的但框架一升级就废了。另外如果你要用 Spring AI 连接国内的模型服务比如百炼上的 Qwen 系列需要注意配置方式。Spring AI 2.0 对 OpenAI 兼容接口的支持更好了基本上改个 base-url 和 api-key 就能用。但有些模型对 function calling 的支持不完整这会导致 Agent 的工具调用失败。选模型的时候一定要确认它支持 function calling这是 Agent 的命脉。3. 用 Spring AI 搭一个能干活的最小 Agent3.1 环境准备中最容易忽略的三个细节先说环境。Spring AI 的项目初始化很简单用 Spring Initializr 勾选 Spring AI 相关的依赖就行。但有几个细节文档里不会重点讲但你不注意就会卡住。第一个是JDK 版本。Spring AI 2.0 要求 JDK 17 以上如果你还在用 JDK 8 或者 11先升级。这不是可选项是硬性要求。我见过有人折腾了半天最后发现是 JDK 版本不对。第二个是API Key 的管理。不要把 API Key 硬编码在代码里也不要在application.yml里明文写。用环境变量或者配置中心。Spring AI 支持从环境变量读取配置方式是在application.yml里写${AI_API_KEY}然后通过环境变量注入。第三个是超时设置。LLM 的响应时间波动很大快的时候一两秒慢的时候十几秒甚至更久。默认的超时时间往往不够你需要手动调大。在 Spring AI 里可以通过配置spring.ai.openai.chat.options.timeout来设置。# application.yml 关键配置 spring: ai: openai: api-key: ${AI_API_KEY} base-url: ${AI_BASE_URL} chat: options: model: qwen-plus temperature: 0.7 timeout: 600003.2 定义工具Agent 的手和脚Agent 能干活靠的是工具。在 Spring AI 里定义一个工具非常简单用一个Tool注解就行Component public class OrderTools { Tool(description 根据用户ID查询订单列表返回订单的详细信息) public ListOrder queryOrders( ToolParam(description 用户ID) String userId) { // 实际的数据库查询逻辑 return orderRepository.findByUserId(userId); } Tool(description 根据订单ID发起退款返回退款结果) public RefundResult refundOrder( ToolParam(description 订单ID) String orderId, ToolParam(description 退款原因) String reason) { // 实际的退款逻辑 return refundService.refund(orderId, reason); } }这里的关键在于description 的写法。工具描述写得好不好直接决定了 Agent 能不能正确选择工具。我总结了几条经验描述要说明这个工具做什么和什么时候用它而不是只写工具名参数描述要明确类型和格式比如用户ID格式为字符串如果工具有副作用比如退款、删除在描述里明确标注工具数量不要太多超过 20 个之后模型的选择准确率会明显下降3.3 组装 Agent把 LLM、工具、记忆串起来工具定义好了接下来就是组装 Agent。Spring AI 里用ChatClient来构建Configuration public class AgentConfig { Bean public ChatClient agentChatClient( ChatModel chatModel, OrderTools orderTools, ChatMemory chatMemory) { return ChatClient.builder(chatModel) .defaultTools(orderTools) .defaultSystem(你是一个电商客服助手负责帮用户查询订单和处理退款。 在调用工具之前先确认用户的需求是否明确。 如果信息不足先向用户询问。) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } }这段代码里有几个点值得展开。defaultSystem是系统提示词它定义了 Agent 的角色和行为边界。我强烈建议在系统提示词里加上如果信息不足先向用户询问这类约束否则 Agent 可能会在信息不全的情况下乱调工具。MessageChatMemoryAdvisor负责管理对话记忆。Agent 需要记住之前的对话内容才能进行多轮交互。Spring AI 提供了几种记忆实现简单的用内存生产环境建议用 Redis 或者数据库。3.4 跑通第一个 Agent 之后你会遇到的坑Demo 跑通的那一刻很爽但别高兴太早。我跑通第一个 Agent 之后遇到了几个典型问题问题一工具调用参数解析失败。模型输出的参数格式和你的方法签名对不上比如它传了一个 JSON 字符串但你的方法参数是 String结果就报错了。解决办法是在ToolParam里把格式写清楚或者在工具方法里做兼容处理。问题二Agent 陷入循环。模型反复调用同一个工具或者在不同的工具之间来回跳。这通常是因为工具返回的结果没有让模型满意它以为还需要继续查。解决办法是设置最大循环次数以及在工具返回结果里加上明确的已完成标识。问题三并发场景下的状态混乱。多个用户同时使用 Agent对话记忆串了。这是典型的并发问题Java 工程师应该很熟悉。解决办法是给每个会话分配独立的 sessionId记忆存储按 sessionId 隔离。4. 让 Agent 真正扛住并发Java 工程师的主场4.1 Agent 系统的并发瓶颈到底在哪Agent 系统和普通 Web 服务的并发模型不太一样。普通 Web 服务的瓶颈通常在数据库和网络 IO而 Agent 系统的瓶颈主要在LLM 调用的延迟和工具调用的串行性。一次完整的 Agent 交互可能包含多轮 LLM 调用和多次工具调用。如果串行执行一个请求可能要好几秒甚至十几秒。在高并发场景下线程池很快就会被占满。我实测过一个简单的客服 Agent单次交互平均需要 3 次 LLM 调用和 2 次工具调用总耗时在 5 到 8 秒之间。如果用 Tomcat 默认的 200 线程理论上最多支撑 200 个并发请求但实际上因为 LLM 调用是阻塞的实际并发能力会更低。4.2 异步化改造从阻塞到响应式解决并发问题的第一步是异步化。Spring AI 支持返回Flux或者CompletableFuture你可以把 LLM 调用改成异步的// 异步调用示例 public CompletableFutureString chatAsync(String message) { return CompletableFuture.supplyAsync(() - { return chatClient.prompt() .user(message) .call() .content(); }, agentExecutor); }但异步化只是第一步。更关键的是要理解 Agent 的调用链路找出可以并行化的部分。比如如果 Agent 需要同时查询订单信息和用户信息这两个工具调用可以并行执行不需要串行等待。4.3 会话隔离与状态管理并发场景下会话隔离是必须解决的问题。每个用户的对话历史必须独立存储不能串。我的做法是用sessionId作为 key把对话记忆存在 Redis 里Component public class RedisChatMemory implements ChatMemory { private final RedisTemplateString, String redisTemplate; Override public void add(String sessionId, Message message) { String key chat:memory: sessionId; redisTemplate.opsForList().rightPush(key, serialize(message)); redisTemplate.expire(key, Duration.ofHours(2)); } Override public ListMessage get(String sessionId) { String key chat:memory: sessionId; return redisTemplate.opsForList() .range(key, 0, -1) .stream() .map(this::deserialize) .collect(Collectors.toList()); } }这里有个细节要注意对话记忆不能无限增长。如果用户聊了几十轮记忆列表会变得很长每次发给 LLM 的 token 数量会爆炸。我的做法是只保留最近 N 轮对话或者用摘要的方式压缩历史对话。4.4 限流、降级与熔断Agent 系统依赖外部 LLM 服务这个服务可能不稳定也可能有速率限制。你必须做好限流和降级。限流方面我建议在 Agent 入口做一层令牌桶限流控制并发请求数。降级方面当 LLM 服务不可用时可以降级到预设的回复模板或者提示用户稍后再试。熔断方面用 Resilience4j 或者 Sentinel 都可以关键是设置合理的熔断阈值。// 用 Resilience4j 做熔断的示例 CircuitBreaker(name llmService, fallbackMethod fallbackResponse) public String callLlm(String prompt) { return chatClient.prompt().user(prompt).call().content(); } public String fallbackResponse(String prompt, Exception e) { return 抱歉当前服务繁忙请稍后再试。; }这些对于 Java 工程师来说都是老本行你平时怎么保护数据库调用就怎么保护 LLM 调用。5. RAG 与多路召回让 Agent 有知识可查5.1 为什么 Agent 需要 RAGAgent 本身只有 LLM 的通识能力它不知道你公司的产品文档、内部规范、历史工单。要让 Agent 能回答这些领域问题就需要 RAG检索增强生成。RAG 的核心逻辑是用户提问 → 从知识库检索相关文档 → 把文档和问题一起发给 LLM → LLM 基于文档生成回答。这样 Agent 就能回答领域问题了。但基础的 RAG 有个问题单一检索策略的召回率有限。比如你只用向量检索可能漏掉一些关键词匹配的文档只用关键词检索又可能漏掉语义相关的文档。这就是多路召回要解决的问题。5.2 LangChain4j 的多路召回实现LangChain4j 对多路召回的支持比较成熟。它的思路是同时用多种检索策略向量检索、关键词检索、全文检索把结果合并后做重排序最后取 top-K 个文档。// 多路召回示意 public ListDocument multiRetrieve(String query) { // 1. 向量检索 ListDocument vectorResults vectorRetriever.retrieve(query); // 2. 关键词检索 ListDocument keywordResults keywordRetriever.retrieve(query); // 3. 合并去重 ListDocument merged mergeAndDeduplicate(vectorResults, keywordResults); // 4. 重排序 return reranker.rerank(query, merged); }重排序这一步很关键。它用一个专门的模型比如 Cross-Encoder对召回的文档重新打分把最相关的排到前面。这一步能显著提升 RAG 的效果但也会增加延迟。我的经验是如果对延迟敏感可以只用向量检索 关键词检索的合并结果跳过重排序如果对准确率要求高加上重排序。5.3 知识库的切分与索引策略RAG 的效果很大程度上取决于文档切分得好不好。切分粒度太粗检索到的文档包含太多无关信息切分太细又可能丢失上下文。我的经验是按语义切分而不是按固定长度切分。比如按段落切分或者按标题层级切分。LangChain4j 提供了几种切分器我常用的是按段落切分然后设置一个重叠区域避免上下文断裂。索引方面向量数据库的选择也很重要。简单的场景用内存向量库就行生产环境建议用 Milvus、Qdrant 或者 Redis 的向量检索功能。选型的时候考虑几个因素数据量、查询延迟、运维成本、和现有技术栈的兼容性。6. 从 Demo 到生产那些没人告诉你的坑6.1 提示词工程不是玄学是工程很多人觉得提示词工程是玄学调来调去全靠感觉。我的经验是提示词工程是有方法论的核心是结构化。一个好的系统提示词应该包含这几个部分角色定义、能力边界、输出格式、约束条件、示例。我习惯用 Markdown 格式来组织提示词这样模型更容易理解结构。另外提示词要版本化管理。每次修改提示词都要记录改了什么、为什么改、效果如何。我见过团队把提示词写在代码里改一次要发一次版效率极低。正确的做法是把提示词抽出来放在配置中心或者数据库里支持热更新。6.2 可观测性Agent 的黑盒问题Agent 最大的问题是它是黑盒。用户问了一个问题Agent 调了哪些工具、中间思考了什么、为什么给出这个答案你都不知道。这在生产环境是灾难。解决办法是做好可观测性。我的做法是记录每一次 Agent 交互的完整链路用户输入、每一轮 LLM 的输入输出、每一次工具调用的参数和结果、最终输出。这些数据存到日志系统里出问题的时候可以回溯。Spring AI 提供了 Advisor 机制你可以写一个自定义 Advisor 来记录这些信息public class LoggingAdvisor implements CallAdvisor { Override public ChatClientResponse adviseCall( ChatClientRequest request, CallAdvisorChain chain) { // 记录请求 log.info(Agent request: {}, request.prompt()); // 执行调用 ChatClientResponse response chain.nextCall(request); // 记录响应 log.info(Agent response: {}, response.chatResponse()); return response; } }6.3 成本控制Token 就是钱LLM 调用是按 token 计费的Agent 因为有多轮调用token 消耗比普通对话大得多。如果不做控制成本会失控。我的成本控制策略有几个第一设置单次交互的最大 token 限制第二对话记忆只保留最近 N 轮避免历史对话无限增长第三工具返回的结果做精简不要把整个数据库查询结果都塞给 LLM第四对简单问题走缓存不要每次都调 LLM。6.4 测试怎么测一个不确定的系统Agent 的输出是不确定的同样的输入可能得到不同的输出。这给测试带来了很大挑战。传统的断言式测试在这里不适用。我的做法是分层测试单元测试测工具方法的逻辑这部分是确定的集成测试测 Agent 的整体行为用模糊匹配而不是精确匹配来断言端到端测试用真实场景的对话集人工评估或者用另一个 LLM 来打分。另外建议建立回归测试集。每次修改提示词或者工具定义都跑一遍回归测试集看看有没有把之前能答对的问题搞砸了。7. 我踩过的几个印象最深的坑7.1 工具描述写得太简略导致 Agent 选错工具有一次我定义了两个工具一个是查询订单一个是查询物流。描述写得很简单就写了工具名。结果 Agent 经常把这两个搞混用户问物流它去查订单。后来我把描述改详细了查询订单根据用户ID查询该用户的所有订单信息包括订单号、金额、状态。适用于用户想了解自己有哪些订单的场景。 查询物流根据订单号查询该订单的物流轨迹包括发货时间、当前位置、预计送达时间。适用于用户想知道包裹到哪了的场景。改完之后选错的概率大幅下降。这个经验告诉我工具描述是给模型看的文档要像写 API 文档一样认真。7.2 对话记忆无限增长导致 token 爆炸早期我没做记忆压缩用户聊了二十多轮之后每次请求的 token 数量飙升成本暴涨而且响应变慢。后来我加了记忆窗口只保留最近 10 轮对话问题就解决了。但这里有个权衡保留太少Agent 会忘记之前的上下文保留太多token 消耗大。我的经验是 10 轮左右是个比较平衡的值具体可以根据业务场景调整。7.3 没有做超时控制导致线程池被占满有一次线上出了故障LLM 服务响应变慢大量请求堆积线程池被占满整个服务不可用。后来我加了超时控制和熔断问题就没再出现过。这个坑的本质是你不能信任外部服务的稳定性。任何外部调用都必须有超时、重试、熔断。这是分布式系统的基本功Java 工程师应该都懂但在做 Agent 的时候容易忘。7.4 提示词里的一个词导致输出格式全乱有一次我在系统提示词里加了一句请用友好的语气回复结果 Agent 开始在每个回复前面加亲爱的用户而且输出格式变得很随意JSON 解析经常失败。后来我把这句话删了输出就稳定了。这个坑让我意识到提示词里的每一句话都可能产生意想不到的影响。改提示词之后一定要做回归测试不要想当然。8. 给 Java 工程师的转型路线建议8.1 学习路径从用到懂再到改我的建议是分三步走。第一步是用找一个现成的框架Spring AI 或 LangChain4j跑通一个 Demo理解 Agent 的基本概念。第二步是懂读框架的源码理解 ReAct 循环是怎么实现的、工具调用是怎么解析的、记忆是怎么管理的。第三步是改基于框架做定制比如自定义 Advisor、自定义记忆存储、自定义工具注册机制。这个过程不需要你从头学 Python也不需要你深入理解 Transformer 的数学原理。你需要的是理解 Agent 的工程架构而这恰恰是 Java 工程师的强项。8.2 需要补的知识点虽然不用学 Python但有几个知识点你需要补Prompt Engineering怎么写出稳定、可控的提示词RAG 原理向量检索、关键词检索、重排序的基本原理Function Calling 机制模型是怎么决定调用哪个工具的Token 计算怎么估算 token 数量怎么控制成本这些知识点都不难网上资料很多花一两周就能入门。8.3 不要做的事最后说几个不要做的事。不要一上来就追求大而全的 Agent 平台先从解决一个具体问题开始。不要盲目追新框架选一个稳定的、社区活跃的就行。不要忽视工程基础并发、超时、熔断、日志这些该做还得做。不要指望 Agent 能解决所有问题它只是一个工具有它的能力边界。我在实际项目中的体会是Java 工程师转型 AI Agent 最大的障碍不是技术而是心态。很多人觉得 AI 是算法工程师的领域自己插不上手。但实际上Agent 的落地恰恰需要大量工程能力而这正是 Java 工程师的主场。你不需要成为算法专家你需要成为那个能把 Agent 稳定跑在生产环境的人。这个定位在当下的市场里非常稀缺。