☰
Java开发者如何用LangChain4j在Spring Boot中集成大模型
2026/10/14 2:42:14 网站建设 项目流程

今年上半年我们团队接了一个内部知识库问答的需求,技术栈是Java 17 + Spring Boot 3,整个组没有专职的Python开发。调研了一圈,发现很多LLM应用框架都是围绕Python生态做的,硬塞进来不仅维护成本高,而且和我们现有的接口体系很难融合。后来看到LangChain4j这个项目,名字起得非常直白——给Java开发者的LLM应用开发框架,更难得的是它对Spring Boot做了单独的starter适配。这里先给结论:如果你的项目是Spring Boot工程,想在大模型接入这件事上少走弯路,LangChain4j是目前Java侧最顺滑的选择。这篇文章适合两类人:一是想在现有业务里调用大模型、但不想搅乱Java工程结构的后端开发;二是被各种Python示例绕晕,想找一套能在JVM里稳定落地的方案的人。我会把它的核心设计拆开讲,再给出一份可以直接复制的依赖和代码。

1. 先把“为什么选它”这件事想清楚

1.1 Java生态缺的不是API,而是抽象

很多团队接入大模型的第一反应是直接用底层SDK写一个HTTP调用,注册个API Key,然后就没然后了。这样确实能满足一次简单的对话需求,但一旦你需要在业务代码里高频调用、切换不同厂商的模型、保留多轮上下文、让模型调用你的内部方法,代码就会迅速失控。你会在每个Service里各写一套解析逻辑,换模型时改几十处,这跟当年Java后端遍地写JDBC样板代码是一个道理。

LangChain4j的切入点就是“抽象”这两个字。它把大模型调用这件事拆成了几个稳定的接口:ChatLanguageModel负责底层对话,AiService负责把接口方法映射成Prompt和模型调用,Memory管理会话上下文,Tool让模型能调用你的业务方法。这些概念在Spring Boot starter的加持下,会全部变成容器里的Bean,配置写在application.yml里,注入方式跟你平时注入一个Mapper没有区别。这套设计思路非常“Java”:先定接口,再谈实现,细节全部下沉到框架和配置层。

1.2 它和原生SDK、Python版LLM框架差在哪

我用一张表把常见的对比整理出来了,方便你评估:

方案能做什么主要痛点
直接调OpenAI等厂商SDK最基本的请求/响应记忆、工具调用、多厂商切换都要自己拼
自己封装HTTP客户端完全可控开发量大,Prompt管理混乱
Python生态的LangChain功能强大,社区资源多Java团队引入Python服务成本高,链路变长
LangChain4j专为JVM设计,天然融合Spring中文资料相对少,部分高级能力还在迭代

Python版的LangChain功能确实丰富,但它是为Python的生态习惯设计的,Java语言并没有官方的完整对应实现。LangChain4j并不是对Python版做一比一的迁移,而是严格按照JVM的语言习惯重新设计了一套抽象,所以你在使用过程中会发现它到处都是接口、Builder、注解,这是典型的Java框架味道。我自己试下来的整体感受是:该给的工具都给了,不该管的也不越界,用起来比想象中克制。

1.3 什么场景最适合用它

这个框架最适合的业务场景有三类:企业内部知识库问答、客服工单的智能分类/回复草稿、以及把大模型能力嵌入到已有业务流程中做辅助决策。这三类场景有一个共同点:它们都需要和现有系统深度联动,而不是仅仅提供一个聊天窗口。如果你是做一个演示用的独立对话页面,随便找个官方SDK就够了;但只要你需要把模型结果接入业务表、触发后续流程、读取用户历史上下文,LangChain4j的价值就会立刻体现出来。

2. 上手前必须理解的三个核心抽象

2.1 聊天模型:最底层的请求出口

你可以把ChatLanguageModel理解成数据库里的Connection,或者Spring里的JdbcTemplate。它是一个无状态的底层组件,负责把用户的消息发给模型,再把结果拿回来。

