☰
Java老兵转型AI Agent实战:不学Python,用LangChain4j落地生产级智能体
2026/10/14 23:57:51 网站建设 项目流程

1. 一个Java老兵决定转型时,到底在纠结什么

干了十三年Java,从SSH时代一路写到Spring Boot微服务,中间经历过银行核心系统重构、电商大促流量洪峰、也带过十几个人的研发团队。说实话,到了这个阶段,写业务代码这件事本身已经很难带来什么新鲜感了。真正让我开始焦虑的,是2023年下半年开始身边越来越多的项目开始提“智能体”“Agent”“大模型应用落地”这些词,而我打开那些技术社区一看,满屏都是Python代码、LangChain、向量数据库,心里第一反应就是:完了,我这十几年Java经验是不是要归零了。

这个心态我相信很多Java老兵都有过。你打开任何一个讲AI Agent的教程,上来就是pip install langchain,然后一段Python脚本调模型、接工具、做记忆管理。你看着那些代码,语法能看懂,但总觉得隔了一层——不是看不懂逻辑,而是觉得“这不是我的主场”。于是很多人就卡在这里了:要么硬着头皮去学Python,学了两周发现语法是学会了,但整个生态、包管理、虚拟环境、异步模型那一套东西又得重新踩坑;要么干脆放弃,觉得AI Agent这事跟自己没关系。

但实际情况是什么呢?我花了大概三个月时间,用Java完整落地了两个AI Agent项目,一个是内部用的智能客服工单分类与自动回复系统,一个是给业务部门做的数据分析助手。做完之后我最大的感受是:90%的Java老兵转型AI Agent失败,不是因为不会Python,而是因为一开始就把方向搞错了。他们以为要“转行”,其实只需要“迁移”。你十几年积累的工程能力、架构思维、并发处理经验、系统稳定性意识,在AI Agent这个领域里不但没有贬值,反而是最稀缺的东西。

这篇文章我想把这三个月的实战复盘完整写出来,包括我踩过的坑、做过的技术选型、写过的核心代码结构、以及那些教程里不会告诉你的工程细节。适合的读者是:有Java后端经验、想切入AI Agent但不知道从哪下手的开发者;或者已经在用Python做Agent但被工程化问题折磨的人。我不讲虚的,直接讲我实际怎么做的。

2. 为什么我说根本不用先学Python

2.1 拆解一个AI Agent到底需要什么

先把概念理清楚。所谓AI Agent,剥开那些花哨的包装,核心就四件事:接收输入、调用大模型推理、根据推理结果决定下一步动作(调用工具或返回结果)、维护上下文状态。就这四件事,没了。什么多智能体协作、什么RAG检索增强、什么长期记忆,都是在这四件事上面叠出来的。

那这四件事里,哪一件是Python独有的?一件都没有。调用大模型就是发HTTP请求,任何语言都能做。工具调用就是函数执行加结果回传,Java的反射和接口体系做这个比Python还规范。上下文状态管理就是数据结构设计,Java的集合框架和并发容器你用了十几年了。唯一Python占优势的地方,是它的AI生态库多、教程多、社区示例多。但“示例多”不等于“你必须用”。

我刚开始也动摇过,想着要不要花一个月把Python捡起来。后来我算了一笔账:学Python基础语法加常用库,大概两周;学LangChain的抽象概念和API,大概一周;学Python的异步编程模型和包管理,大概一周;然后还要把学到的东西跟Java系统做集成,又是一周。一个月过去了,我做出一个demo,但这个demo怎么跟我现有的Java微服务体系对接?怎么用我现有的监控告警?怎么保证并发下的稳定性?全是从零开始。

反过来,如果我用Java做,学习成本只有一样:大模型API的调用方式和Prompt工程技巧。这两样东西跟语言无关,一周就能上手。剩下的工程问题,全是我已经会的东西。这个账算下来,答案很清楚。

2.2 Java做Agent的生态现状

有人会说,Java的AI生态不行啊。这话放在2023年初是对的,但现在已经完全不是了。我实际用过的几个方案:

Spring AI,Spring官方出的AI应用框架,提供了统一的模型调用抽象、Prompt模板管理、向量数据库集成、工具调用支持。它的设计思路跟Spring Data一模一样,你如果用过Spring Data JPA,上手Spring AI几乎零成本。它支持OpenAI、Azure OpenAI、Ollama本地模型等多种后端,切换模型只需要改配置。

