Ohhnews

分类导航

$ cd ..
Baeldung原文

Google GenAI Chat与Spring AI集成入门指南

#spring ai#google gemini#生成式ai#java开发#人工智能

[LOADING...]

1. 引言

生成式 AI 领域正在飞速发展,促使框架维护者重新思考应用程序如何接入多样化的大型语言模型(LLM)。随着 Spring AI 的发布,Java 开发者获得了与认知服务交互的统一、可移植接口。

然而,随着生态系统的成熟,在同一个运行时类路径中集成多个基础模型会引入配置冲突。为了解决这一问题,Spring AI 引入了重大的架构更新,包括专门的 spring-ai-starter-model-google-genai 启动器 模块,以及显式的模型选择范式。

在本教程中,我们将学习如何使用 Gemini Developer API(通过 Google AI Studio)将 Google 的 Gemini 模型集成到 Spring Boot 应用程序中。我们将介绍简化的项目设置,深入探讨使用低级和高级 API 的文本生成模式,应用提示词模板,启用实时网络事实锚定,并使用 WebFlux 以响应式方式流式传输 token。

2. 项目设置

首先,我们需要一个标准的 Spring Boot 3.x 应用程序。由于 Spring AI 模块会频繁更新,强烈建议使用 Spring AI 的 物料清单(BOM) 来管理依赖版本。

2.1. Maven 依赖

首先,让我们在 pom.xml 文件中配置 Spring AI Starter Google GenAISpring AI BOM由于 Spring AI 构件在发布周期内托管在 Spring Milestones 仓库中,我们需要同时正确声明仓库和依赖管理块

$ java
<project>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>2.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-google-genai</artifactId>
        </dependency>
    </dependencies>
</project>

2.2. 通过 spring.ai.model.chat 激活聊天提供者

在最新版本的 Spring AI 中,仅仅将 starter 模块放入 classpath 不再会自动激活它。这样可以避免应用程序引用多个 LLM 提供者时出现运行时配置冲突。

我们必须显式地在 application.properties 文件中声明活动的聊天提供者

$ java
spring.ai.model.chat=google-genai
spring.ai.google.genai.chat.model=gemini-2.5-flash
spring.ai.google.genai.api-key=${SPRING_AI_GOOGLE_GENAI_API_KEY}

可以从 Google AI Studio 获取 API 密钥。使用这种占位符语法,可以明确记录应用程序的外部运行时依赖,同时避免硬编码的机密信息泄露到源代码管理仓库中。

2.3. 环境设置

我们无需编写任何自定义的 Java 解密或绑定代码,即可满足上面定义的属性占位符。只需将凭据直接导出到环境中。自动配置引擎会在启动时自动解析占位符的值:

$ java
export SPRING_AI_GOOGLE_GENAI_API_KEY="YourActualSecureGoogleAIStudioKeyHere"

一旦设置了该环境变量,Spring Boot 就能无缝地将变量桥接到属性文件占位符,使 GoogleGenAiChatModel 可被注入。

3. 核心文本与内容生成策略

Spring AI 为我们提供了与 Gemini 模型通信的两个主要抽象层:基础的 ChatModel Bean 和高度可配置、流畅的 ChatClient API。 为了遵循企业设计模式并保持代码整洁,我们将交互逻辑隔离到一个专门的服务层中。然后,通过标准 Spring REST 控制器暴露这些能力。

3.1. 注入并使用 GoogleGenAiChatModel

GoogleGenAiChatModel 代表了基础的低级客户端抽象。它负责请求序列化、对 Google RPC 端点的 HTTP 执行,以及原始响应的解析。

让我们看一下 ChatService,我们在其中注入这个 Bean 以及 ChatClient.Builder 来初始化操作层:

$ java
@Service
public class ChatService {
    private final GoogleGenAiChatModel chatModel;
    private final ChatClient defaultChatClient;
    private final ChatClient fluentChatClient;
    public ChatService(GoogleGenAiChatModel chatModel, ChatClient.Builder chatClientBuilder) {
        this.chatModel = chatModel;
        this.defaultChatClient = chatClientBuilder.build();
        this.fluentChatClient = chatClientBuilder
          .defaultSystem("You are a concise technical writer summarizing software concepts.")
          .build();
    }
    public String simplifiedPrompt(String message) {
        return chatModel.call(message);
    }
}

当我们调用 chatModel.call(message) 时,框架会将原始字符串打包到默认的提示词上下文中。然后将其发送到配置的模型变体,从响应负载中提取文本块并返回。我们在 ChatController 中暴露该能力:

$ java
@RestController
public class ChatController {
    private final ChatService chatService;
    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }
    @GetMapping("/v1/chat/simple")
    public String simplifiedPrompt(@RequestParam(defaultValue = "Hello") String message) {
        return chatService.simplifiedPrompt(message);
    }
}

