Todo el mundo sabe que, desde hace un tiempo, la IA está de moda (¡¿quién, a estas alturas, no ha escuchado hablar de ChatGPT, DeepSeek o Gemini?!) y, como nos comentó un compañero en su post, también ha llegado a Spring. Teniendo en cuenta toda la comunidad ya existente de Spring/Java y las miles de empresas con aplicaciones desplegadas en esta tecnología queriéndose subir a la ola de la IA… ¿qué mejor forma de integrarla que, simplemente, añadiendo una dependencia o módulo y evitando así múltiples quebraderos de cabeza por desarrollar con distintos frameworks y/o tecnologías? Es por esto mismo que vamos a explorar dicho módulo de IA para ver todo lo que nos ofrece.

Al entrar en la documentación de Spring AI vemos que tiene múltiples secciones que iremos revisando para entender qué nos aporta cada una de cara al desarrollo.

En la propia descripción nos indican que la finalidad de Spring AI es integrar los datos y APIs de empresa con los modelos de IA.

deep learning en spring ai

Conceptos de IA

Como ya se comentó en el post de inteligencia artificial y Spring Boot, Spring AI nos proporciona muchas ventajas. Pero, antes de todo, desde el enfoque de un perfil de desarrollo de Spring que desconoce los conceptos básicos de IA, la propia documentación nos recomienda conocer ciertos términos para poder entender posteriormente cómo funciona Spring AI. Estos conceptos son:

1 Modelos

Básicamente, son los algoritmos diseñados para procesar y generar la información que replican el cerebro humano. Estos algoritmos son capaces de extraer patrones sobre datos para poder hacer predicciones, crear textos, imágenes y resolver otro tipo de problemas.

En este momento, Spring AI soporta inputs y outputs como lenguaje, imágenes y audio, además de vectores/embeddings para casos de uso más avanzados.

2 Prompts

Sirven como base para guiar al modelo de cara a los resultados concretos. En este caso, no se debe pensar solo en los input text, sino también en los “roles” asociados a ese input de cara a proporcionar un contexto. Por ejemplo: ”eres un meteorólogo. Dime qué tiempo va a hacer hoy.” Para simplificar esta interacción, se suelen crear plantillas de prompts como: ”dime un chiste sobre <temática>.”

3 Embeddings

Los embeddings son representaciones numéricas de texto, imágenes o vídeos que capturan las relaciones entre las entradas. Es decir, convierten las entradas en arrays de números llamados vectores, los cuales están diseñados para indicar el significado de las entradas. Calculando la distancia entre la representación vectorial de, por ejemplo, dos textos, se puede conocer la similitud entre esos textos.

embeddings spring AI

4 Tokens

Los tokens se pueden considerar como la unidad más pequeña de información que usa un LLM para funcionar. Para procesar la entrada por un LLM, las palabras se transforman en tokens (un token corresponde aproximadamente con el 75% de una palabra) y para la respuesta, los tokens se transforman en palabras.

división de palabras y tokens

Los tokens son importantes en los LLMs porque son en lo que se basan para la facturación (tanto tokens de entrada como de salida), además de definir los límites de uso de cada modelo. Es decir, cada modelo tiene límites de tokens que se pueden informar en la entrada.

Teniendo ya en mente los conceptos básicos, empecemos a bucear en las funcionalidades de Spring AI y sus posibilidades.

ChatClient

El ChatClient ofrece un API para comunicarse con el modelo IA a través de métodos para crear las distintas partes de los prompts.

Crear un ChatClient

Se puede crear por autoconfiguración o de forma programática:

@RestController
public class TravelController {

    private final ChatClient chatClient;
    
    public TravelController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/travel-recommendation")
    String generation(@RequestBody String userInput) {
        return this.chatClient.prompt()
            .user(userInput)
            .call()
            .content();
    }
    
}
public class ChatClientCodeController {
    
    private ChatModel myChatModel;
    
    private ChatClient chatClient;
    
    public ChatClientCodeController(ChatModel myChatModel) {
        this.myChatModel = myChatModel;
        this.chatClient = ChatClient.create(this.myChatModel);
//        this.chatClient = ChatClient.builder(this.myChatModel).build();
    }

    @GetMapping("/chat-client-programmatically")
    String chatClientGeneration(@RequestBody String userInput) {
        return this.chatClient.prompt()
            .user(userInput)
            .call()
            .content();
    }
}

Respuestas de ChatClient

@GetMapping("/travel-recommendation/chat-response")
ChatResponse travelRecommendationResponse(@RequestBody String userInput) {
    return this.chatClient.prompt()
          .user(userInput)
          .call()
          .chatResponse();
}
@GetMapping("/travel-recommendation/entity")
TravelRecommendation travelRecommendationEntity(@RequestBody String userInput) {
    return this.chatClient.prompt()
        .user(userInput)
        .call()
        .entity(TravelRecommendation.class);
}
public class TravelRecommendation {

    private List<City> cities;
    
}
public class City {

    private String name;
    
}

Configuraciones por defecto

