换模型只改三行配置:LangChain4j + Spring Boot 3.5 实战,一次分别接入 9 家模型服务
把 9 家模型服务统一收进同一套 OpenAI 兼容协议之下,包括本地 Ollama,切换模型只需要改
base-url、api-key和model-name三行配置。完整代码已开源,可直接到Gitee仓库复制使用。
用 Java 做大模型应用,真正让人头疼的往往不是写 prompt,而是"换一家模型就得动一遍代码":API 地址不一样、参数命名不一样、流式协议也不一样,模型一换,之前写好的胶水代码就得跟着重写。
这篇文章要拆的 LangChain4j-Chat 项目走的就是这条路,并且在它之上把流式响应、多轮对话记忆和声明式 AI 服务一并做了进去,技术底座是 LangChain4j + Spring Boot 3.5。
下面从项目定位讲起,一直讲到怎么把它跑起来、踩了坑往哪儿查。
一、项目定位:把"模型可替换"做进架构
项目名很直白,就叫 LangChain4j-Chat。一句话概括它的能力:
基于 LangChain4j + Spring Boot 3.5 的多模型 AI 聊天应用,9 家模型服务全部通过 OpenAI 兼容协议接入。
它解决的是模型可替换的问题。做 AI 功能很少一步到位就选定供应商:先用一家跑通原型,之后因为成本、合规或私有化部署的需要换模型,是常有的事。调用逻辑写死在业务代码里,换一次模型就是一次小型重构;把模型接入单独抽出来做成配置驱动,换模型就只剩改配置、重启这两步。
从工程结构看,它是一个标准的 Spring Boot 单体应用,每块职责都被拆到了独立的类里:
langchain4j-chat/
├── src/main/java/com/song/langchain4j/
│ ├── config/
│ │ └── ChatMemoryConfig.java # 对话记忆配置
│ ├── controller/
│ │ └── ChatController.java # REST API 控制器
│ ├── dto/
│ │ └── ChatRequest.java # 请求 DTO
│ ├── service/
│ │ └── ChatAssistant.java # AI 服务接口(@AiService)
│ └── ChatApplication.java # Spring Boot 启动类
├── src/main/resources/
│ └── application.yml # 应用配置文件
└── pom.xml # Maven 依赖配置
配置类管记忆、控制器管路由、服务接口管 AI 调用,分层清晰,也方便按这个骨架往自己的业务里搬。
项目对外承诺的特性一共五条,基本覆盖了做一个聊天后端最常见的能力:
- 多模型支持:兼容 OpenAI、DeepSeek、通义千问、豆包、文心一言、智谱、Kimi、混元、本地 Ollama,全部走 OpenAI 兼容协议;
- 流式响应:支持 Server-Sent Events(SSE)实时流式输出;
- 对话记忆:基于滑动窗口的多轮对话记忆管理;
- 声明式 API:使用
@AiService注解,简洁优雅地定义服务; - 灵活配置:通过配置文件轻松切换不同模型。
二、技术栈:Java 17 + Spring Boot 3.5 + LangChain4j
项目的技术栈如下:
- Java 17+
- Spring Boot 3.5.16
- LangChain4j 1.20.2-beta30
- Spring WebFlux(用于流式响应)
- Maven
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- OpenAI 兼容协议 starter:DeepSeek、DashScope 等通过它接入 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.20.2-beta30</version>
</dependency>
<!-- 声明式 AI Service starter:提供 @AiService 等自动配置 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.20.2-beta30</version>
</dependency>
<!-- 流式对话 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.20.2-beta30</version>
</dependency>
</dependencies>
两个依赖多说两句。LangChain4j 是 Java 生态的 LLM 应用框架,项目靠它的 OpenAI 兼容客户端和 @AiService 注解,把模型调用简化成接口方法。Spring WebFlux 则是流式输出的底气:SSE 逐字返回,正对上响应式流“边生成边推送”的模型,打字机效果靠的就是它。
Spring Boot 3.5 则决定了整个应用的基础设施风格:自动配置、注解驱动、application.yml 集中管理配置。对熟悉 Spring 的开发者来说,上手成本很低。
顺带说一句流式响应为什么不选 WebSocket。SSE 是单向的服务器推送通道,服务端持续把新生成的文字推给客户端,客户端只负责接收,不需要双向通信。对"AI 逐字吐字"这种场景来说,SSE 更轻量、更贴合 HTTP 语义,也更容易在浏览器和各类 HTTP 客户端里消费;只有当交互真的需要双向实时通信时,才轮到 WebSocket 出场。这个项目选 SSE,图的就是它够用、又简单。
三、9 家模型服务,一套 OpenAI 兼容协议搞定
这是项目最有意思的设计:9 家模型服务全部通过 OpenAI 兼容协议接入。只要你的模型服务提供 OpenAI 兼容接口,接入方式和接 OpenAI 本身没有任何区别,本地 Ollama 也不例外,它在 http://localhost:11434/v1 暴露同样的 /chat/completions 端点。项目覆盖的 9 家模型服务如下(模型名以各平台控制台最新列表为准):
| 模型 | 提供商 | 接入方式 | 备注 |
|---|---|---|---|
| OpenAI | OpenAI | OpenAI 协议 | gpt-4o、GPT-4、GPT-3.5-turbo |
| DeepSeek | DeepSeek | OpenAI 协议 | deepseek-chat |
| 通义千问 | 阿里云 | OpenAI 协议 | qwen-plus、qwen-max、qwen-flash |
| 豆包 | 字节跳动 | OpenAI 协议 | doubao-pro-32k、doubao-1.5-pro-32k |
| 文心一言 | 百度 | OpenAI 协议 | ernie-4.0-8k、ernie-3.5-8k |
| 智谱 AI | 智谱 | OpenAI 协议 | glm-4、glm-3-turbo |
| Kimi | Moonshot | OpenAI 协议 | kimi-k3.1-pro、kimi-k2.6 |
| 混元 | 腾讯 | OpenAI 协议 | hunyuan-turbos-latest、hunyuan-lite |
| Ollama | 本地部署 | OpenAI 协议 | llama3、qwen2.5 等 |
注意:豆包、文心、Kimi 的模型名都带有规格后缀(如
-32k、-8k),填写裸名会报model not found。上线前请以各平台控制台的模型列表为准。
切换模型时,只需要改三样东西:base-url(模型的 API 地址)、api-key(密钥)、model-name(模型名)。各家服务的 Base URL 可以参考这张表:
| 模型 | Base URL |
|---|---|
| OpenAI | https://api.openai.com/v1 |
| DeepSeek | https://api.deepseek.com/v1 |
| 通义千问 | https://dashscope.aliyuncs.com/compatible-mode/v1 |
| 豆包 | https://ark.cn-beijing.volces.com/api/v3 |
| 文心一言 | https://qianfan.baidubce.com/v2 |
| 智谱 AI | https://open.bigmodel.cn/api/paas/v4 |
| Kimi | https://api.moonshot.cn/v1 |
| 混元 | https://api.hunyuan.cloud.tencent.com/v1 |
| Ollama | http://localhost:11434/v1 |
配其他家时照着这张表来就行,后面配任何一家模型都是同一套流程。
四、快速上手:三步把项目跑起来
1. 前置条件
动手之前,先确认环境齐备:
- JDK 17 或更高版本;
- Maven 3.8+;
- 至少一个可用的大模型 API Key(或者使用本地 Ollama,不需要 Key)。
2. 克隆项目
git clone <your-repo-url>
cd langchain4j-chat
3. 配置模型
项目的模型配置集中在 src/main/resources/application.yml 里。下面给出四种典型配置,按需取用。
四组配置结构完全一致:每组都同时声明 chat-model(普通对话)和 streaming-chat-model(流式对话),两者的 api-key、model-name、base-url 必须保持相同,只是前者同步返回、后者逐字推送。下文为节省篇幅,streaming-chat-model 部分仅列出关键字段。
配置一:使用 Ollama 跑本地模型(不花钱、可离线):
langchain4j:
open-ai:
chat-model:
api-key: unused # Ollama 不需要 API Key
model-name: qwen2.5:3b
base-url: http://localhost:11434/v1
temperature: 0.7
# streaming-chat-model 与 chat-model 字段完全相同(api-key/model-name/
# base-url/temperature),实际配置时去掉注释写全,否则流式接口无法启动:
# streaming-chat-model:
# api-key: unused
# model-name: qwen2.5:3b
# base-url: http://localhost:11434/v1
# temperature: 0.7
配置二:使用通义千问(DashScope) :
langchain4j:
open-ai:
chat-model:
api-key: ${DASHSCOPE_API_KEY:sk-xxxxxxxxxxxxxxxx}
model-name: qwen-plus
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
temperature: 0.7
streaming-chat-model:
# streaming-chat-model 段略:字段与 chat-model 完全相同,实际配置时必须两段都写
配置三:使用 DeepSeek:
langchain4j:
open-ai:
chat-model:
api-key: ${DEEPSEEK_API_KEY:sk-xxxxxxxxxxxxxxxx}
model-name: deepseek-chat
base-url: https://api.deepseek.com/v1
temperature: 0.7
streaming-chat-model:
# streaming-chat-model 段略:字段与 chat-model 完全相同,实际配置时必须两段都写
配置四:使用 OpenAI:
langchain4j:
open-ai:
chat-model:
api-key: ${OPENAI_API_KEY:sk-xxxxxxxxxxxxxxxx}
model-name: gpt-4o
base-url: https://api.openai.com/v1
temperature: 0.7
streaming-chat-model:
# streaming-chat-model 段略:字段与 chat-model 完全相同,实际配置时必须两段都写
注意配置里的 api-key 都用了 ${ENV_VAR:default_value} 的写法,这正是为了把密钥从代码库里剥离出去,具体怎么用环境变量兜住敏感信息,后面第七节还会展开。
4. 启动应用
打包后运行:
mvn clean package
java -jar target/langchain4j-chat-1.0-SNAPSHOT.jar
开发阶段也可以直接用 Maven 跑起来:
mvn spring-boot:run
五、接口调用:普通对话、流式对话与控制器实现
应用起来之后,对外暴露的是 REST 风格的接口。项目给了三种典型用法,这一节末尾附上承载它们的控制器实现。
1. 普通对话(非流式)
一次性拿到完整回答,适合后端服务调用:
curl -X POST http://localhost:8080/ai/chat \
-H "Content-Type: application/json" \
-d '{
"memoryId": "user123",
"message": "你好,请介绍一下你自己"
}'
响应是一段完整的文本:
"你好!我是你的 AI 助手,很高兴为你提供帮助。有什么问题都可以问我!"
2. 流式对话(SSE)
这是最能体现"实时感"的一个接口。请求用 curl -N 关掉缓冲,就能看到逐字返回的效果:
curl -N "http://localhost:8080/ai/stream?memoryId=user123&message=请写一首关于春天的诗"
返回的是 SSE 事件流,每个字符一个 data: 事件:
data: 春
data: 天
data: 来
data: 了
data: ,
data: 万
data: 物
data: 复
data: 苏
...
在浏览器里访问,就会看到文字一个接一个冒出来的打字机效果。对前端来说,只要消费 SSE 事件流即可。
3. 多轮对话
多轮对话靠的是请求里的 memoryId。同一个 memoryId 会共享对话历史,不同的 memoryId 则各聊各的、互不干扰:
# 第一轮
curl -X POST http://localhost:8080/ai/chat \
-H "Content-Type: application/json" \
-d '{"memoryId": "user123", "message": "我叫张三"}'
# 响应:"你好张三,有什么可以帮助你的吗?"
# 第二轮(AI 会记得你的名字)
curl -X POST http://localhost:8080/ai/chat \
-H "Content-Type: application/json" \
-d '{"memoryId": "user123", "message": "我叫什么名字?"}'
# 响应:"你叫张三"
第二轮里没人告诉模型名字,它还是答出了“你叫张三”,因为同一个 memoryId 下的历史消息被自动带进了上下文。
4. 控制器:把 AI 能力暴露成 REST 接口
ChatController.java 的核心实现如下,重点是流式端点的 produces = TEXT_EVENT_STREAM_VALUE,没有它,浏览器不会按 SSE 事件流消费响应:
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatAssistant assistant;
public ChatController(ChatAssistant assistant) {
this.assistant = assistant;
}
/**
* 普通对话接口(非流式)
*
* @param request 聊天请求,包含 memoryId 和 message
* @return AI 完整回复
*/
@PostMapping("/chat")
public String chat(@RequestBody ChatRequest request) {
return assistant.chat(request.getMemoryId(), request.getMessage());
}
/**
* 流式对话接口(SSE)
*
* @param memoryId 对话记忆 ID
* @param message 用户消息
* @return 流式响应数据流
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam(value = "memoryId") String memoryId,
@RequestParam(value = "message") String message) {
return assistant.stream(memoryId, message);
}
}
六、核心代码解析:声明式 AI 服务与对话记忆
项目代码量不大,核心设计集中在两块:
1. 声明式 AI 服务:一个注解,接口就有了实现
src/main/java/com/song/langchain4j/service/ChatAssistant.java 用 @AiService 注解自动实现了 AI 调用:
@AiService
public interface ChatAssistant {
@SystemMessage("你是一个乐于助人的 AI 助手,回答简洁准确。")
String chat(@MemoryId String memoryId, @UserMessage String message);
@SystemMessage("你是一个乐于助人的 AI 助手,回答简洁准确。")
Flux<String> stream(@MemoryId String memoryId, @UserMessage String message);
}
@SystemMessage定义系统提示词,也就是给 AI 设定角色;@MemoryId标记对话记忆的归属,对应请求里的memoryId;@UserMessage标记用户输入;- 返回
String是同步一次性响应,返回Flux<String>则是响应式流,正好对应上面的普通对话和流式对话两种接口。
补充一个容易踩的坑:返回
Flux<String>依赖langchain4j-reactor模块的适配支持,漏掉这个依赖应用会在启动时报"不支持的返回类型"。如果不引入 Reactor,也可以改用 LangChain4j 原生的TokenStream返回类型,再在 Controller 里手动转成 SSE 事件流。
比起在每个 Controller 里手写 HTTP 请求、拼 JSON、手动解析响应,这种"把 AI 调用声明成接口"的写法,可读性和可维护性都高出一截。
2. 对话记忆:滑动窗口控制上下文长度
多轮对话的记忆管理在 src/main/java/com/song/langchain4j/config/ChatMemoryConfig.java 里配置,用的是滑动窗口策略:
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20) // 最多保留 20 条消息
.build();
}
记忆按 memoryId 隔离,ChatMemoryProvider 收到 ID 就返回对应的记忆,不同用户互不干扰;maxMessages(20) 限定每个会话保留多少条。窗口大小是连贯性和成本之间的取舍:调大,模型记得的历史多,每次请求带的上下文也长;调小,响应省,但对话容易“忘事”。20 条只是默认值,按业务节奏调。
想调整记忆长度,改 .maxMessages(20) 这个参数即可。
七、常见问题速查
项目 README 里整理了几个高频问题,实际动手时大概率都会碰到。
Q1:如何切换不同的模型?
修改 application.yml 中的 base-url、api-key、model-name 三个配置项,然后重启应用即可。
Q2:中文乱码怎么办?
项目已默认启用 UTF-8 编码。如果仍然遇到乱码,检查两条:终端/浏览器是否使用 UTF-8 编码;配置文件是否保存为 UTF-8 格式。
Q3:如何调整对话记忆长度?
修改 ChatMemoryConfig.java 中的 .maxMessages(20) 参数。
Q4:流式响应不生效?
按顺序排查三点:是否配置了 streaming-chat-model;客户端是否支持 SSE(Server-Sent Events);控制台日志里有没有报错。
Q5:如何使用本地 Ollama?
- 安装并启动 Ollama(官网 https://ollama.com);
- 拉取模型:
ollama pull qwen2.5:3b; - 把配置中的
base-url改为http://localhost:11434/v1; api-key可以填任意值(例如unused)。
关于密钥安全:生产环境建议用环境变量管理敏感配置,不要写死在配置文件里。
# Linux/Mac
export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxx"
export DASHSCOPE_API_KEY="sk-xxxxxxxxxxxxxxxx"
export DEEPSEEK_API_KEY="sk-xxxxxxxxxxxxxxxx"
# Windows
set OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
然后在 application.yml 里用 ${ENV_VAR:default_value} 的格式引用,这正好解释了前面配置示例里那些 ${...} 的写法。
Q6:接入混元有什么要注意的?
腾讯云的大模型服务已升级为 TokenHub 平台,本文所用的混元 OpenAI 兼容地址(https://api.hunyuan.cloud.tencent.com/v1)面向存量用户保留,新用户建议直接在 TokenHub 控制台获取接入地址与模型列表,接入方式同样是 OpenAI 兼容协议。
八、当前限制与可能的演进方向
项目在 README 里很坦诚地列出了三条当前限制:
- 不支持运行时动态切换模型:需要修改配置文件并重启应用;
- 单模型模式:同一时间只能使用一个模型,无法同时调用多个模型;
- 无模型路由功能:所有请求都使用同一个配置好的模型。
一句话概括:它是配置驱动的单模型后端。做原型、做内部工具、学接入方式,都够用;要按场景动态路由、多模型协同,就得自己做架构升级。
九、想拿来改造?三步接进自己的业务
如果你想把这个项目当成自己聊天后端的起点,改造路径其实很清晰,基本围绕三件事。
第一步,定模型。 模型的事全在 application.yml 里,改 base-url、api-key、model-name 三项即可。建议先用本地 Ollama 起步:不用申请 Key、不产生线上调用,把"请求进来 → 模型返回 → 前端展示"这条链路先走通,确认没问题了,再换成线上模型。
第二步,定接口。 业务需要的 AI 能力,都往 ChatAssistant 这个接口里加。每加一个方法,用 @SystemMessage 设定这个场景的角色和约束,用 @UserMessage 接收用户输入,用 @MemoryId 标明它属于哪个会话;返回 String 就是一次性响应,返回 Flux<String> 就是流式响应。写法与前文那两行示例完全一致,不需要自己碰 HTTP 层。
第三步,定路由。 在 ChatController 里把新方法暴露成 REST 接口,并用 memoryId 区分会话,前端给每个用户(或每个会话窗口)分配一个独立 ID,记忆就会自动隔离开,不会串台。
还有两个容易忽略的收尾工作:记忆窗口 .maxMessages(20) 要按业务节奏调,客服问答这类短对话不用留太多,长档案式的对话可以适当放大;api-key 一律走环境变量,不提交进代码仓库。
这三步做完,你就得到一个能切换模型、支持流式输出、自带多轮记忆的 AI 聊天后端。
十、源代码
完整代码已开源:langchain4j-chat(Gitee)
结语
这个项目真正值得学的是把“模型可替换”当成架构里的核心设计:OpenAI 兼容协议抹平各家差异,配置驱动把切换成本压到最低,声明式 API 和滑动窗口记忆补齐对话后端的常用能力。Java 开发者拿它当聊天后端的起点,或者当 LangChain4j 与 Spring Boot 3.5 集成的参考实现,都合适。
如果你想自己跑一遍,可以从本地 Ollama 配置起步,不花一分钱就能把整条链路走通。
用 Java 做大模型应用,真正让人头疼的往往不是写 prompt,而是"换一家模型就得动一遍代码":API 地址不一样、参数命名不一样、流式协议也不一样,模型一换,之前写好的胶水代码就得跟着重写。
浙公网安备 33010602011771号