1. 项目概述与核心价值

如果你正在用Spring Boot做Java后端开发,同时又想在自己的应用里快速集成类似ChatGPT这样的AI对话能力,那你大概率会遇到一个头疼的问题:怎么把OpenAI那套REST API优雅地、符合Spring Boot哲学地整合进来?是直接写一堆 RestTemplate 或者 WebClient 的调用代码,还是自己封装一个笨重的Service?今天要聊的这个 chatgpt-spring-boot-starter 项目,就是来解决这个痛点的。它不是一个简单的HTTP客户端包装,而是一个深度融入Spring生态的、声明式的AI能力集成方案。

简单来说,这个starter让你能用写Spring MVC Controller或者Feign Client的那种感觉,来调用ChatGPT。你只需要定义一个接口,加几个注解,剩下的复杂网络通信、序列化、流处理,它全帮你搞定了。更厉害的是,它原生支持了Spring Boot 3.0+和WebFlux的响应式编程模型,这意味着从设计上就支持高并发、非阻塞的AI调用,非常适合构建需要实时交互的AI应用。项目还紧跟OpenAI API的最新特性,比如函数调用(Functions)和结构化输出(Structured Outputs),让你能轻松实现“让AI写SQL,然后自动执行”或者“让AI返回一个规整的Java对象”这类复杂场景。

我自己在几个内部工具和实验性项目中用了它,最大的感受是“开发体验极好”。你不再需要关心HTTP细节,可以把精力完全放在业务逻辑和Prompt设计上。接下来,我会带你从零开始,深入这个starter的每一个核心特性,分享我在实际使用中踩过的坑和总结的最佳实践。

2. 项目快速入门与环境搭建

2.1 依赖引入与基础配置

上手的第一步是引入依赖。如果你的项目是基于Spring Boot 3.x(这是必须的,因为它依赖Spring 6的HTTP Interface特性),在 pom.xml 里添加以下依赖即可。我建议直接使用中央仓库的最新版本,以获得所有新特性和修复。

<dependency>
    <groupId>org.mvnsearch</groupId>
    <artifactId>chatgpt-spring-boot-starter</artifactId>
    <version>0.8.0</version> <!-- 请检查并使用最新版本 -->
</dependency>

注意 :这个starter内部基于Spring 6的HTTP Interface和WebClient,没有引入额外的第三方HTTP客户端库,保持了依赖的纯净。这也是它能轻松支持GraalVM原生镜像编译的原因之一。

引入依赖后,最关键的一步是配置你的API密钥。在 application.properties application.yml 中,配置你的OpenAI API Key:

# 最基本的配置,设置你的OpenAI API Key
openai.api.key=sk-your-openai-api-key-here

这里有个小技巧:你也可以不把密钥写在配置文件里,而是通过环境变量 OPENAI_API_KEY 来设置。这在容器化部署或者为了安全不将密钥提交到代码库时非常有用。starter会优先读取环境变量。

如果你的网络环境需要访问OpenAI的代理,或者你使用的是Azure OpenAI服务,那么就需要配置自定义的API端点:

# 使用Azure OpenAI的配置示例
openai.api.key=your-azure-openai-api-key
openai.api.url=https://your-resource-name.openai.azure.com/openai/deployments/your-deployment-name/chat/completions?api-version=2023-05-15

配置好之后,一个最简单的ChatGPT服务就已经可用了。starter会自动配置一个 ChatGPTService Bean,你可以直接 @Autowired 注入使用。

2.2 第一个聊天接口实现

让我们写一个最简单的REST接口来验证集成是否成功。创建一个 @RestController ,注入 ChatGPTService ,然后提供一个聊天端点。

import org.mvnsearch.chatgpt.spring.service.ChatGPTService;
import org.mvnsearch.chatgpt.model.ChatCompletionRequest;
import org.mvnsearch.chatgpt.model.ChatCompletionResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/chat")
public class SimpleChatController {

    @Autowired
    private ChatGPTService chatGPTService;

    @PostMapping("/simple")
    public Mono<String> simpleChat(@RequestBody String userMessage) {
        // 1. 构建请求,用户消息作为内容
        ChatCompletionRequest request = ChatCompletionRequest.of(userMessage);
        
        // 2. 调用ChatGPT服务,得到响应(Mono是响应式编程中的单值异步容器)
        Mono<ChatCompletionResponse> responseMono = chatGPTService.chat(request);
        
        // 3. 从响应中提取AI的回复文本
        return responseMono.map(ChatCompletionResponse::getReplyText);
    }
}

启动你的Spring Boot应用,用 curl 或者Postman测试一下:

curl -X POST http://localhost:8080/api/chat/simple \
  -H "Content-Type: application/json" \
  -d '"你好,世界!"'

如果一切正常,你应该会收到AI的回复,比如“你好!有什么可以帮助你的吗?”。这个简单的例子展示了最基础的同步(虽然底层是异步的)聊天调用。但它的能力远不止于此,接下来我们会看到如何利用声明式接口、流式响应等高级特性。

