Java工程师转型AI Agent实战:LangChain4j与Spring AI选型及ReAct落地
1. 为什么 Java 工程师转 AI Agent 有天然优势
1.1 被低估的工程化能力迁移
很多 Java 工程师一提到转 AI Agent,第一反应是“我是不是要从头学 Python”。这个判断其实只对了一半。Python 在 AI 生态里的地位确实强势,但 AI Agent 落地到生产环境,真正卡脖子的往往不是模型调用,而是工程化能力——而这块恰恰是 Java 工程师的主场。
我见过太多用 Python 写出来的 Agent Demo,跑通一个 ReAct 循环很轻松,但一旦要接入企业现有的权限体系、要做行级权限控制、要保证多轮对话下的数据一致性、要扛住并发,代码就开始失控。原因很简单:这些问题的本质是后端架构问题,不是 AI 问题。而 Java 生态里沉淀了十几年的 Spring 体系、成熟的线程模型、完善的事务管理、稳定的连接池,这些在 Agent 落地阶段全是硬通货。
所以我的观点很明确:Java 工程师转 AI Agent,不是“从零开始”,而是把已有的工程能力迁移到一个新场景。你要补的是 AI 侧的认知(Prompt 设计、工具调用协议、记忆机制),而不是重新学一遍编程。
1.2 从“写业务逻辑”到“编排智能行为”的思维切换
Java 工程师的日常是确定性的:输入 A,经过一系列 Service 调用,输出 B。但 AI Agent 的核心是不确定性下的决策。你给模型一个目标,它自己决定调用哪个工具、调用几次、什么时候停止。这个思维切换是转型中最难的部分,比学框架难得多。
举个具体例子。传统 Java 里你写一个订单查询,逻辑是死的:查库、组装、返回。但在 Agent 里,用户说“帮我看看上周那个大额订单现在什么状态”,模型需要自己判断:要不要先查用户身份?要不要先确认“上周”的具体日期范围?“大额”的阈值是多少?这些判断在传统代码里是你写死的 if-else,在 Agent 里是模型根据上下文动态生成的。
理解这个差异之后,你就明白为什么 ReAct(Reasoning + Acting)模式这么重要了。它不是让模型一次性给出答案,而是让模型边想边做:先推理当前该干什么,执行一个动作,观察结果,再推理下一步。这个循环本质上是一个状态机,而 Java 工程师对状态机的理解,比大多数 Python 脚本开发者要深。
1.3 企业级 Agent 落地的真实需求画像
我接触过的企业级 Agent 需求,大致分三类。第一类是内部效率工具,比如自动处理工单、自动生成报表、自动巡检日志。第二类是对外服务增强,比如智能客服、智能导购。第三类是流程自动化,比如把 Dify 上跑通的工作流转成可维护的 Java 代码。
这三类需求的共同点是:都要接入现有系统。你的 Agent 不可能活在一个真空环境里,它要读数据库、要调内部 API、要遵守权限规则、要记录审计日志。这些全是 Java 工程师熟悉的东西。所以我说,Java 工程师转 AI Agent,起点其实比纯算法背景的人更接近落地。
2. LangChain4j 与 Spring AI 的选型逻辑
2.1 两个框架解决的不是同一个问题
很多人把 LangChain4j 和 Spring AI 当成竞品来对比,这个视角有点偏。它们确实有重叠,但设计初衷不同。
LangChain4j更像是把 Python 版 LangChain 的核心概念搬到 Java:Chain、Tool、Memory、Retriever 这些抽象它都有,而且对多路召回、Easy RAG 这类场景支持得比较早。如果你要做的是一个功能密度高的 Agent,比如需要复杂的检索增强、多工具编排,LangChain4j 的 API 会更顺手。
Spring AI的思路是把 AI 能力做成 Spring 生态的一等公民。它的核心卖点是和 Spring Boot 的无缝集成:你可以在现有的 Spring 项目里,用注入 Bean 的方式拿到 ChatClient,用配置中心管理模型参数,用 Spring 的拦截器做统一日志。如果你的团队已经在用 Spring 全家桶,Spring AI 的迁移成本几乎为零。
我的建议是:新项目如果团队 Spring 背景重,优先 Spring AI;如果要做复杂的 RAG 和工具编排,LangChain4j 的抽象更成熟。两者也可以混用,比如用 Spring AI 管模型接入,用 LangChain4j 管检索链路。
2.2 版本迭代期的坑:别追最新,追最稳
Spring AI 和 LangChain4j 都还在快速迭代。我踩过的一个坑是:看到 Spring AI 2.0 出了新特性,兴冲冲升级,结果发现某个连接百炼的配置项改了名字,文档还没更新,排查了半天。
这里给个实操建议:生产项目锁定小版本,升级前先在独立分支跑通全链路。具体来说,在pom.xml里不要用LATEST或者范围版本,直接写死具体版本号。比如:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>1.0.0-M6</version> </dependency>另外,Spring AI Alibaba 这个项目社区里经常有人问“停更了吗”。实际情况是它在跟着上游节奏走,但版本节奏和 Spring AI 主线不完全同步。如果你要用它接百炼、通义千问,建议先看它的 release notes,确认对应关系再动手。
2.3 连接国内模型的配置细节
以 Spring AI 连接百炼(DashScope)上的 Qwen 系列为例,核心配置就几行,但有几个容易忽略的点。
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7第一个坑是api-key 的注入方式。不要硬编码在 yaml 里,用环境变量或者配置中心。第二个坑是model 名称。百炼上的模型名和 OpenAI 的不一样,写错了会报 404 而不是明确的“模型不存在”,排查起来很烦。第三个坑是temperature 的取值。做 Agent 的时候,工具调用场景建议调低到 0.1-0.3,让模型决策更稳定;做创意生成再调高。
提示:连接国内模型时,先单独写一个最小 Demo 验证连通性,再往项目里集成。很多“框架问题”其实是网络或鉴权问题。
3. ReAct 模式在 Java 里的落地拆解
3.1 ReAct 循环的本质是一个状态机
ReAct 的核心循环是:Thought → Action → Observation → Thought → ...直到模型认为可以给出 Final Answer。用 Java 的视角看,这就是一个带终止条件的状态机。
为什么这个理解重要?因为一旦你把它当状态机,很多工程问题就有解了。比如怎么防止死循环?状态机可以设最大步数。怎么记录中间过程?状态机的每个状态转移都可以打日志。怎么扛并发?每个会话是一个独立的状态机实例,天然隔离。
下面是一个简化的 ReAct 循环骨架:
public class ReActAgent { private final ChatClient chatClient; private final List<Tool> tools; private static final int MAX_STEPS = 10; public String run(String userInput) { List<Message> history = new ArrayList<>(); history.add(new UserMessage(userInput)); for (int step = 0; step < MAX_STEPS; step++) { String response = chatClient.call(buildPrompt(history)); ParsedOutput parsed = parse(response); if (parsed.isFinalAnswer()) { return parsed.getAnswer(); } String observation = executeTool(parsed.getAction(), parsed.getInput()); history.add(new AssistantMessage(response)); history.add(new ToolMessage(observation)); } return "达到最大步数,未能完成任务"; } }这段代码里最关键的是MAX_STEPS。我见过线上事故,就是因为没设上限,模型在两个工具之间反复横跳,把 token 烧光了。
3.2 工具定义:给模型一份“说明书”
Agent 能不能用好工具,取决于你给模型的工具描述够不够清楚。这就像你给一个新同事写 API 文档,写得含糊,他肯定调错。
工具定义要包含三部分:名称、功能描述、参数说明。功能描述尤其重要,要写清楚“什么时候用这个工具”,而不只是“这个工具做什么”。
Tool weatherTool = Tool.builder() .name("getWeather") .description("查询指定城市的当前天气。当用户询问天气、温度、是否下雨时使用此工具。") .parameter("city", "城市名称,如'北京'、'上海'") .function(args -> weatherService.query(args.get("city"))) .build();注意 description 里那句“当用户询问天气、温度、是否下雨时使用”。这句话是给模型看的路由提示,能显著提升工具选择的准确率。很多人只写“查询天气”,模型在模糊场景下就容易漏调或错调。
3.3 记忆机制:短期靠上下文,长期靠检索
Agent 的记忆分两层。短期记忆就是当前会话的消息历史,直接塞进上下文。长期记忆需要落库,用的时候检索出来。
短期记忆的坑是上下文长度。多轮对话下来,历史消息会撑爆 token 限制。常见的处理是滑动窗口加摘要:保留最近 N 轮原文,更早的用模型压缩成一段摘要。
长期记忆的坑是检索质量。这就是 LangChain4j 的 Easy RAG 和多路召回派上用场的地方。简单说,用户问一个问题,你不只做向量检索,还同时做关键词检索,两路结果合并去重再排序。这样能兼顾语义相似和精确匹配。
// 多路召回示意 List<Document> vectorResults = vectorStore.search(query, 5); List<Document> keywordResults = keywordIndex.search(query, 5); List<Document> merged = mergeAndRerank(vectorResults, keywordResults);实测下来,多路召回在专业领域问答上的召回率比单路向量检索高不少,代价是多一次检索开销。是否值得,看你的场景对准确率的敏感度。
4. 并发与一致性:Java 工程师的主场
4.1 AI Agent 怎么扛并发
这是热词里出现频率很高的问题。答案分两层。
第一层是模型调用的并发。模型 API 通常有 QPS 限制,你不能无脑开线程池打过去。正确做法是用信号量或限流器控制并发数,超出的请求排队或降级。
private final Semaphore modelSemaphore = new Semaphore(10); public String callModel(String prompt) { modelSemaphore.acquire(); try { return chatClient.call(prompt); } finally { modelSemaphore.release(); } }第二层是会话状态的并发。每个用户的会话是独立的,但如果你的 Agent 有共享状态(比如共享的工具实例、共享的缓存),就要注意线程安全。我的经验是:会话状态尽量做成无共享的,每个请求带自己的上下文,避免锁竞争。
4.2 数据一致性在 Agent 场景的特殊性
传统 Java 里,数据一致性靠事务。但 Agent 场景下,一个任务可能跨多个工具调用,每个调用可能改不同的系统。这时候分布式事务的成本太高,通常用补偿机制:记录每一步的操作,失败时反向补偿。
比如一个“下单并通知”的 Agent,先调订单服务创建订单,再调通知服务发消息。如果通知失败,不应该回滚订单,而是记录待重试。这个思路和微服务里的 Saga 模式一致,Java 工程师应该很熟悉。
注意:Agent 的工具调用不要设计成“全成功或全失败”,要设计成“每步可独立重试”。因为模型决策本身就有不确定性,强一致会让系统非常脆弱。
4.3 行级权限怎么落到 Agent 里
企业场景下,Agent 不能越权访问数据。行级权限的意思是:同一个工具,不同用户调用,能看到的数据范围不同。
实现上,权限判断不要交给模型,要在工具执行层做。模型只负责决定“调哪个工具”,工具内部根据当前用户身份过滤数据。
public List<Order> queryOrders(String userId, String condition) { // 权限过滤在代码里做,不依赖模型 return orderRepo.findByUserAndCondition(userId, condition); }这一点非常重要。我见过有人把用户 ID 当参数让模型填,结果模型填错了或者被 prompt 注入攻击,导致越权。安全边界必须在代码层,不能在模型层。
5. 从 Dify 工作流到 Java 代码的迁移实践
5.1 为什么要从 Dify 迁到代码
Dify 这类平台适合快速验证想法,拖拖拽拽就能跑通一个工作流。但到了生产阶段,问题就来了:版本管理难、测试难、和现有系统集成难。尤其是当你的工作流需要接入内部 API、需要走公司的鉴权体系时,平台的可扩展性就成了瓶颈。
所以很多团队的选择是:用 Dify 做原型,用 Java 代码做生产。这个迁移过程有几个关键点。
5.2 节点到代码的映射关系
Dify 工作流里的节点,大致对应 Java 里的这些结构:
| Dify 节点 | Java 对应实现 |
|---|---|
| LLM 节点 | ChatClient 调用 |
| 知识检索节点 | Retriever + 向量库 |
| 代码节点 | 普通 Service 方法 |
| 条件分支 | if-else 或状态机转移 |
| 变量聚合 | 上下文对象字段 |
| HTTP 请求 | RestTemplate / WebClient |
迁移的时候,先把工作流的拓扑图画出来,然后逐个节点翻译。不要试图一次性翻译完,先翻译主干链路,跑通再加分支。
5.3 迁移后容易丢的东西
从平台迁到代码,最容易丢的是可观测性。Dify 里每个节点的输入输出都可视化,迁到代码后如果不打日志,出问题就是黑盒。
我的做法是:每个工具调用都记录结构化日志,包含会话 ID、步骤序号、工具名、输入、输出、耗时。这样出问题能快速定位是哪一步、哪个工具出的错。
log.info("agent_step sessionId={} step={} tool={} input={} output={} cost={}ms", sessionId, step, toolName, input, output, cost);另外,Prompt 的版本管理也容易丢。建议把 Prompt 模板抽到配置文件或数据库里,不要硬编码在 Java 字符串里,方便调整和回滚。
6. 转型路上的实操心得与避坑清单
6.1 学习路线的取舍
网上 AI Agent 学习路线一大堆,我的建议是别贪多。Java 工程师转型,优先级排序应该是:
- 先跑通一个最小 ReAct Demo,理解循环机制
- 再学工具定义和调用,这是 Agent 的手脚
- 然后学 RAG,这是 Agent 的知识来源
- 最后学多 Agent 协作和复杂编排
很多人一上来就啃多 Agent 框架,结果连单 Agent 的工具调用都没搞明白。基础不牢,越学越乱。
6.2 几个我踩过的坑
坑一:把模型当数据库用。有人让模型记住所有历史数据,结果 token 爆炸。正确做法是数据落库,模型只做推理。
坑二:工具描述写得太简略。模型选错工具,十有八九是描述没写清楚。把工具描述当成给新人的文档来写。
坑三:不做超时和重试。模型调用和工具调用都可能超时,没有超时控制,一个慢请求能拖垮整个线程池。
坑四:忽略 Prompt 注入。用户输入里如果包含“忽略之前的指令”,可能让 Agent 做出意外行为。要在输入层做基本的过滤和转义。
坑五:并发不设限。前面说过,模型 API 有 QPS 限制,无脑并发只会触发限流。
6.3 给不同阶段工程师的建议
如果你是刚工作一两年的 Java 工程师,先把 Spring 和并发基础打牢,这些是转型的地基。AI 侧的 API 调用不难,难的是工程化。
如果你是有几年经验的后端,你的优势在架构和稳定性,转型时重点补 AI 侧的认知,别妄自菲薄。
如果你是团队技术负责人,考虑的是怎么把 Agent 能力沉淀成团队的基础设施,而不是每个人各写各的。统一模型接入、统一工具注册、统一日志规范,这些比单个 Agent 做得多花哨更重要。
我个人在实际操作中的体会是:Java 工程师转 AI Agent,最大的障碍不是技术,是心态。总觉得自己“不懂 AI”,其实你懂的那些工程能力,才是 Agent 从 Demo 走向生产的关键。把模型当成一个不太靠谱但很有创造力的远程服务,用你熟悉的方式去封装它、约束它、监控它,这条路就走通了。