LangChain4j,Java版的LangChain,提供了Chain、Agent、Memory、Tool等抽象。它的API设计比Python版更偏工程化,类型安全做得更好。我特别喜欢它的AiServices接口,你可以定义一个Java接口,用注解标注哪些方法需要调用大模型,框架自动帮你生成实现。

Semantic Kernel Java版,微软出的,思路跟LangChain4j类似,但更偏向企业级集成场景。

这三个框架我都在项目里实际用过,后面会详细讲选型对比。这里先说结论:Java做AI Agent的生态已经足够成熟,你不需要为了做Agent去学Python。

2.3 什么情况下才真的需要Python

我不是说Python完全没用。有两种情况你确实需要碰Python:

第一种,你要做模型微调或者底层推理优化。这个领域Python确实是唯一选择,PyTorch、Transformers这些库没有Java替代品。但问题是,90%的AI Agent应用开发者根本不需要做模型微调。你调API就够了,微调是算法团队的事。

第二种,你要复现某个最新的论文或者开源项目,而它只提供了Python实现。这种情况你可以用Python跑通理解原理,然后用Java重写核心逻辑。注意,是重写,不是集成。因为把Python代码集成到Java生产系统里,那个运维成本高到离谱。

除了这两种情况,其他场景我都建议直接用Java。你省下来的时间,用来研究Prompt工程和Agent架构设计,收益大得多。

3. 我实际踩过的三个大坑

3.1 第一个坑:把Agent当聊天机器人做

我刚开始做第一个项目的时候,思路很简单:用户输入问题,我调大模型,返回答案。这不就完了吗?结果做出来发现,这东西跟直接调ChatGPT网页版没有任何区别,业务方看了一眼就说“这有什么用”。

问题出在哪?Agent和聊天机器人的本质区别在于“行动能力”。聊天机器人只能说话,Agent能做事。什么叫做事?它能调用你的业务系统接口查数据、能操作数据库写记录、能触发工作流、能根据中间结果决定下一步做什么。如果你的Agent只会返回文本,那它就是个聊天机器人,价值有限。

我后来重构了架构,核心变化是引入了工具调用(Tool Calling)机制。具体做法是:定义一组Java方法,每个方法代表一个业务能力,比如“查询订单状态”“创建工单”“获取用户历史记录”。然后用注解标注这些方法的描述和参数,Agent在推理时根据用户意图自动选择调用哪个方法、传什么参数。这个改造做完之后,业务方的评价立刻变了,因为Agent真的能帮他们干活了。

3.2 第二个坑:上下文管理想得太简单

第二个坑更隐蔽。我一开始觉得上下文管理就是把对话历史拼成一个字符串,每次请求都带上。做了才发现,这里面的坑深得很。

首先是Token成本。你每次把完整对话历史发给模型,Token消耗是线性增长的。一个稍微长一点的对话,十几轮之后每次请求的Token数就上万了,成本直接爆炸。其次是上下文窗口限制。模型能处理的Token数是有上限的,超了就得截断,截断策略没做好就会丢失关键信息。最后是状态一致性。多轮对话中,用户可能在不同轮次提到不同的事实,这些事实之间可能矛盾,Agent需要能识别和处理。

我最后的解决方案是分层记忆架构:短期记忆存最近N轮对话的原始文本,长期记忆用向量数据库存关键事实和摘要,工作记忆存当前任务相关的临时状态。每次请求时,根据当前输入从长期记忆里检索相关事实,跟短期记忆一起组装成Prompt。这个架构后面会详细讲实现。

3.3 第三个坑:忽略并发和稳定性

这个坑是最致命的,也是Java老兵最容易犯的——因为太熟悉了反而容易忽略。我第一个项目上线后,有一次做压测,50个并发请求打过来,系统直接卡死了。排查发现是两个问题:一是大模型API调用是阻塞的,每个请求占一个线程,线程池瞬间被打满;二是没有做限流和降级,API返回慢的时候请求堆积,内存暴涨。