2.3 流式聊天体验

对于需要实时显示AI思考过程的应用场景(比如仿ChatGPT的网页对话),流式响应(Streaming)是必备功能。这个starter对此提供了开箱即用的支持。 ChatGPTService 提供了一个 stream 方法,它返回一个 Flux<ChatCompletionResponse> (可以理解为多个响应事件的流)。

@GetMapping("/stream")
public Flux<String> streamChat(@RequestParam String question) {
    ChatCompletionRequest request = ChatCompletionRequest.of(question);
    return chatGPTService.stream(request)
            .map(ChatCompletionResponse::getReplyText);
}

在浏览器或支持Server-Sent Events (SSE)的客户端中访问这个端点,你会看到AI的回答是一个词一个词“流”出来的,而不是等待完整生成后再一次性返回。这对于提升用户体验至关重要。

实操心得 :在开发流式接口时,务必确保你的前端或客户端能够正确处理 text/event-stream 格式(Spring WebFlux默认会为此端点设置此Content-Type)。同时,注意处理连接中断的情况,在服务端,流会在客户端断开时自动取消,避免资源浪费。

3. 声明式服务接口:像Feign一样调用AI

如果你用过Spring Cloud OpenFeign,一定会喜欢那种通过定义接口就能进行远程调用的方式。 chatgpt-spring-boot-starter 把同样的理念带到了AI调用上。这是我认为这个库最优雅、最强大的特性。

3.1 定义你的AI服务接口

你不再需要手动构造 ChatCompletionRequest 对象。相反,你可以定义一个接口,用注解来描述每次调用的行为。

import org.mvnsearch.chatgpt.spring.service.ChatGPTExchange;
import org.mvnsearch.chatgpt.spring.service.ChatCompletion;
import reactor.core.publisher.Mono;

@ChatGPTExchange // 标记这是一个ChatGPT服务交换接口
public interface TranslationService {

    /**
     * 将任意文本翻译成中文。
     * @ChatCompletion 注解中的字符串是系统提示词(System Prompt),它会指导AI的角色和行为。
     */
    @ChatCompletion("You are a professional translator. Translate the following text into concise and natural Chinese.")
    Mono<String> translateToChinese(String text);

    /**
     * 在两种语言间翻译。
     * 提示词中的 {0}, {1}, {2} 是占位符,会被方法参数按顺序替换。
     * 这利用了Java的MessageFormat进行模板渲染。
     */
    @ChatCompletion("You are a professional translator. Translate the following text from {0} to {1}: {2}")
    Mono<String> translate(String sourceLang, String targetLang, String text);

    /**
     * 让AI扮演代码助手,解释一段代码。
     * 系统提示词定义了AI的角色和任务。
     */
    @ChatCompletion(system = "You are a senior software engineer. Explain the given code snippet in simple terms.", user = "Explain this code: {0}")
    Mono<String> explainCode(String codeSnippet);
}

这个接口定义非常清晰: translateToChinese 方法会指示AI扮演专业翻译,并将传入的 text 翻译成中文。 translate 方法则更灵活,允许指定源语言和目标语言。 explainCode 方法展示了如何使用 system user 属性分别定义系统消息和用户消息,这使得提示词的管理更加结构化。

3.2 创建与使用服务Bean

定义了接口,还需要告诉Spring如何创建它的实现。这通过一个配置类来完成:

import org.mvnsearch.chatgpt.spring.service.ChatGPTServiceProxyFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatGPTConfig {

    @Bean
    public TranslationService translationService(ChatGPTServiceProxyFactory proxyFactory) {
        // proxyFactory是starter自动配置的,它负责为@ChatGPTExchange接口生成代理实现
        return proxyFactory.createClient(TranslationService.class);
    }
}

现在,你就可以像使用普通的Spring Bean一样,在任何地方注入并使用 TranslationService 了:

@Service
public class MyBusinessService {
    @Autowired
    private TranslationService translationService;

    public void doSomething() {
        String englishText = "The quick brown fox jumps over the lazy dog.";
        translationService.translateToChinese(englishText)
                .subscribe(translated -> {
                    // 处理翻译结果,例如保存到数据库或返回给前端
                    System.out.println("翻译结果: " + translated);
                });
    }
}

为什么这样设计? 这种声明式的方式有巨大优势。首先,它实现了 高度解耦 ,业务代码完全不知道底层是调用OpenAI还是其他兼容API。其次,它 集中管理了Prompt ,所有与AI交互的“指令”都清晰地定义在接口注解里,方便维护和优化。最后,它天然支持 响应式编程 (返回 Mono / Flux ),与现代Spring应用架构完美契合。

3.3 深入理解@ChatCompletion注解