Haciendo uso de configuraciones por defecto, es posible solo tener que informar el input del usuario en tiempo de ejecución. Por ejemplo, para indicar el system input (con o sin parámetros). El system input indica el comportamiento básico del agente como, por ejemplo, que acote a cierta temática y actúe como un chatbot de viajes, y se podría configurar:

@Bean
ChatClient chatClient(ChatClient.Builder builder) {
         return builder.defaultSystem("You are a travel chat bot that only recommends 3 cities under 50000 population")
       .build();
}
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder.defaultSystem("You are a travel chat bot that only recommends 3 cities under {population} population")
       .build();
}
@GetMapping("/travel-recommendation-population")
String travelRecommendationPopulation(@RequestBody String userInput, @RequestParam Long population) {
    return this.chatClient.prompt()
                .system(sp -> sp.param("population", population))
                .user(userInput)
                .call()
                .content();
}

Otras configuraciones por defecto ya en el ChatClient.Builder, que también pueden sobreescribirse en tiempo de ejecución con métodos similares, son:

Modelos

Para la comunicación con los modelos, Spring AI nos ofrece un API. Este API soporta modelos de Chat, Text to Image, Audio Transcription, Text to Speech y Embeddings, de forma síncrona y asíncrona. Además proporciona funcionalidades específicas de cada modelo. Soporta modelos de distintos proveedores como OpenAI, Microsoft, Amazon, Google, Hugging Face, etc.

Modelos Spring AI: streamingmodel

En primer lugar nos centraremos en las funcionalidades relacionadas con el chat, pero se puede ver que el funcionamiento para el resto de modelos es muy similar.

Chat Model API

Este API ofrece la posibilidad de integrar funcionalidades de chat con IA a partir de LLMs para generar respuestas en lenguaje natural.

Este API funciona enviado un prompt al modelo de IA que genera una respuesta a la conversación basada en sus datos de entrenamiento y su comprensión de los patrones de lenguaje natural. Al recibir la respuesta, se puede devolver directamente o usarla para realizar más funcionalidades. El API se ha diseñado pensando en la simplicidad y portabilidad para interactuar entre los distintos modelos de la forma más transparente posible.

API Overview

A continuación comentamos las principales clases de este API:

public interface ChatModel extends Model<Prompt, ChatResponse> {

    default String call(String message) {...}

    @Override
    ChatResponse call(Prompt prompt);
}
public class Prompt implements ModelRequest<List<Message>> {

    private final List<Message> messages;
    private ChatOptions modelOptions;

    @Override
    public ChatOptions getOptions() {...}

    @Override
    public List<Message> getInstructions() {...}

}
public interface Content {

    String getContent();

    Map<String, Object> getMetadata();
}

public interface Message extends Content {

    MessageType getMessageType();
}

public interface MediaContent extends Content {

    Collection<Media> getMedia();
}

Existen varias implementaciones de Message que corresponden con las categorías que los modelos pueden procesar.

Spring AI Message API

Antes de enviar el mensaje al LLM, se distingue entre los distintos roles (system, user, function, assistant) a través del MessageType para saber cómo tiene que actuar el mensaje. En ciertos modelos que no soportan roles, el UserMessage actúa como la categoría estándar o por defecto.

public interface ChatOptions extends ModelOptions {

    String getModel();
    Float getFrequencyPenalty();
    Integer getMaxTokens();
    Float getPresencePenalty();
    List<String> getStopSequences();
    Float getTemperature();
    Integer getTopK();
    Float getTopP();
    ChatOptions copy();
}

Todo esto permite sobrescribir las opciones a informar al modelo en tiempo de ejecución para cada petición, existiendo una configuración por defecto para la aplicación, lo que permite una mayor flexibilidad.

Prompt, chatmodel y chatresponse

El flujo de interacción con el modelo sería:

  1. Configuración de inicio de ChatOptions que actúa como valores por defecto.
  2. Por cada petición, ChatOptions puede informar al prompt que sobreescribe las configuraciones iniciales.
  3. Se combinan las opciones por defecto y las enviadas en cada petición, siendo estas últimas las que tienen prioridad.
  4. Se transforman las opciones enviadas a formatos nativos de cada modelo.
public class ChatResponse implements ModelResponse<Generation> {

    private final ChatResponseMetadata chatResponseMetadata;
    private final List<Generation> generations;

    @Override
    public ChatResponseMetadata getMetadata() {...}

    @Override
    public List<Generation> getResults() {...}
}

Puesto que el API de ChatModel está construido sobre el API genérico de Model, nos permite cambiar de forma transparente entre los distintos servicios de IA manteniendo el mismo código.

Construcción del api chatmodel

En la siguiente imagen se puede ver las relaciones entre las distintas clases del API de Model de Spring AI:

Relación de API Model con Spring AI

Comparación de modelos

En la documentación existe una sección en donde se comparan los distintos modelos basados en las funcionalidades que poseen:

Comparación de modelos: provider, multimodality, tools/functions, streaming, retry, observability

Para los ejemplos de implementación se ha seleccionado Ollama, debido a que es uno de los que más funcionalidades ofrece y, sobre todo, por la posibilidad de ejecutarlo en local para no incurrir en costes, ya que la gran mayoría de los modelos públicos son de pago.

