Spring Boot集成ChatGPT:声明式AI服务开发实践
在现代企业应用开发中,微服务架构与API集成是核心技术范式。Spring Boot作为Java领域的主流框架,通过其Starter机制和约定优于配置的理念,极大地简化了外部服务的接入复杂度。其核心原理在于依赖注入与控制反转,结合Spring 6的HTTP Interface等特性,实现了类型安全、声明式的远程调用。这一技术价值在于将基础设施关注点与业务逻辑解耦,提升开发效率与代码可维护性。在AI能
1. 项目概述:一个为Spring Boot应用量身打造的ChatGPT集成方案
如果你正在开发一个基于Spring Boot 3.0+的应用,并且希望以最“Spring”的方式——也就是声明式、低侵入、高集成的风格——来接入OpenAI的ChatGPT API,那么 linux-china/chatgpt-spring-boot-starter 这个项目绝对值得你深入研究。它不是一个简单的HTTP客户端封装,而是一个深度拥抱Spring生态,特别是Spring 6的HTTP Interface和响应式编程模型的“胶水”层。简单来说,它让调用ChatGPT变得像调用一个本地的Spring Bean服务一样自然,同时提供了函数调用、结构化输出、提示词管理等高级特性,极大地提升了开发效率和代码的可维护性。
这个Starter的核心价值在于,它将复杂的AI能力集成抽象为开发者熟悉的Spring Boot Starter模式。你不再需要手动处理HTTP请求、解析JSON、管理连接池,或者为如何优雅地组织提示词模板而烦恼。它帮你处理了这些底层细节,让你可以专注于业务逻辑:定义你想要AI做什么,然后像调用普通方法一样去调用它。无论是构建一个智能客服机器人、一个代码生成工具,还是一个需要复杂推理和外部工具调用的AI助手,这个Starter都能提供一套优雅的解决方案。
2. 核心设计思路与架构解析
2.1 为什么选择基于Spring 6 HTTP Interface?
项目选择基于Spring 6的HTTP Interface作为底层通信框架,这是一个非常明智且前瞻性的设计。HTTP Interface是Spring Framework 6引入的一个新特性,它允许你通过定义一个Java接口,并使用注解来描述HTTP请求,Spring会动态生成这个接口的实现。这类似于Feign或Retrofit,但它是Spring原生的一部分。
这样做的好处显而易见:
- 声明式与类型安全 :你通过接口和方法签名定义API调用,编译器能进行类型检查,避免了字符串拼接URL和手动设置请求体带来的错误。
- 与Spring生态无缝集成 :可以轻松利用Spring的依赖注入、AOP、配置属性绑定等特性。例如,API Key、Base URL都可以通过
application.properties配置,并由Spring自动注入。 - 原生支持响应式编程 :HTTP Interface天然支持返回
Mono或Flux类型,这与项目选择的响应式编程模型(Spring WebFlux)完美契合,能够高效处理流式响应。 - 减少第三方依赖 :正如项目特性所述“No third-party library”,它避免了引入另一个重量级的HTTP客户端库,减少了依赖冲突的可能,也让项目更轻量、更纯粹。
2.2 响应式编程(WebFlux)的必然性
项目强制使用Spring WebFlux和响应式类型( Mono , Flux ),这并非为了追赶技术潮流。调用远程AI API,尤其是处理流式响应(Chat Stream)时,本质上是高延迟的I/O操作。阻塞式线程模型(传统的Spring MVC)会在此类场景下大量占用线程资源,导致应用吞吐量下降。
响应式编程模型通过异步非阻塞的方式处理这些I/O,一个事件循环线程可以处理成千上万的并发连接,特别适合AI对话这种“一问一答”但可能长时间等待的场景。当你调用 chatGPTService.stream() 返回一个 Flux 时,数据是一段一段“流”回来的,你可以实时地将每个Token推送给前端(如SSE),实现打字机效果,用户体验极佳。即使你不使用流式,返回 Mono 也能保证整个调用过程是非阻塞的。
注意 :对于不熟悉WebFlux的团队,这确实有学习成本。但项目也提供了折中方案:你可以在Spring Web(Servlet栈)中使用
Mono/Flux,结合Spring Boot 3.2的虚拟线程(Virtual Threads)特性,可以在一定程度上简化并发模型,同时享受非阻塞IO的好处。这需要你仔细权衡线程模型。
2.3 模块化功能设计
Starter的功能模块设计清晰,层层递进:
- 基础聊天与补全 :最核心的功能,对应OpenAI的
/v1/chat/completions和/v1/completions端点。 - 函数调用(Functions) :这是实现AI与外部世界交互的关键。通过
@GPTFunction注解,你可以将任何Java方法暴露给AI作为可调用的工具。 - 结构化输出(Structured Outputs) :利用OpenAI的JSON Mode或Function Calling能力,强制AI返回结构化的数据(如特定的Java Record),直接反序列化为对象,省去了手动解析和校验的麻烦。
- 提示词管理 :将提示词模板外部化到
prompts.properties文件,支持变量替换,并通过@PropertyKey实现IDE智能提示,这解决了提示词工程中维护难的问题。 - 服务接口声明 :通过
@ChatGPTExchange和@ChatCompletion注解,你可以像定义Spring Data Repository一样定义AI服务接口,这是声明式编程的极致体现。 - 批量处理与代理 :支持OpenAI Batch API用于离线大规模处理,以及轻松构建一个OpenAI API代理,方便内网穿透或统一管理。
3. 从零开始集成与核心配置详解
3.1 环境准备与依赖引入
首先,确保你的项目是基于Spring Boot 3.0或更高版本。在 pom.xml 中添加依赖。这里需要注意版本号,建议在集成前查看项目的GitHub Releases或Maven中央库以获取最新稳定版。
<dependency>
<groupId>org.mvnsearch</groupId>
<artifactId>chatgpt-spring-boot-starter</artifactId>
<version>0.8.0</version> <!-- 请检查并使用最新版本 -->
</dependency>
添加后,Maven/Gradle会自动引入相关的传递依赖,主要是Spring WebFlux和相关的Jackson库。你不需要再显式添加OpenAI的官方SDK或其他HTTP客户端。
3.2 关键配置解析
配置集中在 application.properties 或 application.yml 中。最核心的是API密钥和端点。
基础OpenAI配置:
# 必需:你的OpenAI API Key。也可以通过环境变量 OPENAI_API_KEY 设置,优先级更高。
openai.api.key=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 可选:指定使用的模型,默认为 gpt-3.5-turbo。可根据需要改为 gpt-4 等。
openai.model=gpt-3.5-turbo
# 可选:设置API基础URL,默认为 https://api.openai.com/v1。用于对接代理或特定端点。
# openai.api.base-url=https://your-proxy.com/v1
对接Azure OpenAI配置: 如果你使用微软Azure的OpenAI服务,配置方式有所不同。你需要获取Azure门户中的资源终结点和部署名称。
openai.api.key=你的Azure OpenAI密钥
# URL格式固定,需要替换 YOUR_RESOURCE_NAME 和 YOUR_DEPLOYMENT_NAME
openai.api.url=https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2023-05-15
这里有一个 关键细节 :当设置了 openai.api.url 时,Starter会优先使用这个完整的URL进行请求,而忽略 openai.api.base-url 和路径拼接。这是为了兼容Azure等API路径格式不同的服务。
其他实用配置:
# 设置全局的HTTP调用超时时间(毫秒)
openai.api.timeout=60000
# 设置连接池的最大连接数
openai.api.max-connections=100
# 启用或禁用请求/响应的详细日志(调试用)
logging.level.org.mvnsearch.chatgpt=DEBUG
3.3 基础使用:快速发起一次对话
配置完成后,最简单的使用方式就是注入核心的 ChatGPTService 。下面是一个在Controller中使用的完整示例:
import org.mvnsearch.chatgpt.spring.service.ChatGPTService;
import org.mvnsearch.chatgpt.model.ChatCompletionRequest;
import org.mvnsearch.chatgpt.model.ChatCompletionResponse;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/ai")
public class SimpleChatController {
@Autowired
private ChatGPTService chatGPTService;
/**
* 普通聊天接口(非流式)
*/
@PostMapping("/chat")
public Mono<String> chat(@RequestBody String userMessage) {
// 1. 构建请求。ChatCompletionRequest.of() 是快速创建用户消息的辅助方法。
ChatCompletionRequest request = ChatCompletionRequest.of(userMessage);
// 可选:设置系统角色消息,引导AI行为
// request.addMessage(ChatMessage.systemMessage("你是一个专业的翻译助手。"));
// 2. 调用服务。chat()方法返回Mono<ChatCompletionResponse>
return chatGPTService.chat(request)
// 3. 从响应中提取AI的回复文本
.map(ChatCompletionResponse::getReplyText);
}
/**
* 流式聊天接口
* 返回Content-Type为text/event-stream,前端需使用EventSource或Fetch API读取
*/
@GetMapping(value = "/stream-chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String q) {
ChatCompletionRequest request = ChatCompletionRequest.of(q);
// stream()方法返回Flux<ChatCompletionResponse>,每个响应包含一个Token或一段文本
return chatGPTService.stream(request)
.map(ChatCompletionResponse::getReplyText);
}
}
这段代码演示了最基础的集成。 ChatCompletionRequest 和 ChatCompletionResponse 是对OpenAI API请求/响应体的完整封装,你可以通过它们设置温度( temperature )、最大Token数( maxTokens )等所有参数。
4. 进阶特性深度实践与避坑指南
4.1 声明式服务接口:像调用本地方法一样调用AI
这是本Starter最优雅的特性之一。你可以定义一个接口,通过注解来声明一个AI服务。
import org.mvnsearch.chatgpt.spring.ChatGPTExchange;
import org.mvnsearch.chatgpt.spring.ChatCompletion;
import reactor.core.publisher.Mono;
@ChatGPTExchange // 标记这是一个ChatGPT服务交换接口
public interface TranslationService {
/**
* 将文本翻译成中文。
* @ChatCompletion 注解中的字符串是系统提示词,会预置在每次请求中。
*/
@ChatCompletion("You are a professional translator. Translate the following text into Chinese accurately and naturally.")
Mono<String> translateToChinese(String text);
/**
* 支持占位符的翻译方法。
* {0}, {1}, {2} 对应方法参数的顺序。
*/
@ChatCompletion("Translate the following {0} text into {1}: {2}")
Mono<String> translate(String sourceLang, String targetLang, String text);
/**
* 一个更复杂的例子:生成代码示例。
* 返回类型不是String,Starter会尝试将其作为结构化输出处理(需配合@StructuredOutput)。
*/
@ChatCompletion(system = "You are a senior Java developer.", user = "Write a method in Java to reverse a string: {0}")
Mono<CodeExample> generateCodeExample(String requirement);
}
// 对应的结构化输出Record
import org.mvnsearch.chatgpt.spring.StructuredOutput;
import org.mvnsearch.chatgpt.spring.Parameter;
import jakarta.annotation.Nonnull;
@StructuredOutput(name = "code_example")
public record CodeExample(
@Nonnull @Parameter("explanation") String explanation,
@Nonnull @Parameter("code") String code,
@Parameter("time_complexity") String timeComplexity) {
}
定义好接口后,你需要通过 ChatGPTServiceProxyFactory 来创建它的代理Bean。通常在一个 @Configuration 类中完成:
@Configuration
public class ChatGPTConfig {
@Bean
public TranslationService translationService(ChatGPTServiceProxyFactory proxyFactory) {
return proxyFactory.createClient(TranslationService.class);
}
}
之后,你就可以在任何地方 @Autowired 注入 TranslationService ,并像调用普通方法一样使用它了。Spring会在背后自动处理HTTP请求、提示词组装和响应解析。
实操心得 :
@ChatCompletion注解中的提示词是“静态”的,会在每次调用时附加。对于需要动态上下文(如多轮对话历史)的场景,这种方式可能不够灵活。此时,直接使用ChatGPTService并手动构建ChatCompletionRequest和消息列表会更合适。
4.2 函数调用(Functions):赋予AI操作现实世界的能力
函数调用是让AI从“聊天”走向“执行”的关键。Starter通过 @GPTFunction 注解将其实现得非常直观。
第一步:定义功能Bean 创建一个Spring组件,在里面定义你希望AI可以调用的方法。
import org.mvnsearch.chatgpt.spring.GPTFunction;
import org.mvnsearch.chatgpt.spring.Parameter;
import org.springframework.stereotype.Component;
import jakarta.annotation.Nonnull;
import java.util.List;
@Component
public class BusinessFunctions {
public record WeatherQueryRequest(
@Nonnull @Parameter("The name of the city") String city,
@Parameter("The date in YYYY-MM-DD format, defaults to today") String date) {
}
@GPTFunction(name = "get_weather", value = "Get the current weather or forecast for a specific city.")
public String getWeather(WeatherQueryRequest request) {
// 这里模拟或调用真实天气API
String city = request.city();
String date = request.date() != null ? request.date() : "today";
return String.format("The weather in %s on %s is sunny, 25°C.", city, date);
}
public record DBActionRequest(
@Nonnull @Parameter("The SQL command to execute: SELECT, INSERT, UPDATE, or DELETE") String sql) {
}
@GPTFunction(name = "execute_sql", value = "Execute a SQL command on the business database and return results.")
public String executeSQL(DBActionRequest request) {
// !!! 安全警告:在实际生产中,绝不能直接执行用户或AI生成的SQL!
// 这里应进行严格的SQL解析、权限校验和参数化查询。
System.out.println("[模拟执行SQL]: " + request.sql());
// 模拟返回结果
return "id, name, age\n1, Alice, 30\n2, Bob, 25";
}
}
第二步:在聊天中启用函数 当你构建 ChatCompletionRequest 时,需要指定本次对话可用的函数列表。
@Service
public class WeatherAssistantService {
@Autowired
private ChatGPTService chatGPTService;
public Mono<String> chatWithWeatherFunction(String userQuestion) {
// 1. 构建请求,并指定可用的函数名
ChatCompletionRequest request = ChatCompletionRequest.functions(
userQuestion, // 用户问题
List.of("get_weather") // 允许调用的函数名列表
);
// 2. 可以添加系统消息来设定AI的角色
request.addMessage(ChatMessage.systemMessage("You are a helpful weather assistant. Use the provided functions to get weather information."));
// 3. 调用ChatGPT
return chatGPTService.chat(request)
.flatMap(response -> {
// 4. 检查响应中是否包含函数调用
for (ChatMessage message : response.getReply()) {
FunctionCall functionCall = message.getFunctionCall();
if (functionCall != null) {
// 5. 执行函数调用
Object functionResult = functionCall.getFunctionStub().call();
// 6. (可选)将函数执行结果再次发送给AI,让AI生成最终回答给用户
// 这需要构建一个新的请求,包含原始对话历史和函数执行结果
ChatCompletionRequest followUpRequest = ChatCompletionRequest.functions(userQuestion, List.of("get_weather"));
followUpRequest.getMessages().addAll(request.getMessages()); // 保留历史
followUpRequest.addMessage(ChatMessage.functionMessage(functionCall.getName(), functionResult.toString()));
return chatGPTService.chat(followUpRequest).map(ChatCompletionResponse::getReplyText);
}
}
// 如果没有函数调用,直接返回AI的回复
return Mono.just(response.getReplyText());
});
}
}
response.getReplyCombinedText() 方法是一个便利方法,它会尝试将函数调用和AI的思考过程组合成一个对人类友好的文本,但在需要精确执行函数时,还是推荐像上面一样手动处理 FunctionCall 。
避坑指南 :
- 安全性 :像
execute_sql这样的函数极其危险。必须对AI生成的SQL进行白名单校验、语法解析、防止注入攻击,最好只允许执行特定的存储过程或经过严格校验的查询模板。- 参数校验 :
@Parameter注解中的required和description非常重要,它们会帮助AI更好地理解如何填充参数。确保描述清晰准确。- 错误处理 :函数执行可能失败。你的代码应该处理异常,并返回一个清晰的错误信息,AI可能会将这个信息解释给用户。
- 嵌套对象 :当前版本(根据文档)可能不支持嵌套的
object类型参数。如果函数需要复杂参数,尽量将其扁平化,或用JSON字符串传递,在函数内部解析。
4.3 结构化输出:让AI返回规整的数据
当你需要AI返回一个格式固定的数据结构(例如一个用户信息对象、一个订单摘要)时,结构化输出比让AI返回自由文本然后手动解析要可靠得多。
定义输出结构: 使用Java Record(推荐)或Class,并用 @StructuredOutput 标记。
import org.mvnsearch.chatgpt.spring.StructuredOutput;
import org.mvnsearch.chatgpt.spring.Parameter;
import jakarta.annotation.Nonnull;
import java.util.List;
@StructuredOutput(name = "user_profile_extract")
public record UserProfile(
@Nonnull @Parameter("Full name of the user") String name,
@Nonnull @Parameter("Email address") String email,
@Parameter("Company name, if mentioned") String company,
@Parameter("List of job titles or roles mentioned") List<String> roles,
@Parameter("Summary of skills or technologies mentioned") String skillsSummary) {
}
在服务接口中使用: 只需将方法的返回类型定义为你的Record,Starter和底层的AI模型(需要支持JSON Mode,如gpt-3.5-turbo-1106及以上版本)会协同工作,确保返回JSON并自动反序列化。
@ChatGPTExchange
public interface ProfileParserService {
@ChatCompletion("Extract the user profile information from the following text. Return only the structured data.")
Mono<UserProfile> extractProfileFromText(String text);
}
调用 extractProfileFromText 方法,你将直接得到一个 UserProfile 对象,而不是需要解析的字符串。
技术原理 :底层实现通常利用OpenAI的Function Calling功能,隐式地创建一个描述输出结构的“虚拟函数”,要求AI以调用该函数的形式返回数据。或者,对于支持JSON Mode的模型,直接在请求中设置
response_format: { "type": "json_object" },并在提示词中严格要求JSON格式。Starter帮你隐藏了这些细节。
4.4 提示词工程与管理
管理散落在代码各处的提示词字符串是AI应用开发的一大痛点。Starter提供了基于属性文件的提示词模板管理方案。
创建 prompts.properties 文件: 将其放在 src/main/resources 目录下。
# prompts.properties
email.translator=You are an email translation expert. Translate the following English email into polite and professional Chinese business language. Keep the tone and intent.\n\n{0}
code.reviewer=You are a senior {0} developer. Review the following code snippet for potential bugs, performance issues, and style improvements. Provide specific suggestions.\n\n{1}
story.generator=Generate a short children's story about a {0} named {1}. The story should have a moral about {2}. The story should be no longer than 3 paragraphs.
在代码中使用 PromptManager :
@Service
public class PromptService {
@Autowired
private PromptManager promptManager;
public Mono<String> translateEmail(String englishEmail) {
String promptTemplate = promptManager.prompt("email.translator");
// 使用MessageFormat进行变量替换
String finalPrompt = MessageFormat.format(promptTemplate, englishEmail);
ChatCompletionRequest request = ChatCompletionRequest.of(finalPrompt);
return chatGPTService.chat(request).map(ChatCompletionResponse::getReplyText);
}
}
更优雅的方式:与 @ChatCompletion 注解结合 @ChatCompletion 注解的 system 、 user 、 assistant 属性可以直接引用提示词键。
@ChatGPTExchange
public interface PromptBasedService {
@ChatCompletion(system = "prompt:email.translator") // 使用 `prompt:` 前缀引用
Mono<String> translateEmail(String email);
}
提示词Lambda化: 这是非常函数式的一个特性,将提示词模板转换为一个 Function ,便于组合和流式处理。
Function<String, Mono<String>> translateFunc = chatGPTService.promptAsLambda("email.translator");
Function<String, Mono<String>> summarizeFunc = chatGPTService.promptAsLambda("email.summarizer");
// 组合使用:先翻译,再总结
Mono.just("Long English email text...")
.flatMap(translateFunc)
.flatMap(summarizeFunc)
.subscribe(result -> System.out.println("Summary in Chinese: " + result));
经验技巧 :
- IDE支持 :在IntelliJ IDEA中,为
prompts.properties文件添加@PropertyKey(resourceBundle = "prompts")注解,可以获得代码自动补全和导航功能。- 外部化与版本控制 :将业务逻辑相关的提示词放在配置文件中,方便非开发人员(如产品经理、AI训练师)进行修改和优化,而无需改动代码和重新部署。
- 多环境配置 :可以为不同环境(开发、测试、生产)准备不同的
prompts-{env}.properties文件,实现提示词的差异化配置。
5. 生产环境考量与高级配置
5.1 性能、超时与重试
调用外部AI API是网络I/O操作,必须配置合理的超时和重试策略。
# application.properties
# 连接超时(毫秒)
openai.api.connect-timeout=5000
# 读取响应超时(毫秒),对于长文本生成或流式响应,这个值要设大
openai.api.read-timeout=60000
# 写入请求超时(毫秒)
openai.api.write-timeout=10000
# 响应式调度器线程池大小,影响并发能力
spring.threads.virtual.enabled=true # 如果使用虚拟线程
对于重试,Starter可能依赖底层的Spring Reactor Netty客户端或你配置的HTTP客户端。你可以通过自定义 WebClient Bean来配置更复杂的策略,如指数退避重试。
@Bean
public WebClient customOpenAIWebClient(OpenAIProperties properties) {
// 基于Starter的配置构建一个带有重试机制的WebClient
return WebClient.builder()
.baseUrl(properties.getApi().getBaseUrl())
.defaultHeader("Authorization", "Bearer " + properties.getApi().getKey())
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.responseTimeout(Duration.ofMillis(properties.getApi().getReadTimeout()))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getApi().getConnectTimeout())
))
.filter(ExchangeFilterFunction.ofRequestProcessor(
clientRequest -> Mono.just(ClientRequest.from(clientRequest)
.header("User-Agent", "MySpringBootApp/1.0")
.build())
))
.build();
}
// 然后需要通过某种方式让Starter使用这个自定义的WebClient,可能需要查看Starter的扩展点或重写配置类。
5.2 异常处理与监控
AI API可能返回各种错误:认证失败、额度不足、模型过载、内容过滤等。你需要全局处理这些异常。
@RestControllerAdvice
public class GlobalAIExceptionHandler {
@ExceptionHandler(OpenAIHttpException.class) // 假设Starter抛出的异常类型
public ResponseEntity<ErrorResponse> handleOpenAIException(OpenAIHttpException ex) {
log.error("OpenAI API call failed: {}", ex.getMessage(), ex);
// 根据ex.getStatusCode()等判断错误类型
if (ex.getStatusCode() == 401) {
return ResponseEntity.status(401).body(new ErrorResponse("AI服务认证失败,请检查配置"));
} else if (ex.getStatusCode() == 429) {
return ResponseEntity.status(429).body(new ErrorResponse("请求过于频繁,请稍后再试"));
} else if (ex.getStatusCode() == 503) {
return ResponseEntity.status(503).body(new ErrorResponse("AI服务暂时不可用"));
}
return ResponseEntity.status(500).body(new ErrorResponse("AI服务处理出错"));
}
@ExceptionHandler(TimeoutException.class)
public ResponseEntity<ErrorResponse> handleTimeout(TimeoutException ex) {
return ResponseEntity.status(504).body(new ErrorResponse("AI服务响应超时,请重试"));
}
}
同时,集成Micrometer等监控组件,对API调用的耗时、成功率、Token使用量进行监控和告警,这对于成本控制和稳定性保障至关重要。
5.3 对接其他大模型(如DeepSeek、Ollama)
Starter的灵活性在于其HTTP接口抽象。只要目标API与OpenAI ChatCompletions API兼容或高度相似,就可以通过修改配置轻松对接。
对接DeepSeek:
openai.api.base-url=https://api.deepseek.com
openai.api.key=sk-your-deepseek-key
openai.model=deepseek-chat # 或 deepseek-coder
对接本地部署的Ollama(需使用其OpenAI兼容端点):
openai.api.base-url=http://localhost:11434/v1 # Ollama的OpenAI兼容端点
openai.api.key=ollama # Ollama通常不需要key,但有些封装需要任意字符串
openai.model=llama2 # 你的本地模型名称
对接不完全兼容的API: 如果目标API路径或请求/响应格式有差异,你可能需要自定义 OpenAIChatAPI 的实现。Starter通常提供了 OpenAIChatAPI 这个Bean,你可以通过创建自己的配置类来覆盖它,或者实现一个 ClientHttpConnector 来拦截和修改请求/响应。
5.4 构建API代理
有时你可能需要在自己的服务器上封装一层代理,用于添加统一认证、日志记录、限流或转换API格式。利用Starter提供的 OpenAIChatAPI Bean,这非常简单。
@RestController
@RequestMapping("/proxy/openai")
public class OpenAIProxyController {
@Autowired
private OpenAIChatAPI openAIChatAPI; // 核心API客户端
@PostMapping("/v1/chat/completions")
public Mono<ChatCompletionResponse> proxyChatCompletions(@RequestBody ChatCompletionRequest request,
@RequestHeader(value = "Authorization", required = false) String authHeader) {
// 1. 在这里进行身份验证、权限检查、请求日志记录、限流...
log.info("Proxying OpenAI request for model: {}", request.getModel());
// if (!isValidToken(authHeader)) { return Mono.error(...); }
// 2. 可以修改请求,例如强制使用某个模型,或添加系统提示词
// request.setModel("gpt-3.5-turbo");
// request.addMessage(ChatMessage.systemMessage("你是一个中文助手。"));
// 3. 代理请求到真实的OpenAI API
return openAIChatAPI.chat(request);
}
@GetMapping(value = "/v1/chat/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> proxyStreamChatCompletions(@RequestParam String message) {
// 处理流式代理
ChatCompletionRequest request = ChatCompletionRequest.of(message);
request.setStream(true);
return openAIChatAPI.stream(request)
.map(response -> ServerSentEvent.builder(response.getReplyText()).build());
}
}
这样,你的前端应用就可以调用 http://your-server/proxy/openai/v1/chat/completions ,而无需暴露原始的OpenAI API密钥给前端。
6. 常见问题排查与实战技巧
6.1 依赖冲突与版本问题
问题 :启动项目时出现 ClassNotFoundException 或 MethodNotFoundException ,特别是与Spring Framework、Reactor Netty或Jackson相关的错误。
排查 :
- 使用
mvn dependency:tree或gradle dependencies命令检查依赖树,查看是否有多个不同版本的Spring、Netty或Jackson被引入。 - 确认Spring Boot版本是否为3.0+。Spring Boot 2.x与Spring Framework 6不兼容。
- 本Starter可能依赖特定版本的
spring-boot-starter-webflux。确保你没有同时引入spring-boot-starter-web(Servlet栈),这会导致冲突。如果必须使用Web Servlet栈,请参考前文关于虚拟线程的说明,并排除WebFlux的Tomcat依赖,使用Reactor Netty作为底层客户端即可。
解决 :在 pom.xml 中使用 <exclusions> 排除冲突的传递依赖,或统一升级相关依赖到兼容的版本。
6.2 流式响应(SSE)不工作或中断
问题 :前端通过EventSource连接流式接口,连接很快关闭,或者收到不完整的数据。
排查 :
- 检查响应头 :确保Controller方法 produces 属性为
MediaType.TEXT_EVENT_STREAM_VALUE。 - 检查网络层 :是否有网关、负载均衡器或防火墙中断了长连接?检查其超时配置。
- 检查服务器端超时 :确保Spring WebFlux和底层HTTP客户端的读超时(
read-timeout)设置得足够长,大于AI生成完整响应所需的时间。 - 检查响应格式 :流式响应应该是多个SSE数据块。使用
curl或Postman直接测试接口,看返回的是否是data: {...}格式。可能是响应被意外缓冲或转换了。
解决 :
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> stream(@RequestParam String q) {
return chatGPTService.stream(ChatCompletionRequest.of(q))
.map(response -> ServerSentEvent.builder(response.getReplyText()).build()) // 包装成标准SSE格式
.onErrorResume(e -> Flux.just(ServerSentEvent.builder("[ERROR] " + e.getMessage()).build()));
}
6.3 函数调用未被触发
问题 :明明定义了 @GPTFunction ,并在请求中指定了函数名,但AI回复仍然是普通文本,没有调用函数。
排查 :
- 函数名匹配 :检查
ChatCompletionRequest.functions(prompt, functionNames)中传入的函数名列表,是否与@GPTFunction(name = "...")中定义的name完全一致(大小写敏感)。 - Bean扫描 :确保定义了
@GPTFunction的类被Spring容器管理(即使用了@Component,@Service等注解),并且所在的包在Spring Boot的主应用扫描路径下。 - 提示词引导 :AI有时需要明确的指令才会调用函数。在系统消息或用户消息中明确指示它使用工具,例如:“请使用提供的get_weather函数来查询天气。”
- 模型支持 :确保你使用的AI模型(如
gpt-3.5-turbo或gpt-4)支持函数调用功能。较旧的模型可能不支持。
6.4 结构化输出反序列化失败
问题 :使用 @StructuredOutput Record作为返回类型时,收到 JsonProcessingException 或返回的Mono发生错误。
排查 :
- Record定义 :检查Record中的所有字段是否都有正确的getter/setter(Record默认有)。确保
@Parameter注解的描述清晰。 - AI输出格式 :虽然要求结构化输出,但AI有时仍会在JSON外包裹额外文本。启用DEBUG日志,查看原始的AI响应内容,确认是否是纯JSON。
- 提示词约束 :在
@ChatCompletion的提示词中,必须强烈要求AI“只返回JSON数据,不要有任何其他解释或标记”。例如:“Return the data strictly as a JSON object matching the defined schema, with no additional text.” - Jackson配置 :确保项目中Jackson库版本兼容,并能正确处理Java Record。Spring Boot 3默认使用较新的Jackson,通常没问题。
6.5 提示词模板加载失败
问题 :使用 prompt:key 语法或 PromptManager 时,提示找不到对应的提示词。
排查 :
- 文件位置与名称 :确认
prompts.properties文件位于src/main/resources目录下,且构建后存在于classpath根目录。 - 键名正确性 :检查代码中引用的key(如
email.translator)是否与properties文件中的键完全一致。 - 编码问题 :确保
prompts.properties文件使用UTF-8编码,特别是包含中文等非ASCII字符时。 - 多模块项目 :在多模块的Maven/Gradle项目中,确保包含
prompts.properties的模块被正确依赖,并且资源文件被打包。
6.6 关于虚拟线程(Virtual Threads)的使用
项目文档提到可以与Spring Web(Servlet栈)和虚拟线程协同工作。这确实是一个降低响应式编程心智负担的途径。
配置 :
# application.properties
spring.threads.virtual.enabled=true
在Controller中使用 :
@RestController
public class BlockingStyleController {
@Autowired
private TranslationService translationService; // 返回Mono的服务
@GetMapping("/translate")
public String translateBlocking(@RequestParam String text) throws InterruptedException, ExecutionException {
// 在虚拟线程中,可以“阻塞”地等待Mono的结果,而不会占用宝贵的平台线程
return translationService.translateToChinese(text).block(); // 使用 block() 方法
}
}
重要提示 :虽然可以 block() ,但这并不意味着你可以随意编写阻塞IO代码。虚拟线程的优势在于其轻量级,在遇到阻塞操作(如 block() 等待网络响应)时,能够被挂起,从而释放底层的载体线程去服务其他请求。但如果你在虚拟线程中执行CPU密集型计算或同步的阻塞IO(如传统的JDBC),仍然会浪费资源。对于数据库访问,仍推荐使用响应式驱动(如R2DBC)或至少是连接池配合异步方式。
更多推荐



所有评论(0)