它在LangChain4j整个体系中是最基础的一层,几乎所有高级能力最终都要落到这个组件上。它的接口很简单,核心方法就是generate。如果你临时想做一个最简单的对话,可以直接注入ChatLanguageModel,然后调用model.generate("你好")拿到字符串结果。但真实业务里一般不直接用它,因为单靠这个类,你要手动管理上下文、手动处理工具调用结果,麻烦事儿全暴露在业务层了。

需要注意一点:starter会自动配置一个ChatLanguageModel的Bean,前提是你在配置文件中给出了必要的参数,比如API Key、模型名。如果你的项目里同时配了多个模型的依赖,记得指定默认模型,否则启动时会因为多个候选Bean而报错。

2.2 AI服务:把LLM遮起来,按接口编程

真正值得重点学习的是AiService,它解决的是“LLM调用怎么融入业务代码”的问题。它背后的思路和Spring Data非常像:你定义一个接口,写上方法签名,框架自动帮你实现,你只管调用。

比如定义一个接口:

@RegisterAiService public interface GreetingAssistant { @SystemMessage("你是一个友善的助手,回答尽量简洁。") String chat(@UserMessage String message); }

在容器里注入这个接口,直接调用chat方法,LangChain4j就会在运行时生成一个代理对象,把方法参数、方法上的注解拼成完整的Prompt,发给模型,再把模型返回的Text转成你需要的类型交给业务层。对调用方来说,它真的就是一个普通接口,你可以在Controller里直接注入使用。

这种设计的好处非常明显:替换底层模型厂商不需要改业务代码,你只要换配置;Prompt变成了方法上的注解,好维护;而且接口本身就能被Spring的AOP、事务机制兼容,这在业务集成时尤其重要。

2.3 工具调用:让模型学会使用你的方法

大模型本身只是一个文本生成器,它不知道天气、订单数据、库存状态这些实时信息。但LangChain4j提供了一个机制:把你写好的Java方法描述给模型,模型在回答用户问题时,如果判断需要这些数据,会先“请求”调用你的方法,然后再基于方法返回值组织最终答案。这个机制叫工具调用,在注解层面就是@Tool。

@Component public class WeatherTool { @Tool("根据城市名获取当前天气") public String getWeather(String city) { // 这里可以调用你公司内部的天气服务 return city + ":晴,气温26摄氏度"; } }

然后在AiService上声明使用这个工具:

@RegisterAiService(tools = WeatherTool.class) public interface WeatherAssistant { String chat(@UserMessage String message); }

整个过程框架帮你封装好了:模型返回一个“我要调用方法A,参数是B”的指令,LangChain4j用反射调用你的Java方法,把返回值再塞回给模型生成最终结果。这一步理解透了,后面做复杂业务就不会发怵。

3. 实战:从零搭建一个Spring Boot + LangChain4j项目

3.1 依赖引入和工程选择

建议直接在Spring Initializr里创建Spring Boot 3.x的工程,Java版本选17或者21。然后往pom.xml里加两个依赖,一个是对Spring Boot的自动装配支持,一个是对具体模型厂商的实现:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> <version>1.0.0-beta1</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>1.0.0-beta1</version> </dependency>

版本号建议去官方GitHub仓库看一眼最新release,不同大版本之间的配置属性名会有一点差异。如果你的模型是通过OpenAI兼容接口访问的,用这两个依赖就能覆盖大多数情况。如果公司内部走的是私有化模型,且提供了兼容接口,也可以继续用OpenAI这个包,只要改一下base-url即可。

3.2 最小可用配置与第一个对话

在application.yml里做如下配置:

langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY} model-name: gpt-4o-mini temperature: 0.7 timeout: 60s log-requests: true log-responses: true

注意,api-key我建议从环境变量里读,不要写死在配置文件里。log-requests和log-responses是调试阶段特别好用的开关,它会打印请求和响应的完整内容,排查Prompt写错问题时能省大量时间。

然后写一个最基础的Controller验证链路:

@RestController public class ChatController { private final ChatLanguageModel chatLanguageModel; public ChatController(ChatLanguageModel chatLanguageModel) { this.chatLanguageModel = chatLanguageModel; } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatLanguageModel.generate(message); } }

