先说结论,过去大半年,我一个写了13年Java的后端老兵,把AI Agent从“听说过”到“跑在线上”,完整走了一遍。现在生产环境里挂着两个Agent服务,稳定跑了两月。这段时间最大的体会就一句话:90%想转AI Agent的Java开发者,挂在同一个坑上——都以为得先把Python学明白。其实根本不用,至少做工程化Agent应用不用。
这篇文章就把我踩过的坑、走过的弯路、最后沉淀下来的技术路线和代码模板,一次性写透。适合这些读者:想转型AI Agent方向但被“必须会Python”劝退的Java开发、已经在学Python但越学越迷茫的同学、以及想知道Java生态到底能不能做Agent的架构师和管理者。内容完全基于我实际开发中的经验,尽量不绕弯子。
1. 先搞明白90%的人到底挂在哪个坑
1.1 那个坑根本不是技术问题,是认知问题
我接到第一个Agent相关的需求时,第一反应和大多数人一模一样:完了,这是AI项目,得转Python。于是花了整整两个月刷Python语法、看某个AI框架的教程、写爬虫练手。等我把Python勉强能用了,回头一看需求——用一个Java服务调大模型接口,让模型能自己决定调用哪些内部工具来完成任务。这个需求Java完全能做,我那两个月的Python基本白学。
后来我陆续接触了很多想转型的Java同行,发现这是个极其普遍的路径错误。大家把“AI领域搞学术研究需要用Python”和“AI应用开发必须用Python”划了等号。实际上这是两回事:AI算法训练、模型微调、论文复现,确实是Python的主场;但做Agent应用,本质上是一个工程问题——怎么把一个推理核心接入你的业务系统,怎么编排工具调用,怎么做上下文管理,怎么做超时重试限流。这些是老Java开发最擅长的领域。
1.2 为什么敢说“根本不用学Python”
我先说个判断标准:如果你的目标是做Agent应用开发、接入大模型API做智能业务系统、给现有Java项目加AI能力,那Python可学可不学。如果你要做的是从零训练一个大模型、研究新算法、发论文,那确实绕不开Python。问题在于,90%的人根本不会走到后者那一步,却被后者的门槛拦在了门外。
我自己现在的开发主力语言还是Java,Agent服务全部用Java写,核心依赖就几百行。为什么能这样?因为大模型厂商提供的API本质上是HTTP接口,你传文本进去,拿文本回来。Java发HTTP请求的能力全球顶尖,没有任何语言层面的障碍。所谓“Agent框架”,提供的是对话循环、工具注册、上下文管理这些能力,这些能力Java生态早就有一套完善的工程化实现。
而且从团队角度来看,一个Java团队全员转型学Python的代价极高:语法习惯、依赖管理、部署方式、监控体系全要换。如果能让现有团队用Java直接做Agent,那融入现有代码库、复用基建、降低维护成本,都是实打实的收益。
1.3 想清楚Agent的本质,你就不会慌了
在我把Agent拆明白之前,它对我来说是个“黑盒”:好像很高端,好像得用某种特殊技术。拆开之后,Agent的本质极其朴素——一个带着决策能力的对话循环,加一堆可调用的工具。
可以这样理解:传统程序是“你写死逻辑,机器照着走”。Agent是“你给模型一个目标,模型自己拆解步骤,需要时调用你注册的工具,然后把结果整理给你”。核心就三样东西:
- 模型(大脑):负责理解意图、做决策、生成回复。对外就是一个HTTP接口,你发消息,它回消息。
- 工具(手脚):一段段普通Java方法,比如查数据库、发短信、调订单接口。注册给模型之后,模型在需要时会请求调用它们。
- 记忆(会议纪要):保存多轮对话的上下文,让模型能记住前面聊过的内容。
想通了这一点,你再看市面上那些花里胡哨的Agent产品,底层逻辑都是这个三角结构。Java做这种结构天然合适,差的无非是“模型API调用”这个环节的封装——而这也早就有成熟的Java库了。
2. Java做Agent的底气,到底从哪来
2.1 你以为的需求,90%只是两行API调用
很多Java开发者不敢碰Agent,是因为一搜教程全是Python代码、Jupyter Notebook、研究论文,有一种“这玩意儿属于另一群人”的错觉。但等到需求真正落到你头上,你会发现绝大多数业务场景是这样:输入一段用户问题,模型理解后去查业务数据,把结果整理成自然语言回复。
比如我做过的一个实际案例:给企业内部做一个“工单智能助理”,员工直接问“帮我查一下单号12345到哪一步了”,Agent收到后调用一个Java方法查工单状态接口,拿回结构化数据,再让模型生成一句话回复给员工。
这整个链路里,模型只干两件事:判断“需要调用查单工具”和“把数据翻译成人话”。剩下的全是用Java写的CRUD和内部接口调用。你觉得这需要Python吗?
2.2 Java生态里有哪些Agent开发“零件”
这里我一次性说清楚,Java生态不是没有Agent开发工具,只是它们没像Python生态那样被铺天盖地地宣传。我在生产里用到的,基本就是下面这几个层次的东西:
| 层次 | 作用 | 我的选择 |
|---|---|---|
| 模型接入层 | 封装大模型API,提供统一的对话调用接口 | Spring生态的官方AI模块 |
| 编排框架层 | 提供Agent循环、工具注册、消息管理 | 同上的ChatClient机制 |
| 工具定义层 | 让Java方法暴露成模型可调用的工具 | 注解式方法注册 |
| 向量存储层 | RAG检索所需的向量库客户端 | 成熟向量库的Java客户端 |
| 工程能力层 | 超时、重试、限流、监控 | Java原生生态,信手拈来 |
我知道有同行会担心:Spring生态的AI模块够不够稳定?我的答案是:你去看它的实现,底层就是对模型HTTP接口的封装,加上一套对话上下文管理逻辑。经过我这几个月的生产使用,稳定性没问题。你甚至可以不用它,直接用Spring的RestTemplate或WebClient去调模型接口,自己维护对话轮次,一样能实现Agent——只是多写点代码。框架的意义是让你省去重复造轮子。
2.3 13年Java经验真正能迁移的能力
这半年我最大的认知转变是:以前引以为傲的是会写多少代码,现在觉得真正值钱的是工程判断力。做Agent之后,我的Java经验几乎全部通用。
比如并发控制。Agent在调用工具时如果同时触发多个请求,怎么控制并发数?这在Java里有现成的线程池方案,我闭着眼都能配。再比如系统稳定性。模型API偶尔会超时或返回异常,我在Java里用重试和熔断机制轻松解决。这些东西Python教程里不会教,因为那是工程问题,不是AI问题。
所以如果你有扎实的Java功底,你缺的只是“Agent工作方式”的知识,而不是“重新学一门语言”的成本。这也是我写这篇文章的核心动机——告诉同行们,咱们手里的家伙什比想象中好用得多。
3. 动手做一个最小可用Agent,代码走一遍
3.1 选型:项目怎么建、依赖怎么配
我先交代我的项目环境,你照着搭就行。我用的是JDK 17 + Maven,Spring Boot 3.x。引入Spring生态的AI模块,它统一封装了主流大模型接口,我配置的是国内可直连的商用模型API。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model</artifactId> </dependency>然后配置模型接入参数:
spring: ai: model: api-key: ${MODEL_API_KEY} base-url: ${MODEL_BASE_URL} chat: options: model: ${MODEL_NAME} temperature: 0.2这里要说一个关键点:temperature参数。我一开始用默认值,发现模型经常“发挥创意”,把工具参数都改乱了。降到0.2之后,回答变得稳定可控。对于Agent场景,你需要的是“确定性”,不是“灵感”。
3.2 核心代码拆解:Agent循环怎么写
把框架搭好之后,Agent的核心是一个循环:把消息发给模型 → 模型决定是回复还是调用工具 → 如果是调用工具,执行工具并返回结果 → 把结果加入消息列表 → 再发给模型。循环往复,直到模型给出最终回复。
这段逻辑我用ChatClient的机制实现,代码非常清爽:
@Service public class AgentService { private final ChatClient chatClient; private final AgentTools agentTools; public AgentService(ChatClient.Builder builder, AgentTools agentTools) { this.chatClient = builder.build(); this.agentTools = agentTools; } public String chat(String userMessage) { // system prompt 里明确告诉模型有哪些工具可用,什么场景下调用 String system = String.format("你是企业智能助手。你可以调用以下工具:%s。" + "当用户的需求与工具无关时,直接用自然语言回答。" + "当用户需要查询数据时,必须先调用对应工具,再根据工具返回结果回答。", agentTools.toolDescriptions()); return chatClient.prompt() .system(system) .user(userMessage) .call() .content(); } }等下,可能有同学要问了:上面这段里模型的“决策循环”在哪?我上面用的是ChatClient的自动工具调用机制,模型服务端自己会决定是否调用工具,并在必要时自动完成往返。如果你用的模型API不支持自动工具调用,那就得自己写循环了,逻辑像这样:
public String runWithManualLoop(String userInput) { List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage(SYSTEM_PROMPT)); messages.add(new UserMessage(userInput)); for (int i = 0; i < 5; i++) { ChatResponse response = chatClient.call(messages); String content = response.getContent(); // 解析模型输出,判断是否需要调用工具 ToolCallRequest request = parseToolCall(content); if (request == null) { return content; // 模型给出了最终回复 } // 执行工具,把结果追加进对话历史 String toolResult = agentTools.execute(request); messages.add(new AssistantMessage(content)); messages.add(new ToolResultMessage(request.id(), toolResult)); } return "已达最大轮次,请简化问题"; }自己写循环会更接近Agent本质,也能更好控制流程。但大部分场景下,我建议直接用封装好的自动机制,省力且稳定。
3.3 让Agent会“使用工具”,关键就一个注解
工具定义这一步,是Java开发最容易上手的地方:你写普通方法,加一个注解,它就成了Agent能调用的工具。
@Component public class AgentTools { @Tool(description = "根据城市名查询实时天气") public String queryWeather(String city) { // 这里可以是任何Java逻辑:调用HTTP接口、查数据库、读文件 // 假设天气服务返回结构化文本 return "城市:" + city + ",天气:晴,温度:26℃"; } }注意几个要点。第一,方法名和参数名要准确,模型靠这些信息决定怎么调用你。第二,description要写清楚工具的用途和触发条件,比如“当用户询问天气时调用”,这能显著提高模型选对工具的概率。第三,返回结果最好是简洁的文本或JSON,别返回一大坨HTML日志,那会污染上下文。
3.4 配置模型参数的几个实操建议
这里把我在生产里实际用的参数配置和理由全部列出来,你直接抄作业:
| 参数 | 推荐值 | 原因 |
|---|---|---|
| temperature | 0.1 - 0.3 | 太低机械,太高失控;Agent场景偏低更好 |
| maxTokens | 1024 - 2048 | 太长会导致响应慢、费用高;工具交互场景不需要超长回复 |
| timeout | 10 - 30秒 | 模型API偶尔慢,太短频繁失败,太长拖垮调用链 |
| maxIterations | 5 - 8轮 | 防止Agent陷入无限循环调工具的泥潭 |
还有一个小技巧,定义system prompt的时候,把“工具使用规则”放在靠前的位置。这个策略听着简单,但实测下来,工具命中率有明显提升。
4. 从Demo到生产环境,五个必须做的工程化改造
4.1 上下文管理:别让Agent“记不住刚才的事”
Demo里你可以每次都只发当前用户一句话,模型没有历史记忆,两轮对话就“失忆”了。生产环境必须做上下文管理。我采用的是两种方案混合:
第一个是滑动窗口:只保留最近N轮消息。最多放20轮,超过就把最早的丢掉。第二个是摘要压缩:当对话很长时,把早期消息喂给模型生成一段摘要,替换掉原始内容。这两个方案组合使用,基本能覆盖绝大多数业务场景。
这里有个血泪教训:千万别贪心把全部历史都塞给模型。我有一次塞了10万字的聊天记录,结果模型开始“抄写”历史,回答质量急转直下,费用还翻了好几倍。后来才明白,上下文越精炼,模型越清醒。
4.2 结构化输出:让模型按你的格式说话
Agent经常需要返回JSON给前端或下游系统。但大模型的自然语言输出天然不守规矩,你可能要求它返回{"city":"北京","weather":"晴"},它非要多说一句“好的,为您查询到了北京的天气情况”。
解决方案是启用模型服务的结构化输出模式,或使用框架内置的输出转换器。我在Spring生态的AI模块里,用它的转换器机制,定义对象类型和字段,框架会强制模型按JSON格式输出并自动反序列化成Java对象。
public WeatherResponse queryWeatherStructured(String userInput) { // 转换器强制输出为 WeatherResponse 类型的 JSON return chatClient.prompt() .user(userInput) .call() .entity(WeatherResponse.class); }这个改造带来的收益是革命性的——下游解析逻辑不再需要跟模型输出“斗智斗勇”,稳定性大幅提升。
4.3 超时、重试与限流:保护你的系统
模型API是外部依赖,它一定会出问题:超时、限流、返回5xx。在Java生态里解决这些问题对我来说几乎是本能反应。我用的是Spring的Resilience4j,核心配置如下:
resilience4j: retry: instances: modelApi: max-attempts: 3 wait-duration: 1s retry-exceptions: - org.springframework.web.client.HttpServerErrorException bulkhead: instances: modelApi: max-concurrent-calls: 20解释一下逻辑:模型API返回5xx错误时,最多重试3次,间隔1秒;同时限制并发调用数不超过20,防止突发流量把上游打崩。还有一个容易忽略的点:当模型API连续失败时,要有一个“快速失败开关”,别让所有请求都卡在等待上,直接走降级逻辑返回兜底话术。
4.4 可观测性:看到Agent在“想什么”
传统接口的日志很容易排查:接收请求、调数据库、返回响应,没了。Agent不一样,它有多个环节:模型调用了哪个工具、工具返回了什么、模型怎么理解这个结果、最终回复是什么。这些过程不记录下来,出问题根本没法查。
我的做法很简单:每次Agent调用,生成一个TraceId贯穿整条链路,把system prompt、用户输入、模型中间决策、工具调用参数和结果、最终回复全部打到日志里。排查问题的时候打开日志一看,“哦,模型第三轮就想调工具了,但是工具参数传的是空值”——问题一目了然。
4.5 记忆方案:长期记忆怎么存
会话窗口管的是“当前这轮对话的记忆”,但用户可能隔一天再来问“上次那个单子处理得怎么样了”。这时候就要做长期记忆,把关键信息落库。
我的方案:用关系型数据库存两样东西。一个是会话摘要,每轮对话结束后把这段对话的要点存进去;另一个是用户偏好和事实信息,比如用户提到的“我常用的收货地址是公司”。这些信息在下一次会话开始时作为system prompt的一部分注入,实现“换了一天,它还记得你”。
5. 踩坑实录:那些我调试了整整一晚的问题
5.1 症状:工具参数乱传,模型“听不懂人话”
我第一次做天气查询Agent时,模型总把城市参数传成“北京天气预报”,而不是“北京”。工具方法接收参数时直接报错。
排查下来原因有两个:一是工具方法参数名语义不清晰,模型不知道该怎么传;二是系统提示词里没有明确“城市名不带任何修饰词”的约束。解决方法是改工具方法参数名为cityName,并在描述里写死“只接受城市名本身,例如:北京”。后来实测,参数正确率从八成提升到了近满分。
5.2 症状:模型“假装”调用了工具,实际在瞎编
这是个非常隐蔽的问题。模型服务端开启了工具调用后,可能返回“我要调用queryWeather”但工具参数是空的,或者压根没调工具就直接编造了一个答案。
排查思路是这样:看日志里模型返回的原始内容,区分“真调用”和“幻觉调用”。我在系统提示词里加了一条规则:“如果你决定调用工具,则只输出工具调用,不输出任何解释;如果工具结果不可用,明确告诉用户无法获取数据,禁止编造。”这之后,幻觉调用的情况明显减少。
5.3 症状:Agent陷入死循环,不停调同一个工具
有段时间,我的Agent面对某些问题会陷入无限循环:反复调用工具,每次结果都一样,但模型始终不给出最终回复。这背后有两种原因:一是工具的返回信息不够完整,模型总觉得没拿到答案;二是系统提示词没有告诉模型“拿到结果后必须回复用户”。
解法是双管齐下:在工具描述里写明“该接口返回的数据即为最终答案来源,无需再次查询”;在代码里加上最大轮数限制,到上限后强制返回“已尽力处理,请补充信息或稍后再试”。生产环境跑到现在再没发生过死循环。
5.4 症状:高并发下,模型API疯狂限流
Agent服务一上线,我就遇到了瓶颈:模型API的并发限制远低于我的业务请求量。高峰期大量请求被限流,用户体验直线下降。
我的处理策略分三层:第一层,接入本地缓存,相同的问题短时间直接返回缓存结果;第二层,用线程池加队列把请求峰值削平,设置合理的超时等待;第三层,给不同业务场景分配不同的API并发配额,保证核心链路永远有额度。三层组合下来,线上再也没出现大面积限流告警。
5.5 高频问题速查表
| 现象 | 原因 | 快速处理 |
|---|---|---|
| 模型输出解析失败 | 返回了非JSON格式 | 启用结构化输出强制JSON |
| 回答与业务无关 | 缺少业务上下文注入 | 在system prompt中加入业务规则 |
| 首次访问很慢 | 模型冷启动或无连接池 | 配置HTTP连接池和预热机制 |
| 费用异常上涨 | 历史消息无裁剪 | 加滑动窗口和摘要压缩 |
| 上下文内容和回复不匹配 | 多轮消息顺序错乱 | 严格按user/assistant/tool顺序追加 |
6. 转型路线:不学Python,那你到底该学什么
6.1 三个必学的知识点,全部跟语言无关
想清楚转做Agent之后,我给自己列了一份学习清单,这三个知识点和你用什么语言完全无关,但它们决定了你能不能真正做Agent:
提示词工程。不是会写几句Prompt就叫会了。我系统学了角色设定、上下文注入、输出约束、few-shot示例这些方法论。这相当于给模型写“产品需求文档”,是Agent效果的第一决定因素。
Function Calling机制。透彻理解模型API的工具调用协议——请求格式长什么样、参数怎么校验、结果怎么回填。这属于协议层面的知识,Java和Python面对的是同一套协议。
RAG基础。也就是检索增强生成。当Agent需要回答开放性的业务问题时,你要先把文档切片、向量化存储,再在提问时检索最相关的片段注入上下文。理解了RAG,你就理解了“知识库问答”类Agent的原理。
这三个知识点,前两个我花了不到两周就吃透了,第三个也只用了一个月。剩下的什么Transformer架构、注意力机制,可以看懂,但不必深究。
6.2 循序渐进的项目路径,从最安全的地方入手
我建议你不要一上来就做那种“全自主决策Agent”,风险和难度都太高。我自己是按这个过程走过来的:
第一阶段:给现有的业务系统加一个“智能问答入口”。比如你们有个订单查询系统,用Agent做“用自然语言查订单状态”。这一步只做了“模型理解意图 + 调用一个查询工具”,但足以让你跑通全链路。
第二阶段:做一个多工具Agent。给Agent挂上三五个工具:查天气、查快递、查会议室。让它学会在不同工具之间做选择。这一步训练的是“决策能力”。
第三阶段:做带记忆的助手。保存会话历史,实现“追上下文”的连续对话,并引入长期记忆落库。
第四阶段:再上RAG。给Agent挂一个知识库,让它能基于内部文档、产品资料回答复杂问题。
这四个阶段走完,你基本具备了独立设计Agent产品的能力。而且每一阶段都是在原有Java工程上迭代,不需要重写任何东西。
6.3 老Java人的优势清单,别妄自菲薄
后面跟几个应届同学组队做项目,我意外发现一件事:论写Prompt、论了解新模型的特性,我比不上他们,但一旦涉及到系统设计、并发控制、异常处理、部署监控,他们的经验和判断力差了不是一星半点。
这就是老Java人的底气。你有多年的分布式系统经验,你知道怎么保证线上稳定性,你理解什么是可维护性、可观测性、可扩展性。这些能力放到Agent开发里一样值钱。Agent不是只要“聪明”,而是要“可靠”。让模型变聪明是模型厂商的事,让Agent落地稳定可靠,是我们工程人的活。
我在实际开发中还发现一个通用的小技巧:不要一开始就追最新的Agent概念,比如什么自主规划、自我反思,那些东西在绝大多数业务场景里用不上,反而会引入不可控的复杂度和幻觉风险。真正跑在生产里的Agent,往往是那些“结构简单、逻辑清晰、每个环节都可控”的方案。
最后分享一个最让我受益的经验:转型AI Agent最好的切入点,不是另起炉灶做新项目,而是从你现有的业务里找一个“重复性高、需要人脑判断”的场景,先用Agent做出来,哪怕第一天效果一般,先让它在你的系统里跑起来。我正是从一个“自动整理客服回复”的小工具开始,慢慢理解了这个领域真正核心的东西。这条路不需要你从零学Python,只需要你迈出第一步。