Artículo

Construcción de un chatbot de IA utilizando modelos DeepSeek con Spring AI

Construcción de un chatbot de IA utilizando modelos DeepSeek con Spring AI

1. Resumen

Las aplicaciones web modernas están integrando cada vez más con Modelos de Lenguaje Grande (LLMs) para construir soluciones.

DeepSeek es una compañía china de investigación en IA que desarrolla potentes LLMs y recientemente ha revolucionado el mundo de la IA con sus modelos DeepSeek‑V3 y DeepSeek‑R1. El último modelo, junto con su respuesta, expone su Chain of Thought (CoT), lo que nos brinda una visión de cómo el modelo AI interpreta y aborda el prompt dado.

En este tutorial, exploraremos la integración de modelos DeepSeek con Spring AI. Construiremos un chatbot sencillo capaz de mantener conversaciones de texto de varias rondas.

2. Dependencias y Configuración

Hay varias formas de integrar modelos DeepSeek en nuestra aplicación, y en esta sección discutiremos algunas opciones populares. Podemos elegir la que mejor se ajuste a nuestros requisitos.

2.1. Usando las APIs de OpenAI

Los modelos DeepSeek son totalmente compatibles con las APIs de OpenAI y se pueden acceder con cualquier cliente o biblioteca de OpenAI.

Empecemos añadiendo la dependencia de inicio de Spring AI OpenAI starter al archivo pom.xml de nuestro proyecto:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

Dado que la versión actual, 1.0.0-M6, es una versión de hito, también necesitaremos añadir el repositorio Spring Milestones a nuestro pom.xml:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

Este repositorio es donde se publican las versiones de hito, en lugar del repositorio Maven Central estándar. Tendremos que añadir este repositorio de hito independientemente de la opción de configuración que elijamos.

A continuación, configuremos nuestra clave API de DeepSeek y el modelo de chat en el archivo application.yaml:

spring:
  ai:
    openai:
      api-key: ${DEEPSEEK_API_KEY}
      chat:
        options:
          model: deepseek-reasoner
      base-url: https://api.deepseek.com
      embedding:
        enabled: false

Además, especificamos la URL base de la API de DeepSeek y desactivamos los embeddings ya que DeepSeek actualmente no ofrece modelos compatibles con embeddings.

Al configurar las propiedades anteriores, Spring AI crea automáticamente un bean del tipo ChatModel, lo que nos permite interactuar con el modelo especificado. Lo usaremos para definir algunos beans adicionales para nuestro chatbot más adelante en el tutorial.

2.2. Usando la API de Amazon Bedrock Converse

Alternativamente, podemos usar la API de Amazon Bedrock Converse para integrar el modelo DeepSeek R1 en nuestra aplicación.

Para seguir con este paso de configuración, necesitaremos una cuenta activa de AWS.

El modelo DeepSeek‑R1 está disponible a través de Amazon Bedrock Marketplace y se puede alojar usando Amazon SageMaker. Esta guía de despliegue puede consultarse para configurarlo.

Empecemos añadiendo la dependencia de inicio de Bedrock Converse a nuestro pom.xml:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-bedrock-converse-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

A continuación, para interactuar con Amazon Bedrock, necesitamos configurar nuestras credenciales de AWS para la autenticación y la región donde se aloja el modelo DeepSeek en el archivo application.yaml:

spring:
  ai:
    bedrock:
      aws:
        region: ${AWS_REGION}
        access-key: ${AWS_ACCESS_KEY}
        secret-key: ${AWS_SECRET_KEY}
      converse:
        chat:
          options:
            model: arn:aws:sagemaker:REGION:ACCOUNT_ID:endpoint/ENDPOINT_NAME

Utilizamos el marcador de posición de propiedad ${} para cargar los valores de nuestras propiedades desde variables de entorno.

Además, especificamos el ARN de la URL del endpoint de SageMaker donde se aloja el modelo DeepSeek. Debemos recordar reemplazar los marcadores REGION, ACCOUNT_ID y ENDPOINT_NAME con los valores reales.

Finalmente, para interactuar con el modelo, necesitaremos asignar la siguiente política IAM al usuario IAM que hemos configurado en nuestra aplicación:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock:InvokeModel",
      "Resource": "arn:aws:bedrock:REGION:ACCOUNT_ID:marketplace/model-endpoint/all-access"
    }
  ]
}

