1. Java 工程师转型 AI Agent 的底层逻辑与路径选择
1.1 为什么 Java 工程师转 AI Agent 有天然优势
这两年 AI Agent 火得一塌糊涂,但真正落到企业级项目里,你会发现一个尴尬的现实:大部分 Agent 的 Demo 用 Python 写得很漂亮,一旦要接入现有的订单系统、风控系统、权限体系,立马就卡住了。原因很简单,企业里跑着的核心业务系统,八成以上是 Java 写的。你让一个 Python 脚本去调 Java 的 Dubbo 接口、去操作分库分表的 MySQL、去对接公司内部的统一认证,中间要搭多少桥?
所以 Java 工程师转型 AI Agent,不是从零开始,而是带着工程化的底子进场。你熟悉 Spring 的依赖注入、AOP、事务管理,这些能力在构建 Agent 的“工具调用链”时直接就能复用。你懂线程池、连接池、熔断降级,这些在 Agent 高并发场景下就是保命的东西。你写过 MyBatis 的动态 SQL,理解怎么把自然语言映射到结构化查询,这本身就是 Agent 里“意图识别到动作执行”的雏形。
我见过不少 Java 老哥一上来就焦虑:是不是得先把 Python 学透?是不是得把 Transformer 的论文啃完?其实不用。你需要的不是从头训练模型,而是学会怎么“编排”模型。就像你不需要会造发动机才能开车,你需要的是懂交通规则、会看仪表盘、知道什么时候该踩刹车。Java 生态里的 LangChain4j 和 Spring AI,就是给你准备的“驾驶舱”。
1.2 两条主流技术路线的选型对比
目前 Java 圈子里做 AI Agent,主要就两条路:LangChain4j 和 Spring AI。这两个不是非此即彼的关系,但起步阶段你得先选一个主攻方向。
LangChain4j 的定位更偏向“AI 能力工具箱”。它把提示词模板、对话记忆、向量检索、工具调用这些能力都封装好了,你不需要依赖 Spring 容器就能跑起来。适合快速验证想法,或者你的项目本身不是 Spring 体系。它的AiServices接口设计得很巧妙,你定义一个 Java 接口,加几个注解,它就能帮你生成一个能对话、能调工具的代理对象。这种“声明式”的写法,对 Java 工程师来说上手极快。
Spring AI 则是“Spring 生态的原生 AI 扩展”。它的核心优势在于和 Spring Boot 的无缝集成。你现有的@Service、@Configuration、@Bean那一套东西,直接就能用来管理 AI 组件。比如你要做一个 RAG 应用,Spring AI 的VectorStore接口可以直接注入到你的业务 Service 里,和操作数据库的 Repository 没什么两样。而且 Spring AI Alibaba 这个子项目,把国内常用的模型服务(比如百炼、通义千问)都做了适配,省去了自己写 HTTP 客户端的麻烦。
我的建议是:如果你的公司已经在用 Spring Cloud 那一套,直接上 Spring AI,后期维护成本最低。如果你是想快速做个原型,或者想深入理解 Agent 的每个环节,LangChain4j 的文档和示例更直观。两者底层原理相通,学会一个,另一个花半天看文档就能切换。
1.3 从“写接口”到“编排智能体”的思维转变
Java 工程师最熟悉的思维模式是:定义接口 -> 实现逻辑 -> 暴露服务。但在 AI Agent 的世界里,逻辑不是完全由你写死的,而是由模型根据上下文动态决定的。你需要从“过程式编程”转向“目标式编排”。
举个例子。传统做法是:用户输入查询条件 -> 你写 if-else 判断走哪个分支 -> 调用对应的 Service 方法 -> 返回结果。Agent 的做法是:你把所有可用的工具(查询订单、退款、查物流)都注册进去,把用户的原话扔给模型,模型自己决定调哪个工具、传什么参数。你的代码从“决策者”变成了“工具提供者”。
这个转变带来的最大挑战是:你不能再假设输入是干净的、格式化的。用户可能说“我上周买的那双鞋怎么还没到”,也可能说“订单号 12345 的物流信息发我一下”。你需要设计好工具的“描述信息”,让模型能准确理解每个工具是干什么的、需要什么参数。这就像写 API 文档,但读者不是人,是模型。描述写得越清楚,模型调用的准确率越高。
2. 核心概念拆解:ReAct 模式与工具调用机制
2.1 ReAct 模式到底在“反应”什么
ReAct 这个名字听起来玄乎,拆开看就是 Reasoning + Acting。它的核心思想是让模型在“思考”和“行动”之间交替进行。传统的一次性问答是:你问 -> 模型答。ReAct 是:你问 -> 模型想“我需要查一下库存” -> 调用库存查询工具 -> 拿到结果 -> 模型再想“库存有货,我需要查一下物流时效” -> 调用物流工具 -> 综合结果给出回答。
这个过程在 LangChain4j 里体现得很直观。你定义一个接口,方法上标注@Tool,框架会自动生成工具的 JSON Schema 描述。当用户提问时,框架把问题、历史对话、所有工具的描述一起发给模型。模型返回的不是最终答案,而是一个“我要调用某个工具”的指令。框架执行工具,把结果追加到对话历史里,再次发给模型。如此循环,直到模型认为可以给出最终答案。
这里有个关键点:模型本身不执行任何操作,它只负责“决定调什么”。真正的执行逻辑还是你的 Java 代码。所以安全性是可控的。你可以在工具方法里加权限校验、参数校验、审计日志,和写普通 Service 方法一模一样。
2.2 工具调用的参数映射与类型安全
Java 是强类型语言,但模型返回的工具调用参数是 JSON。这中间的映射关系如果处理不好,很容易出问题。LangChain4j 和 Spring AI 都提供了自动映射机制,但有几个坑你得提前知道。
第一,模型对参数类型的理解可能不准确。比如你定义了一个Integer pageSize,模型可能传"10"字符串过来。框架一般会做自动转换,但如果模型传了"十",那就直接报错。所以工具方法的参数尽量用String接收,在方法内部自己做解析和校验。这样容错率更高。
第二,枚举类型要特别小心。模型可能返回一个你枚举里不存在的值。比如你定义了OrderStatus { PAID, SHIPPED, DELIVERED },模型可能返回"已发货"。解决办法是在工具描述里明确列出所有可选值,并且在方法内部做switch的default分支处理。
第三,必填参数和可选参数的区分。在 LangChain4j 里用@P注解描述参数时,可以标注required = true。但模型有时候会“忘记”传必填参数。所以工具方法内部一定要做空值检查,返回一个友好的错误信息,让模型知道“你漏了参数,请重新调用”。
2.3 对话记忆与上下文窗口的管理策略
Agent 不是一次性问答,它需要记住之前的对话内容。但模型的上下文窗口是有限的,你不能把几十轮对话全塞进去。LangChain4j 提供了ChatMemory接口,常用的实现是MessageWindowChatMemory,它只保留最近 N 条消息。
这里有个经验:不要只保留“用户说了什么”和“模型答了什么”,还要保留“模型调用了什么工具、返回了什么结果”。因为模型在后续推理时,可能需要参考之前的工具调用结果。比如用户先问“订单 123 的状态”,模型查了之后回答“已发货”。用户接着问“那大概什么时候到”,模型需要知道之前查的是哪个订单,才能去调物流接口。
我的做法是给ChatMemory设置一个合理的窗口大小,比如 20 条消息。同时在系统提示词里明确告诉模型:“你可以参考之前的对话历史和工具调用结果来回答当前问题。”这样模型在生成工具调用参数时,会自动从历史里提取订单号等信息。
3. 基于 Spring AI 的 Agent 落地实操
3.1 环境准备与依赖配置
假设你用的是 Spring Boot 3.x 和 JDK 17。首先在pom.xml里引入 Spring AI 的 starter。如果你用的是百炼或者通义千问,引入对应的适配包。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-alibaba-spring-boot-starter</artifactId> <version>1.0.0-M6.1</version> </dependency>然后在application.yml里配置模型服务的地址和密钥。注意不要把密钥硬编码在代码里,用环境变量注入。
spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-max temperature: 0.7temperature这个参数控制输出的随机性。做 Agent 工具调用时,建议设低一点,比如 0.1 到 0.3,让模型更“确定性”地选择工具。做创意类任务时再调高。
3.2 定义工具与注册到 Agent
假设我们要做一个订单查询 Agent。先定义一个工具类,里面的方法就是模型可以调用的“能力”。
@Component public class OrderTools { @Tool(description = "根据订单号查询订单状态,返回订单的当前状态和预计送达时间") public String queryOrderStatus( @ToolParam(description = "订单号,格式为纯数字,例如 123456789") String orderId) { // 这里调用你的订单服务 Order order = orderService.getById(orderId); if (order == null) { return "未找到订单号为 " + orderId + " 的订单,请确认订单号是否正确"; } return String.format("订单 %s 当前状态:%s,预计送达时间:%s", orderId, order.getStatus(), order.getEstimatedDelivery()); } @Tool(description = "根据订单号查询物流轨迹,返回最新的物流节点信息") public String queryLogistics( @ToolParam(description = "订单号,格式为纯数字") String orderId) { List<LogisticsNode> nodes = logisticsService.getNodes(orderId); if (nodes.isEmpty()) { return "该订单暂无物流信息,可能尚未发货"; } return nodes.stream() .map(n -> n.getTime() + " " + n.getDescription()) .collect(Collectors.joining("\n")); } }注意@Tool里的description写得越具体,模型调用越准确。我试过把描述写成“查询订单”,模型经常把“查物流”也路由到这个工具上。改成“根据订单号查询订单状态,返回订单的当前状态和预计送达时间”之后,准确率明显提升。
然后创建一个配置类,把工具注册进去,并构建一个ChatClient。
@Configuration public class AgentConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, OrderTools orderTools) { return builder .defaultSystem("你是一个订单助手,可以帮用户查询订单状态和物流信息。" + "当用户提供订单号时,优先调用工具查询,不要自己编造答案。" + "如果用户没有提供订单号,请先询问订单号。") .defaultTools(orderTools) .build(); } }系统提示词里明确告诉模型“不要自己编造答案”,这一点很重要。模型有时候会“幻觉”出一个订单状态,尤其是当工具返回空结果时。加上这句话之后,模型会更倾向于说“我没有查到相关信息”。
3.3 实现多轮对话与工具调用链
有了ChatClient,写一个 Controller 来接收用户消息。
@RestController @RequestMapping("/agent") public class AgentController { private final ChatClient chatClient; public AgentController(ChatClient chatClient) { this.chatClient = chatClient; } @PostMapping("/chat") public String chat(@RequestParam String sessionId, @RequestBody String message) { return chatClient.prompt() .user(message) .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId)) .call() .content(); } }这里的sessionId用来区分不同用户的对话记忆。Spring AI 的ChatMemory会根据这个 ID 自动隔离上下文。你不需要自己维护 Map 或者 Redis,框架帮你处理了。
实测下来,一个完整的工具调用链大概是这样的:用户说“帮我查一下订单 123456 到哪了” -> 模型决定调用queryOrderStatus-> 工具返回“已发货,预计明天送达” -> 模型决定再调用queryLogistics-> 工具返回物流节点 -> 模型综合两个结果给出最终回答。整个过程在 2 到 3 秒内完成,取决于模型服务的响应速度。
3.4 并发场景下的线程安全与资源隔离
Java 工程师最关心的并发问题,在 Agent 场景下同样存在。如果你的 Agent 服务要扛住几百 QPS,有几个地方必须注意。
第一,ChatClient本身是线程安全的,可以单例使用。但ChatMemory如果用的是内存实现,在高并发下会有锁竞争。生产环境建议换成基于 Redis 的实现,或者每个会话独立一个ChatMemory实例。
第二,工具方法里如果调用了外部服务,一定要加超时和熔断。模型调用工具时,如果工具卡住了,整个对话就挂起了。用 Resilience4j 或者 Sentinel 给工具方法加上@CircuitBreaker和@TimeLimiter,超时时间设短一点,比如 2 秒。超时后返回一个“查询超时,请稍后重试”的消息,让模型继续往下走。
第三,注意模型服务的 Rate Limit。国内很多模型服务对并发调用有限制。你可以在ChatClient外面包一层信号量或者令牌桶,控制同时发给模型的请求数。我一般设成 50 到 100,具体看模型服务的配额。
4. 常见问题排查与避坑经验实录
4.1 模型不调用工具怎么办
这是最常见的问题。你明明注册了工具,但模型就是不用,直接自己编答案。排查思路如下。
先检查工具描述是否足够清晰。模型判断是否调用工具,主要看工具的名称和描述。如果描述太模糊,模型会觉得“这个问题我自己能答”。把描述写具体,包含“什么时候该用这个工具”的说明。
再检查系统提示词。在系统提示词里明确要求“优先使用工具查询”,并且给出一个示例。比如:“当用户询问订单状态时,你必须调用 queryOrderStatus 工具,不要自己猜测。”
如果还不行,试试降低temperature。温度越高,模型越“自由发挥”。设成 0.1 试试。
最后检查模型本身的能力。有些小参数模型对工具调用的支持不好。换一个更大的模型,或者专门针对 Function Calling 微调过的模型。
4.2 工具调用参数错误的高频场景
模型传错参数是家常便饭。我整理了一个速查表,覆盖了八成以上的参数问题。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 参数值为空 | 模型没从对话中提取到 | 在系统提示词里强调“如果用户未提供订单号,先询问” |
| 参数类型不匹配 | 模型返回字符串但方法要 Integer | 工具方法参数统一用 String,内部转换 |
| 参数值不在枚举范围内 | 模型自由发挥 | 在工具描述里列出所有可选值 |
| 参数名拼写错误 | 模型对参数名理解偏差 | 用@ToolParam明确描述参数含义 |
| 多传了不存在的参数 | 模型“幻觉” | 框架一般会忽略未知参数,但最好在描述里写清楚参数列表 |
还有一个坑:如果工具方法抛异常了,框架默认会把异常信息返回给模型。但异常堆栈可能很长,浪费上下文窗口。建议在工具方法内部try-catch,返回一个简短的错误描述。
4.3 对话记忆丢失与上下文溢出
用户聊了十几轮之后,模型突然“失忆”了,不记得之前的订单号。这通常是ChatMemory的窗口大小设小了。默认可能是 10 条消息,用户和模型各 5 条,几轮就满了。
解决办法是调大窗口,比如 30 条。但也不能无限大,否则上下文溢出,模型会报错。一个折中方案是用TokenWindowChatMemory,按 Token 数量而不是消息条数来限制。比如设成 4000 个 Token,大概能容纳 20 到 30 轮对话。
另外,如果对话历史里包含很长的工具返回结果(比如物流轨迹有几十条),会快速消耗 Token。可以在工具方法里做截断,只返回最近 5 条物流节点。
4.4 生产环境部署的注意事项
第一,日志要打全。每次工具调用都要记录:谁调的、调了什么工具、传了什么参数、返回了什么结果、耗时多少。出问题的时候,这些日志就是排查依据。
第二,做好降级方案。如果模型服务挂了,Agent 不能整个不可用。可以降级到关键词匹配的规则引擎,虽然笨一点,但至少能回答“查订单”这种简单意图。
第三,敏感操作要加二次确认。比如退款工具,不能模型说退就退。可以在工具方法里检查一个“确认令牌”,只有用户明确说了“确认退款”之后,才允许执行。
第四,注意数据脱敏。工具返回的结果里如果包含手机号、地址等敏感信息,在返回给模型之前先脱敏。模型不需要知道完整的手机号,只需要知道“尾号 1234”就够了。
5. 从单 Agent 到多 Agent 协作的演进思路
5.1 什么时候需要拆分成多个 Agent
单 Agent 能搞定的事情,不要急着拆。拆分的代价是复杂度上升、调试难度加大。我一般建议在以下情况才考虑多 Agent:单个 Agent 的工具数量超过 15 个,模型选择工具的准确率明显下降;或者不同工具需要不同的系统提示词,放在一起会互相干扰。
比如一个电商客服 Agent,既要处理订单查询,又要处理退换货,还要处理投诉建议。这三个场景的对话风格和工具集差异很大。拆成三个 Agent,每个专注一个领域,准确率会高很多。
5.2 用 Spring AI 实现 Agent 路由
最简单的多 Agent 架构是一个“路由 Agent”加多个“专家 Agent”。路由 Agent 只做一件事:判断用户的问题属于哪个领域,然后转发给对应的专家 Agent。
在 Spring AI 里,你可以定义多个ChatClientBean,每个配置不同的系统提示词和工具集。然后写一个RouterService,先用一个轻量级的模型调用做意图分类,再路由到对应的ChatClient。
@Service public class RouterService { private final ChatClient routerClient; private final Map<String, ChatClient> expertClients; public String route(String message) { String intent = routerClient.prompt() .user("判断以下用户问题的意图,只返回一个词:订单、退换货、投诉。问题:" + message) .call() .content(); ChatClient expert = expertClients.get(intent); if (expert == null) { return "抱歉,我暂时无法处理这个问题"; } return expert.prompt().user(message).call().content(); } }这个路由的准确率取决于分类提示词的质量。我试过用qwen-turbo做路由,响应快、成本低,准确率能到 90% 以上。路由错了也没关系,专家 Agent 可以返回“这个问题不属于我的职责范围”,然后路由回退到默认 Agent。
5.3 多 Agent 之间的状态共享与冲突处理
多 Agent 协作时,最大的问题是状态共享。比如用户先问了订单 Agent,又问了退换货 Agent,退换货 Agent 需要知道之前的订单号。如果每个 Agent 有独立的ChatMemory,信息就断了。
解决办法是用一个共享的ChatMemory,所有 Agent 都往同一个会话里写。但这样又会导致上下文膨胀。我的做法是:路由 Agent 负责维护一个“会话摘要”,把关键信息(订单号、用户 ID、问题类型)提取出来,作为元数据传给专家 Agent。专家 Agent 在自己的系统提示词里引用这些元数据。
冲突处理方面,如果两个 Agent 给出了矛盾的建议,以“更具体”的那个为准。比如订单 Agent 说“已发货”,退换货 Agent 说“未发货”,那以订单 Agent 为准,因为它直接查询了订单系统。
6. 性能优化与成本控制的实战技巧
6.1 减少不必要的模型调用
每一次模型调用都是钱和时间。能缓存的就缓存,能本地判断的就本地判断。比如用户问“你好”,不需要调模型,直接返回预设的问候语。用户问“订单 123”,如果订单号格式不对(比如包含字母),直接返回“订单号格式错误”,不需要调模型。
我一般会在 Agent 前面加一个“前置过滤器”,用正则表达式和关键词匹配处理掉 30% 的简单请求。剩下的 70% 再走模型。这样整体成本能降三成。
6.2 提示词压缩与 Token 优化
系统提示词不要写太长。我见过有人写了 2000 字的系统提示词,每次调用都带上,Token 消耗巨大。其实核心指令就那几条,用简洁的语言表达清楚就行。
工具描述也要精简。@Tool的description控制在 50 字以内,把关键信息说清楚就行。参数描述控制在 20 字以内。
对话历史方面,定期做摘要。比如每 10 轮对话,让模型生成一个摘要,然后用摘要替换掉原始对话。这样上下文长度能控制在合理范围内。
6.3 模型选型的性价比考量
不是所有任务都需要用最贵的模型。我的经验是:路由和意图分类用最便宜的模型(比如qwen-turbo),工具调用用中等模型(比如qwen-plus),最终回答生成用最好的模型(比如qwen-max)。这样组合下来,成本比全程用qwen-max低一半以上,效果差异用户基本感知不到。
另外,国内模型服务经常有免费额度或者新用户优惠。多注册几个平台,把不同任务分散到不同平台,既能降低成本,又能避免单点故障。
7. 个人实操体会与后续扩展方向
我在实际项目里踩过最大的坑,是低估了工具描述的重要性。一开始觉得“模型那么聪明,肯定能理解”,结果模型把“查物流”和“查订单”混着调。后来把每个工具的描述都当成 API 文档来写,准确率从 60% 提到了 95% 以上。所以如果你刚开始做 Agent,先把工具描述写好,比调模型参数管用得多。
另一个体会是:不要追求“全自动”。Agent 再智能,也有边界。在关键业务节点上,该加人工确认就加人工确认。比如退款金额超过 100 元,自动转人工审核。这样既享受了 Agent 的效率,又控制了风险。
后续如果想深入,可以看看 LangChain4j 的Easy RAG模块,把企业内部的文档知识库接进来,让 Agent 能回答“退货政策是什么”这类问题。也可以研究一下Spring AI Alibaba的图编排能力,把多个 Agent 串成工作流,处理更复杂的业务场景。Java 工程师做 AI Agent,优势不在算法,在工程。把工程化的事情做好,你就已经超过大部分只会写 Demo 的人了。