这建立了一个简单的字符串进、字符串出的端点。它使用可用的最低级模型客户端抽象来验证与 Gemini 引擎的基本连接。

3.2. 使用流式 ChatClient API 构建提示词

直接使用模型可以处理快速操作,但生产架构更倾向于使用流畅的 ChatClient API。** ChatClient 充当门面层,简化了提示词的组装,并附加默认的配置基线。**

如我们的服务构造函数所示,我们预先配置了一个带有系统指令的专用流畅 ChatClient。让我们向 ChatService 添加相应的执行方法:

$ java
public String fluentPrompt(String prompt) {
    return this.fluentChatClient.prompt()
      .user(prompt)
      .call()
      .content();
}

通过这个预配置的实例路由请求,每个用户查询在执行前都会自动附加目标系统指令。我们在控制器中暴露此端点:

$ java
@GetMapping("/v1/chat/fluent")
public String fluentPrompt(@RequestParam String prompt) {
    return chatService.fluentPrompt(prompt);
}

这使我们可以将重复的角色或系统性行为直接封装在客户端包装器实例中。因此,我们无需手动修改每个传入的文本参数。

3.3. 通过提示词模板处理动态输入

在字符串中硬编码参数逻辑会导致字符串拼接混乱和代码库脆弱。Spring AI 通过结构化提示词模板解决了这个问题,将指令与用户变量分离开来。

让我们在 ChatService 中使用多行文本块占位符布局实现代码审查功能:

$ java
public String reviewCode(String language, String codeSnippet) {
    String template = """
            Analyze the following {language} code snippet for memory leaks or inefficiencies.
            Provide an optimized version.
            Code:
            {code}
            """;
    return this.defaultChatClient.prompt()
      .user(u -> u.text(template).params(Map.of("language", language, "code", codeSnippet)))
      .call()
      .content();
}

客户端会在运行时替换目标标签({language}{code} ),并在提交前将文本格式化。我们通过控制器中的 POST 请求包装器将其暴露:

$ java
@PostMapping("/v1/chat/review")
public String reviewCode(@RequestParam(defaultValue = "Java") String language, @RequestBody String codeSnippet) {
    return chatService.reviewCode(language, codeSnippet);
}

这种模式将结构化的提示词工程边界与易变的业务数据清晰解耦,产生了一种高度可复用、参数驱动的调用模式。

3.4. 通过实时 Google 搜索检索对响应进行事实锚定

LLM 天然存在训练截止时间窗口和关于实时现实世界发展的知识空白。Google GenAI 模块提供了一个显式的事实锚定属性,使我们的模型能够与实时 Google 搜索索引无缝连接。

为了在整个应用程序中启用实时搜索事实锚定,我们需要在配置属性中将 grounding 标志切换为 true

$ java
spring.ai.google.genai.chat.google-search-retrieval=true

启用该属性后,关于当前事件或突发新闻的查询会自动使用最新搜索结果进行事实锚定。让我们编写一个端点来处理实时信息请求:

$ java
public String searchGroundedPrompt(String currentEventQuery) {
    return this.defaultChatClient.prompt()
      .user(currentEventQuery)
      .call()
      .content();
}

我们在控制器配置中将此方法映射到 HTTP 层:

$ java
@GetMapping("/v1/chat/grounded")
public String searchGroundedPrompt(@RequestParam String currentEventQuery) {
    return chatService.searchGroundedPrompt(currentEventQuery);
}

如果我们传入类似 “昨天谁赢得了最近的足球锦标赛?” 的请求,模型会使用实时 Google 搜索索引将其响应锚定在经过验证的事实上,从而大幅减少幻觉。此配置弥合了静态训练截止时间与实时现实世界事件之间的差距。

3.5. 使用 Flux 处理实时流式响应

对于面向用户的界面,等待服务器完整生成一段长文本会产生明显的延迟。相反,我们可以使用 Spring Boot 的 WebFlux 集成,将文本块逐 token 地流式返回给用户。

让我们向 ChatService 添加一个流式方法,返回响应式的 Flux<String> 结构:

$ java
public Flux<String> streamChatTokens(String prompt) {
    return this.defaultChatClient.prompt()
      .user(prompt)
      .stream()
      .content();
}

我们将创建一个专门的 StreamingChatController,通过开放的连接管道干净地传递这些 token。该控制器显式生成 text/event-stream 媒体响应:

$ java
@RestController
public class StreamingChatController {
    private final ChatService chatService;
    public StreamingChatController(ChatService chatService) {
        this.chatService = chatService;
    }
    @GetMapping(value = "/v1/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChatTokens(@RequestParam String prompt) {
        return chatService.streamChatTokens(prompt);
    }
}

当访问此端点时,消费者以响应式方式读取 token 数据。这建立了一个非阻塞的执行管道,可以逐块构建交互式、低延迟的 UI 流程。

4. 测试