Ollama

Con Ollama es posible ejecutar varios LLMs de forma local. En esta sección se verán algunos de los temas importantes de cara a su configuración y uso.

Prerrequisitos

Crear una instancia de Ollama a través de:

Instalación de Ollama en local

Para poder comprobar las posibilidades que nos ofrece Ollama, instalaremos una instancia en nuestro ordenador. Para ello, simplemente se descargará Ollama como se indica en su página oficial (en este caso usaremos Linux - Ubuntu).

curl -fsSL https://ollama.com/install.sh | sh

Una vez descargado Ollama (puede tardar un tiempo), se descarga un modelo. En este caso, se ejecutará el modelo llama3.2:1b (pero se puede comprobar que existen muchos más con distintas características) puesto que es más ligero a través del siguiente comando:

ollama run llama3.2:1b

Como puede observarse al lanzar el comando anterior, ya muestra el prompt para interactuar con él:

Prompt ollama: Send a message (/? for help)

Pudiendo hacerle preguntas:

Interacción con el prompt de ollama. se le hace preguntas y las va respondiendo

Autoconfiguración

Spring AI ofrece autoconfiguración para Spring Boot para la integración de Ollama a través de la dependencia:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

De cara a esta autoconfiguración, se pueden indicar propiedades base o propiedades de chat para las distintas funcionalidades, siendo algunas de las más importantes:

Property Descripción Default
spring.ai.ollama.base-url URL donde se ejecuta el servidor de Ollama localhost:11434
spring.ai.ollama.init.pull-model-strategy Si se deben descargar los modelos al inicio de la aplicación y cómo never
spring.ai.ollama.init.chat.additional-models Modelos adicionales además del configurado por defecto en las propierties [ ]
spring.ai.ollama.chat.enabled Habilitar el chat model de Ollama true
spring.ai.ollama.chat.options.model El nombre del modelo a usar mistral
spring.ai.ollama.chat.options.num-ctx Configura el tamaño de la context window empleada para generar el siguiente token 2048
spring.ai.ollama.chat.options.top-k Reduce la probabilidad de generar respuestas sin sentido. Un valor alto (100) dará respuestas más diversas, mientras que un valor más bajo (10) será más conservador 40
spring.ai.ollama.chat.options.top-p Funciona conjuntamente con la propiedad top-k. Un valor alto (0.95) hará que se responda de una forma más diversa, mientras que valores más bajos (0.5) generará respuestas más enfocadas y conservadoras 0.9
spring.ai.ollama.chat.options.temperature Configura la temperatura del modelo. Valores más altos harán que el modelo responda de una forma más creativa 0.8

Opciones en tiempo de ejecución

En tiempo de ejecución se pueden sobrescribir las opciones por defecto a enviar al prompt como, por ejemplo, la “temperatura”:

@GetMapping("/city-name-generation")
String cityNameGeneration() {
    return chatModel
        .call(new Prompt("Inventa 5 nombres de ciudades.",
            OllamaOptions.builder()
                .model(OllamaModel.LLAMA3_2_1B)
                .temperature(0.4)
                .build()))
        .getResult().getOutput().getText();
}

Descarga de modelos

Spring AI Ollama puede descargar automáticamente modelos cuando no existen en la instancia Ollama. Existen tres formas de descargar los modelos:

En la siguiente imagen se puede observar cómo se descarga el modelo inexistente al arrancar la aplicación:

Descargar modelo inexistente con Ollama en Spring AI

Los modelos definidos por properties se pueden descargar en el arranque, indicando propiedades como el tipo de estrategia, el timeout o el número máximo de reintentos:

spring:
  ai:
    ollama:
      init:
        pull-model-strategy: always
        timeout: 60s
        max-retries: 1

También es posible iniciar otros modelos en el arranque de cara a usarlos posteriormente en tiempo de ejecución:

spring:
  ai:
    ollama:
      init:
        pull-model-strategy: always
        chat:
          additional-models:
            - llama3.2
            - qwen2.5

Existiendo además la posibilidad de excluir ciertos tipos de modelos:

spring:
  ai:
    ollama:
      init:
        pull-model-strategy: always
        chat:
          include: false

Ollama APIClient

A modo de nota informativa, la siguiente imagen muestra las interfaces y clases en el Ollama API (aunque el uso de este API no está recomendado. En su lugar, es mejor usar OllamaChatModel):

estructura de ollama apiclient

Conclusión

Hemos visto algunos de los conceptos importantes a tener en cuenta para trabajar con los LLMs, así como los principales APIs de Spring AI que nos permiten interactuar con dichos LLMs. Podemos constatar cómo el módulo está pensado de forma abstracta para poder conectarnos con los últimos modelos de LLM (OpenAI, Gemini, Anthropic, Mistral, etc), haciendo más foco en Ollama por la cantidad de funcionalidades que aporta, además de poder ejecutarse en local y no ser exclusivamente de pago.

En sucesivos post se continuará explorando otras partes del módulo en el camino a construir e integrar aplicaciones de IA con Spring.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete