ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java后端工程师如何用LangChain4j与Spring AI落地AI Agent

Java后端工程师如何用LangChain4j与Spring AI落地AI Agent Java 后端开发这几年最明显的变化不是某个框架的版本号又跳了一格而是招聘 JD 里开始频繁出现“熟悉 AI Agent 开发”“有大模型应用落地经验优先”这类描述。很多写了五六年 CRUD 的工程师第一反应是这玩意儿跟我有啥关系我又不搞算法。但实际情况恰恰相反——AI Agent 的工程化落地拼的根本不是模型训练能力而是后端工程师最擅长的那套东西服务编排、状态管理、异常重试、并发控制、接口抽象。LangChain4j 和 Spring AI 这两个框架的出现本质上就是把 Agent 的开发范式拉回到了 Java 工程师熟悉的舒适区。这篇内容面向的是有 Java 基础、想切入 AI Agent 方向但不知道从哪下手的后端开发。我会从 Agent 的核心运行原理讲起把 ReAct 模式拆开揉碎然后落到 LangChain4j 和 Spring AI 的具体代码实现最后聊并发、RAG、工具调用这些真正上生产才会遇到的问题。不堆概念只讲能跑起来的东西。1. 先搞清楚 AI Agent 到底比普通接口调用多了什么1.1 从“一问一答”到“自主决策循环”的本质差异大部分人第一次接触大模型都是通过一个简单的 HTTP 接口传一段 prompt 进去拿一段回复出来。这本质上跟调用一个普通的 REST API 没有区别——输入确定输出确定中间没有决策过程。但 Agent 不一样它的核心特征是多轮自主决策给定一个目标它会自己判断下一步该做什么、该调用哪个工具、拿到结果后是否继续、什么时候停止。举个具体的例子。你让普通接口“帮我查一下北京今天的天气”它只能根据训练数据瞎编一个答案。但如果你给 Agent 配备了天气查询工具它会这样运转先理解你的意图是查天气然后决定调用天气 API传入“北京”和“今天”两个参数拿到返回结果后组织成自然语言回复你。这个“理解意图→选择工具→构造参数→执行→整合结果”的循环就是 Agent 的最小工作单元。用后端工程师熟悉的话来说普通接口调用是同步的一问一答Agent 是带状态机的异步任务编排。你之前写过的那些工作流引擎、状态机、责任链模式在这里全部用得上。1.2 ReAct 模式Agent 的“思考-行动”循环到底怎么转ReActReasoning Acting是目前最主流的 Agent 运行范式它的核心思想非常朴素让模型在每一步都先输出一段“思考”再输出一个“行动”然后根据行动的结果继续下一轮思考。整个循环长这样Thought思考模型分析当前状态判断需要做什么Action行动模型选择一个工具并给出调用参数Observation观察系统执行工具把结果返回给模型重复 1-3直到模型认为任务完成输出 Final Answer这个循环用伪代码表示大概是这样while (!taskCompleted) { String thought llm.reason(context); ToolCall action llm.selectAction(thought); if (action null) { return llm.finalAnswer(context); } String observation toolExecutor.execute(action); context.append(thought, action, observation); }看起来简单但工程上的坑全在细节里。比如模型可能陷入死循环反复调用同一个工具模型可能构造出格式错误的参数导致工具执行抛异常模型可能在拿到足够信息后仍然不停止。这些问题在后面讲 LangChain4j 实现时会具体展开。1.3 Java 工程师做 Agent 的天然优势在哪我见过不少 Java 后端转 AI 方向时特别不自信觉得自己不懂 PyTorch、没读过 Transformer 论文做不了这个。但实际上Agent 开发中真正难的部分跟深度学习关系不大。Agent 落地要解决的核心问题包括工具调用的参数校验和异常处理、多轮对话的上下文管理和截断策略、并发场景下的会话隔离、外部 API 调用的超时和重试、RAG 检索的向量库选型和性能调优。这些东西哪一个不是后端工程师天天在干的活你写过的 Feign 客户端、Hystrix 熔断、Redis 会话缓存、MyBatis 分页查询换个场景就是 Agent 的基础设施。LangChain4j 和 Spring AI 之所以在 Java 圈火起来就是因为它们把 Agent 的抽象层做得跟 Spring 的编程模型高度一致——注解式声明工具、依赖注入管理组件、AOP 处理横切逻辑。你不需要学新语言不需要换技术栈用现有的 Java 工程能力就能把 Agent 搭起来。2. LangChain4j 和 Spring AI 的选型逻辑与核心抽象2.1 两个框架的定位差异一个偏底层灵活一个偏生态整合LangChain4j 和 Spring AI 经常被拿来比较但它们的设计哲学其实不太一样。LangChain4j 更像是一个独立的 Agent 开发工具包它不依赖 Spring 容器你可以把它用在任何 Java 项目里甚至是一个简单的 main 方法。它的抽象层次更贴近“我需要什么就组装什么”灵活度高但需要自己管理组件的生命周期。Spring AI 则是深度绑定 Spring 生态的产物。它的核心优势在于如果你已经在用 Spring Boot 写业务系统引入 Spring AI 几乎零成本——自动配置、依赖注入、Actuator 监控、配置中心全部无缝衔接。它的 API 设计也更有“Spring 味”比如用Tool注解声明工具用ChatClient链式调用构建对话。选型上我的建议很直接新项目且技术栈是 Spring Boot优先 Spring AI需要嵌入到非 Spring 的老系统或者需要更细粒度控制 Agent 循环选 LangChain4j。两者并不互斥LangChain4j 的一些组件比如向量库集成在 Spring AI 里也有对应实现概念是相通的。对比维度LangChain4jSpring AI容器依赖无可独立运行强依赖 Spring 容器工具声明方式接口 注解Tool注解 方法对话记忆ChatMemory接口ChatMemory 自动配置RAG 支持内置多种向量库内置多种向量库学习曲线中等需自己组装低Spring 开发者友好适合场景独立 Agent 服务、嵌入式Spring Boot 业务系统集成2.2 工具调用Function Calling的底层机制Agent 能“干活”的关键在于工具调用。不管哪个框架底层机制都是一样的把 Java 方法的签名转换成模型能理解的 JSON Schema模型返回一个结构化的调用请求框架再反射调用对应的方法。以 LangChain4j 为例你定义一个工具类public class WeatherTool { Tool(查询指定城市的天气) public String getWeather(P(城市名称) String city) { // 实际调用天气 API return weatherApi.query(city); } }框架在运行时会把这个方法转换成类似这样的描述传给模型{ name: getWeather, description: 查询指定城市的天气, parameters: { type: object, properties: { city: { type: string, description: 城市名称 } }, required: [city] } }模型看到这个描述后如果判断需要查天气就会返回一个tool_call里面包含方法名和参数。框架解析后反射调用getWeather(北京)把返回值作为 Observation 塞回上下文。这里有个容易被忽略的细节方法的 description 和参数的 description 直接决定了模型能不能正确调用工具。我踩过的坑是工具方法名叫query描述写的是“查询数据”结果模型根本不知道什么时候该用它。后来改成queryOrderStatus描述写“根据订单号查询订单的当前状态返回待支付/已支付/已发货/已完成”调用准确率立刻上来了。工具描述要写得像给一个新同事解释这个方法是干嘛的越具体越好。2.3 对话记忆ChatMemory的实现与陷阱Agent 的多轮对话依赖记忆机制。LangChain4j 和 Spring AI 都提供了ChatMemory抽象核心逻辑是维护一个消息列表每次调用模型时把历史消息一起传进去。最简单的实现是MessageWindowChatMemory它只保留最近 N 条消息。但这里有个陷阱如果 Agent 执行了很多轮工具调用消息列表会迅速膨胀。一次完整的 ReAct 循环可能产生 10 条以上的消息用户输入、模型思考、工具调用、工具结果、模型再思考……如果窗口设成 20可能两轮任务就把窗口占满了导致早期的关键信息被挤掉。我的做法是分层管理系统提示词和用户原始问题永远保留中间的思考-行动-观察过程按需截断。LangChain4j 允许你自定义ChatMemory实现我通常会写一个TokenWindowChatMemory按 token 数而不是消息条数来截断并且给系统消息和用户首条消息设置“不可驱逐”标记。public class CustomChatMemory implements ChatMemory { private final ListChatMessage messages new ArrayList(); private final int maxTokens; Override public void add(ChatMessage message) { messages.add(message); trimIfNeeded(); } private void trimIfNeeded() { // 保留系统消息和首条用户消息从中间开始驱逐 while (countTokens(messages) maxTokens) { // 找到第一条可驱逐的消息并移除 } } }3. 用 LangChain4j 搭一个能跑起来的 Agent3.1 最小可运行 Demo 的依赖与配置先看依赖。LangChain4j 的模块化做得比较细你需要什么就引什么dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency如果你用的是国产模型比如通义千问、DeepSeekLangChain4j 也有对应的集成模块或者用 OpenAI 兼容协议接入。配置上核心就是三样东西API Key、Base URL、模型名称。ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(API_KEY)) .baseUrl(https://your-api-endpoint/v1) .modelName(qwen-plus) .temperature(0.7) .timeout(Duration.ofSeconds(60)) .build();这里timeout一定要设。Agent 场景下模型调用可能涉及多轮默认超时往往不够但设太长又会拖垮整个请求链路。我的经验值是单次模型调用 60 秒整个 Agent 任务总超时 5 分钟超过就中断并返回已完成的部分。3.2 定义工具并接入 Agent 执行链有了模型之后定义工具并组装 Agentpublic interface Assistant { String chat(String userMessage); } WeatherTool weatherTool new WeatherTool(); OrderTool orderTool new OrderTool(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(weatherTool, orderTool) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); String answer assistant.chat(帮我查一下北京天气然后看看我订单12345的状态);AiServices是 LangChain4j 的核心入口它用动态代理把接口方法转换成 Agent 调用。当你调用chat方法时框架会自动把用户消息加入记忆、把工具描述传给模型、进入 ReAct 循环、执行工具调用、整合结果返回。实测下来这个最小 Demo 在工具数量少于 5 个、任务步骤少于 3 步的场景下表现很稳。但一旦工具数量上去、任务变复杂就需要做额外优化后面会讲。3.3 工具执行失败的兜底策略工具执行失败是必然会发生的事——外部 API 超时、参数格式错误、返回结果为空各种情况都有。如果不在框架层面做兜底模型会收到一个异常堆栈然后大概率陷入“重试同一个错误调用”的死循环。我的做法是在工具方法内部就把异常消化掉返回一个结构化的错误信息Tool(根据订单号查询订单状态) public String queryOrderStatus(P(订单号) String orderId) { try { Order order orderService.getById(orderId); if (order null) { return 未找到订单号为 orderId 的订单请确认订单号是否正确; } return 订单状态 order.getStatus(); } catch (Exception e) { log.error(查询订单失败, e); return 查询订单时发生系统错误请稍后重试; } }关键在于返回给模型的是自然语言的错误描述而不是异常堆栈。模型看到“未找到订单”这样的描述会自然地调整策略比如询问用户确认订单号而不是反复重试。这个技巧在 ReAct 模式下特别重要因为模型对自然语言的理解远好于对错误码的理解。4. Spring AI 的工程化落地从配置到生产4.1 Spring Boot 项目中的自动配置与 ChatClientSpring AI 最大的卖点就是跟 Spring Boot 的无缝集成。引入 starter 之后大部分配置都可以放在application.yml里spring: ai: openai: api-key: ${API_KEY} base-url: https://your-api-endpoint chat: options: model: qwen-plus temperature: 0.7然后在代码里直接注入ChatClientService public class AgentService { private final ChatClient chatClient; public AgentService(ChatClient.Builder builder, OrderTool orderTool) { this.chatClient builder .defaultSystem(你是一个订单助手帮助用户查询和处理订单) .defaultTools(orderTool) .build(); } public String handle(String message) { return chatClient.prompt() .user(message) .call() .content(); } }ChatClient的链式 API 设计得很顺手prompt().user().call().content()这套写法比 LangChain4j 的接口代理模式更直观尤其是需要动态调整系统提示词或工具集的场景。4.2 用 Tool 注解声明工具的最佳实践Spring AI 的工具声明用Tool注解跟 LangChain4j 类似但更简洁Component public class OrderTool { Tool(description 根据订单号查询订单的当前状态) public String queryOrderStatus( ToolParam(description 订单号格式为纯数字) String orderId) { // ... } }这里有个 Spring AI 特有的优势工具类本身就是一个 Spring Bean可以直接注入OrderService、RedisTemplate等任何依赖。这意味着你现有的业务逻辑可以几乎零改造地暴露成 Agent 工具。但要注意一个坑工具方法的返回值会被序列化成字符串传给模型如果返回的是一个复杂的 Java 对象默认的 JSON 序列化可能产生大量冗余字段。我建议工具方法直接返回精简后的字符串或者用一个专门的 DTO 控制序列化字段。4.3 多模型切换与配置隔离生产环境往往需要同时对接多个模型——比如用便宜的小模型处理简单意图识别用大模型处理复杂推理。Spring AI 支持通过配置多个ChatClientBean 来实现Configuration public class ModelConfig { Bean(fastClient) public ChatClient fastClient(ChatModel fastModel) { return ChatClient.builder(fastModel).build(); } Bean(smartClient) public ChatClient smartClient(ChatModel smartModel) { return ChatClient.builder(smartModel).build(); } }然后在业务代码里按需注入。这种隔离方式比在代码里硬编码模型名称要干净得多也方便后续做灰度切换。5. 上生产才会遇到的并发与性能问题5.1 Agent 请求的并发模型与线程安全这是 Java 工程师最关心的问题也是面试里高频出现的“AI Agent 怎么扛并发”。先说结论Agent 服务的并发瓶颈不在模型调用本身而在会话状态管理和工具执行。模型调用通常是 HTTP 请求天然支持并发你只需要控制好连接池大小和超时。真正麻烦的是ChatMemory——如果你把会话状态存在 JVM 内存里多线程访问必然出问题。LangChain4j 的MessageWindowChatMemory默认不是线程安全的多个请求同时操作同一个会话会导致消息错乱。解决方案有两个方向一是每个会话独立实例用ConcurrentHashMap管理会话 ID 到 Memory 的映射二是把会话状态外置到 Redis。前者适合单机部署后者适合多实例水平扩展。Service public class SessionManager { private final ConcurrentHashMapString, ChatMemory sessions new ConcurrentHashMap(); public ChatMemory getOrCreate(String sessionId) { return sessions.computeIfAbsent(sessionId, id - MessageWindowChatMemory.withMaxMessages(20)); } }如果要用 Redis 存储需要自己实现ChatMemory接口把消息列表序列化后存到 Redis List 里。注意序列化时要保留消息类型UserMessage、AiMessage、ToolExecutionResultMessage否则反序列化后框架无法正确识别。5.2 工具调用的超时控制与熔断Agent 执行过程中可能调用多个外部工具任何一个工具卡住都会拖垮整个请求。必须给每个工具调用设置独立的超时并且在整个 Agent 任务层面设置总超时。我的做法是用CompletableFuture包装工具执行public String executeWithTimeout(ToolCall call, Duration timeout) { CompletableFutureString future CompletableFuture.supplyAsync( () - toolExecutor.execute(call), toolExecutorPool); try { return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); return 工具执行超时请稍后重试; } }工具执行线程池要跟 Web 请求线程池隔离避免工具阻塞把 Tomcat 线程占满。线程池大小根据工具的平均耗时和 QPS 来算一般 10-20 个核心线程够用。5.3 流式输出与前端交互的配合Agent 的响应时间通常比普通接口长用户等 10 秒才看到结果体验很差。流式输出SSE是标配。Spring AI 和 LangChain4j 都支持流式返回GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }但流式输出跟工具调用结合时有个问题工具执行阶段是没有内容输出的用户会看到一段空白。我的处理方式是在工具调用前后插入状态提示比如“正在查询订单信息……”让用户知道系统在工作。Spring AI 的流式 API 允许你在工具调用回调里发送自定义事件前端根据事件类型展示不同的加载状态。6. RAG 与多路召回在 Agent 中的实际应用6.1 为什么 Agent 需要 RAG模型的知识有截止日期而且不知道你公司的内部文档。RAG检索增强生成解决的就是这个问题先把相关文档检索出来作为上下文塞给模型让模型基于这些文档回答。在 Agent 场景下RAG 通常作为一个工具存在。用户问“我们公司的报销标准是什么”Agent 判断需要查内部知识库调用 RAG 工具检索相关文档片段然后基于检索结果生成回答。6.2 LangChain4j 的 Easy RAG 与多路召回LangChain4j 提供了EasyRAG模块几行代码就能搭一个基础 RAGEmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); EmbeddingModel embeddingModel new OpenAiEmbeddingModel(...); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .embeddingModel(embeddingModel) .embeddingStore(store) .build(); ingestor.ingest(Document.fromFile(knowledge.pdf)); ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.7) .build();但单路向量检索在专业领域效果往往不够。多路召回的思路是同时用多种检索策略向量检索、关键词检索、BM25然后合并去重。LangChain4j 支持自定义ContentRetriever你可以组合多个检索器ContentRetriever hybridRetriever new HybridContentRetriever( vectorRetriever, // 向量检索 keywordRetriever // 关键词检索 );实测下来多路召回在专业术语密集的场景下召回率能提升 20% 以上。代价是检索耗时增加需要根据场景权衡。6.3 检索结果的重排序与上下文压缩检索出来的文档片段往往包含大量无关内容直接塞给模型会浪费 token 且干扰判断。两个优化手段重排序Rerank和上下文压缩。重排序是用一个专门的模型对检索结果重新打分把最相关的排前面。LangChain4j 支持接入 Cohere Rerank 等重排序服务。上下文压缩则是用模型对每个片段做摘要只保留跟问题相关的部分。这两个手段都能显著提升回答质量但都会增加延迟建议在离线评估确认收益后再上线。7. 从 Demo 到生产的几个关键决策7.1 会话状态存哪里内存、Redis 还是数据库小规模用内存多实例用 Redis需要审计和回溯用数据库。我的建议是生产环境直接用 Redis因为 Agent 会话的读写频率高、数据量不大、对持久化要求不高Redis 的 List 或 Hash 结构刚好合适。数据库可以作为异步落库用于审计但不作为主存储。7.2 工具数量膨胀后的路由策略当工具数量超过 10 个把所有工具描述都塞给模型会导致两个问题token 消耗大、模型选择准确率下降。解决方案是工具分组 意图路由先用一个小模型判断用户意图属于哪个领域然后只加载该领域的工具。public ListTool routeTools(String userMessage) { String domain intentClassifier.classify(userMessage); return toolRegistry.getToolsByDomain(domain); }这个思路跟微服务里的 API 网关路由是一个道理本质上是把“全量选择”变成“先分类再选择”。7.3 可观测性日志、指标与链路追踪Agent 的调试比普通接口难得多因为中间经过了多轮模型调用和工具执行。必须做好可观测性每次模型调用的输入输出、每次工具调用的参数和结果、整个 Agent 任务的耗时分解全部要打日志。指标方面重点关注模型调用 P99 延迟、工具调用失败率、Agent 任务平均轮数、token 消耗量。链路追踪可以用 Micrometer OpenTelemetry把 Agent 任务作为一个 Span内部的模型调用和工具调用作为子 Span。这样出问题时能快速定位是模型慢还是工具慢。8. 一些踩过的坑和实际体会工具描述写得太抽象是新手最容易犯的错。我见过有人写“处理数据”模型完全不知道什么时候该调用。描述要具体到“输入什么、输出什么、什么场景用”。ReAct 循环一定要设最大轮数限制。我遇到过模型反复调用同一个工具十几次的情况最后是加了maxIterations(10)才止住。超过轮数就强制返回当前已有信息并提示用户任务未完成。流式输出和工具调用同时使用时前端要做好状态管理。用户看到的应该是“思考中→调用工具→生成回答”这样的过程而不是一段长时间的空白。模型选择上不要迷信大模型。意图识别、参数提取这类任务小模型完全够用成本只有大模型的十分之一。把大模型留给真正需要复杂推理的环节。最后说一个实际体会Agent 项目的复杂度不在模型而在工程。你把工具调用的异常处理、会话的并发控制、超时熔断这些基础设施搭好了换任何模型都能跑。这些恰恰是 Java 工程师积累多年的东西。所以转型这件事与其说是学 AI不如说是把后端工程能力迁移到一个新场景。
返回列表