Nuevamente, debemos recordar reemplazar los marcadores REGION y ACCOUNT_ID con los valores reales en el ARN del Resource.

2.3. Configuración local con Ollama

Para el desarrollo y las pruebas locales, podemos ejecutar los modelos DeepSeek a través de Ollama, que es una herramienta de código abierto que nos permite ejecutar LLMs en nuestras máquinas locales.

Importemos la dependencia necesaria en el archivo pom.xml de nuestro proyecto:

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

La dependencia de inicio de Ollama nos ayuda a establecer una conexión con el servicio Ollama.

A continuación, configuremos nuestro modelo de chat en el archivo application.yaml:

spring:
  ai:
    ollama:
      chat:
        options:
          model: deepseek-r1
      init:
        pull-model-strategy: when_missing
      embedding:
        enabled: false

Aquí especificamos el modelo deepseek-r1, sin embargo, también podemos probar esta implementación con un modelo diferente disponible.

Además, configuramos la estrategia pull-model-strategy a when_missing. Esto garantiza que Spring AI descargue el modelo especificado si no está disponible localmente.

Spring AI se conecta automáticamente a Ollama al ejecutarse en localhost en su puerto predeterminado de 11434. Sin embargo, podemos anular la URL de conexión usando la propiedad spring.ai.ollama.base-url. Alternativamente, podemos usar Testcontainers para configurar el servicio Ollama.

Aquí, nuevamente, Spring AI creará automáticamente el bean ChatModel para nosotros. Si por alguna razón tenemos las tres dependencias -- API de OpenAI, Bedrock Converse y Ollama -- en nuestro classpath, podemos referenciar el bean específico que deseamos usando el qualifier de openAiChatModel, bedrockProxyChatModel o ollamaChatModel, respectivamente.

3. Construyendo un chatbot

Ahora que hemos discutido las distintas opciones de configuración, vamos a construir un chatbot sencillo utilizando el modelo DeepSeek configurado.

3.1. Definiendo Beans del chatbot

Empecemos definiendo los beans necesarios para nuestro chatbot:

@Bean
ChatMemory chatMemory() {
    return new InMemoryChatMemory();
}

@Bean
ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
    return ChatClient
      .builder(chatModel)
      .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
      .build();
}

Primero, definimos un bean ChatMemory usando la implementación InMemoryChatMemory, que almacena el historial de chat en memoria para mantener el contexto de la conversación.

Luego, creamos un bean ChatClient usando los beans ChatModel y ChatMemory. La clase ChatClient sirve como nuestro punto de entrada principal para interactuar con el modelo DeepSeek que hemos configurado.

3.2. Creando un StructuredOutputConverter personalizado

Como se mencionó anteriormente, la respuesta del modelo DeepSeek‑R1 incluye su CoT, y obtenemos la respuesta en el siguiente formato:

<think>
Chain of Thought
</think>
Answer

Desafortunadamente, debido a este formato único, todos los convertidores de salida estructurada presentes en la versión actual de Spring AI fallan y lanzan una excepción cuando intentamos analizar la respuesta en una clase Java.

Entonces, creemos nuestra propia implementación personalizada de StructuredOutputConverter para analizar la respuesta y el CoT del modelo AI por separado:

record DeepSeekModelResponse(String chainOfThought, String answer) {
}

class DeepSeekModelOutputConverter implements StructuredOutputConverter<DeepSeekModelResponse> {
    private static final String OPENING_THINK_TAG = "<think>";
    private static final String CLOSING_THINK_TAG = "</think>";

    @Override
    public DeepSeekModelResponse convert(@NonNull String text) {
        if (!StringUtils.hasText(text)) {
            throw new IllegalArgumentException("Text cannot be blank");
        }
        int openingThinkTagIndex = text.indexOf(OPENING_THINK_TAG);
        int closingThinkTagIndex = text.indexOf(CLOSING_THINK_TAG);

        if (openingThinkTagIndex != -1 && closingThinkTagIndex != -1 && closingThinkTagIndex > openingThinkTagIndex) {
            String chainOfThought = text.substring(openingThinkTagIndex + OPENING_THINK_TAG.length(), closingThinkTagIndex);
            String answer = text.substring(closingThinkTagIndex + CLOSING_THINK_TAG.length());
            return new DeepSeekModelResponse(chainOfThought, answer);
        } else {
            logger.debug("No <think> tags found in the response. Treating entire text as answer.");
            return new DeepSeekModelResponse(null, text);
        }
    }
}

