你在 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。实际模型参数与可用调用名以 开发者文档 为准。
