Artículo

Introducción a Google GenAI Chat y Spring AI

Introducción a Google GenAI Chat y Spring AI

1. Introducción

El panorama de IA generativa está evolucionando rápidamente, lo que obliga a los mantenedores de marcos a repensar cómo las aplicaciones se conectan a diversos modelos de lenguaje grande (LLMs). Con el lanzamiento de Spring AI, los desarrolladores de Java obtuvieron una interfaz unificada y portable para interactuar con servicios cognitivos.

Sin embargo, a medida que el ecosistema maduró, integrar múltiples modelos fundamentales dentro de una sola ruta de clase de tiempo de ejecución introdujo conflictos de configuración. Para resolver esto, Spring AI introdujo importantes actualizaciones arquitectónicas. Estas incluyen el iniciador especializado spring-ai-starter-model-google-genai y un paradigma explícito de selección de modelo.

En este tutorial, exploraremos cómo integrar los modelos Gemini de Google en una aplicación Spring Boot usando la Gemini Developer API (a través de Google AI Studio). Veremos una configuración de proyecto simplificada, profundizaremos en patrones de generación de texto usando tanto APIs de bajo nivel como de alto nivel, aplicaremos plantillas de prompt, habilitaremos el apoyo web en tiempo real y transmitiremos tokens de forma reactiva con WebFlux.

2. Configuración del proyecto

Para comenzar, necesitamos una aplicación estándar Spring Boot 3.x. Dado que los módulos de Spring AI se actualizan activamente, se recomienda encarecidamente gestionar las versiones de dependencias mediante el Bill of Materials (BOM) de Spring AI.

2.1. Dependencias de Maven

Primero, configuremos el Spring AI Starter Google GenAI y el Spring AI BOM en nuestro archivo pom.xml. Como los artefactos de Spring AI se alojan en el repositorio de Spring Milestones durante los ciclos de lanzamiento, aseguramos que tanto el repositorio como los bloques de gestión de dependencias estén declarados correctamente:

<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. Activando el proveedor de chat vía spring.ai.model.chat

En las versiones más recientes de Spring AI, agregar un módulo de inicio al classpath ya no lo activa automáticamente. Esto evita conflictos de configuración en tiempo de ejecución cuando su aplicación referencia múltiples proveedores de LLM.

Debemos declarar explícitamente nuestro proveedor de chat activo dentro de nuestro archivo application.properties:

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}

Obtenga una clave API desde el Google AI Studio. Al usar esta sintaxis de marcador de posición, se documenta explícitamente la dependencia externa de tiempo de ejecución de la aplicación mientras se evita que secretos codificados se filtren en nuestro repositorio de control de versiones.

2.3. Configuración del entorno

Podemos satisfacer la propiedad de marcador de posición definida anteriormente sin escribir ningún código Java personalizado de descifrado o vinculación. Simplemente exportaremos las credenciales directamente en nuestro entorno. El motor de auto-configuración resolverá automáticamente el valor del marcador de posición al iniciar:

export SPRING_AI_GOOGLE_GENAI_API_KEY="YourActualSecureGoogleAIStudioKeyHere"

Una vez que se establezca esta variable de entorno, Spring Boot conecta sin problemas la variable al marcador de posición del archivo de propiedades, haciendo que GoogleGenAiChatModel esté listo para la inyección.

3. Estrategias centrales de generación de texto y contenido

Spring AI nos ofrece dos capas de abstracción principales para comunicarnos con los modelos Gemini: el bean fundacional ChatModel y la API fluida ChatClient altamente configurable. Separa nuestra lógica de interacción en una capa de servicio dedicada para seguir los patrones de diseño empresarial y mantener el código limpio. Luego, exponemos estas capacidades a través de controladores REST estándar de Spring.

3.1. Inyección y uso de GoogleGenAiChatModel

El GoogleGenAiChatModel representa la abstracción de cliente de bajo nivel.Maneja la serialización de solicitudes, la ejecución HTTP contra los puntos finales RPC de Google y el análisis de respuesta sin procesar.

Echemos un vistazo a nuestro ChatService, donde inyectamos este bean junto con un ChatClient.Builder para inicializar nuestras capas operativas:

@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);
    }
}

Cuando invocamos chatModel.call(message), el framework empaqueta la cadena sin procesar en un contexto de prompt predeterminado. Luego la envía al modelo configurado, extrae el bloque de texto del payload de respuesta y lo devuelve. Exponemos esto en nuestro ChatController:

@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);
    }
}

Esto establece un endpoint de entrada y salida de cadena simple. Utiliza la abstracción de cliente de modelo de nivel más bajo disponible para verificar la conectividad básica con el motor Gemini.

3.2. Construyendo prompts con la API fluida ChatClient

Aunque trabajar directamente con el modelo facilita operaciones rápidas, las arquitecturas de producción prefieren la API fluida ChatClient. El ChatClient actúa como una capa fachada que simplifica la construcción de prompts y adjunta líneas base de configuración predeterminadas.

Como se muestra en el constructor de nuestro servicio, preconfiguramos un ChatClient fluido especializado con instrucciones del sistema. Agreguemos el método de ejecución correspondiente a ChatService:

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

Al enrutar las solicitudes a través de esta instancia preconfigurada, cada consulta de usuario automáticamente adjunta las instrucciones del sistema objetivo antes de la ejecución. Exponemos este endpoint en el controlador:

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

Esto nos permite encapsular personas recurrentes o comportamientos sistémicos directamente dentro de la instancia del wrapper de cliente. Como resultado, no necesitamos modificar manualmente los argumentos de texto entrantes individuales.

3.3. Manejo de entradas dinámicas a través de plantillas de prompt

Codificar la lógica de parámetros directamente dentro de cadenas genera concatenaciones de cadena desordenadas y bases de código frágiles. Spring AI resuelve esto con plantillas de prompt estructuradas, separando instrucciones de variables de usuario.

Implementemos una capacidad de revisión de código dentro de nuestro ChatService usando una plantilla de bloque de texto multilínea:

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();
}

El cliente reemplaza las etiquetas objetivo ({language}, {code}) en tiempo de ejecución, formateando el texto de manera limpia antes de la presentación. Exponemos esto mediante un wrapper de solicitud POST en nuestro controlador:

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

Este patrón desacopla de manera limpia los límites de ingeniería estructural del prompt de los datos empresariales volátiles, generando un patrón de invocación altamente reutilizable y basado en parámetros.

3.4. Fundiendo respuestas con búsqueda en Google en vivo

Los LLMs sufren naturalmente de ventanas de corte de entrenamiento y brechas de conocimiento sobre desarrollos en tiempo real. El módulo GenAI de Google expone una propiedad de fundamentación explícita para conectar sin problemas nuestro modelo con el índice de búsqueda en vivo de Google.

Para activar la búsqueda en vivo en toda nuestra aplicación, debemos activar la bandera de fundamentación a true dentro de nuestras propiedades de configuración:

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

Cuando esta propiedad está habilitada, las consultas sobre eventos actuales o noticias de última hora se fundamentan automáticamente usando resultados de búsqueda frescos. Escribamos un endpoint para manejar solicitudes informativas en tiempo real:

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

Mapeamos este método a nuestra capa HTTP dentro de la configuración del controlador:

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

Si pasamos una solicitud como "¿Quién ganó el partido de fútbol más reciente ayer?", el modelo utiliza la indexación de búsqueda en vivo de Google para anclar su respuesta en hechos verificados, reduciendo drásticamente las alucinaciones. Esta configuración cierra la brecha entre los cortes de entrenamiento estáticos y los eventos del mundo real en tiempo real.

3.5. Procesamiento de respuestas de streaming en tiempo real con Flux

Para interfaces de usuario finales, esperar a que un bloque de texto largo se genere completamente en el servidor introduce latencia visible. En su lugar, podemos transmitir fragmentos de vuelta al usuario, token por token, usando la integración WebFlux de Spring Boot.

Agreguemos un método de streaming a nuestro ChatService que devuelva una estructura reactiva Flux<String>:

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

Crearemos un StreamingChatController especializado para entregar estos tokens de manera limpia sobre una tubería de conexión abierta. Este controlador produce explícitamente una respuesta de medios text/event-stream:

@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);
    }
}

Al acceder a este endpoint, los consumidores leen datos de token de forma reactiva. Esto establece una tubería de ejecución no bloqueante que construye un flujo de interfaz de usuario interactivo y de baja latencia fragmento por fragmento.

4. Pruebas

Las pruebas de aplicaciones que dependen de endpoints externos de IA generativa requieren una clara separación de responsabilidades. Ejecutar pruebas no debería disparar llamadas externas de red, alcanzar límites de tasa ni agotar cuotas de API durante la compilación local. Para evitar esto, aislamos nuestra capa HTTP usando pruebas por capas de Spring Boot junto con @MockitoBean.

4.1. Prueba de unidad vía MockMvc de capa Web

Dado que nuestra implementación encapsula todas las comunicaciones con LLM dentro de ChatService, podemos simular completamente este componente de negocio. Esto nos permite afirmar de manera limpia el enrutamiento del controlador, los parámetros de solicitud, las expectativas de cuerpo de respuesta y los patrones de manejo de streaming.

Escribamos una prueba aislada usando MockMvc para verificar todos los endpoints basados en texto, impulsados por plantillas y reactivos sin disparar una negociación de red real. Primero, configuramos una prueba de prompt simple para verificar la ruta de parámetros GET básicos y el mapeo de respuesta sin procesar:

@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");
    }
}

A continuación, confirmamos que nuestro endpoint de prompt fluido reenvía los parámetros de consulta con precisión al servicio subyacente. Luego devuelve el contenido configurado con el prompt del sistema:

@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");
}

Para nuestro endpoint impulsado por plantillas, validamos que el controlador procese correctamente una carga útil multipart que consiste en parámetros de consulta y un cuerpo de solicitud text/plain:

@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 { }");
}

Verificaremos ahora que las consultas fundamentadas por búsqueda pasan sus parámetros sin errores de vinculación de parámetros a través de la capa del controlador:

@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");
}

Finalmente, probamos nuestro endpoint reactivo para asegurar que Spring MVC negocie correctamente el tipo de medio text/event-stream. También formateará los elementos Flux reactivos en marcos estándar de Eventos del Servidor (data:):

@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");
}

Al aprovechar la moderna anotación @MockBean, inyectamos nuestras definiciones simuladas directamente en el contenedor de la aplicación, reemplazando la configuración de bean real. Esto nos permite simular operaciones síncronas estándar así como respuestas de streaming reactivo Flux complejas de manera segura y confiable sin sobrecarga de red.

5. Conclusión

En este tutorial, configuramos una aplicación Spring Boot usando el motor actualizado spring-ai-starter-model-google-genai de Spring AI para interactuar directamente con Google AI Studio.

Vimos cómo la definición explícita de spring.ai.model.chat=google-genai resuelve las dependencias modernas de múltiples modelos en el classpath. Desde allí, establecimos una arquitectura limpia usando un ChatService dedicado. Construimos flujos de prompts flexibles con la API fluida ChatClient y aislamos parámetros dinámicos usando plantillas. Finalmente, habilitamos la fundamentación web en vivo y transmitimos salidas responsivas mediante WebFlux Flux.

Como siempre, los ejemplos de código completos utilizados en este artículo están disponibles en GitHub.

Newsletter Semanal de Java

Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.

Sin spam. Cancela cuando quieras.

Compartir artículo