Spring Boot 调用大模型 api 最小可跑教程

你在 Spring Boot 工程里用 RestTemplate 调 OpenAI 兼容接口,一跑就收到 401,或者返回体里带一个 gateway_error。这种问题多半不是模型能力不行,而是 base_url 和密钥读取方式一开始就偏了。这篇教程用一个能直接复制的 Spring Boot 最小控制器,把调用大模型 api 的链路跑通,并把这几个最容易填错的参数逐个说清楚。

一、场景与前置条件

这篇解决的是一个很具体的需求:你手上已经有一个 Spring Boot 项目,想给后台加一个对话接口,前端把用户问题传进来,后端转给大模型 api,最后把模型回复原样返回。我们不走 Spring AI 这类封装,也不引入厂商专属 SDK,只用 Spring 自带的 HTTP 客户端。

示例代码运行在 Spring Boot 3.2.5 和 JDK 17 上。RestClient 在 Spring Framework 6.1 之后才稳定,所以 Spring Boot 2.x 不能直接照搬。2.x 的工程把 RestClient 换成 RestTemplate 后,请求地址、请求头和 body 完全一样,关键点不变。浏览器能直接访问的 api 地址和网关地址不是一回事,这个区别后面会反复用到。

二、环境准备

依赖只加一个 spring-boot-starter-web。项目里本来就有 Controller 的话,这个依赖和启动类都已存在,不用再动。新建模块时,用 Spring Initializr 选 Web 依赖即可。

密钥放到环境变量里,不要写进 application.yml,更不要硬编码在 Java 类中。原因很简单:源码仓库一旦公开,硬编码的 key 就是泄漏源。环境变量让部署、测试、本地三套环境可以换 key 而不改代码。启动前先执行:

bash 复制代码
export SILVAMUX_API_KEY='sk_live_your_key'

Spring Boot 会通过 @Value 从环境变量里读取这个值。这里的 key 前缀是 sk_live_,只在创建时显示一次,之后控制台不会再次展示,所以拿到手要立即放进环境变量。

三、最小可跑示例

先给出最小闭环:两个类,一个 Service 负责调用 api,一个 Controller 暴露 GET 接口。访问 /api/chat?q=你好 就能拿到模型回答。代码不涉及数据库、不依赖缓存。

java 复制代码
package com.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;
import java.util.Map;

@Service
public class ChatService {

    private static final String BASE_URL = "https://www.silvamux.com/api/v1";

    private final RestClient restClient;
    private final String apiKey;

    public ChatService(@Value("${SILVAMUX_API_KEY}") String apiKey) {
        this.apiKey = apiKey;
        this.restClient = RestClient.builder()
                .baseUrl(BASE_URL)
                .build();
    }

    public String chat(String question) {
        Map<String, Object> body = Map.of(
                "model", "qwen3.5-flash",
                "messages", List.of(
                        Map.of("role", "user", "content", question)
                ),
                "temperature", 0.2
        );

        return restClient.post()
                .uri("/chat/completions")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .body(body)
                .retrieve()
                .body(String.class);
    }
}

这段代码的关键在几个地方。BASE_URL 固定指向 OpenAI 兼容端点,RestClient.builder().baseUrl(BASE_URL) 会在这个根地址上拼 /chat/completions。如果 Base URL 少写 /api/v1,最终路径会错,网关通常返回 404;如果把域名写成不带 www 的形式,请求根本到不了端点。model 这里填 qwen3.5-flash,这是模型广场稳定 alias,不是自己根据厂商名拼出来的。很多 400 报错来自模型名后加了日期或版本号。Authorization 的值是 Bearer 加 apiKey,注意 Bearer 后必须有一个空格。temperature 填 0.2 是为了让演示输出稳定,换成 0.7 也能跑,但不要写成字符串 "0.2",Java 会把它序列化成字符串,网关可能报参数类型错误。

java 复制代码
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping("/api/chat")
    public String ask(@RequestParam String q) {
        return chatService.chat(q);
    }
}

Controller 不做业务处理,@RequestParam String q 会拿到 URL 中的查询参数。q 为空时,请求体里的 content 可能变成空字符串,一般不会导致 400,但不建议在业务层不做校验。接口跑通后,可以先把 q 固定成"你好"测试一次,确认不是浏览器请求地址写错。

四、逐步扩展