这两个问题在Java体系里都有成熟解法:异步调用用CompletableFuture或者Reactor,限流降级用Sentinel或者Resilience4j。但如果你用Python写,这些方案要么没有,要么用起来很别扭。这也是我后来越来越坚定用Java做Agent的原因——AI Agent最终是要上生产的,生产系统需要的那套稳定性保障,Java生态是最成熟的。

4. 我的技术选型与架构设计

4.1 框架选型对比

三个主流Java AI框架我都实际用过,下面是真实对比:

维度Spring AILangChain4jSemantic Kernel Java
上手难度低,Spring风格中,概念较多中,文档偏少
模型支持丰富,官方适配多丰富,社区活跃一般,偏Azure
工具调用注解式,简洁注解式,灵活插件式,较重
记忆管理基础支持完善,多种实现基础支持
向量库集成多种主流库多种主流库较少
生产成熟度高,Spring背书高,社区验证多中
学习资源官方文档完善文档+示例丰富相对少

我最终选的是LangChain4j为主、Spring AI为辅的组合。原因是我需要比较灵活的工具调用和记忆管理,LangChain4j在这两块做得更细。Spring AI我用来做模型调用的统一抽象层,因为它切换模型后端确实方便。

4.2 整体架构分层

我的Agent系统分四层:

接入层:Spring Boot的Controller,负责接收请求、参数校验、鉴权。这一层跟普通Web应用没区别。

编排层:Agent的核心逻辑,包括Prompt组装、模型调用、工具选择、结果解析、记忆更新。这一层用LangChain4j的AiServices实现。

能力层:各种工具的实现,每个工具是一个Spring Bean,通过接口暴露给编排层调用。这一层就是普通的Java业务代码。

基础设施层:模型API客户端、向量数据库客户端、缓存、消息队列、监控。这一层也是标准Java基础设施。

这个分层的好处是,只有编排层是AI特有的,其他三层都是你熟悉的Java工程。你不需要重新学一套东西,只需要在编排层用新的思维方式。

4.3 核心依赖配置

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-pgvector</artifactId> <version>0.35.0</version> </dependency>

配置文件里设置模型参数:

langchain4j: open-ai: chat-model: api-key: ${API_KEY} model-name: gpt-4o-mini temperature: 0.3 timeout: 60s max-retries: 2 embedding-model: api-key: ${API_KEY} model-name: text-embedding-3-small

这里有几个参数值得说明。temperature设0.3,是因为Agent场景需要稳定输出,不需要太多创造性。timeout设60秒,是因为复杂推理可能耗时较长,但也不能无限等。max-retries设2,是因为API偶尔会抖动,重试两次基本能覆盖。

5. 核心模块的实操实现

5.1 工具调用的完整实现

工具调用是Agent的灵魂,我以“查询订单”这个工具为例,展示完整实现。

首先定义一个工具接口:

public interface OrderTools { @Tool("根据订单号查询订单状态和详情") OrderInfo queryOrder(@P("订单号") String orderId); @Tool("根据用户ID查询该用户最近的订单列表") List<OrderInfo> queryRecentOrders( @P("用户ID") String userId, @P("查询条数,默认5条") int limit ); }

然后实现这个接口:

@Component public class OrderToolsImpl implements OrderTools { @Autowired private OrderService orderService; @Override public OrderInfo queryOrder(String orderId) { try { return orderService.getById(orderId); } catch (Exception e) { log.error("查询订单失败: {}", orderId, e); return OrderInfo.error("订单查询失败,请稍后重试"); } } @Override public List<OrderInfo> queryRecentOrders(String userId, int limit) { return orderService.getRecentByUser(userId, Math.min(limit, 20)); } }

关键点在于异常处理。工具方法绝对不能抛异常出去,因为异常会中断Agent的推理流程。所有异常都要捕获并返回一个描述性的错误信息,让模型知道发生了什么,它才能决定下一步怎么做。

然后定义Agent接口:

public interface CustomerServiceAgent { @SystemMessage(""" 你是一个专业的客服助手。你可以调用工具查询订单信息。 回答时请遵循以下规则: 1. 先确认用户意图,再调用相应工具 2. 工具返回错误时,向用户说明情况并建议替代方案 3. 不要编造工具没有返回的信息 4. 回答简洁,控制在200字以内 """) String chat(@MemoryId String sessionId, @UserMessage String message); }

