1. Visión general
Inteligencia Artificial está cambiando la forma en que construimos aplicaciones web. Hugging Face es una plataforma popular que ofrece una gran colección de código abierto y modelos LLMs preentrenados.
Podemos usar Ollama, una herramienta de código abierto, para ejecutar LLMs en nuestras máquinas locales. Admite la ejecución de modelos en formato GGUF de Hugging Face.
En este tutorial, exploraremos cómo usar modelos de Hugging Face con Spring AI y Ollama. Construiremos un chatbot simple utilizando un modelo de completado de chat e implementaremos búsqueda semántica con un modelo de incrustación.
2. Dependencias
Comencemos añadiendo la dependencia necesaria al 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 ayuda a establecer una conexión con el servicio Ollama. La usaremos para descargar y ejecutar nuestros modelos de completado de chat e incrustación.
Dado que la versión actual, 1.0.0-M5, es una versión de hito, también necesitaremos añadir el repositorio de 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.
3. Configuración de Ollama con Testcontainers
Para facilitar el desarrollo y las pruebas locales, usaremos Testcontainers para configurar el servicio Ollama.
3.1. Dependencias de prueba
Primero, añadamos las dependencias de prueba necesarias al pom.xml:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>ollama</artifactId>
<scope>test</scope>
</dependency>
Importamos la dependencia de Spring AI Testcontainers para Spring Boot y el módulo Ollama de Testcontainers.
3.2. Definición del bean de Testcontainers
A continuación, creemos una clase @TestConfiguration que defina nuestros beans de Testcontainers:
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
public OllamaContainer ollamaContainer() {
return new OllamaContainer("ollama/ollama:0.5.4");
}
@Bean
public DynamicPropertyRegistrar dynamicPropertyRegistrar(OllamaContainer ollamaContainer) {
return registry -> {
registry.add("spring.ai.ollama.base-url", ollamaContainer::getEndpoint);
};
}
}
Especificamos la última versión estable de la imagen de Ollama al crear el bean OllamaContainer.
Luego, definimos un bean DynamicPropertyRegistrar para configurar el base-url del servicio Ollama. Esto permite que nuestra aplicación se conecte al contenedor Ollama iniciado.
3.3. Uso de Testcontainers durante el desarrollo
Si bien Testcontainers se usa principalmente para pruebas de integración, también podemos usarlo durante el desarrollo local.
Para lograrlo, crearemos una clase principal separada en nuestro directorio src/test/java:
public class TestApplication {
public static void main(String[] args) {
SpringApplication.from(Application::main)
.with(TestcontainersConfiguration.class)
.run(args);
}
}
Creamos una clase TestApplication y, dentro de su método main(), arrancamos nuestra clase principal Application con la clase TestcontainersConfiguration.
Esta configuración nos ayuda a ejecutar nuestra aplicación Spring Boot y a hacer que se conecte al servicio Ollama, iniciado mediante Testcontainers.
4. Uso de un modelo de completado de chat
Ahora que tenemos nuestro contenedor Ollama local configurado, usaremos un modelo de completado de chat para construir un chatbot simple.
4.1. Configuración de los beans de ChatModel y Chatbot
Comencemos configurando un modelo de completado de chat en nuestro archivo application.yaml:
spring:
ai:
ollama:
init:
pull-model-strategy: when_missing
chat:
options:
model: hf.co/microsoft/Phi-3-mini-4k-instruct-gguf
Para configurar un modelo de Hugging Face, usamos el formato hf.co/{usuario}/{repositorio}. Aquí, especificamos la versión GGUF del modelo Phi-3-mini-4k-instruct proporcionado por Microsoft.
No es obligatorio usar este modelo para nuestra implementación. Nuestra recomendación sería configurar el código localmente y experimentar con más modelos de completado de chat.
Además, configuramos el pull-model-strategy como when_missing. Esto garantiza que Spring AI descargue el modelo especificado si no está disponible localmente.
Al configurar un modelo válido, Spring AI crea automáticamente un bean de tipo ChatModel, lo que nos permite interactuar con el modelo de completado de chat.
Vamos a usarlo para definir los beans adicionales necesarios para nuestro chatbot:
@Configuration
class ChatbotConfiguration {
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
return ChatClient
.builder(chatModel)
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
}
}
Primero, definimos un bean ChatMemory y usamos la implementación InMemoryChatMemory. Esto mantiene el contexto de la conversación almacenando el historial de chat en la memoria.
Luego, utilizando los beans ChatMemory y ChatModel, creamos un bean de tipo ChatClient, que es nuestro punto de entrada principal para interactuar con nuestro modelo de completado de chat.
4.2. Implementación de un chatbot
Con nuestras configuraciones listas, crearemos una clase ChatbotService. Inyectaremos el bean ChatClient que definimos anteriormente para interactuar con nuestro modelo.
Primero, definamos dos registros simples para representar la solicitud y respuesta del chat:
record ChatRequest(@Nullable UUID chatId, String question) {}
record ChatResponse(UUID chatId, String answer) {}
El ChatRequest contiene la pregunta del usuario y un chatId opcional para identificar una conversación en curso.
De manera similar, el ChatResponse contiene el chatId y la respuesta del chatbot.
Ahora, implementemos la funcionalidad prevista:
public ChatResponse chat(ChatRequest chatRequest) {
UUID chatId = Optional
.ofNullable(chatRequest.chatId())
.orElse(UUID.randomUUID());
String answer = chatClient
.prompt()
.user(chatRequest.question())
.advisors(advisorSpec ->
advisorSpec
.param("chat_memory_conversation_id", chatId))
.call()
.content();
return new ChatResponse(chatId, 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 pregunta del usuario al bean chatClient y establecemos el parámetro chat_memory_conversation_id al chatId resuelto para mantener el historial de la conversación.
Finalmente, devolvemos la respuesta del chatbot junto con el chatId.
4.3. Interacción con nuestro chatbot
Ahora que hemos implementado nuestra capa de servicio, expondremos una API REST sobre ella:
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
ChatResponse chatResponse = chatbotService.chat(chatRequest);
return ResponseEntity.ok(chatResponse);
}
Usaremos el endpoint API anterior para interactuar con nuestro chatbot.
Utilicemos la CLI HTTPie para iniciar una nueva conversación:
http POST :8080/chat question="Who wanted to kill Harry Potter?"
Enviamos una pregunta simple al chatbot y veamos la respuesta:
{
"chatId": "7b8a36c7-2126-4b80-ac8b-f9eedebff28a",
"answer": "Lord Voldemort, also known as Tom Riddle, wanted to kill Harry Potter because of a prophecy that foretold a boy born at the end of July would have the power to defeat him."
}
La respuesta contiene un chatId único y la respuesta del chatbot a nuestra pregunta.
Continuemos esta conversación enviando una pregunta de seguimiento usando el chatId de la respuesta anterior:
http POST :8080/chat chatId="7b8a36c7-2126-4b80-ac8b-f9eedebff28a" question="Who should he have gone after instead?"
Veamos si el chatbot puede mantener el contexto de nuestra conversación y proporcionar una respuesta relevante:
{
"chatId": "7b8a36c7-2126-4b80-ac8b-f9eedebff28a",
"answer": "Based on the prophecy's criteria, Voldemort could have targeted Neville Longbottom instead, as he was also born at the end of July to parents who had defied Voldemort three times."
}
Como podemos ver, el chatbot sí mantiene el contexto de la conversación al referirse a la profecía que discutimos en el mensaje anterior.
El chatId permanece igual, lo que indica que la respuesta de seguimiento es una continuación de la misma conversación.
5. Uso de un modelo de incrustación
Pasando del modelo de completado de chat, ahora utilizaremos un modelo de incrustación para implementar búsqueda semántica en un pequeño conjunto de citas.
Recogeremos las citas de una API externa, las almacenaremos en un vector store en memoria y realizaremos una búsqueda semántica.
5.1. Obtención de registros de citas desde una API externa
Para nuestra demostración, usaremos la API QuoteSlate para obtener citas.
Creamos una clase utilitaria QuoteFetcher para esto:
class QuoteFetcher {
private static final String BASE_URL = "https://quoteslate.vercel.app";
private static final String API_PATH = "/api/quotes/random";
private static final int DEFAULT_COUNT = 50;
public static List<Quote> fetch() {
return RestClient
.create(BASE_URL)
.get()
.uri(uriBuilder ->
uriBuilder
.path(API_PATH)
.queryParam("count", DEFAULT_COUNT)
.build())
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
}
record Quote(String quote, String author) {}
Con RestClient, llamamos a la API de QuoteSlate con el recuento predeterminado de 50 y usamos ParameterizedTypeReference para deserializar la respuesta de la API en una lista de registros Quote.
5.2. Configuración y población de un vector store en memoria
Ahora, configuremos un modelo de incrustación en nuestro application.yaml:
spring:
ai:
ollama:
embedding:
options:
model: hf.co/nomic-ai/nomic-embed-text-v1.5-GGUF
Usamos la versión GGUF del modelo nomic-embed-text-v1.5 proporcionado por nomic-ai. Nuevamente, siéntete libre de probar esta implementación con un modelo de incrustación diferente.
Después de especificar un modelo válido, Spring AI crea automáticamente un bean de tipo EmbeddingModel.
Utilicémoslo para crear un bean VectorStore:
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore
.builder(embeddingModel)
.build();
}
Para nuestra demostración, creamos un bean de la clase SimpleVectorStore. Es una implementación en memoria que emula un vector store usando la clase java.util.Map.
Para poblar nuestro vector store con citas durante el arranque de la aplicación, crearemos una clase VectorStoreInitializer que implemente la interfaz ApplicationRunner:
@Component
class VectorStoreInitializer implements ApplicationRunner {
private final VectorStore vectorStore;
// constructor estándar
@Override
public void run(ApplicationArguments args) {
List<Document> documents = QuoteFetcher
.fetch()
.stream()
.map(quote -> {
Map<String, Object> metadata = Map.of("author", quote.author());
return new Document(quote.quote(), metadata);
})
.toList();
vectorStore.add(documents);
}
}
En nuestro VectorStoreInitializer, inyectamos una instancia de VectorStore.
Dentro del método run(), usamos nuestra clase utilitaria QuoteFetcher para obtener una lista de registros Quote. Luego, mapeamos cada quote en un Document y configuramos el campo author como metadata.
Finalmente, almacenamos todos los documents en nuestro vector store. Al invocar el método add(), Spring AI convierte automáticamente nuestro contenido de texto en una representación vectorial antes de almacenarlo en el vector store. No necesitamos convertirlo explícitamente usando el bean EmbeddingModel.
5.3. Prueba de búsqueda semántica
Con nuestro vector store poblado, validemos la funcionalidad de búsqueda semántica:
private static final int MAX_RESULTS = 3;
@ParameterizedTest
@ValueSource(strings = {"Motivation", "Happiness"})
void whenSearchingQuotesByTheme_thenRelevantQuotesReturned(String theme) {
SearchRequest searchRequest = SearchRequest
.builder()
.query(theme)
.topK(MAX_RESULTS)
.build();
List<Document> documents = vectorStore.similaritySearch(searchRequest);
assertThat(documents)
.hasSizeBetween(1, MAX_RESULTS)
.allSatisfy(document -> {
String title = String.valueOf(document.getMetadata().get("author"));
assertThat(title)
.isNotBlank();
});
}
Pasamos algunos temas comunes de citas a nuestro método de prueba usando @ValueSource. Luego creamos un objeto SearchRequest con el tema como consulta y MAX_RESULTS como el número deseado de resultados.
A continuación, llamamos al método similaritySearch() del bean vectorStore con el searchRequest. Al igual que el método add() del VectorStore, Spring AI convierte nuestra consulta en su representación vectorial antes de consultar el vector store.
Los documentos devueltos contendrán citas que están semánticamente relacionadas con el tema dado, incluso si no contienen la palabra clave exacta.
6. Conclusión
En este artículo, hemos explorado el uso de modelos de Hugging Face con Spring AI.
Usando Testcontainers, configuramos el servicio Ollama, creando un entorno de prueba local.
Primero, utilizamos un modelo de completado de chat para construir un chatbot simple. Luego, implementamos búsqueda semántica con un modelo de incrustación.
El código que respalda este artículo está disponible en GitHub. Una vez que estés conectado como un Miembro Baeldung Pro, comienza a aprender y a programar en el proyecto.
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.