Spring Boot 接入大模型 API:先跑通一个简单问答接
前面几篇更多是在聊认知,比如 Java 后端为什么关注 AI 应用开发、AI 应用和传统 CRUD 有什么不同、后端学习 AI 要不要先啃算法。
聊这些当然有必要,但如果一直停留在概念层面,很容易越看越虚。所以这篇我想从一个最小的实战开始:用 Spring Boot 接入大模型 API,先跑通一个简单的问答接口。
目标不用定太大,不做知识库,不做上下文,也不做复杂 Agent。先完成一件事:用户传一个问题,后端调用大模型接口,然后把回答返回给用户。
对后端开发来说,第一步能跑起来,比一开始设计得很复杂更重要。
先把它当成一次普通的第三方接口调用
刚开始接触大模型 API 时,我发现如果一上来就想着“大模型”“AI”“Prompt 工程”,反而容易把事情想复杂。
换成后端熟悉的说法,其实它就是一次 HTTP 调用。
以前我们可能接过短信接口、支付接口、物流接口、地图接口。调用流程大概都是:
准备请求参数
带上认证信息
发起 HTTP 请求
解析返回结果
做异常处理
返回给业务方
大模型 API 也是类似的。
区别在于,普通第三方接口返回的大多是结构化字段,而大模型返回的核心内容通常是一段文本。这个文本是生成出来的,不是数据库里查出来的,所以后面还要额外考虑格式约束、内容稳定性、超时、成本这些问题。
但第一篇实战先不展开太多,先把链路跑通。
项目结构不用搞复杂
这里可以先用一个普通 Spring Boot 项目。
大概结构如下:
com.simple.ai ├── controller │ └── ChatController.java ├── service │ └── ChatService.java ├── config │ └── AiProperties.java ├── dto │ ├── ChatRequest.java │ └── ChatResponse.java └── SimpleAgentApplication.java如果只是验证接口,也可以更简单。但我习惯还是把 Controller、Service、配置类分开,不然后面加上下文、日志、知识库时容易乱。
配置 API 地址和 Key
实际项目里,API Key 不建议直接写在代码里,最好放到配置文件或者环境变量里。
比如:
ai: # 本文用到是硅基流动提供的模型,apiKey api-url: https://api.siliconflow.cn/v1/chat/completions api-key: sk-********************** model: deepseek-ai/DeepSeek-V4-Pro对应一个配置类:
package com.simple.ai.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "ai") @Component public class AiProperties { private String apiUrl; private String apiKey; private String model; public String getApiUrl() { return apiUrl; } public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } }这里的地址和参数根据你使用的平台调整即可。现在不少大模型平台都兼容类似的 Chat Completions 格式,所以整体思路差不多。
定义一个简单请求对象
前端或者 Postman 传一个问题过来,后端接收即可。
package com.simple.ai.dto; public class ChatRequest { private String question; public String getQuestion() { return question; } public void setQuestion(String question) { this.question = question; } }返回对象也先简单一点:
package com.simple.ai.dto; public class ChatResponse { private String answer; public ChatResponse() { } public ChatResponse(String answer) { this.answer = answer; } public String getAnswer() { return answer; } public void setAnswer(String answer) { this.answer = answer; } }第一版不要急着设计太多字段。等后面真的需要保存 token、模型名称、耗时、会话 ID 时,再慢慢加。
Service 层负责调用模型
这里用 Spring 自带的RestClient举例。如果项目版本比较老,也可以用RestTemplate或者WebClient。
package com.simple.ai.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.simple.ai.config.AiProperties; import org.springframework.http.MediaType; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; import java.util.Map; @Service public class ChatService { private final AiProperties aiProperties; private final RestClient restClient; private final ObjectMapper objectMapper; public ChatService(AiProperties aiProperties) { this.aiProperties = aiProperties; this.objectMapper = new ObjectMapper(); // 给外部大模型接口设置超时时间,避免网络异常时请求一直阻塞。 SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setConnectTimeout(Duration.ofSeconds(10)); requestFactory.setReadTimeout(Duration.ofSeconds(30)); this.restClient = RestClient.builder() .requestFactory(requestFactory) .build(); } public String chat(String question) { if (question == null || question.isBlank()) { return "请输入问题"; } // 硅基流动 Chat Completions 接口兼容 OpenAI 消息格式。 Map<String, Object> message = Map.of( "role", "user", "content", question ); // 最小可用的非流式对话请求体:模型名称 + 消息列表。 Map<String, Object> body = Map.of( "model", aiProperties.getModel(), "messages", List.of(message), "max_tokens", 512, "temperature", 0.7, "stream", false ); try { byte[] response = restClient.post() .uri(aiProperties.getApiUrl()) .header("Authorization", "Bearer " + aiProperties.getApiKey()) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(body) .retrieve() .body(byte[].class); // 部分平台返回的 Content-Type 不一定是 application/json,先按 UTF-8 字节解码再解析更稳。 String responseJson = new String(response, StandardCharsets.UTF_8); return parseAnswer(objectMapper.readValue(responseJson, Map.class)); } catch (JsonProcessingException e) { return "模型返回内容不是有效 JSON:" + e.getMessage(); } catch (RestClientException e) { return "调用大模型接口失败:" + e.getMessage(); } } private String parseAnswer(Map response) { if (response == null) { return "模型没有返回有效内容"; } List choices = (List) response.get("choices"); if (choices == null || choices.isEmpty()) { return "模型没有返回有效内容"; } // 常规非流式响应内容位于 choices[0].message.content。 Map firstChoice = (Map) choices.get(0); Map message = (Map) firstChoice.get("message"); if (message == null) { return "模型返回格式异常"; } Object content = message.get("content"); return content == null ? "模型返回内容为空" : content.toString(); } }这段代码只是为了说明调用流程,真实项目里不建议大量使用Map硬解析。后面可以定义明确的请求和响应 DTO,这样代码更清晰,也更方便维护。
不过第一版用Map有个好处:写起来快,能先验证链路。
Controller 暴露问答接口
Controller 就很简单了:
package com.simple.ai.controller; import com.simple.ai.dto.ChatRequest; import com.simple.ai.dto.ChatResponse; import com.simple.ai.service.ChatService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } // 前端或 curl 只需要提交 {"question": "..."},这里返回 {"answer": "..."}。 @PostMapping public ChatResponse chat(@RequestBody ChatRequest request) { String answer = chatService.chat(request.getQuestion()); return new ChatResponse(answer); } }启动项目后,用 Postman 请求:
{ "question": "请用简单的话解释一下什么是 Redis 缓存穿透" }如果配置没问题,就能拿到模型返回的回答。
到这一步,一个最简单的 AI 问答接口就跑通了。
第一版跑通后,马上会遇到几个问题
这个 Demo 很简单,但跑起来以后,会很快发现一些后端必须考虑的问题。
第一个是超时。
大模型接口不像普通查询接口,有时候响应会比较慢。如果用户一直等,体验不好;如果接口线程一直被占着,对服务也不友好。所以后面要设置合理的连接超时和读取超时。
第二个是异常处理。
API Key 错误、余额不足、模型服务异常、网络波动、返回格式变化,这些都有可能发生。不能让异常直接抛到前端,至少要包装成统一错误返回,并且记录日志。
第三个是参数校验。
用户问题不能为空,长度也不能无限制。否则一个超长输入可能会带来很高的调用成本,也可能让接口变慢。
第四个是调用成本。
传统接口多调用几次,主要压力在服务器和数据库。大模型接口不一样,调用次数和输入输出长度都可能影响费用。所以后面要考虑限流、用户级配额、调用日志等功能。
第五个是安全问题。
不要把 API Key 写死在代码里,更不要提交到代码仓库。这个问题看起来基础,但实际开发里很容易因为测试方便就顺手写进去了。
后端视角下,这只是第一层封装
从 Spring Boot 接入大模型 API 这件事来看,它和传统后端开发并没有完全割裂。
Controller 还是 Controller,Service 还是 Service,配置还是配置,HTTP 调用还是 HTTP 调用。
只是 Service 里接入的能力变了。
以前 Service 可能调用数据库、Redis、MQ、第三方接口。现在多了一个模型接口。这个接口能根据自然语言生成内容,所以它带来了新的使用场景,也带来了新的不确定性。
我觉得后端做 AI 应用,重点不是把模型说得多神,而是要把它放进一个稳定的工程结构里。
比如后面可以继续扩展:
把每次问答记录存到 MySQL
用 Redis 保存短期会话上下文
给接口加限流,避免被频繁调用
支持流式输出,让回答边生成边返回
接入知识库,让模型基于文档回答
对 Prompt 做模板化管理
这些才会慢慢接近真实 AI 应用。
写在最后
这篇只是先跑通一个最简单的问答接口。
它不复杂,甚至看起来有点像普通的第三方 API 封装。但我觉得这一步很重要。因为只有先把链路跑起来,后面再去理解 Prompt、上下文、RAG、Agent,才不会一直停留在概念里。
对 Java 后端来说,AI 应用开发的第一步可以很朴素:用 Spring Boot 调一次大模型 API,拿到结果,然后想办法把它变成一个稳定的业务接口。
下一篇我准备继续聊 Prompt:做 AI 应用时,Prompt 到底算不算“代码”?
