换模型只改三行配置: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 单体应用,每块职责都被拆到了独立的类里:

bash 复制代码
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
xml 复制代码
<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. 克隆项目

bash 复制代码
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 跑本地模型(不花钱、可离线):

yaml 复制代码
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) :

yaml 复制代码
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:

yaml 复制代码
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:

yaml 复制代码
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. 启动应用

打包后运行:

bash 复制代码
mvn clean package
java -jar target/langchain4j-chat-1.0-SNAPSHOT.jar

开发阶段也可以直接用 Maven 跑起来:

arduino 复制代码
mvn spring-boot:run

五、接口调用:普通对话、流式对话与控制器实现

应用起来之后,对外暴露的是 REST 风格的接口。项目给了三种典型用法,这一节末尾附上承载它们的控制器实现。

1. 普通对话(非流式)

一次性拿到完整回答,适合后端服务调用:

arduino 复制代码
curl -X POST http://localhost:8080/ai/chat \
  -H "Content-Type: application/json" \
  -d '{
    "memoryId": "user123",
    "message": "你好,请介绍一下你自己"
  }'

响应是一段完整的文本:

arduino 复制代码
"你好!我是你的 AI 助手,很高兴为你提供帮助。有什么问题都可以问我!"

2. 流式对话(SSE)

这是最能体现"实时感"的一个接口。请求用 curl -N 关掉缓冲,就能看到逐字返回的效果:

arduino 复制代码
curl -N "http://localhost:8080/ai/stream?memoryId=user123&message=请写一首关于春天的诗"

返回的是 SSE 事件流,每个字符一个 data: 事件:

makefile 复制代码
data: 春
data: 天
data: 来
data: 了
data: ,
data: 万
data: 物
data: 复
data: 苏
...

在浏览器里访问,就会看到文字一个接一个冒出来的打字机效果。对前端来说,只要消费 SSE 事件流即可。

3. 多轮对话

多轮对话靠的是请求里的 memoryId。同一个 memoryId 会共享对话历史,不同的 memoryId 则各聊各的、互不干扰:

json 复制代码
# 第一轮
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 事件流消费响应:

less 复制代码
@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 调用:

less 复制代码
@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 里配置,用的是滑动窗口策略:

scss 复制代码
@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?

  1. 安装并启动 Ollama(官网 ollama.com);
  2. 拉取模型:ollama pull qwen2.5:3b;
  3. 把配置中的 base-url 改为 http://localhost:11434/v1;
  4. api-key 可以填任意值(例如 unused)。

关于密钥安全:生产环境建议用环境变量管理敏感配置,不要写死在配置文件里。

ini 复制代码
# 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 里很坦诚地列出了三条当前限制:

  1. 不支持运行时动态切换模型:需要修改配置文件并重启应用;
  2. 单模型模式:同一时间只能使用一个模型,无法同时调用多个模型;
  3. 无模型路由功能:所有请求都使用同一个配置好的模型。

一句话概括:它是配置驱动的单模型后端。做原型、做内部工具、学接入方式,都够用;要按场景动态路由、多模型协同,就得自己做架构升级。

九、想拿来改造?三步接进自己的业务

如果你想把这个项目当成自己聊天后端的起点,改造路径其实很清晰,基本围绕三件事。

第一步,定模型。 模型的事全在 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 配置起步,不花一分钱就能把整条链路走通。

相关推荐
愚农搬码1 小时前
2026 年开源表单设计器选型:哪个更好用?
后端
vipxieliang1 小时前
ValidX 正则表达式预编译优化:Pattern 复用策略
java·spring boot·后端
vipxieliang1 小时前
OpenTelemetry 生产实践:Skyscanner 24 集群管理经验与 Mastodon 小团队方案
后端
明月_清风2 小时前
为什么 Skill 很难商业化?Bot 才是 AI 时代真正的商业机会
人工智能·后端·agent
苍何2 小时前
DeepSeek 的 Super-agent 设计,和我们的工程实践
后端
挖掘狂人2 小时前
缓存和数据库的一致性,别停在 “延时双删”
java·后端·程序员
Rain的Java大神实战圈2 小时前
面试官让你把 LLM、RAG、Agent、MCP 串起来讲清楚,90% 的 Java 工程师当场露馅
ai编程
codigger2 小时前
缓存和数据库的一致性,别停在 “延时双删”
java·后端·程序员·后端开发
10年前端老司机2 小时前
Agent 商业化困局:热闹的发布会,冷清的付费客户
前端·后端·agent