组装Agent:

@Configuration public class AgentConfig { @Bean public CustomerServiceAgent customerServiceAgent( ChatLanguageModel model, OrderTools orderTools, ChatMemoryProvider memoryProvider) { return AiServices.builder(CustomerServiceAgent.class) .chatLanguageModel(model) .tools(orderTools) .chatMemoryProvider(memoryProvider) .build(); } @Bean public ChatMemoryProvider memoryProvider() { return sessionId -> MessageWindowChatMemory.builder() .id(sessionId) .maxMessages(20) .build(); } }

这套代码跑起来之后,用户说“帮我查一下订单12345”,Agent会自动调用queryOrder("12345"),拿到结果后组织语言回复。整个过程你不需要写任何解析意图的代码,模型自己会判断。

5.2 记忆管理的分层实现

前面说了分层记忆,具体怎么落地?我用的是短期记忆+长期记忆的组合。

短期记忆用MessageWindowChatMemory,只保留最近20条消息。这个简单,框架自带。

长期记忆用向量数据库。每次对话结束后,把关键信息抽取出来存进去:

@Service public class LongTermMemoryService { @Autowired private EmbeddingStore<TextSegment> embeddingStore; @Autowired private EmbeddingModel embeddingModel; public void saveMemory(String userId, String content) { TextSegment segment = TextSegment.from( content, Metadata.from(Map.of("userId", userId, "timestamp", String.valueOf(System.currentTimeMillis()))) ); Embedding embedding = embeddingModel.embed(segment).content(); embeddingStore.add(embedding, segment); } public List<String> recall(String userId, String query, int maxResults) { Embedding queryEmbedding = embeddingModel.embed(query).content(); EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(maxResults) .minScore(0.7) .filter(metadataKey("userId").isEqualTo(userId)) .build(); return embeddingStore.search(request).matches().stream() .map(m -> m.embedded().text()) .collect(Collectors.toList()); } }

然后在Agent调用前,先检索长期记忆,拼到Prompt里:

public String chatWithMemory(String userId, String message) { List<String> memories = longTermMemoryService.recall(userId, message, 3); String memoryContext = memories.isEmpty() ? "" : "以下是该用户的历史相关信息:\n" + String.join("\n", memories); String fullMessage = memoryContext + "\n\n用户当前问题:" + message; String response = agent.chat(userId, fullMessage); // 异步保存本轮对话摘要到长期记忆 CompletableFuture.runAsync(() -> { String summary = summarize(message, response); longTermMemoryService.saveMemory(userId, summary); }); return response; }

这里有个细节:保存长期记忆是异步的,不能阻塞主流程。因为embedding计算和向量写入都有延迟,同步做会让响应变慢。

5.3 异步与限流的工程实现

Agent调用大模型是IO密集型操作,必须异步化。我用的是Spring的@Async配合自定义线程池:

@Configuration @EnableAsync public class AsyncConfig { @Bean("agentExecutor") public ThreadPoolTaskExecutor agentExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setThreadNamePrefix("agent-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }

线程池参数怎么定?核心线程数20,是因为大模型调用平均耗时3-5秒,20个线程大概能支撑每秒4-6个请求的吞吐。最大线程数50,是应对突发流量。队列容量100,超过就触发拒绝策略。拒绝策略用CallerRunsPolicy,让调用线程自己执行,起到背压作用,避免请求丢失。

限流用Resilience4j:

@Bean public RateLimiter rateLimiter() { RateLimiterConfig config = RateLimiterConfig.custom() .limitForPeriod(30) .limitRefreshPeriod(Duration.ofSeconds(1)) .timeoutDuration(Duration.ofMillis(500)) .build(); return RateLimiter.of("agentLimiter", config); }

这段配置的意思是:每秒最多30个请求,超过的请求最多等500毫秒,等不到就降级。降级策略是返回一个预设的友好提示,而不是报错。

6. 常见问题与排查实录

6.1 模型返回格式不稳定怎么办

这是最常见的问题。你让模型返回JSON,它有时候返回带markdown代码块的JSON,有时候返回纯JSON,有时候还给你加一段解释文字。我的处理方式是三层防护:

第一层,Prompt里明确格式要求,并且给一个示例。比如“请严格按照以下JSON格式返回,不要添加任何其他文字:{"intent": "...", "params": {...}}”。

第二层,代码里做容错解析。先尝试直接解析,失败就提取第一个{到最后一个}之间的内容再解析,再失败就用正则提取关键字段。

第三层,解析失败时触发重试,把解析错误信息作为反馈发给模型,让它重新生成。重试最多两次。

public <T> T parseWithRetry(String raw, Class<T> clazz, int maxRetry) { for (int i = 0; i <= maxRetry; i++) { try { String cleaned = cleanJson(raw); return objectMapper.readValue(cleaned, clazz); } catch (Exception e) { if (i == maxRetry) throw new AgentParseException("解析失败", e); raw = retryGenerate(raw, e.getMessage()); } } return null; }

6.2 Token超限怎么处理

Token超限分两种情况:输入超限和输出超限。

输入超限的解决思路是压缩上下文。我的做法是:当对话历史超过阈值时,把最早的一半消息用模型总结成一段摘要,用摘要替换原始消息。这样既保留了关键信息,又大幅减少了Token数。

输出超限的解决思路是分段生成。如果预期输出很长,就在Prompt里要求模型分点回答,或者明确限制“每个点不超过50字”。实在需要长输出的场景,就做流式返回,边生成边推送给前端。

6.3 工具调用选错怎么办

模型有时候会选错工具,或者参数传错。我的经验是三个改进方向:

一是优化工具描述。@Tool注解里的描述要写得非常明确,包括什么时候用、什么时候不用、参数格式是什么。描述写得好,选错率能降一半。

二是减少工具数量。一次给模型暴露的工具不要超过10个,太多了它会混淆。如果业务工具有几十个,就做分组,根据用户意图先路由到某一组,再在该组内选择。

三是加校验和兜底。工具执行前校验参数合法性,不合法就返回错误信息让模型重新选。同时设置一个默认工具,当模型无法确定时调用默认工具做澄清提问。

6.4 常见问题速查表

问题现象可能原因排查方向解决方案
响应超时模型API慢或网络抖动查看API调用耗时分布加超时+重试+降级
内存暴涨上下文未清理或请求堆积检查ChatMemory和线程池限制记忆窗口+限流
工具不调用描述不清或Prompt未引导查看模型原始输出优化工具描述+加示例
回答胡编模型幻觉检查是否有工具结果约束加“不得编造”指令+引用来源
并发下错乱会话ID冲突检查MemoryId传递确保sessionId唯一且透传
成本过高Token消耗大统计每次请求Token数压缩上下文+用小模型

7. 一些没人告诉你的实操心得

第一个心得:先用小模型跑通流程,再用大模型优化效果。我刚开始直接用GPT-4,成本高得吓人。后来改成开发阶段用GPT-4o-mini,效果调优阶段再切GPT-4,成本降了80%,效果差异在大部分场景下用户感知不到。

第二个心得:Prompt要版本化管理。我把所有Prompt存在数据库里,每次修改都记录版本和效果指标。这样出问题可以快速回滚,也能做A/B测试。别把Prompt硬编码在代码里,改一次发一次版,效率太低。

第三个心得:一定要做全链路日志。每次Agent调用,把输入、Prompt、模型原始输出、工具调用记录、最终输出全部落库。出问题时这是唯一的排查依据。我吃过亏,有一次线上回答异常,因为没有记录原始输出,排查了整整一天。

第四个心得:工具方法要幂等。Agent可能会重复调用同一个工具,如果你的工具方法有副作用(比如创建订单),重复调用会出问题。所有有副作用的工具都要加幂等键,或者设计成“查询+确认”两步。

第五个心得:别追求一次做完美。我第一个版本只做了最简单的问答,第二个版本加工具调用,第三个版本加记忆,第四个版本加限流降级。每加一个能力就上线验证一次,比憋大招一次做完靠谱得多。

这套东西我前后调了三个月,现在两个Agent系统都在稳定运行,日均处理请求量在几千次,P99响应时间控制在8秒以内。回头看,最难的不是技术,是心态——是愿不愿意承认自己在新领域是个新手,同时又不否定自己已有的积累。Java老兵做AI Agent,优势从来不在模型本身,而在模型之外的那一整套工程体系。把编排层学好,剩下的都是你本来就会的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询