前面几篇更多是在聊认知比如 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 消息格式。 MapString, Object message Map.of( role, user, content, question ); // 最小可用的非流式对话请求体模型名称 消息列表。 MapString, 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 还是 ControllerService 还是 Service配置还是配置HTTP 调用还是 HTTP 调用。只是 Service 里接入的能力变了。以前 Service 可能调用数据库、Redis、MQ、第三方接口。现在多了一个模型接口。这个接口能根据自然语言生成内容所以它带来了新的使用场景也带来了新的不确定性。我觉得后端做 AI 应用重点不是把模型说得多神而是要把它放进一个稳定的工程结构里。比如后面可以继续扩展把每次问答记录存到 MySQL用 Redis 保存短期会话上下文给接口加限流避免被频繁调用支持流式输出让回答边生成边返回接入知识库让模型基于文档回答对 Prompt 做模板化管理这些才会慢慢接近真实 AI 应用。写在最后这篇只是先跑通一个最简单的问答接口。它不复杂甚至看起来有点像普通的第三方 API 封装。但我觉得这一步很重要。因为只有先把链路跑起来后面再去理解 Prompt、上下文、RAG、Agent才不会一直停留在概念里。对 Java 后端来说AI 应用开发的第一步可以很朴素用 Spring Boot 调一次大模型 API拿到结果然后想办法把它变成一个稳定的业务接口。下一篇我准备继续聊 Prompt做 AI 应用时Prompt 到底算不算“代码”