
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 groupIdorg.springframework.ai/groupId artifactIdspring-ai-alibaba-spring-boot-starter/artifactId version1.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) { ListLogisticsNode 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 MapString, 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 的人了。