最小示例只传用户问题。真正接口里,你通常还想固定模型的角色,并且把每次请求的追踪 ID 记下来。扩展后,chat 方法多接收一个 systemMessage,并把响应头里的 X-Request-Id 打印出来。

改动点有两处。第一处是 messages 列表,原来只有一条 user 消息,现在前面加一条 Map.of("role", "system", "content", systemMessage)。这条 system 消息必须放在列表第一位,role 不能写成 assistant 或 developer,否则下游路由可能不识别。第二处是 retrieve() 后面不用 .body(String.class),改成 .toEntity(String.class)。之前只拿响应正文,网关返回的 X-Request-Id 会直接丢掉;改成 ResponseEntity 后,状态码、响应头和正文都保留下来。拿到响应头后,打印 response.getHeaders().getFirst("X-Request-Id"),这个 ID 在排错时非常关键。Controller 里把 chatService.chat(q) 改成 chatService.chat("你是后端助手", q),给一个固定角色即可。

五、排错

  • 报错文本:401 Unauthorized。直接原因:密钥环境变量没有注入,或者 Authorization 头少了 Bearer 前缀。修法:启动前先确认环境变量有值,但不要打印完整 key;检查 @Value 放置的类是否被 Spring 扫描到;请求头里 Bearer 和 key 之间必须有空格。

  • 报错文本:400 Bad Request,返回体带 type: gateway_error。直接原因:模型名不属于模型广场可用列表,或 messages 数组为空。修法:模型调用名从模型广场详情页复制,不要自行加 -latest、日期等后缀;检查 Map.of 里 messages 是否包含至少一个 user 角色。

  • 报错文本:429 Too Many Requests。直接原因:请求频率超过当前限流。修法:使用指数退避,不要固定间隔 1 秒重试;循环调用前加保护,避免前端连点触发风暴。

  • 报错文本:502 Bad Gateway。直接原因:网关或上游瞬时错误。修法:先等待 1 到 5 秒重试一次;重试前记录 X-Request-Id,如果多次失败,拿着这个 ID 去排查。4xx 一律不要重试,改参数才有用。

常见问题

Q:Spring Boot 2.x 能用这段代码吗?

A:不能直接用。RestClient 是 Spring Framework 6.0 引入、Spring Boot 3.2 之后才顺手的 API。2.x 项目把 RestClient 换成 RestTemplate,Base URL、请求头和 body 不变,逻辑照搬即可。原因在于底层 HTTP 行为一致,只是编程接口不同。

Q:为什么我的请求一直返回 404?

A:先看日志里最终拼接好的完整 URL。Base URL 少 /api/v1 或写成了不带 www 的域名,都会让请求落到错误路径。代码里的 Base URL 是唯一调用根地址,和文档页地址不是同一个入口,别混用。路径错的时候,不用怀疑模型名,先看域名和版本段。

Q:X-Request-Id 到底有什么用?

A:它是每次调用返回的请求追踪 ID,格式类似 REQ-xxxx。把这个字段写进日志后,单次调用出错时可以直接用它去查。自己设置这个请求头时,网关也会沿用,不会覆盖。这个字段在多个服务同时调用大模型 api 时尤其有用。

以上示例在千木SilvaMux的 OpenAI 兼容端点上验证,模型调用名为 qwen3.5-flash。Spring Boot 版本为 3.2.5,JDK 17。实际模型参数与可用调用名以 开发者文档 为准。

相关推荐
今年下半年1 小时前
【Spring Boot 】通过 OpenFeign 请求第三方接口
java·spring boot·后端·openfeign
范什么特西1 小时前
亲和路由(inspect)
java
她说可以呀1 小时前
Docker 镜像的两种创建方式
java·分布式·docker
霸道流氓气质1 小时前
AgentFlow 入门与实战:Task-Based 并发、EWMA 动态调度与四道门禁的 Java 生产级编排
java·开发语言
UrbanJazzerati2 小时前
AutoHotkey v2 零基础快速入门 Part 3 像素亮度检测完全指南
后端
浮链序2 小时前
Shopify 把 10 亿商品开放给任意 agent 了:我用零依赖 Python 撸了个带预算闸门的代购网关
后端·代码规范
愿得一猪蹄2 小时前
# Java线程池实战,别再new Thread了
java
ee又momo2 小时前
# 深入理解Java注解,从使用到自定义
java
Qlly2 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端