Aquí, nuestro convertidor extrae el chainOfThought y el answer de la respuesta del modelo AI y los devuelve como un registro DeepSeekModelResponse.

Si la respuesta del AI no contiene las etiquetas <think>, tratamos toda la respuesta como la respuesta. Esto garantiza la compatibilidad con otros modelos DeepSeek que no incluyen CoT en sus respuestas.

3.3. Implementando la capa de servicio

Con nuestras configuraciones en su lugar, creemos una clase ChatbotService. Inyectaremos el bean ChatClient que definimos anteriormente para interactuar con el modelo DeepSeek especificado.

Pero primero, definamos dos registros simples para representar la solicitud y la respuesta del chat:

record ChatRequest(@Nullable UUID chatId, String question) {}

record ChatResponse(UUID chatId, String chainOfThought, String answer) {}

ChatRequest contiene la question del usuario y un chatId opcional para identificar una conversación en curso.

Del mismo modo, ChatResponse contiene el chatId, junto con el chainOfThought y el answer del chatbot.

Ahora, implementemos la funcionalidad deseada:

ChatResponse chat(ChatRequest chatRequest) {
    UUID chatId = Optional
      .ofNullable(chatRequest.chatId())
      .orElse(UUID.randomUUID());
    DeepSeekModelResponse response = chatClient
      .prompt()
      .user(chatRequest.question())
      .advisors(advisorSpec ->
          advisorSpec
            .param("chat_memory_conversation_id", chatId))
      .call()
      .entity(new DeepSeekModelOutputConverter());
    return new ChatResponse(chatId, response.chainOfThought(), response.answer());
}

Si la solicitud entrante no contiene un chatId, generamos uno nuevo. Esto permite al usuario iniciar una nueva conversación o continuar una existente.

Pasamos la question del usuario al bean chatClient y establecemos el parámetro chat_memory_conversation_id al chatId resuelto para mantener el historial de conversación.

Finalmente, creamos una instancia de nuestra clase personalizada DeepSeekModelOutputConverter y la pasamos al método entity() para analizar la respuesta del modelo AI en un registro DeepSeekModelResponse. Luego, extraemos el chainOfThought y el answer de él y los devolvemos junto con el chatId.

3.4. Interactuando con nuestro chatbot

Ahora que hemos implementado nuestra capa de servicio, expondremos una API REST encima de ella:

@PostMapping("/chat")
ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
    ChatResponse chatResponse = chatbotService.chat(chatRequest);
    return ResponseEntity.ok(chatResponse);
}

Usemos la CLI HTTPie para invocar el punto de enlace API anterior y comenzar una nueva conversación:

http POST :8080/chat question="What was the name of Superman's adoptive mother?"

Aquí, enviamos una simple question al chatbot; veamos qué recibimos como respuesta:

Respuesta de la API del chatbot DeepSeek a una pregunta de usuario.

La respuesta contiene un chatId único, así como el chainOfThought y el answer del chatbot a nuestra pregunta. Podemos ver cómo el modelo AI razona y aborda la solicitud usando el atributo chainOfThought.

Continuemos esta conversación enviando una question de seguimiento usando el chatId de la respuesta anterior:

http POST :8080/chat question="Which bald billionaire hates him?" chatId="1e3c151f-cded-4f10-a5fc-c52c5952411c"

Veamos si el chatbot puede mantener el contexto de nuestra conversación y proporcionar una respuesta relevante:

Respuesta de la API del chatbot DeepSeek a una pregunta de seguimiento de usuario.

Como podemos ver, el chatbot mantiene realmente el contexto de la conversación. El chatId permanece igual, indicando que la respuesta de seguimiento es una continuación de la misma conversación.

4. Conclusión

En este artículo, hemos explorado el uso de modelos DeepSeek con Spring AI.

Discutimos varias opciones para integrar modelos DeepSeek en nuestra aplicación, incluyendo una en la que usamos directamente la API de OpenAI ya que DeepSeek es compatible con ella, y otra en la que trabajamos con la API de Amazon Bedrock Converse. Además, exploramos la configuración de un entorno de prueba local usando Ollama.

Luego, construimos un chatbot sencillo capaz de conversaciones de texto de varias rondas y usamos una implementación personalizada de StructuredOutputConverter para extraer la cadena de pensamiento y la respuesta de la respuesta del modelo AI.

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