2026/10/20 2:28:45

Spring Boot集成LangChain4j:构建生产级AI Agent的完整指南

Spring Boot集成LangChain4j:构建生产级AI Agent的完整指南 如果你是一个长期用 Spring Boot 写业务系统的后端最近大概率被各种大模型应用刷屏。我这两年陆续接了不少把 LLM 塞进业务线的需求第一反应和大多数人一样——先跑去 Python 生态试了一圈。LangChain 搭原型确实爽各类现成的模块拖进来就能跑。但等到要把原型落进现有 Java 工程时问题开始冒头独立的 Python 服务怎么跟现有账号体系对接怎么部署监控业务团队愿不愿意长期维护都是现实门槛。于是我开始认真研究 LangChain4j在 Spring Boot 里把整个 AI Agent 架构重新搭了一遍。这篇文章不吹不黑只聊实际操作。我假设你懂 Spring Boot 基础但对 AI Agent 的落地方式还有点模糊。你会看到我为什么决定“逃离 Python 舒适区”LangChain4j 适合解决哪类问题以及一个带工具调用、会话记忆和流式输出的 Agent 是怎么一步步跑起来的。整个过程中踩过的坑也会一并列出来。1. 逃离“Python 舒适区”这个标题背后到底在讲什么1.1 Python 之所以“舒适”是因为先发优势太明显大模型应用的开源生态几乎都从 Python 起步。模型推理框架、向量库客户端、智能体编排库、各种微调工具链全部是 Python 优先。尤其是写原型阶段一个文件往下堆代码就能调通模型接口实在顺畅。我见过很多后端团队因此在项目里塞了一个专门的 Python 服务只负责承接模型调用、做点 Prompt 拼接和工具调度。这种“业务 Java 模型 Python”的分布式组合原型阶段没有问题生产阶段却很麻烦。首先多一个语言就是多一套工程体系新的依赖管理、新的部署镜像、新的日志采集方式。其次Agent 的逻辑往往要跟业务数据打通比如查订单、读库存、调内部接口。数据都在 Java 侧Python 服务要跨语言去拿通信链路长字段要反复对调试成本不低。再往下走团队里 Java 开发者占了绝大多数他们去维护 Python 服务其实是赶鸭子上架。1.2 为什么 Spring Boot 团队需要第二方案Spring Boot 项目普遍有统一的分层结构、接口规范和运维通道。如果一个 AI Agent 只是被包装成独立服务它就得额外处理鉴权、限流、trace 透传、内部 RPC而这些能力在 Java 后端里都是现成的。与其让 Agent 逻辑游离在体系之外不如让 Agent 作为一个普通模块直接注册进 Spring 容器复用原有的一切基础设施。这才是 LangChain4j 吸引我的核心点它把大模型编排能力做成 Java 原生的类库同时提供 Spring Boot Starter。项目里加一个依赖就能在 Service 层里直接注入模型和 Agent不需要另起炉灶。那些原本写 Spring 的同事看这些代码也不会觉得陌生。1.3 LangChain4j 到底解决哪一类问题LangChain4j 的定位是“Java 版的 LLM 编排框架”。在它出现之前Java 开发者接入大模型基本是手工组装HttpClient 调接口、把历史消息拼成数组、解析 JSON 返回、自己写工具调用循环。这套逻辑不复杂但很碎而且每个项目都要重写一遍。LangChain4j 把这些重复工作收敛成统一 API同时补齐了结构化输出、会话窗口管理、工具方法注册、流式响应、RAG 集成等常用能力。它并不是简单翻译 LangChain而更像是“面向 Java 工程习惯的重新设计”。例如它提供了AiServices这样一个入口你只要定义接口、配置模型和工具就可以生成一个 Agent 实例。这种基于接口和注解的风格是 Java 开发者舒适区内就能理解的抽象方式。2. 先想清楚Agent 系统在架构上拆成几层2.1 Agent 的本质不是“调用一次模型”而是“带工具使用能力的循环”很多刚接触 Agent 的同学会把 Agent 理解成一个加强版聊天接口用户说一句模型回一句。实际上生产可用的 Agent 是一个循环系统。模型收到用户请求后并不总是能直接回答。比如用户问“我的订单到了吗”模型本身不知道订单状态它需要调用你的订单查询方法拿到结果再基于结果组织回答。这个过程可能还不止一轮查完订单后模型发现要补充物流信息又会再调一次物流接口。这个“模型提出工具调用意图 - 框架执行工具 - 把结果回传给模型 - 模型继续推理”的循环就是 Agent 的核心运行时。比起单次生成它更像是让模型带着一个待办清单工作边做边看直到得出最终结论。LangChain4j 的AiServices封装了这套循环我们只需要把工具定义好剩下的交给框架调度。2.2 我设计系统时的分层模型层 / Agent 编排层 / 工具层 / 记忆层落地到具体项目里我会把 Agent 相关代码拆成四层。第一层是模型层负责接入具体的模型服务可能是云端接口也可能是本地推理服务这一层只关注对话补全和流式能力。第二层是 Agent 编排层它不关心业务只负责告诉模型“你是谁、可以调用哪些工具、上下文窗口怎么维护”。第三层是工具层这是业务与被被模型连接的桥梁每一个方法都对应一个可以被模型发现并调用的能力。第四层是记忆层负责管理会话历史让多轮对话不至于失忆。这样的分层让我在替换模型、调整工具、改变提示词时都能做到局部修改。比如模型从通用大模型换成垂直领域模型只需要动模型层配置新增一个业务能力只需要在工具层加方法。Agent 编排层保持稳定。这比把所有逻辑堆在一个 Service 类里要清晰得多。2.3 为什么这些层适合用 LangChain4j 落地因为这几层正好能对应 LangChain4j 里的核心组件ChatLanguageModel是模型层AiServices是编排层Tool注解标记的工具类是工具层ChatMemory是记忆层。组件边界和架构分层天然对齐代码按层写下来不会有一团乱麻的感觉。另外一点LangChain4j 在 Spring Boot 里支持把模型实例、记忆实例定义为 Bean工具类也交给容器管理。也就是说分层不只是纸面设计而是真的能在 Spring 容器里各就各位需要哪个注入哪个。这一点对生产项目的可维护性帮助很大。3. Spring Boot 项目接入 LangChain4j 的完整实操3.1 依赖引入与最小工程结构先用 Maven 建一个标准的 Spring Boot 工程然后引入 LangChain4j 的 Spring Boot Starter。实际坐标是dev.langchain4j组下的langchain4j-spring-boot-starter另外根据你选的模型服务商引入对应的模块。我这里演示同时接入云端模型和本地模型所以额外加了两个依赖版本号以 Maven Central 的为准。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version1.0.0-beta2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version1.0.0-beta2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-ollama/artifactId version1.0.0-beta2/version /dependency包结构方面我建议按config、agent、tool、controller四个包来组织。config放模型和记忆的配置类agent放 Agent 接口和构建逻辑tool放工具类controller放暴露给前端的接口。这样一进来就能知道每个类的职责。3.2 模型接入配置从在线 API 到本地模型Spring Boot 里集成 LangChain4j 之后最简单的接入方式是利用自动配置属性。在线模型可以直接配置 API Key、模型名称和温度等参数。如果你的网络环境里需要通过网关访问把 base-url 换成网关地址即可。我习惯把这些信息放到环境变量里避免写死在配置文件中尤其是 api-key绝对不能进 Git 仓库。langchain4j: open-ai: chat-model: api-key: ${LLM_API_KEY} model-name: ${LLM_MODEL_NAME:gpt-4o-mini} temperature: 0.7 log-requests: true log-responses: true本地模型适合开发阶段调试或者对数据隐私要求更高的场景。Ollama 跑一个模型然后把 base-url 指向本机端口就行。langchain4j: ollama: chat-model: base-url: http://localhost:11434 model-name: qwen2.5 temperature: 0.7调试完成后别忘了把log-requests和log-responses关掉否则生产日志会被 Prompt 上下文刷爆。3.3 第一个能对话的 Agent配置好模型后接下来定义 Agent 入口。LangChain4j 的AiServices可以根据接口动态生成实现所以我不需要写具体的 Agent 类只需要定义接口。假设我要做一个“订单助手”只负责处理用户的订单相关问题public interface OrderAssistant { String chat(String userMessage); }构建 Agent 实例时把模型和工具传进去Configuration public class AgentConfig { Bean public OrderAssistant orderAssistant(ChatLanguageModel chatModel, OrderTool orderTool) { return AiServices.builder(OrderAssistant.class) .chatLanguageModel(chatModel) .tools(orderTool) .build(); } }AiServices.builder()会解析接口方法、扫描工具类上的注解并生成一个代理对象。默认情况下它不启用记忆每次调用都是无状态的独立请求。这一点后面专门说。3.4 把 Agent 暴露为 REST 接口为了给前端页面或者小程序提供能力我习惯用 Controller 包一层。接口只接收用户消息Agent 返回字符串。这样调用方不需要关心 Prompt、工具调用这些内部机制。RestController RequestMapping(/api/agent) public class AgentController { private final OrderAssistant orderAssistant; public AgentController(OrderAssistant orderAssistant) { this.orderAssistant orderAssistant; } PostMapping(/chat) public String chat(RequestBody ChatRequest request) { return orderAssistant.chat(request.message()); } }到这里一个能响应“帮我查一下订单 A123 的状态”的 Agent 就已经可以跑了。用户消息进来后模型会判断需要调用工具框架自动执行并生成最终回答。整个链路从代码上看非常简单核心逻辑都被框架封装起来。4. 把 Agent 做“活”工具调用、记忆与流式响应的细节4.1 让 Agent 会“动手”Tools 的定义与执行原理把业务能力暴露给模型就是在普通的 Spring Bean 方法上加Tool注解。LangChain4j 会把方法名、描述、参数结构发送给模型模型在需要时返回一个“我想调用这个方法”的信号框架就在本地执行它再把返回值塞回给模型继续生成回答。实际开发中工具描述写得好不好直接决定模型能不能正确调用。比如下面这个查询订单方法如果描述含糊模型就可能不知道该传什么参数。我的习惯是写清功能、参数含义和格式要求。Component public class OrderTool { Tool(根据订单号查询订单状态订单号格式为字母O开头后接数字如O12345) public String queryOrderStatus(String orderId) { // 调用业务服务查订单 return orderService.queryOrderStatus(orderId); } Tool(根据订单号查询物流轨迹参数为订单号) public String queryLogistics(String orderId) { return logisticsService.queryTrajectory(orderId); } }关于工具执行有个重要原理模型返回的不是结果而是“调用意图”和参数 JSON。真正执行工具的是本地 Java 代码。模型无法直接访问你的数据库它只能通过你的方法间接获得信息。所以工具的返回值必须是模型容易理解的内容最好是简洁的文本摘要或者结构清晰的 JSON避免一长串无意义的日志。我踩过的坑是一开始工具返回字段太细节模型被一堆内部状态码干扰回答反而变差。后来改成让工具方法直接返回面向用户的描述性文本效果立刻好转。4.2 记忆管理为什么默认无状态怎么加状态AiServices默认构建出的 Agent 不携带历史记忆。这意味着用户上一句说“帮我查订单 O123”下一句说“它的物流到哪了”模型会一头雾水因为它只看到了“它的物流到哪了”不知道“它”指代什么。要让 Agent 有上下文需要给它配置ChatMemory。LangChain4j 提供多种记忆实现常见的是按消息条数约束的MessageWindowChatMemory。它维护一个滑动窗口只保留最近 N 条消息避免 Token 无限增长。ChatMemory chatMemory MessageWindowChatMemory.builder() .maxMessages(20) .build();配置进 AgentAiServices.builder(OrderAssistant.class) .chatLanguageModel(chatModel) .chatMemory(chatMemory) .tools(orderTool) .build();这里有个必须重视的问题ChatMemory是按实例绑定的如果所有用户共用一个 Agent 实例那么 A 用户的消息会出现在 B 用户的上下文里这是严重的越权。生产环境必须按会话维度隔离记忆。我当时的方案是定义会话级别的记忆工厂根据请求头里的会话 ID 取对应的记忆实例ChatMemory chatMemory ChatMemoryProvider.persistent(sessionId);不同用户各持各的记忆互不干扰。记忆层也可以接外部存储比如把消息持久化到 Redis 或数据库服务重启后历史不丢。4.3 流式输出从等待到打字机效果同步接口简单直接但大模型生成需要几秒甚至更久。让前端一直等待白屏体验很差。LangChain4j 对这种情况提供了TokenStream流式接口把生成的 token 边生成边推送出去。定义流式接口时把返回类型换成TokenStreampublic interface StreamingOrderAssistant { TokenStream chat(String userMessage); }使用方式streamingOrderAssistant.chat(帮我查一下订单O123的物流) .onPartialResponse(token - emitToClient(token)) .onComplete(executionResult - emitDoneSignal()) .onError(exception - emitErrorSignal()) .start();onPartialResponse会收到一个个片段实时展示给用户onComplete表示生成结束onError负责异常兜底。最终实现前端打字机效果用户看到内容逐步出现等待感会大幅降低。这个模式最适合配合 WebFlux 或 SSE 使用。如果用的是传统 Tomcat 同步线程需要注意不能阻塞线程池。另外流式接口在并发场景下同样要做会话隔离确保每个用户拿到的历史上下文是正确的。5. 实践中的问题排查与避坑清单5.1 高频报错与修复速查表症状可能原因处理方式模型不调用工具直接编造答案工具描述不清晰重写Tool描述补充参数格式和适用场景工具执行了但回答仍然不正确工具返回内容过于复杂精简返回值让模型只拿到关键信息上下文超限报错记忆窗口过大或消息过期调小maxMessages或换用按 Token 数控制的记忆实现多用户互相串话多个会话共用同一个ChatMemory按 sessionId 创建独立记忆流式接口没有输出忘记调用start()调用流式 API 后必须显式start()本地模型工具调用效果差模型能力偏弱换用支持工具调用的更强模型或简化工具数量日志里出现大量 Prompt 内容打开了请求日志生产环境关闭log-requests/log-responses5.2 工具调用失效的排查思路工具调用是 Agent 最容易出问题的地方而且问题往往不是程序报错而是逻辑悄悄出错。比如用户问“哪个订单还没发货”模型没有调用查单工具而是根据常识瞎答。这时候不要急着改代码先从三层去排查。第一层是模型配置确认当前模型供应商是否支持 function calling。第二层是工具描述把描述改成更贴合用户意图的话术。第三层是参数解析检查工具方法参数类型和模型返回的 JSON 是否匹配。我遇到过模型把数字参数当字符串传、导致框架反序列化失败的情况解决办法是把参数描述写得明确比如“status 参数仅接受枚举值 PENDING、SHIPPED、COMPLETED”。另外要说一个容易忽视的点工具数量不是越多越好。模型理解十几个工具名称和参数说明会消耗 Token并且增加误选概率。如果某个工具很少被用到就把它从 Agent 的工具列表中去掉而不是一直挂在上面。5.3 与“手写 HttpClient JSON 解析”相比LangChain4j 帮我省掉了什么我很长一段时间是用原生 HTTP 方式写大模型接入的对那段代码的繁琐深有体会。组装请求头、拼接历史消息、处理流式返回的 SSE 格式、自己维护工具调用轮询这些工作每个项目都要重新写一遍而且大多与业务逻辑无关。LangChain4j 把这些细节封装后业务代码的注意力回到了“Agent 能做什么”而不是“怎么让模型听指令”。框架还处理了不同模型服务商之间的差异比如兼容 OpenAI 协议的模型都能通过langchain4j-open-ai模块接入换服务商主要是改配置而不是重写代码。对我这种以业务交付为导向的人来说省下来的时间可以花在真正的业务规则梳理上。6. 落地到生产前我还会再做的几件事Agent 在本地跑通只是第一步真正让它变成生产能力还有一堆事情要做。就我实际推进项目的经验下面这几件事优先级最高。第一是接入完整的日志和链路追踪。大模型接口比较慢一旦用户反馈异常你得能还原当时的 Prompt、工具调用结果和模型回答。可以把一次 Agent 会话的完整流程封装成一条 trace记录每一轮调用和耗时。这样排查问题不再靠猜。第二是给 Agent 加上权限控制和成本控制。用户能问什么、工具能调什么必须在一个明确边界内。比如查询订单接口要校验当前用户是不是订单归属人不能因为模型拿到了方法就绕过权限模型。成本方面可以统计每个会话的 Token 消耗设置单用户预算。第三是要把模型的输出当成不稳定的输入来对待。模型可能返回格式错误的 JSON、自相矛盾的信息甚至在没有把握时给出看似笃定的回答。我在面向用户的场景里会加一道校验层关键字段必须匹配格式否则让模型重新推断或者友好地表示无法回答。最后说一句个人体会。从 Python 迁徙到 LangChain4j 并不是因为哪边更高级而是工程现实决定的。你的团队、技术栈、运维体系落在哪里Agent 就应该贴合哪里生长。Java 生态里做大模型应用不再是“凑合用”的状态LangChain4j 已经给出了相当顺手的一套框架抽象。希望你也能拿着这篇文章的思路在 Spring Boot 项目里搭出第一个真正能落地的 Agent然后用它去解决实际业务问题。过程中遇到奇怪的问题可以按上面的排查思路逐层拆开看大多数时候答案都在模型、工具、记忆这三个环节里。