@ChatCompletion 注解是定义AI交互行为的核心。除了上面用到的 value (或 user )和 system 属性,它还有其他重要参数:

  • functions : 指定本次调用允许AI使用的函数列表(函数调用功能在下文详述)。
  • temperature , maxTokens : 覆盖全局默认参数,为特定接口方法设置不同的生成参数。
  • model : 指定使用的模型,例如 gpt-4-turbo-preview 。如果不指定,则使用全局配置的 openai.model
@ChatCompletion(
    system = "你是一个严格的代码审查员。",
    user = "审查这段Java代码:{0}",
    functions = {"check_security", "suggest_refactor"}, // 允许AI调用这两个函数
    temperature = 0.2, // 低温度,输出更确定、更保守
    maxTokens = 500
)
Mono<String> codeReview(String code);

这种细粒度的控制让你能为不同的任务定制最合适的AI行为。

4. 解锁强大能力:函数调用(Functions)实战

OpenAI的函数调用功能允许AI在对话中决定调用你预先定义好的工具函数,并将结果纳入后续的思考中。这彻底打破了传统“一问一答”的模式,开启了“AI驱动工作流”的大门。 chatgpt-spring-boot-starter 通过 @GPTFunction 注解让这一功能的集成变得异常简单。

4.1 如何定义AI可调用的函数

首先,你需要在一个Spring管理的Bean(比如 @Component )中定义你的函数。关键是用 @GPTFunction 注解标记方法,并用 @Parameter 注解描述参数。

import org.mvnsearch.chatgpt.spring.service.GPTFunction;
import org.mvnsearch.chatgpt.spring.service.Parameter;
import org.springframework.stereotype.Component;
import jakarta.annotation.Nonnull;
import java.util.List;

@Component
public class BusinessFunctions {

    // 定义一个发送邮件的函数
    public record SendEmailRequest(
            @Nonnull // JSR-305注解,标记参数非空,会体现在函数schema中
            @Parameter("收件人邮箱地址列表") // 描述参数,帮助AI理解
            List<String> recipients,
            
            @Nonnull
            @Parameter("邮件主题")
            String subject,
            
            @Parameter("邮件正文内容")
            String content) {
    }

    @GPTFunction(name = "send_email", value = "向指定的收件人发送一封电子邮件")
    public String sendEmail(SendEmailRequest request) {
        // 这里是实际的业务逻辑,例如调用邮件发送服务
        System.out.println("[模拟发送邮件]");
        System.out.println("收件人: " + String.join(", ", request.recipients()));
        System.out.println("主题: " + request.subject());
        System.out.println("内容: " + request.content());
        // 返回执行结果,这个结果会被传回给AI
        return "邮件已成功发送至 " + String.join(", ", request.recipients());
    }

    // 定义一个查询数据库的函数
    public record QueryDatabaseRequest(
            @Nonnull
            @Parameter(value = "要执行的SQL查询语句", required = true)
            String sql) {
    }

    @GPTFunction(name = "query_database", value = "在公司的员工数据库上执行给定的SQL查询,并返回结果")
    public String queryDatabase(QueryDatabaseRequest request) {
        System.out.println("[模拟执行SQL]: " + request.sql());
        // 模拟返回一个CSV格式的结果
        return "id,name,department,salary\n1,张三,研发,15000\n2,李四,市场,12000\n3,王五,研发,16000";
    }
}

定义好之后,starter会在启动时自动扫描这些被 @GPTFunction 注解的方法,将它们注册到全局的函数库中。AI在需要的时候,就能“看到”并“调用”这些函数。

4.2 在对话中触发函数调用

现在,我们看看如何在一次聊天对话中,让AI使用我们刚定义的函数。你需要构建一个特殊的请求,指明本次对话允许AI使用哪些函数。

@RestController
public class FunctionChatController {
    @Autowired
    private ChatGPTService chatGPTService;

    @PostMapping("/chat-with-email")
    public Mono<String> chatWithEmailFunction(@RequestBody String userRequest) {
        // 关键:使用ChatCompletionRequest.functions()方法构建请求
        // 第一个参数是用户的问题,第二个参数是允许调用的函数名列表
        ChatCompletionRequest request = ChatCompletionRequest.functions(
                userRequest, 
                List.of("send_email") // 只允许调用'send_email'函数
        );
        
        return chatGPTService.chat(request)
                .map(response -> {
                    // getReplyCombinedText() 方法非常有用!
                    // 如果AI调用了函数,它会返回一个包含函数调用和AI思考的格式化文本。
                    // 如果没调用函数,则直接返回AI的回复。
                    return response.getReplyCombinedText();
                });
    }
}

假设用户请求是:“请给libing@example.com和sam@example.com发封邮件,通知他们明天下午两点开会。” AI的分析过程可能是:

  1. 理解用户意图是发送邮件。
  2. 发现它有一个叫 send_email 的函数可用。
  3. 从用户消息中提取出收件人、主题(可能需要推断)、内容。
  4. 生成一个对 send_email 函数的调用请求,并暂停回复,等待函数执行结果。

response.getReplyCombinedText() 返回的文本可能会是这样的:

我需要为您发送这封邮件。我将调用发送邮件功能。

<function_calls>
<function_call name="send_email">
<parameters>
{
"recipients": ["libing@example.com", "sam@example.com"],
"subject": "会议通知",
"content": "您好,请于明天下午两点准时参加会议。"
}
</parameters>
</function_call>
</function_calls>

4.3 手动执行函数并继续对话

当AI决定调用函数时,它不会自动执行。你需要从响应中取出函数调用的信息,手动执行对应的Java方法,然后将结果返回给AI,让AI基于结果生成最终回复给用户。

public Mono<String> executeFunctionCall(ChatCompletionResponse response) {
    for (ChatMessage message : response.getReply()) {
        FunctionCall functionCall = message.getFunctionCall();
        if (functionCall != null) {
            // 1. 获取函数存根,它包含了要调用的函数信息和参数
            FunctionStub stub = functionCall.getFunctionStub();
            try {
                // 2. 执行函数!stub.call()会找到对应的Bean方法并传入参数
                Object functionResult = stub.call();
                System.out.println("函数执行结果: " + functionResult);
                
                // 3. 将函数执行结果作为新的消息,继续发送给AI
                ChatCompletionRequest followUpRequest = ChatCompletionRequest.functions(
                    "用户的原问题...", 
                    List.of("send_email")
                );
                // 添加之前的对话历史(可选,但推荐用于多轮)
                // followUpRequest.setMessages(history);
                // 添加函数执行结果的消息
                followUpRequest.addMessage(ChatMessage.functionMessage(functionCall.getName(), functionResult.toString()));
                
                // 4. 再次调用chat,让AI生成最终回复
                return chatGPTService.chat(followUpRequest)
                        .map(nextResponse -> nextResponse.getReplyCombinedText());
            } catch (Exception e) {
                return Mono.just("调用函数时出错: " + e.getMessage());
            }
        }
    }
    // 如果没有函数调用,直接返回AI的回复
    return Mono.just(response.getReplyCombinedText());
}

这个过程模拟了AI与外部工具的交互循环:AI提议调用工具 -> 用户(你的代码)执行工具 -> 将工具结果反馈给AI -> AI生成最终回答。

避坑指南 :函数调用目前主要支持基本数据类型( string , number , integer , array )。复杂的嵌套 object 在参数传递上可能有限制。定义函数参数时,尽量使用扁平化的Record结构。如果必须传递复杂对象,可以考虑将其序列化为JSON字符串作为单个 string 参数传递,然后在函数内部反序列化。

5. 结构化输出:让AI返回规整的数据

很多时候,我们不仅想要AI的一段文本回复,更希望它返回结构化的数据,比如一个JSON对象、一个列表,或者直接映射成我们的Java Bean。OpenAI的结构化输出功能和这个starter的 @StructuredOutput 注解就是为了这个场景而生。

5.1 定义结构化输出的格式

假设我们想让AI根据问题生成一个包含代码示例、解释和依赖的Java教学案例。我们首先定义一个Record(或Class)来描述这个结构:

import org.mvnsearch.chatgpt.spring.service.StructuredOutput;
import org.mvnsearch.chatgpt.spring.service.Parameter;
import jakarta.annotation.Nonnull;
import java.util.List;

// 使用@StructuredOutput注解标记这个Record,并给它一个名字
@StructuredOutput(name = "java_tutorial_example")
public record JavaTutorialExample(
        @Nonnull
        @Parameter("对代码示例的简要解释")
        String explanation,
        
        @Nonnull
        @Parameter("针对问题的直接答案")
        String answer,
        
        @Nonnull
        @Parameter("完整的、可运行的Java代码片段")
        String code,
        
        @Parameter("代码所需的Maven依赖项,例如 'com.google.guava:guava:32.1.3-jre'")
        List<String> dependencies
) {}

这个Record的每个字段都用 @Parameter 进行了描述,这些描述会作为Schema的一部分传给AI,指导它生成符合要求的字段内容。

5.2 在声明式接口中使用结构化输出

在你的 @ChatGPTExchange 接口中,只需将方法的返回类型设置为你的Record(包装在 Mono 中),AI就会自动尝试将回复填充到这个结构里。

@ChatGPTExchange
public interface CodingAssistantService {

    @ChatCompletion("你是一个资深的Java开发助手。请根据用户的问题,提供一个完整的代码示例。")
    Mono<JavaTutorialExample> generateJavaExample(@Param("question") String userQuestion);

    // 另一个例子:让AI分析一段代码并返回结构化的分析报告
    @StructuredOutput(name = "code_analysis")
    public record CodeAnalysis(String complexity, List<String> issues, String suggestion) {}
    
    @ChatCompletion(system = "你是一个静态代码分析工具。", user = "分析这段代码:{0}")
    Mono<CodeAnalysis> analyzeCode(String code);
}

当调用 generateJavaExample(“如何用Java读取文件?”) 时,AI不会返回一段自由文本,而是会返回一个已经填充好的 JavaTutorialExample 对象,你可以直接访问它的 explanation code 等属性。

JavaTutorialExample example = codingAssistant.generateJavaExample("如何用Stream API对列表求和?")
        .block(); // 注意:实际生产环境应避免阻塞,这里仅为示例

System.out.println("答案:" + example.answer());
System.out.println("示例代码:");
System.out.println(example.code());
// 输出可能:
// 答案:可以使用`stream().mapToInt().sum()`或`stream().reduce()`。
// 示例代码:List<Integer> numbers = Arrays.asList(1,2,3); int sum = numbers.stream().mapToInt(i->i).sum();

核心原理 :底层上,starter利用了OpenAI的 response_format: { "type": "json_object" } 参数,并在系统提示词中附加了基于你Record生成的JSON Schema。这“强迫”AI以指定的JSON格式输出,然后starter再将其反序列化成你的Java对象。这比用普通聊天接口然后自己用正则表达式去解析输出要可靠和优雅得多。

5.3 结构化输出与函数调用的结合

这是更高级的用法。你可以让AI先通过结构化输出生成一个规整的数据(比如一个JSON配置),然后自动调用一个函数来处理这个数据。

@Component
public class DeploymentFunctions {
    @GPTFunction(name = "apply_k8s_yaml", value = "将提供的Kubernetes YAML配置应用到集群")
    public String applyK8sConfig(@Parameter("完整的K8S YAML内容") String yaml) {
        // 调用Kubectl或K8S客户端API
        System.out.println("应用配置:\n" + yaml);
        return "配置已成功提交到集群。";
    }
}

@ChatGPTExchange
public interface DevOpsService {
    // 假设我们有一个生成K8S部署YAML的专用输出结构
    @StructuredOutput(name = "k8s_deployment")
    public record K8sDeployment(String name, String image, int replicas, String yaml) {}
    
    @ChatCompletion(system = "你是一个Kubernetes专家。", user = "为名为{0}的应用,使用镜像{1},创建一份Deployment YAML。", functions = {"apply_k8s_yaml"})
    Mono<K8sDeployment> generateDeploymentYaml(String appName, String image);
}

在这个场景中,AI会生成一个结构化的 K8sDeployment 对象,其中包含格式化好的YAML字符串。同时,因为 functions 参数包含了 apply_k8s_yaml ,AI可能会在回复中建议或直接调用这个函数,并将生成的 yaml 字段作为参数传入。这就形成了一个“AI生成配置 -> 自动执行部署”的自动化流水线。

6. 高级特性与生产实践

6.1 Prompt模板管理:告别硬编码的魔法字符串

在业务代码中到处散落着Prompt字符串是维护的噩梦。这个starter提供了一个基于 prompts.properties 文件的模板管理方案。

  1. 创建模板文件 :在 src/main/resources 下创建 prompts.properties 文件。

    # prompts.properties
    translate.to.chinese=你是一名专业翻译。请将以下文本翻译成流畅、地道的中文:{0}
    code.review.system=你是一名严格的资深程序员,擅长{0}语言。请审查以下代码,指出潜在bug、性能问题和风格缺陷。
    code.review.user=请审查这段{0}代码:\n```{1}\n```\n
    customer.service.reply=你是一家名为{company}的公司的客服代表。请用{style}的风格回复以下客户咨询:{query}
    
  2. 在注解中引用模板 :使用 # 加属性名来引用。

    @ChatGPTExchange
    public interface PromptTemplateService {
        // 直接引用prompts.properties中的键
        @ChatCompletion("#translate.to.chinese")
        Mono<String> translate(String text);
        
        // 模板支持参数化,{0}会被方法第一个参数替换,以此类推
        @ChatCompletion(system = "#code.review.system", user = "#code.review.user")
        Mono<String> reviewCode(String language, String code);
        
        // 更复杂的例子,使用Record来传递多个命名参数,避免参数顺序混淆
        public record CustomerServiceContext(String company, String style, String query) {}
        @ChatCompletion("#customer.service.reply")
        Mono<String> generateReply(CustomerServiceContext context);
    }
    
  3. 动态加载与扩展 PromptManager 类提供了编程式访问模板的能力。你也可以实现 PromptStore 接口,从数据库、配置中心等地方加载Prompt模板,实现动态更新Prompt而无需重启应用。

6.2 提示词即Lambda:函数式编程风格

这是一个非常巧妙的功能,它允许你将一个Prompt模板转换成一个 Function ,从而可以用流式(Stream)或响应式(Reactor)编程的方式链式调用多个AI处理步骤。

@Service
public class ContentProcessingPipeline {
    @Autowired
    private ChatGPTService chatGPTService;

    public Mono<String> processArticle(String rawArticle) {
        // 1. 将prompt模板转换为函数
        Function<String, Mono<String>> translateFunc = chatGPTService.promptAsLambda("translate.to.chinese");
        Function<String, Mono<String>> summarizeFunc = chatGPTService.promptAsLambda("summarize.article");
        Function<String, Mono<String>> addTitlesFunc = chatGPTService.promptAsLambda("add.subtitles");
        
        // 2. 构建处理流水线:翻译 -> 总结 -> 添加小标题
        return Mono.just(rawArticle)
                .flatMap(translateFunc)        // 第一步:翻译
                .flatMap(summarizeFunc)        // 第二步:总结
                .flatMap(addTitlesFunc)        // 第三步:格式化
                .onErrorResume(e -> {
                    // 优雅地处理错误,例如返回原始文章
                    return Mono.just("处理失败,返回原文: " + rawArticle);
                });
    }
}

这种模式特别适合构建 AI Agent工作流 。每个Prompt函数代表一个独立的“技能”或“处理节点”,你可以像搭积木一样组合它们,实现复杂的多步AI任务。

6.3 批处理API集成

对于需要处理大量独立文本的任务(比如批量翻译、批量情感分析),使用循环逐个调用API效率低下且可能触达速率限制。OpenAI提供了Batch API,允许你提交一个任务列表,并在24小时内异步获取结果。starter也对此提供了支持。

@Component
public class BatchTranslationService {
    @Autowired
    private OpenAIFileAPI openAIFileAPI;
    @Autowired
    private OpenAIBatchAPI openAIBatchAPI;
    @Autowired
    private ObjectMapper objectMapper;

    public Mono<String> submitBatchTranslation(List<String> texts, String targetLang) {
        // 1. 将多个请求转换为JSONL格式(每行一个JSON)
        String jsonl = texts.stream()
                .map(text -> ChatCompletionRequest.of("Translate to " + targetLang + ": " + text))
                .map(ChatCompletionBatchRequest::new) // 包装成批处理请求
                .map(req -> {
                    try {
                        return objectMapper.writeValueAsString(req);
                    } catch (JsonProcessingException e) {
                        return "";
                    }
                })
                .filter(StringUtils::hasText)
                .collect(Collectors.joining("\n"));
        
        // 2. 将JSONL文件上传到OpenAI
        Resource resource = new ByteArrayResource(jsonl.getBytes(StandardCharsets.UTF_8));
        return openAIFileAPI.upload("batch", resource)
                .flatMap(fileObject -> {
                    // 3. 创建批处理任务
                    CreateBatchRequest batchRequest = new CreateBatchRequest(fileObject.getId());
                    batchRequest.setEndpoint("/v1/chat/completions");
                    batchRequest.setCompletionWindow("24h");
                    return openAIBatchAPI.create(batchRequest);
                })
                .map(BatchObject::getId); // 返回批处理ID,用于后续查询结果
    }
    
    public Mono<List<ChatCompletionResponse>> retrieveBatchResults(String batchId) {
        // 4. 稍后(如24小时后)通过批处理ID获取结果文件ID
        return openAIBatchAPI.retrieve(batchId)
                .flatMap(batch -> {
                    if ("completed".equals(batch.getStatus())) {
                        String resultFileId = batch.getOutputFileId();
                        // 5. 下载并解析结果文件
                        return openAIFileAPI.retrieveContent(resultFileId)
                                .map(content -> parseJsonlResults(content));
                    }
                    return Mono.empty();
                });
    }
    
    private List<ChatCompletionResponse> parseJsonlResults(String jsonlContent) {
        // 解析JSONL格式的响应
        return Arrays.stream(jsonlContent.split("\n"))
                .filter(line -> line.startsWith("{"))
                .map(line -> {
                    try {
                        // 注意:这里需要根据Batch响应的实际结构进行解析
                        return objectMapper.readValue(line, ChatCompletionBatchResponse.class).getResponse();
                    } catch (Exception e) {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
}

重要提示 :Batch API是异步的,适合离线处理大量数据。它比实时调用成本更低,但有较长的延迟。务必妥善保存返回的 batchId ,并实现一个轮询或回调机制来获取最终结果。

6.4 集成其他模型与代理设置

这个starter不仅支持OpenAI官方接口,还兼容任何遵循OpenAI API格式的兼容服务,例如国内的DeepSeek、智谱AI等。

# 配置使用DeepSeek
openai.api.url=https://api.deepseek.com/v1
openai.api.key=sk-your-deepseek-key
openai.model=deepseek-chat # 指定模型名称

如果你需要在服务端做一个统一的AI代理(比如为了统一鉴权、日志或路由),可以轻松地创建一个代理控制器:

@RestController
@RequestMapping("/v1") // 模拟OpenAI的API路径
public class OpenAIProxyController {
    @Autowired
    private OpenAIChatAPI openAIChatAPI; // 更底层的Chat API客户端

    @PostMapping("/chat/completions")
    public Publisher<ChatCompletionResponse> proxyCompletions(@RequestBody ChatCompletionRequest request, 
                                                               @RequestHeader Map<String, String> headers) {
        // 在这里可以添加统一的逻辑:身份验证、限流、日志记录、请求改写等
        log.info("Proxying chat request for model: {}", request.getModel());
        // 直接将请求转发给配置的AI服务(可能是OpenAI、Azure、DeepSeek等)
        return openAIChatAPI.proxy(request);
    }
}

这样,你的前端或其他服务就可以直接调用 http://your-spring-server/v1/chat/completions ,就像在调用OpenAI官方接口一样,而实际的后端服务可以由你灵活配置和切换。

7. 常见问题、故障排查与性能优化

7.1 配置与连接问题

  • 问题: 启动应用时报错,提示 OpenAI API key must start with 'sk-' ,但我配置的是Azure的Key。

    • 排查: 检查 application.properties 。如果你使用Azure OpenAI, 必须同时设置 openai.api.url 。仅设置Key时,starter会默认使用OpenAI官方端点,而Azure的Key格式不同,会导致验证失败。
    • 解决: 确保Azure配置完整:
      openai.api.key=your-azure-key
      openai.api.url=https://your-resource.openai.azure.com/.../chat/completions?api-version=...
      # 也可以显式指定模型,但Azure的部署名可能更重要
      # openai.model=你的部署名
      
  • 问题: 调用接口超时或连接被拒绝。

    • 排查:
      1. 检查网络连通性: curl https://api.openai.com (或你的配置端点)。
      2. 检查防火墙或代理设置。如果你的服务器在国内,直接访问OpenAI可能需要配置网络代理。
      3. 检查 openai.api.url 是否配置正确,特别是Azure的URL,确保包含正确的 deployments 路径和 api-version 参数。
    • 解决:
      • 对于网络问题,可以考虑使用可靠的代理服务或在云服务商处选择可访问的国际区域。
      • 在Spring Boot配置中为WebClient设置全局代理(注意:这需要谨慎处理,且必须符合相关法律法规和使用条款):
        @Configuration
        public class WebClientConfig {
            @Bean
            public WebClient.Builder webClientBuilder() {
                // 这是一个示例,实际代理配置应来自环境变量或安全配置中心
                HttpClient httpClient = HttpClient.create()
                    .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP)
                                         .host("proxy-host")
                                         .port(8080));
                return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient));
            }
        }
        

        重要安全提示 :任何网络代理的使用都必须严格遵守国家法律法规和公司政策,确保用于合法合规的科研、开发及业务用途。

7.2 响应式编程与阻塞问题

  • 问题: 我在一个 @RestController 的方法里调用了 chatGPTService.chat(...).block() ,应用警告说可能阻塞线程。
    • 分析: Starter的API默认返回 Mono / Flux ,这是响应式(Reactive)类型。在传统的Servlet容器(如Tomcat)线程上调用 .block() 会阻塞该线程,违背了响应式非阻塞的初衷,在高并发下会严重影响性能。
    • 正确做法:
      1. (推荐)保持响应式链 :在Controller中直接返回 Mono<String> Flux<String> ,Spring WebFlux会正确处理。
        @GetMapping("/ask")
        public Mono<String> askQuestion(@RequestParam String q) {
            return chatGPTService.chat(ChatCompletionRequest.of(q))
                                 .map(ChatCompletionResponse::getReplyText);
            // 不要调用 .block()!
        }
        
      2. 使用虚拟线程(Spring Boot 3.2+) :如果你坚持使用阻塞风格,可以配合Spring Boot 3.2的虚拟线程特性。在 application.properties 中启用虚拟线程:
        spring.threads.virtual.enabled=true
        
        然后在阻塞调用上使用 @Transactional (如果涉及数据库)或确保在标记了 @Async 的方法中执行。虚拟线程可以更高效地处理阻塞操作,但本质上仍应优先考虑非阻塞编程模型。

7.3 函数调用不生效

  • 问题: 我定义了 @GPTFunction ,但在对话中AI似乎“看不到”这个函数,从不调用它。
    • 排查步骤:
      1. 检查Bean扫描 :确保定义了 @GPTFunction 的类本身是Spring管理的Bean(即被 @Component , @Service 等注解标记)。
      2. 检查函数名 :在 ChatCompletionRequest.functions(prompt, List.of(“your_function_name”)) 中传入的函数名,必须与 @GPTFunction(name = “your_function_name”) 中定义的 name 完全一致(大小写敏感)。
      3. 检查请求构建 :你是否使用了正确的 ChatCompletionRequest.functions() 方法来构建请求?普通的 ChatCompletionRequest.of() 不会携带函数定义。
      4. 检查AI理解 :有时AI可能认为不需要调用函数。尝试在系统提示词(System Prompt)中更明确地指示它使用函数,例如:“请使用可用的工具函数来帮助完成用户请求。”
    • 调试技巧 :在调用 chatGPTService.chat(request) 后,打印 response.getReply() 。查看其中是否有 ChatMessage 对象的 functionCall 字段不为空。这能帮你确认AI是否生成了函数调用请求。

7.4 性能优化与最佳实践

  1. 连接池与超时设置 :默认的WebClient配置可能不适合生产环境。建议在 application.yml 中自定义:

    openai:
      api:
        key: ${OPENAI_API_KEY}
        connect-timeout: 10s # 连接超时
        read-timeout: 60s    # 读取超时,对于长文本生成可以设长一些
        max-connections: 100 # 最大连接数
        max-life-time: 5m    # 连接最大存活时间
    

    (注意:具体的配置属性名需查看starter的官方文档或源码中的 ChatGPTProperties 类。)

  2. 合理使用流式响应 :对于需要即时反馈的对话场景,务必使用 chatGPTService.stream() 。对于后台批量处理、无需即时交互的任务,使用普通的 chat() 即可。流式响应会保持HTTP连接,占用资源时间更长。

  3. Prompt设计优化

    • 明确系统角色 :在 @ChatCompletion(system = “...”) 中清晰定义AI的角色,这能极大提高回复质量。
    • 结构化用户输入 :尽量使用模板和参数,而不是拼接字符串。例如, @ChatCompletion(user = “总结这篇文章:{0}”) @ChatCompletion(“总结这篇文章:” + article) 更清晰且利于复用。
    • 温度(Temperature)控制 :对于需要确定性输出的任务(如代码生成、翻译),将 temperature 设低(如0.1-0.3)。对于需要创造性的任务(如写作、头脑风暴),可以设高(如0.7-0.9)。可以在 @ChatCompletion 注解或全局配置中设置。
  4. 异常处理与重试 :网络请求和AI服务都可能不稳定。务必为你的AI调用添加健壮的异常处理和重试逻辑。可以使用Reactor的 retryWhen 操作符。

    return chatGPTService.chat(request)
            .timeout(Duration.ofSeconds(30)) // 设置超时
            .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) // 指数退避重试3次
                    .filter(throwable -> throwable instanceof IOException || 
                                         (throwable instanceof WebClientResponseException e && e.getStatusCode().is5xxServerError())))
            .onErrorResume(e -> {
                log.error(“调用AI服务失败”, e);
                return Mono.just(ChatCompletionResponse.error(“服务暂时不可用,请稍后重试。”));
            });
    
  5. 监控与日志 :为重要的AI服务接口添加详细的日志,记录请求的Prompt、Token使用量、响应时间等。这有助于分析成本、优化Prompt和排查问题。考虑集成Micrometer等指标库,将AI调用耗时、成功率等暴露给监控系统。

8. 总结与项目展望

经过对 chatgpt-spring-boot-starter 从入门到高级特性的全面拆解,我们可以看到,它绝不仅仅是一个简单的API客户端。它通过深度整合Spring Boot和响应式编程范式,提供了一套声明式、类型安全、可组合的AI集成方案,极大地提升了开发效率和代码可维护性。

核心价值再回顾

  1. 声明式接口 :让AI调用像定义Feign Client一样简单直观,将业务逻辑与底层通信彻底解耦。
  2. 函数调用集成 :以极低的成本实现了AI与外部工具/系统的联动,打开了AI Agent应用的大门。
  3. 结构化输出 :将非结构化的文本对话转变为结构化的数据交换,让AI真正成为可编程的组件。
  4. Prompt工程支持 :通过模板管理和Lambda化,使得复杂的Prompt设计、管理和组合变得井然有序。
  5. 生产就绪 :支持流式响应、批处理、连接池配置、异常处理等,满足企业级应用的要求。

个人使用体会 : 在实际项目中引入这个starter后,最明显的改变是团队协作效率的提升。后端开发人员不再需要深入理解OpenAI API的细节,只需要关注“定义接口”和“处理结果”这两头。Prompt可以像接口文档一样被集中管理,方便A/B测试和迭代优化。函数调用功能让我们快速构建了几个内部自动化工具,比如“根据自然语言描述生成SQL并预览数据”、“分析日志文件并自动创建Jira工单”,这些都是以前需要大量定制开发才能实现的功能。

未来的想象空间 : 随着OpenAI API能力的不断演进(比如更长的上下文、更强的推理能力),这个starter的底层也会持续更新。社区已经开始探讨更多的集成模式,例如与Spring AI项目的互补、更复杂的多步骤工作流编排(Workflow)、以及对本地大模型(LLM)的适配支持。对于开发者而言,拥抱这样的抽象层,意味着我们能更专注于利用AI能力解决业务问题,而不是反复造轮子。

如果你正在Spring Boot技术栈中探索AI应用, chatgpt-spring-boot-starter 是一个非常值得投入时间学习和使用的工具。它的设计理念与Spring生态一脉相承,学习曲线平缓,但带来的能力提升是巨大的。建议从官方示例和测试用例入手,先尝试用声明式接口重构一两个简单的AI调用,再逐步探索函数调用和结构化输出等高级特性,相信你很快就能感受到它带来的开发愉悦感。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