2023 年底,我还在维护一个日均几千万请求的 Java 交易系统。干了八年 Java,Spring Boot、JVM 调优、分库分表、消息队列这些基本都算熟了。但也就是那段时间,我明显感觉到一个趋势绕不过去:AI Agent 开始成为项目交付和产品设计里绕不开的词。恰好公司有个新方向要做智能体,我主动申请转了过去。从 Java 后端到 AI Agent 开发,这中间不是“学个 Python、调个 API”那么简单,我在第一周就被上了一课。这篇文章把我这大半年转型过程中真正需要补的东西、踩过的坑、以及反复验证过的做法整理一遍,希望能让想往这个方向走的 Java 工程师少走弯路,也让那些正在犹豫“Java 够不够用”的人有个参照。
这篇内容主要面向至少有三五年后端经验、但对 LLM 和 Agent 工程还没有体系化认知的开发者。如果你已经会用 Spring Boot,那我默认你懂 HTTP 服务、线程池、数据库事务和分布式缓存,这些基础不会白费;但如果你还停留在“写个 Controller 调 OpenAI 接口”的阶段,那看完这篇应该能对 Agent 工程化有个完整的地图。
1. 先想明白:Java 工程师做 AI Agent 到底在做什么
1.1 Agent 不是服务,而是一个会“思考、行动、观察”的循环
我刚开始转的时候,犯了一个很典型的错误:把 Agent 当成一个高并发接口来设计。我以为核心工作是写好 REST API、把任务丢进去、返回结果就完事。结果真的上手才发现,AI Agent 的本质是一个循环,不是一次调用。
它的大致过程是这样的:模型先理解用户目标,拆解成几个步骤;然后决定调用哪个工具,比如查数据库、调接口、执行一段代码;工具返回结果后,模型再看这个结果是否符合预期;如果不符合,它会修正思路,再调用别的工具,直到得出结论或撞上终止条件。这个模式在业界叫 ReAct,也就是 Reason + Act 循环。和 Java 服务端那种“请求进来,业务逻辑处理,返回响应”的线性模型完全不同。
举个生活化类比:传统后端像外卖平台,你下单,平台按固定流程接单、做菜、配送,流程是预设死的。Agent 更像一个居家阿姨,你说“把家里收拾干净”,她会自己判断先扫地还是先洗碗,发现洗洁精没了会下楼买,买完发现时间不够还会调整顺序。她的每一步不是预先写死的,而是根据当前观察到的信息动态决定的。
所以 Java 工程师要补的第一课,不是 Python 语法,而是把“确定性逻辑”和“不确定性逻辑”分开。Agent 的核心不是算法多复杂,而是你如何组织“模型推理 + 外部工具 + 状态记忆”这三样东西,让它们形成闭环。
1.2 Java 服务端思维与 Agent 编排的四个核心差异
我把八年来 Java 服务的习惯思维和 Agent 开发的要求放在一起对比,发现至少有四个维度是完全不同的。这些差异不搞清楚,后面写代码全是别扭。
| 维度 | Java 服务端习惯 | Agent 开发实际要求 |
|---|---|---|
| 数据流 | 请求、响应,结构固定,类型安全 | 对话上下文,自由文本,结构多变 |
| 错误处理 | 抛异常、回滚、重试机制成熟 | 模型会“一本正经地胡说”,需要校验与兜底 |
| 状态管理 | 服务无状态,状态放 DB、Redis | 每次对话都带着上下文状态,天然有状态 |
| 执行路径 | 代码决定分支,逻辑透明 | LLM 决定分支,逻辑有概率性,需要可观测和人工介入 |
第一点是数据流。写 Java 接口时,我们很依赖 DTO、VO、校验注解,字段少了编译期就能发现。但 LLM 的输入输出都是字符串和 token,你给它的工具返回数据结构再清晰,它也可能理解为另一种意思。所以你必须在工具调用边界加一层 schema 校验,不能让模型“自由发挥”太远。
第二点是错误处理。Java 的 try-catch 很成熟,数据库事务回滚也很可靠,但 LLM 调用出错不是简单的异常,它可能是“成功了但回答错了”。比如你让它查订单号,它调了工具,工具返回了空值,它会自己编一个“订单不存在”的结论。这种错误用异常捕获不到,必须在设计 Agent 时加入证据链校验。
第三点是状态管理。Java 后端一般追求无状态,把状态丢给 Redis 和数据库,方便水平扩容。但 Agent 的每次推理都依赖完整的对话历史和工具调用记录,这些状态既是性能负担,也是正确性保障。你需要显式规划哪些状态放内存、哪些放 Redis、哪些要做摘要压缩。
第四点是执行路径。传统代码的分支清晰,出问题能快速定位到哪一行。LLM 的执行路径是概率性的,同样的输入,温度参数一改结果就变。所以你不能指望“测试一次通过就永远通过”,必须加最大步数、超时、人工确认节点,这在 Java 开发者看来像“代码不可控”,但其实是 Agent 工程化的常态。
2. 转型要补的硬核清单:语言、框架、部署和可观测
2.1 语言选型:Python 不是唯一解,但基本绕不开
很多 Java 老兵第一反应是:我用 Java 照样能调 OpenAI API,为什么还要学 Python?确实,Java 有 Spring AI、LangChain4j,也能写 Agent。但我的真实体验是,如果你要真正进入这个领域,Python 依然是最省力的选择。
原因有三个。一是生态,目前最活跃的 Agent 编排框架、向量库客户端、模型评测工具、微调工具链,大部分都是 Python 优先,Java 版本往往慢半拍,或者是社区搬运。二是实验效率,Agent 开发大量涉及“快速尝试、快速验证”的迭代,Python 的交互式开发比 Java 的编译-重启循环舒服太多。三是资料密度,你搜到一个问题的解法,十篇里有八篇是 Python 代码,Java 示例少得可怜。
我建议的策略是“两条腿走路”:主学 Python,但 Java 能力别丢。因为真实企业里,Agent 最终要接入 Java 存量系统,你要能用 Java 写好工具层、数据层和运维脚本,再用 Python 胶水层把 Agent 编排起来。我当时花了大概两周,把 Python 的基础语法、asyncio、类型注解、装饰器过了一遍,够用。要提醒一点,重点学async/await和pydantic,前者是并发基础,后者是做工具输入输出校验的利器。
2.2 框架落点:LangChain、LangGraph、Spring AI Agent 怎么选
框架选择这件事,我折腾过好几轮。最早从 LangChain 开始,然后转 LangGraph,中间也试用过 Spring AI Agent。这里说下我的最终看法。
LangChain 早期版本对我来说太“重”了,它把很多东西抽象成链和组件,概念多,调试困难。后来我觉得 LangGraph 的设计才更符合 Agent 的直觉:它把流程建模成一张图,节点就是“调用模型”或“调用工具”,边就是“条件跳转”。这对我来说很亲切,因为我习惯把业务流程画成状态机或 DAG。你用 LangGraph 可以显式控制哪些节点可以循环、哪些节点可以并行、哪些节点需要人工确认,可控性比 LangChain 高很多。
Spring AI Agent 我也试过,对 Java 团队确实友好,你可以在熟悉的 Spring 生态里定义 Tool、接入模型。但它的问题在于社区和案例都还不够厚,遇到奇怪的报错,能找到的参考很少,很多底层封装逻辑你还要自己去翻源码。我用它的结论是:Java 团队如果只想做“内部工具集成 + 对话”的轻量场景,可以用;如果要做复杂多步 Agent,建议还是把编排层放在 Python 的 LangGraph 上,Java 只做工具服务。
FastAPI 在这里的角色也值得说一句。它不是我用来“开发 Agent”的,而是用来“暴露 Agent 服务”的。我会用 FastAPI 写一个薄薄的服务层,把 LangGraph 的调用包成 HTTP 接口,支持流式输出,再对接 Java 侧的业务系统。这套组合在真实项目里跑得很顺。
2.3 部署形态:从 Fat Jar 到镜像、GPU 环境和可观测
Java 后端部署,我们习惯打一个 Fat Jar,丢到服务器上java -jar就完事。Agent 服务的部署形态完全不同,需要注意的点也更多。
首先,Agent 服务通常需要连接一个或多个 LLM 服务。这个 LLM 服务要么是云厂商 API,要么是自己部署的开源模型。自己部署就会涉及 GPU 资源、显存大小、量化等级、推理服务框架这些概念,这对 Java 工程师是全新领域。我当时第一次分配 GPU 时连显存怎么看都不知道,只能一步步查。
其次,Agent 服务是有状态的,部署时不能像 Java 服务那样随意重启。你重启一个 Java 节点,只要数据库还在,问题不大;但 Agent 节点如果存在内存里的对话状态,一旦重启,用户上下文就丢了。所以部署上要设计会话状态的外部化存储,或者至少能接受重启丢失部分会话的风险。
最后是可观测性。Java 服务我们习惯看日志、链路追踪、Metrics,Agent 服务更需要看的是“模型调用链路”,包括每一次 LLM 请求的 prompt、响应、token 消耗、工具调用结果、决策路径。如果这些不可观测,出了问题基本只能靠猜。我当时做了个很朴素的办法:把每次 Agent 运行的完整轨迹打印成 JSON 日志,存起来,出问题再回放。这个习惯救了我很多次。
3. 实操:从零搭一个能分析 Java 代码仓库的 Agent
3.1 整体设计:输入、工具、编排、输出
理论讲再多,不如动手。我设计了一个最小但能真实跑起来的场景:给一段 Java 代码,Agent 自动做代码评审,输出一份结构化报告,内容包括潜在问题、优化建议、复杂度评估。
这个任务选得很有心机:它既能让 Java 老兵发挥优势,又能完整覆盖 Agent 的核心组件。整体拆成四层:第一层是 FastAPI 服务,接收请求;第二层是 LangGraph 编排,负责“理解任务、调用工具、生成报告”的循环;第三层是工具层,我用 Java 写了一个静态分析工具,捡出代码里的循环复杂度、异常吞掉、空指针风险等信息,然后暴露成 Python 能调用的接口;第四层是 LLM 调用层,负责把工具结果转成最终报告。
这个架构的关键在于:Agent 不需要自己“看”代码,它只需要调用工具拿到结构化指标,再基于这些指标做推理。这比直接把整段代码塞给模型要省钱得多,也可靠得多。
3.2 代码实现:LLM 调用层和消息结构
在 Python 里,调用 LLM 最核心的数据结构就是“消息列表”。你发出去的消息、模型返回的消息、工具返回的消息,全部是列表里的元素。Java 那边用对象传递数据,Python 这边用字典和类型化模型传递。
我当时的代码大概是这样的:
from openai import AsyncOpenAI client = AsyncOpenAI() async def call_llm(messages, tools=None, max_tokens=1024): response = await client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=tools, max_tokens=max_tokens, ) return response.choices[0].message这段代码看起来简单,但有三个容易踩的细节。第一,tools参数是一个 JSON Schema 列表,它告诉模型“你可以调用这些工具”,但模型的输出可能是一个“工具调用请求”,也可能是一段普通文本,你必须判断。第二,max_tokens一定要设置,否则模型可能会一直输出,账单会很难看。第三,消息列表要完整保存,模型没有记忆,你必须在每次请求时把历史消息原样传回。
刚开始我图省事,只传当前输入,结果模型完全不知道上下文在聊什么。后来才意识到,对话历史是 Agent 记忆的底层基础,丢掉了就等于失忆。
3.3 把 Java 侧最擅长的能力做成工具
工具层是整个 Agent 里最像 Java 后端的地方。我用 Java 写了几个静态分析方法,比如对一个 Java 类文件做 AST 分析,统计圈复杂度、捕获异常后是否打印、有没有明显的空指针隐患。然后我把它包装成一个 REST 接口,Python 侧通过 HTTP 调用。
在 LangGraph 里,工具的定义要让模型“看得懂”。我给每个工具写了名称、描述、输入参数 schema。描述是给模型看的,写得越清楚,模型越不会乱调。比如“analyze_java_ast”这个工具,描述我会写“输入一个 Java 源码字符串,返回 JSON 报告,包含复杂度、潜在风险、建议检查的行号”,这样模型就知道该在什么时候用它。
工具输出结果也要注意格式。我的一个教训是:工具返回内容过长时,模型处理会变慢,token 消耗也会飙升。所以工具的返回不要直接丢原始 AST 结构,而是先聚合出摘要,只给模型需要的结论性数据。这就像 Java 的分页查询一样,不能一次查出全表再过滤。
3.4 用 LangGraph 把流程编排成图
LangGraph 的核心是状态图和节点。我定义的状态类里有任务描述、工具结果列表、最终报告三个字段。然后定义几个节点:分析任务节点、调用工具节点、生成报告节点,再加一个条件判断:如果工具结果不足,就回到分析节点继续循环,否则跳到生成报告。
核心代码大致是这个思路:
from langgraph.graph import StateGraph, END from typing import TypedDict class AgentState(TypedDict): task: str tool_results: list report: str def analyze_node(state: AgentState): # 决定调用哪个工具 ... def tool_node(state: AgentState): # 执行外部工具并返回结果 ... def generate_node(state: AgentState): # 基于工具结果生成最终报告 ... graph = StateGraph(AgentState) graph.add_node("analyze", analyze_node) graph.add_node("tools", tool_node) graph.add_node("generate", generate_node) graph.add_edge("analyze", "tools") graph.add_conditional_edges("tools", route_by_tool_results) graph.add_edge("generate", END)这个流程对 Java 工程师来说其实不陌生:它就像一个状态机,你定义状态流转的条件和节点逻辑。唯一不同的是,节点的逻辑一部分是你写死的,一部分是由模型决定的。比如“该调用哪个工具、调用几次”,你都要交给模型去判断,而不是硬编码。
要注意的是,图流程一定要有“最大步数限制”,否则模型可能在一个失败的工具调用上反复重试,形成死循环。我一般设置最大执行步数为 10,超过就强制终止并输出“无法完成分析”。
3.5 Token 预算与流式输出:不可忽视的两个工程细节
Token 是 AI Agent 世界里绕不开的概念。你可以理解成模型的计费单位,大概 1 个英文单词约等于 1.3 个 token,1 个汉字大约 1 到 2 个 token。Java 工程师第一次算这个账会心疼,因为一次 Agent 调用,连系统提示词带工具描述带上下文,可能还没正式回答就烧掉几千 token。
我一般做三层控制。第一层是限制上下文长度,使用滑动窗口,只保留最近 N 轮对话,更早的做摘要压缩。第二层是给每次 LLM 调用设置max_tokens,避免一次输出过长。第三层是工具输出精简,工具只返回结论性内容,不返回原始数据。
流式输出也是必须做的。Agent 推理往往要几秒甚至十几秒,如果让用户一直转圈,体验非常差。FastAPI 里我会用 SSE(Server-Sent Events)把模型的增量输出推给前端。这里的工程点在于:要让模型先输出一段文字,再调工具,再输出下一段文字,整个过程的每一部分都能实时推送,而不是等全部完成再返回。
4. Agent 扛并发:Java 并发经验迁移到 LLM 世界
4.1 线程模型之外的关键瓶颈是什么
Java 老兵看到“AI Agent 怎么扛并发”,第一反应往往是线程池开多大、Tomcat 最大连接数多少。这些经验有用,但 Agent 场景下第一个瓶颈根本不在线程池,而在模型服务的吞吐。
如果调用的是云模型 API,它有限流策略,你每秒只能发固定数量的请求,超了会被拒。如果模型是本地部署,GPU 显存和算力决定了并发上限,一个 7B 模型在单张消费级显卡上可能只能同时跑几个并发请求。所以你要做的第一件事,不是调大线程池,而是给模型调用层加“限流器”和“重试机制”。
重试也不是简单重试。模型 API 返回 429 限流或 5xx 错误时,要使用指数退避策略,比如第一次等 1 秒重试,第二次等 2 秒,第三次等 4 秒,同时要加一个最大重试次数。这个套路 Java 里在封装第三方 API 时很常见,迁移过来完全适用。
4.2 任务拆分与异步化:从 CompletableFuture 到 asyncio
Java 这边做异步并发,常用CompletableFuture、ThreadPoolExecutor。Python 这边对应的就是asyncio和协程。我一开始特别不适应 Python 的异步语法,总想着用同步代码加线程池,结果性能和设计都别扭。
后来我总结出一个迁移方法:把 Agent 的一次执行当成一个大的异步任务,内部涉及多个工具调用时,能并行就并行,需要顺序的就用状态依赖。比如“分析代码复杂度”和“检查异常处理”这两个工具互不依赖,那就可以并发调用;但“生成报告”依赖这两个结果,就必须等它们都结束。LangGraph 的节点设计天然支持并行分支,你只要在状态图上把没有依赖的节点并行连起来就行。
还有一个点要特别注意:Java 的 Spring 事务是线程绑定的,很多状态通过 ThreadLocal 传递;Python 的异步任务里,同一个协程在不同 await 之间状态是保留的,但你不能指望它像 Spring 事务那样自动回滚。Agent 任务一旦开始执行,工具调用的副作用可能已经发生,比如发出了邮件、产生了数据变更,这些没法“回滚”,只能在设计上尽量把有副作用的工具放到流程较后阶段,或者加人工确认。
4.3 会话状态隔离与用户级缓存的正确姿势
并发场景下,最怕的是“串号”。Java 服务如果静态变量里存了用户数据,并发一高就出问题;Agent 环境也一样,对话上下文如果放成全局变量,两个用户同时请求就会互相污染。
正确做法是给每个会话分配独立的 Context 对象,用会话 ID 作为 key 存到 Redis。这个 Context 里包含对话历史、工具调用记录、临时变量。每次 Agent 执行节点时,从 Redis 读取该会话的 Context,执行完再写回去。由于 LLM 上下文很大,直接全量存在 Redis 可能浪费内存,我一般只保存最近几轮消息和一个经过摘要的历史记录。
用户级缓存也很重要。很多 Agent 任务会重复调用同样的工具,比如查同一个用户的订单信息,这在 Java 后端我们习惯用 Redis 做缓存。Agent 里同样适用,但要注意缓存失效策略:不能缓存太久,否则用户数据变更了 Agent 还在用旧数据。我一般给工具层加一个 TTL(过期时间),过期后强制重新查询。
4.4 流式响应与连接耗尽量级
Agent 服务对外提供接口时,如果使用 FastAPI 的 SSE 流式输出,连接占用时间会很长,因为模型生成一个 token 就要推送一次。并发一高,连接数迅速膨胀,网关层和负载均衡层很快成为瓶颈。
我的经验是两个方向:第一,给 FastAPI 服务设置合理的超时时间,单个请求如果超过 60 秒还没完成,就断开重试或返回提示;第二,流式输出和普通 JSON 输出分成两个接口,“需要实时打字机效果”的走 SSE,“只要最终结果”的走普通 JSON 轮询。很多内部系统根本不需要流式,用普通接口的吞吐会高很多,资源消耗也小。
还有一点是关于 WebSocket 和 SSE 的选择。Agent 编排过程如果想支持中途人工干预,WebSocket 更好;如果只是单向展示进度,SSE 足够。Java 写 Netty 或 Spring WebSocket 的经验可以迁移过来,但概念别混了。
5. 常见问题与排查实录:Java 转型最容易踩的坑
5.1 工具返回结构不统一,Agent 理解出错
我第一个正式 Agent 项目上线后,发现模型频繁在“调用同一个工具”和“直接编造答案”之间反复横跳。排查半天,发现是工具返回结构不统一:有时候返回 JSON,有时候返回纯文本,模型被搞晕了。
解决办法是给工具输出强制加一层 JSON Schema 校验。我写了一个轻量函数,工具返回前先做pydantic模型校验,字段缺失就补齐,类型错误就转换。这就像 Java 里给接口加 @Valid 注解一样,把“模型可能出错”当成默认预期,而不是意外。
5.2 上下文越塞越长,Token 账单失控
真实使用时用户不会只说一句话,往往是十几轮对话来回,每轮都带着代码和结果。如果不处理,上下文迟早会撑爆。我当时看一个月账单,差点以为被攻击了。
后来采取的策略是:对话超过 8 轮后,把最早的消息用摘要模型压缩成一段简短的历史说明,再替换进上下文。这个“摘要替换”操作要放在用户无感知的时机做,比如用户正在输入时异步处理。同时在每轮请求前计算当前 token 数,超过阈值就强制压缩。这个方法直到今天,我仍认为是最实用的省钱手段。
5.3 模型陷入重试死循环,白烧钱
最可怕的一个 bug:模型反复调用同一个查询工具,每次都因为参数格式错误而失败,但它不放弃,连续调用七八次,每次都是同样的错误。我的代码没有设置最大步数,结果一次单用户对话烧掉了近千次工具调用。
根治办法有两个:一是给 LangGraph 的执行图加最大步数限制,超过即终止;二是在工具节点前加“参数校验 + 修正”逻辑,如果模型连续两次给出相同错误格式的参数,就把这个工具的配置重新描述一遍,或者干脆让它直接结束。
5.4 幻觉与“看起来正确”的答案
Java 工程师最不能忍的是:模型给出一个看起来数据完整、但事实错误的结果。比如它分析代码后,说“第 15 行有空指针风险”,但实际上第 15 行是注释。
我找了很久才找到一个相对有效的方案:让工具返回的每一项结论都带上证据行号、原代码片段、置信度。模型生成报告时,必须引用这些证据,如果没有证据支撑,它必须明确说“这一点不确定”。这个约束写进系统提示词里,效果立竿见影。本质上就是把 Agent 的输出从“自由发挥”改成“引用式写作”,幻觉概率大幅下降。
5.5 本地部署还是调用云 API:成本拐点怎么算
Java 团队经常问:我是不是搞几张卡本地部署个开源模型就省钱了?答案是看你的量。我算过一笔账:一个 7B 模型量化后大约需要 6GB 到 8GB 显存,一张中高端卡能跑,但并发只能做到个位数,推理速度也一般。如果一天请求量只有几百次,本地部署可行;但一旦上到每秒两位数请求,单卡根本顶不住,必须多卡或升级硬件,成本瞬间超过云 API。
我的建议:初期先用云 API 跑通业务,等确认周调用量稳定在一个量级,再回头算本地部署的 TCO(总拥有成本)。不要为了“技术自主”而提前买显卡,因为模型迭代太快,硬件投入可能半年就过时。
5.6 关于 Rust Agent、GraalVM 和一些“热词”的观察
转型期间我关注过“基于 Rust 语言写 AI Agent”的讨论,也试过用 GraalVM 把 Python 和 Java 混编。结论比较直接:Rust Agent 目前生态还太薄,如果你不是要做极致性能的基础设施,不建议 Java 老兵在这个阶段转去 Rust 做 Agent;GraalVM 更适合让 Java 直接调 Python 脚本,但 Agent 编排层如果用 LangGraph,这条路会很别扭。
我还看到很多“AI Agent 怎么扛并发”的讨论其实都集中在模型层,但我发现真正的瓶颈往往在工具调用和状态管理。只要把工具层做成无状态幂等服务,把会话上下文外部化,并发能力自然就上去了。这也是 Java 后端多年积累的核心竞争力,转过来一点不亏。
写在最后
转型这大半年,我最大的体会是:Java 到 AI Agent 不是从头学一门语言,而是换一种“编程思维”。Java 后端追求确定性、可预测、可回滚;Agent 开发接受概率性、可修复、可兜底。你把这两套思维结合起来,反而比纯 Python 出身的人更有优势,因为 Agent 一旦上生产环境,拼的不是谁的 prompt 写得好,而是谁的工程质量高、谁的系统扛得住流量、谁的错误处理更完善——这恰恰是 Java 老兵的看家本事。
最后再分享一个小技巧:转岗第一周,先不要读那些又长又抽象的 Agent 理论文章,直接选一个你身边最熟悉的场景,比如“自动生成代码 review 报告”,从写工具开始,一步步把循环跑通。跑通一次,你对 Agent 的理解会比读十篇文章都深刻。我就是靠这个办法真正入了门。