启动Spring Boot应用,访问/chat?message=你好,如果配置没问题,你会收到模型返回的文本。走到这一步,你的Spring Boot工程已经具备基本的大模型对话能力,后面的工作都是在这个基础上做增强。

3.3 补充一点:本地模型的接入

不少团队出于数据安全考虑,没法直接用云端API,这时候可以考虑本地部署的开源模型,配合LangChain4j的Ollama模块接入。依赖换成:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>1.0.0-beta1</version> </dependency>

配置改成:

langchain4j: ollama: chat-model: base-url: http://localhost:11434 model-name: llama3.2:3b temperature: 0.7

只要你本地把模型拉起来,链路一样能通。这也是LangChain4j做得好的地方:你在业务接口里不用关心背后是云模型还是本地模型,切换成本基本只是改依赖和两行配置。

4. 更实用的两种进阶玩法

4.1 结构化输出:让模型按你的POJO吐结果

很多业务场景不需要模型返回一大段散文,我们需要的是一个能被程序直接消费的对象。比如查询天气,你需要温度、天气状况、建议这几个字段,然后拿去渲染页面或者落库保存。

虽然你可以写一大段Prompt让模型“返回JSON”,但解析JSON、容错、字段映射这些事还得自己写。LangChain4j的AiService支持把返回类型直接定义成POJO,框架会负责把模型输出转换成指定类型。

public record WeatherReport( String city, int temperature, String condition, String advice ) {}

接口定义:

@RegisterAiService public interface WeatherAssistant { @SystemMessage("你是一个天气播报助手。") @UserMessage("请帮我查询{{city}}今天的天气,并整理成报告。") WeatherReport reportWeather(@V("city") String city); }

调用方拿到的是已经类型安全的WeatherReport对象,字段名、类型都由编译器保证了。接口里的{{city}}是模板占位符,@V("city")负责把方法参数绑定到占位符上。

这里有个实操要点:模型返回的字段名不一定和你定义的POJO字段完全一致,尤其是字段特别多的时候。我的习惯是在@UserMessage里主动加上一句“字段命名严格遵循中文含义对应的英文单词”,降低转换失败的几率。如果发现解析一直失败,可以先查看日志里模型实际返回的JSON内容,再调整字段名或补充说明。

4.2 工具调用落地:做一个天气查询助手

我们继续拿天气助手做例子,这次让模型真实调用一个工具方法,而不是凭空生成数据。这个场景比结构化输出更接近真实业务,因为真实业务中天气数据一定来自外部接口或数据库。

定义一个工具组件:

@Component public class WeatherTool { @Tool("根据城市名获取实时天气,参数城市名使用中文") public String getRealWeather(String city) { // 这里替换成真实业务接口调用 return "当前城市:" + city + ",晴,气温26度,湿度50%"; } }

然后定义使用该工具的AI服务:

@RegisterAiService(tools = WeatherTool.class) public interface WeatherAssistant { String chat(@UserMessage String message); }

测试一下,当用户输入“北京天气怎么样”时,LangChain4j的运行流程是这样的:

  1. 用户消息传到模型,模型判断需要天气工具。
  2. 框架根据@Tool描述,自动调用getRealWeather("北京")。
  3. 工具返回值被框架回传给模型。
  4. 模型基于工具返回值生成最终自然语言回答。

所以在你的业务Controller里,你只需要:

@RestController public class WeatherController { private final WeatherAssistant weatherAssistant; public WeatherController(WeatherAssistant weatherAssistant) { this.weatherAssistant = weatherAssistant; } @GetMapping("/weather") public String weather(@RequestParam String city) { return weatherAssistant.chat(city + "天气怎么样"); } }

底层工具方法怎么调用、返回结果怎么处理,业务层完全不用关心。将来如果把天气服务换掉,只需要改WeatherTool里的实现,接口签名不变,上层一行代码都不用动。

有一点要提醒:@Tool注解的方法必须是public方法,方法的参数个数、类型要和模型可能返回的参数匹配得上。工具描述一定要写清楚,模型是靠着这段描述来判断什么时候该调用哪个方法的,描述含糊会导致模型该调不调、不该调乱调。

5. 我踩过的一些坑与排查思路

5.1 容器启动失败,先检查这三处

刚上手那几天,我最常遇到的就是应用直接启动报错,排查下来九成是下面三种原因:

  • 配置文件里没有api-key,或者环境变量名写错,导致自动配置无法创建ChatLanguageModel的Bean。
  • 多个模型厂商的依赖同时存在,又没有指定唯一模型,Spring容器发现两个候选Bean后直接报错。
  • 自定义配置类里手动创建了OpenAiChatModel的Bean,和starter自动配置的Bean冲突,表面看是Bean冲突,本质是重复定义。

我的排查顺序是:先看启动日志里和langchain4j相关的报错,再检查配置项名称,最后就是确认依赖版本。很多时候直接把版本统一到官方文档推荐版本,问题就消失了大半。

5.2 模型越聊越笨,排查会话上下文

我在最初使用裸ChatLanguageModel做多轮对话时,发现模型完全没有记忆,每一轮都是独立回答,用户体验非常割裂。后来才意识到,问题出在应该使用AiService,并在定义接口时给它显式配置ChatMemory。

LangChain4j里保留多轮对话的标准做法是,接口定义加上记忆相关配置:

@RegisterAiService public interface ChatAssistant { @SystemMessage("你是长期陪伴用户的助手,回答请自然、亲切。") String chat(@UserMessage String message); }

在application.yml里配置记忆:

langchain4j: memory: chat-memory-provider: mode: last-n max-messages: 20

这里max-messages决定模型能看到过去多少条消息。不建议设置太多,太大会占据很快变长的上下文空间。如果接入了消息持久化功能,还可以把历史消息存到数据库,做到真正意义上的多端同步。记住一句话:无状态模型永远不知道“刚才聊了什么”,有记忆才叫对话。

5.3 工具一直不触发?多半是模型能力问题

有一次我把天气助手部署到测试环境,结果发现所有请求都返回“我是一个AI,无法获取实时天气”,完全没走到工具调用。排查了好半天,最后发现测试环境配置的模型版本是基础版,工具调用的稳定性本身就不够好。

后来我把模型换成支持工具调用的新版本,并且把工具描述写得更具体,比如“当用户提到任意城市名时,调用此工具查询该城市实时天气”,触发成功率立刻上去了。另外有个小细节:工具方法尽量让返回内容带上字段名,比如城市:北京,天气:晴,模型在生成最终回答时,能更好地格式化信息。

5.4 断点调试比想象中重要

LangChain4j的底层有很多动态代理和反射调用,单靠阅读框架源码定位问题效率不高。我在实践中最常用的方式是在工具方法里打上断点,观察模型返回的工具调用参数是否正确。如果你发现工具方法根本没有被调用,那问题大概率出在模型判断;如果工具方法被调用了但参数是乱码,那大概率是Prompt或工具描述写得有歧义。

借助log-requests和log-responses这两个配置,把请求和响应都打印出来后,整个调用链路是非常透明的。框架到底往模型那边发了什么、模型回传了什么,一目了然。别一上来就怀疑框架有Bug,多数情况下都是配置、模型或Prompt三件事的问题。

文章最后分享一个我自己的习惯:把所有@UserMessage和@SystemMessage都集中写在接口层,不要散落在业务方法里。这让团队其他人只需要看接口定义,就能理解这个功能让模型干什么。另外,如果你后续想提升用户体验,可以关注LangChain4j的流式接口,把逐字返回接入SSE,对话页面会顺滑很多。这次就讲到这儿,希望这套入门路径能帮你少踩几个坑,尽快把项目跑起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询