测试依赖外部生成式 AI 端点的应用程序需要明确关注点分离。在本地编译期间,运行测试不应触发外部网络调用、命中速率限制或耗尽 API 配额。 为防止这种情况,我们使用 Spring Boot 的切片测试和 @MockitoBean 来隔离 HTTP 层。

4.1. 通过 WebLayer MockMvc Mock 进行单元测试

由于我们的实现将所有的 LLM 通信封装在 ChatService 中,因此我们可以完全模拟这个业务组件。这使我们能够干净地断言控制器路由、请求参数、响应体预期以及流处理模式。

让我们使用 MockMvc 编写一个隔离测试,以验证所有基于文本、模板驱动和响应式流端点,而不会触发真实的网络握手。首先,我们设置一个简单提示词测试,以验证基本的 GET 参数路由和原始响应映射:

$ java
@SpringBootTest(properties = {
    "spring.ai.model.chat=google-genai",
    "spring.ai.google.genai.chat.model=gemini-2.5-flash",
    "spring.ai.google.genai.api-key=test-key"
})
@AutoConfigureMockMvc
class ChatControllerUnitTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private ChatService chatService;
    @Test
    void whenSimpleEndpointIsInvoked_thenReturnsMockedModelResponse() throws Exception {
        when(chatService.simplifiedPrompt(anyString())).thenReturn("Mocked Gemini Response");
        mockMvc.perform(get("/v1/chat/simple").param("message", "Hello"))
          .andExpect(status().isOk())
          .andExpect(content().string("Mocked Gemini Response"));
        verify(chatService).simplifiedPrompt("Hello");
    }
}

接下来,我们确认流式提示词端点能将查询参数准确转发到底层服务。然后返回配置的 基于系统提示词 的响应:

$ java
@Test
void whenFluentEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.fluentPrompt(anyString())).thenReturn("Fluent Response");
    mockMvc.perform(get("/v1/chat/fluent").param("prompt", "Explain DI"))
      .andExpect(status().isOk())
      .andExpect(content().string("Fluent Response"));
    verify(chatService).fluentPrompt("Explain DI");
}

对于我们的模板驱动端点,我们验证控制器是否正确处理由查询参数和 text/plain 请求体组成的复合负载:

$ java
@Test
void whenReviewEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.reviewCode(anyString(), anyString())).thenReturn("Optimized Code");
    mockMvc.perform(post("/v1/chat/review")
        .param("language", "Java")
        .contentType(MediaType.TEXT_PLAIN)
        .content("class A { }"))
      .andExpect(status().isOk())
      .andExpect(content().string("Optimized Code"));
    verify(chatService).reviewCode("Java", "class A { }");
}

现在,我们验证搜索事实锚定查询能够干净地通过控制器层传递参数,而不会出现参数绑定错误:

$ java
@Test
void whenGroundedEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.searchGroundedPrompt(anyString())).thenReturn("Grounded Response");
    mockMvc.perform(get("/v1/chat/grounded").param("currentEventQuery", "latest match winner"))
      .andExpect(status().isOk())
      .andExpect(content().string("Grounded Response"));
    verify(chatService).searchGroundedPrompt("latest match winner");
}

最后,我们测试响应式端点,以确保 Spring MVC 正确协商 text/event-stream 媒体类型。它还会将响应式 Flux 元素格式化为标准的 Server-Sent Event(data:)帧:

$ java
@Test
void whenStreamEndpointIsInvoked_thenReturnsEventStreamContent() throws Exception {
    when(chatService.streamChatTokens(anyString()))
      .thenReturn(reactor.core.publisher.Flux.just("token-1", "token-2"));
    mockMvc.perform(get("/v1/chat/stream").param("prompt", "Stream tokens"))
      .andExpect(status().isOk())
      .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM))
      .andExpect(content().string("data:token-1\n\ndata:token-2\n\n"));
    
    verify(chatService).streamChatTokens("Stream tokens");
}

通过使用现代的 @MockBean 注解,我们可以直接将 mock 定义注入到应用程序上下文包装器中,替换真实的 Bean 配置。这使我们能够安全、可靠地模拟标准的同步操作以及复杂的响应式 Flux 流响应,而无需网络开销。## 5. 结论

在本教程中,我们使用 Spring AI 更新后的 spring-ai-starter-model-google-genai 引擎配置了一个 Spring Boot 应用,以直接与 Google AI Studio 交互。

我们看到了如何通过显式定义 spring.ai.model.chat=google-genai 来解决现代多模型类路径依赖。在此基础上,我们使用专门的 ChatService 建立了简洁的架构;借助流畅的 ChatClient API 构建了灵活的提示词工作流,并使用模板隔离了动态参数。最后,我们启用了实时 Web 搜索(web grounding),并通过 WebFlux 的 Flux 流式输出响应。

与往常一样,本文使用的完整代码示例可在 GitHub 上获取。

本文最初发布于 Baeldung.