1. 为什么 Java 工程师转 AI Agent 是当下最顺的一条路
先把结论摆在前面:Java 工程师转 AI Agent,不是从零开始学一门新语言,而是把已有的工程能力迁移到一个新的调用范式上。你过去写 Spring Boot 那套东西——依赖注入、分层架构、统一异常处理、连接池、线程池、可观测性——在 Agent 开发里一个都没浪费,反而成了稀缺优势。市面上大量 Agent 教程是 Python 视角的,讲的是 prompt 怎么写、chain 怎么串,但真到了要扛并发、要做权限、要接企业已有系统的时候,纯脚本思维就顶不住了。这时候 Java 工程师的价值就出来了。
这篇内容我打算讲透三件事:Agent 的底层运行原理到底是什么,LangChain4j 和 Spring AI 这两条 Java 路线怎么选,以及一个能真正落地的 Agent 从搭建到扛并发的完整过程。适合有 Java 基础、想切入 AI Agent 但不知道从哪下手的开发,也适合已经在用 Python 玩 Agent、想看看 Java 生态现在到什么程度的同学。我不会只给你概念,会把参数、配置、踩过的坑都摊开讲。
先对齐一个认知:AI Agent 不是"更聪明的聊天机器人"。普通 LLM 调用是"你问我答",一问一答就结束了。Agent 的核心是它能自己决定下一步做什么——调用哪个工具、要不要再查一次、结果够不够、要不要重试。这个"自己决定"的循环,就是 ReAct(Reasoning + Acting)范式。理解了这个循环,你就理解了 Agent 的一切。
2. Agent 底层原理拆解:ReAct 循环到底在转什么
2.1 从一次普通 LLM 调用说起
普通调用长这样:你给模型一段 prompt,模型返回一段文本。整个过程是单向的,模型没有"手",它不能查数据库、不能调接口、不能读文件。它只能基于训练时见过的知识和你给的上下文来回答。
问题就来了。你问"我们系统里订单号 12345 现在什么状态",模型根本不知道,因为这是你私有系统的实时数据。你要么把数据塞进 prompt(上下文有限、还贵),要么让模型有能力自己去查。Agent 就是干后面这件事的。
2.2 ReAct 的三步循环
ReAct 把一次任务拆成不断重复的三步:
- Thought(思考):模型分析当前状态,决定下一步要干什么。比如"我需要先查订单状态,得调用订单查询工具"。
- Action(行动):模型输出一个结构化的调用请求,指定工具名和参数。比如
queryOrder(orderId="12345")。 - Observation(观察):你的代码真正执行这个工具,把结果返回给模型。模型看到结果后,回到第一步继续思考,直到它认为可以给出最终答案。
这个循环会转很多圈。关键在于:模型不直接执行任何东西,它只输出"我想调用什么",真正执行的是你的 Java 代码。这一点极其重要,因为它意味着所有权限控制、参数校验、超时处理、审计日志,都握在你手里。这也是 Java 工程师的主场——你写的不是 prompt,你写的是那个安全执行工具的运行时。
2.3 工具调用是怎么被"结构化"出来的
模型输出的是自然语言,怎么变成可靠的函数调用?两条路。一条是靠 prompt 约定格式,让模型按{"tool": "...", "args": {...}}输出,然后你解析。另一条是现在主流模型都支持的Function Calling / Tool Calling能力——你在请求里声明有哪些工具、每个工具的参数 schema,模型会直接返回结构化的调用意图,不用你自己解析字符串。
Java 侧的做法通常是后者。LangChain4j 和 Spring AI 都提供了注解方式,把一个普通 Java 方法标记成工具,框架自动生成 schema 塞进请求。比如:
public class OrderTools { @Tool("根据订单号查询订单当前状态") public String queryOrder(@P("订单号") String orderId) { return orderService.getStatus(orderId); } }框架会把这个方法的名称、描述、参数类型转成模型能理解的工具定义。模型决定调用时,框架负责把参数反序列化、反射调用你的方法、把返回值再喂回去。你写的还是普通 Java 方法,只是多了一个注解。
2.4 记忆与上下文管理
Agent 要能多轮对话、要能记住前面查过什么,就涉及记忆。简单做法是把历史消息全塞进上下文,但 token 会爆。工程做法是分层:短期记忆放当前会话的消息窗口,长期记忆落到向量库做检索(这就是 RAG 的用武之地)。LangChain4j 里的ChatMemory、Spring AI 里的ChatMemory都是干这个的,底层可以接内存、Redis 或者数据库。
这里有个容易忽略的点:记忆不是越多越好。上下文塞太多无关历史,模型反而会跑偏,还费钱。我一般会给会话设一个消息条数上限,超出后做摘要压缩,把老消息压成一段总结再放回去。
3. Java 路线选型:LangChain4j 还是 Spring AI
3.1 两条路线的定位差异
这是 Java 工程师最纠结的问题。我的判断是:看你的项目是"以 AI 为核心"还是"给现有 Spring 系统加 AI 能力"。
LangChain4j 更像一个独立的 AI 应用框架,抽象层次丰富,Agent、RAG、工具、记忆、多模型适配都做得比较全,社区活跃,迭代快。你想快速搭一个功能完整的 Agent,它上手更顺。
Spring AI 则是把 AI 能力"Spring 化"。它的哲学是:如果你已经在写 Spring Boot,那接入 AI 应该像接入一个JdbcTemplate一样自然。配置走application.yml,Bean 走依赖注入,可观测性接 Micrometer。它和 Spring 生态的融合度是 LangChain4j 比不了的。
3.2 关键能力对照
| 维度 | LangChain4j | Spring AI |
|---|---|---|
| 定位 | 独立 AI 应用框架 | Spring 生态的 AI 抽象层 |
| 工具调用 | @Tool注解,成熟 | @Tool注解,逐步完善 |
| RAG 支持 | 内置 Easy RAG,开箱即用 | 有 Advisor 机制,需自己组装 |
| 多模型适配 | 覆盖广,切换方便 | 覆盖主流,配置化 |
| 与 Spring 集成 | 需手动整合 | 原生,自动配置 |
| 学习曲线 | 概念多,需理解框架 | 对 Spring 开发者几乎零成本 |
| 适合场景 | AI 为核心的新应用 | 存量系统加 AI |
3.3 我的实际选择建议
如果你是从零起一个新项目、Agent 逻辑复杂、要频繁试不同模型和工具组合,我倾向 LangChain4j。它的AiServices能把一个接口直接变成 Agent,声明式写法很省事。
如果你是在一个已经跑了几年的 Spring Boot 单体或微服务里加 AI 能力,比如给客服系统加个智能问答、给运营后台加个数据查询助手,那 Spring AI 更合适。你不用引入一套新的编程范式,团队里其他 Spring 开发者也能看懂。
还有个现实情况:这两个不是互斥的。我见过有项目用 Spring AI 做基础模型接入和配置管理,用 LangChain4j 做复杂的 Agent 编排,各取所长。别被"必须二选一"的思维框住。
4. 从零搭一个能落地的 Agent:完整实操
4.1 环境与依赖准备
先明确版本。Java 至少 17,我建议直接上 21,虚拟线程对 Agent 这种大量 IO 等待的场景收益明显。构建工具用 Maven 或 Gradle 都行,下面以 Maven 举例。
LangChain4j 的核心依赖:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.35.0</version> </dependency>Spring AI 的话,用 starter 更省事:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency>注意:模型接入这块,国内很多团队会用兼容 OpenAI 协议的国产模型服务。配置时把 base-url 和 api-key 换成对应服务的即可,接口协议一致,代码基本不用改。具体用哪家按团队合规要求来定。
4.2 定义工具:Agent 的"手"
Agent 的能力边界由工具决定。我建议工具设计遵循三个原则:单一职责、参数明确、返回结构化。
@Component public class DataQueryTools { private final OrderRepository orderRepository; public DataQueryTools(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Tool("根据订单号查询订单的当前状态和金额") public OrderInfo queryOrder(@P("订单号,纯数字字符串") String orderId) { return orderRepository.findById(orderId) .map(o -> new OrderInfo(o.getId(), o.getStatus(), o.getAmount())) .orElseThrow(() -> new IllegalArgumentException("订单不存在: " + orderId)); } @Tool("统计指定日期范围内的订单总数") public long countOrders(@P("开始日期,格式 yyyy-MM-dd") String start, @P("结束日期,格式 yyyy-MM-dd") String end) { return orderRepository.countBetween(LocalDate.parse(start), LocalDate.parse(end)); } }工具描述要写清楚,因为模型是靠描述来判断该不该调用这个工具的。描述含糊,模型就会乱调或者不调。参数说明也一样,格式要求写明白,能大幅降低模型传错参数的概率。
4.3 组装 Agent
LangChain4j 的声明式写法:
public interface OrderAgent { @SystemMessage("你是一个订单助手,只能基于工具返回的真实数据回答,禁止编造。") String chat(@MemoryId String sessionId, @UserMessage String message); } OrderAgent agent = AiServices.builder(OrderAgent.class) .chatLanguageModel(model) .tools(new DataQueryTools(orderRepository)) .chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20)) .build();MessageWindowChatMemory.withMaxMessages(20)就是前面说的记忆窗口,只保留最近 20 条消息,防止上下文无限膨胀。@MemoryId用来区分不同会话,多用户场景下每个用户一个独立记忆。
Spring AI 的写法思路类似,用ChatClient加工具:
ChatClient client = ChatClient.builder(chatModel) .defaultSystem("你是一个订单助手,只能基于工具返回的真实数据回答。") .defaultTools(new DataQueryTools(orderRepository)) .build(); String answer = client.prompt() .user("帮我查一下订单 12345 的状态") .call() .content();4.4 一次完整调用的执行链路
拿"帮我查一下订单 12345 的状态"举例,实际发生的事是:
- 用户消息进入 Agent,连同系统提示、工具定义、历史记忆一起打包成请求发给模型。
- 模型返回一个工具调用意图:
queryOrder(orderId="12345")。 - 框架反射调用你的
queryOrder方法,拿到OrderInfo。 - 框架把结果序列化后作为 Observation 追加到消息里,再次请求模型。
- 模型看到数据,生成自然语言回答:"订单 12345 当前状态是已发货,金额 299 元。"
- 框架把最终回答返回,同时把这一轮消息写入记忆。
整个过程可能只转一圈,也可能转好几圈(比如模型先查订单、再统计、再对比)。你要清楚的是,每一圈都是一次真实的模型请求,都消耗 token 和时间。这就是后面讲并发和成本优化的基础。
5. 扛并发:Agent 上生产必须过的坎
5.1 并发瓶颈到底在哪
很多人以为 Agent 慢是因为模型推理慢。对,但不全对。真实链路里,一次 Agent 调用可能包含 3 到 5 次模型请求,每次请求的延迟在几百毫秒到几秒不等,再加上工具执行(查库、调接口)的时间。这些时间绝大部分是 IO 等待,不是 CPU 计算。
这意味着什么?意味着你不需要堆很多 CPU 核,你需要的是让线程在等待时别占着资源。这正是虚拟线程的用武之地。
5.2 用虚拟线程扛住高并发
Java 21 的虚拟线程,对这种"大量阻塞在 IO 上"的场景几乎是量身定做。传统线程池里,一个线程等模型响应时就被占住了,池子满了新请求就得排队。虚拟线程可以在等待时让出载体线程,几千上万个并发请求也不会把平台线程耗尽。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<String>> futures = requests.stream() .map(req -> executor.submit(() -> agent.chat(req.sessionId(), req.message()))) .toList(); // 收集结果 }实测下来,同样的机器配置,把线程池换成虚拟线程后,Agent 接口的吞吐能提升好几倍,尤其是那种单次调用要转好几圈的复杂任务。
5.3 超时、重试与降级
模型服务不是永远可用的。必须设超时,而且要分层设:单次模型请求的超时、整个 Agent 循环的总超时、工具执行的超时。我一般给单次模型请求设 30 秒,整个 Agent 循环设 60 秒,工具执行设 5 秒。
重试要谨慎。模型请求失败重试是合理的,但工具调用失败重试要小心副作用——如果工具是"下单"这种写操作,重试可能造成重复下单。我的做法是:读操作可以自动重试,写操作要么不重试,要么用幂等键保证安全。
降级方案也得有。模型服务挂了怎么办?可以降级到规则引擎,或者返回一个"当前服务繁忙,请稍后再试"的友好提示,而不是让请求一直挂着直到超时。
5.4 限流与成本控制
Agent 是烧 token 的。一个复杂任务转五圈,token 消耗是普通问答的好几倍。生产环境必须做限流,按用户、按接口、按时间窗口都行。同时要监控 token 消耗,设预算告警。
还有个省钱技巧:简单任务别用 Agent。如果一个问题不需要调用工具、不需要多步推理,直接走普通 LLM 调用甚至走缓存就行。Agent 的循环是有成本的,别滥用。
6. 常见问题与排查技巧实录
6.1 模型不调用工具怎么办
这是最高频的问题。模型该调工具的时候不调,直接编了个答案。排查顺序:
- 先看工具描述是不是太模糊。描述里要明确"什么时候用这个工具"。
- 看系统提示有没有强调"必须基于工具返回的真实数据回答,禁止编造"。
- 看模型本身的能力。有些小模型对工具调用的支持就是弱,换个能力强的模型试试。
- 看工具数量。工具太多(比如超过 20 个)模型会挑花眼,考虑分组或者按场景动态注入。
6.2 参数传错或格式不对
模型把日期传成"2024年1月1日"而不是"2024-01-01",或者把数字传成字符串。解决办法:在参数描述里把格式要求写死,同时在工具方法里做防御性校验,格式不对就抛出清晰的错误信息,让模型看到错误后自己纠正重试。
6.3 循环停不下来
模型一直调工具,转十几圈还不给最终答案。这通常是任务描述太开放,或者工具返回的结果让模型觉得"还不够"。对策:设最大循环次数(比如 10 次),到了就强制让模型基于现有信息给答案;同时优化工具返回,别返回一大堆无关数据。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 不调用工具 | 描述模糊/模型弱/工具太多 | 优化描述、换模型、分组注入 |
| 参数格式错 | 描述未约束格式 | 描述写死格式+方法内校验 |
| 循环不停 | 任务太开放/返回太杂 | 设最大轮次、精简工具返回 |
| 响应慢 | 循环多/模型慢/无并发 | 虚拟线程、超时、减少轮次 |
| token 超预算 | 记忆太长/滥用 Agent | 压缩记忆、简单任务走直连 |
| 结果不稳定 | 温度太高 | 降低 temperature,结构化输出 |
6.5 几个我踩过的坑
第一个坑:别把敏感数据直接塞进 prompt。用户隐私、密钥这类东西,要么脱敏,要么根本不让模型看到。工具执行在你这边,你完全可以在返回给模型前做过滤。
第二个坑:日志要打全。Agent 出问题时,你需要看到每一轮的 Thought、Action、Observation。没有这些日志,排查就是盲人摸象。我一般会把整个循环过程结构化记录下来,方便回放。
第三个坑:别迷信"通用 Agent"。一个 Agent 什么都能干,往往什么都干不好。按业务场景拆成多个专用 Agent,每个 Agent 工具少、提示清晰,效果和稳定性都好得多。
7. 关于转型这件事,我自己的体会
从 Java 后端转 AI Agent,最难的不是学框架,是换一种思维方式。以前写代码,逻辑是你写死的,输入输出确定。现在你要接受"模型是个不确定的组件",你的工程能力要用在约束和兜底上——用工具定义约束它能做什么,用校验兜住它可能犯的错,用超时和降级兜住它可能挂掉的情况。
LangChain4j 和 Spring AI 这两个框架,我建议都花点时间摸一遍。不用纠结先学哪个,先动手把一个能查数据库、能多轮对话的小 Agent 跑起来,比看十篇原理文章都管用。跑通之后你自然会遇到并发、成本、稳定性这些问题,那时候再回头看这篇里的排查表,会有完全不一样的感受。
最后分享一个我常用的调试技巧:把 Agent 的每一轮循环单独打日志,标上轮次编号和耗时。你会发现很多"模型不听话"的问题,其实是某一轮工具返回了意料之外的数据,模型被带偏了。定位到具体是哪一轮出的问题,解决起来就快多了。