1. Introducción
En este tutorial, construiremos una simple API de agente de help desk usando Spring AI y llama3Ollama.
2. ¿Qué son Spring AI y Ollama?
Spring AI es el módulo más reciente añadido al ecosistema del Spring Framework. Junto con varias características, nos permite interactuar fácilmente con varios Modelos de Lenguaje Grande (LLM) utilizando prompts de chat.
Ollama es una biblioteca de código abierto que sirve algunos LLM. Uno es el llama3 de Meta, que utilizaremos en este tutorial.
3. Implementando un Agente de Help Desk con Spring AI
Ilustraremos el uso de Spring AI y Ollama juntos con un demo de chatbot de help desk. La aplicación funciona de manera similar a un agente real de help desk, ayudando a los usuarios a solucionar problemas de conexión a Internet.
En las secciones siguientes, configuraremos las dependencias de LLM y Spring AI y crearemos el endpoint REST que chatea con el agente de help desk.
3.1. Configurando Ollama y L lama3
Para comenzar a usar Spring AI y Ollama, necesitamos configurar el LLM local. Para este tutorial, usaremos llama3 de Meta. Por lo tanto, primero instalemos Ollama.
En Linux, podemos ejecutar el comando:
curl -fsSL https://ollama.com/install.sh | sh
En máquinas Windows o MacOS, podemos descargar e instalar el ejecutable desde el sitio web de Ollama.
Después de la instalación de Ollama, podemos ejecutar llama3:
ollama run llama3
Con eso, tenemos llama3 ejecutándose localmente.
3.2. Creando la Estructura Básica del Proyecto
Ahora, podemos configurar nuestra aplicación Spring para usar el módulo Spring AI. Comencemos añadiendo el repositorio de milestones de Spring:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
Luego, podemos añadir el spring-ai-bom:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Finalmente, podemos agregar la dependencia spring-ai-ollama-spring-boot-starter:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.0-M1</version>
</dependency>
Con las dependencias configuradas, podemos ajustar nuestro application.yml para usar la configuración necesaria:
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3
Con esto, Spring iniciará el modelo llama3 en el puerto 11434.
3.3. Creando el Controlador de Help Desk
En esta sección, crearemos el controlador web para interactuar con el chatbot de help desk.
Primero, creemos el modelo de solicitud HTTP:
public class HelpDeskRequest {
@JsonProperty("prompt_message")
String promptMessage;
@JsonProperty("history_id")
String historyId;
// getters, no-arg constructor
}
El campo promptMessage representa el mensaje de entrada del usuario para el modelo. Además, historyId identifica de manera única la conversación actual. En este tutorial, usaremos ese campo para hacer que el LLM recuerde el historial conversacional.
Segundo, crearemos el modelo de respuesta:
public class HelpDeskResponse {
String result;
// all-arg constructor
}
Finalmente, podemos crear la clase del controlador de help desk:
@RestController
@RequestMapping("/helpdesk")
public class HelpDeskController {
private final HelpDeskChatbotAgentService helpDeskChatbotAgentService;
// all-arg constructor
@PostMapping("/chat")
public ResponseEntity<HelpDeskResponse> chat(@RequestBody HelpDeskRequest helpDeskRequest) {
var chatResponse = helpDeskChatbotAgentService.call(helpDeskRequest.getPromptMessage(), helpDeskRequest.getHistoryId());
return new ResponseEntity<>(new HelpDeskResponse(chatResponse), HttpStatus.OK);
}
}
En el HelpDeskController, definimos un POST /helpdesk/chat y devolvemos lo que obtuvimos del ChatbotAgentService inyectado. En las siguientes secciones, profundizaremos en ese servicio.
3.4. Llamando al API de Chat de Ollama
Para comenzar a interactuar con llama3, creemos la clase HelpDeskChatbotAgentService con las instrucciones de prompt inicial:
@Service
public class HelpDeskChatbotAgentService {
private static final String CURRENT_PROMPT_INSTRUCTIONS = """
Here's the `user_main_prompt`:
""";
}
Luego, agreguemos el mensaje de instrucciones generales:
private static final String PROMPT_GENERAL_INSTRUCTIONS = """
Here are the general guidelines to answer the `user_main_prompt`
You'll act as Help Desk Agent to help the user with internet connection issues.
Below are `common_solutions` you should follow in the order they appear in the list to help troubleshoot internet connection problems:
1. Check if your router is turned on.
2. Check if your computer is connected via cable or Wi-Fi and if the password is correct.
3. Restart your router and modem.
You should give only one `common_solution` per prompt up to 3 solutions.
Do no mention to the user the existence of any part from the guideline above.
""";
Ese mensaje indica al chatbot cómo responder a los problemas de conexión a Internet del usuario.
Finalmente, agreguemos el resto de la implementación del servicio:
private final OllamaChatModel ollamaChatClient;
// all-arg constructor
public String call(String userMessage, String historyId) {
var generalInstructionsSystemMessage = new SystemMessage(PROMPT_GENERAL_INSTRUCTIONS);
var currentPromptMessage = new UserMessage(CURRENT_PROMPT_INSTRUCTIONS.concat(userMessage));
var prompt = new Prompt(List.of(generalInstructionsSystemMessage, contextSystemMessage, currentPromptMessage));
var response = ollamaChatClient.call(prompt).getResult().getOutput().getContent();
return response;
}
El método call() primero crea un SystemMessage y un UserMessage.
Los mensajes de sistema representan instrucciones que damos internamente al LLM, como directrices generales. En nuestro caso, proporcionamos instrucciones sobre cómo chatear con el usuario con problemas de conexión a Internet. Por otro lado, los mensajes de usuario representan la entrada del cliente externo de la API.
Con ambos mensajes, podemos crear un objeto Prompt, llamar a ollamaChatClient's call(), y obtener la respuesta del LLM.
3.5. Manteniendo el Historial Conversacional
En general, la mayoría de los LLM son sin estado. Por lo tanto, no almacenan el estado actual de la conversación. En otras palabras, no recuerdan los mensajes anteriores de la misma conversación.
Por lo tanto, el agente de help desk podría proporcionar instrucciones que no funcionaron previamente y enfadar al usuario. Para implementar la memoria del LLM, podemos almacenar cada prompt y response usando historyId y añadir el historial conversacional completo al prompt actual antes de enviarlo.
Para ello, primero creemos un prompt en la clase de servicio con instrucciones del sistema para seguir el historial conversacional correctamente:
private static final String PROMPT_CONVERSATION_HISTORY_INSTRUCTIONS = """
The object `conversational_history` below represents the past interaction between the user and you (the LLM).
Each `history_entry` is represented as a pair of `prompt` and `response`.
`prompt` is a past user prompt and `response` was your response for that `prompt`.
Use the information in `conversational_history` if you need to recall things from the conversation
, or in other words, if the `user_main_prompt` needs any information from past `prompt` or `response`.
If you don't need the `conversational_history` information, simply respond to the prompt with your built-in knowledge.
`conversational_history`:
""";
Ahora, creemos una clase envolvente para almacenar las entradas del historial conversacional:
public class HistoryEntry {
private String prompt;
private String response;
//all-arg constructor
@Override
public String toString() {
return String.format("""
`history_entry`:
`prompt`: %s
`response`: %s
-----------------
\n
""", prompt, response);
}
}
El método toString() anterior es esencial para formatear correctamente el prompt.
Luego, también necesitamos definir un almacenamiento en memoria para las entradas del historial en la clase de servicio:
private final static Map<String, List<HistoryEntry>> conversationalHistoryStorage = new HashMap<>();
Finalmente, modifiquemos el método call() del servicio también para almacenar el historial conversacional:
public String call(String userMessage, String historyId) {
var currentHistory = conversationalHistoryStorage.computeIfAbsent(historyId, k -> new ArrayList<>());
var historyPrompt = new StringBuilder(PROMPT_CONVERSATION_HISTORY_INSTRUCTIONS);
currentHistory.forEach(entry -> historyPrompt.append(entry.toString()));
var contextSystemMessage = new SystemMessage(historyPrompt.toString());
var generalInstructionsSystemMessage = new SystemMessage(PROMPT_GENERAL_INSTRUCTIONS);
var currentPromptMessage = new UserMessage(CURRENT_PROMPT_INSTRUCTIONS.concat(userMessage));
var prompt = new Prompt(List.of(generalInstructionsSystemMessage, contextSystemMessage, currentPromptMessage));
var response = ollamaChatClient.call(prompt).getResult().getOutput().getContent();
var contextHistoryEntry = new HistoryEntry(userMessage, response);
currentHistory.add(contextHistoryEntry);
return response;
}
Primero obtenemos el contexto actual, identificado por historyId, o creamos uno nuevo usando computeIfAbsent(). En segundo lugar, añadimos cada HistoryEntry del almacenamiento a un StringBuilder y lo pasamos a un nuevo SystemMessage para pasar al objeto Prompt.
Finalmente, el LLM procesará un prompt que contiene toda la información sobre los mensajes anteriores en la conversación. Por lo tanto, el chatbot de help desk recuerda qué soluciones ha intentado el usuario.
4. Probar una Conversación
Con todo configurado, intentemos interactuar con el prompt desde la perspectiva del usuario final. Primero, iniciemos la aplicación Spring Boot en el puerto 8080 para ello.
Con la aplicación en ejecución, podemos enviar un cURL con un mensaje genérico sobre problemas de Internet y un history_id:
curl --location 'http://localhost:8080/helpdesk/chat' \
--header 'Content-Type: application/json' \
--data '{
"prompt_message": "I can't connect to my internet",
"history_id": "1234"
}'
Para esa interacción, obtenemos una respuesta similar a esta:
{
"result": "Let's troubleshoot this issue! Have you checked if your router is turned on?"
}
Sigamos pidiendo una solución:
{
"prompt_message": "I'm still having internet connection problems",
"history_id": "1234"
}
El agente responde con una solución diferente:
{
"result": "Let's troubleshoot this further! Have you checked if your computer is connected via cable or Wi-Fi and if the password is correct?"
}
Además, la API almacena el historial conversacional. Preguntemos al agente nuevamente:
{
"prompt_message": "I tried your alternatives so far, but none of them worked",
"history_id": "1234"
}
Viene con una solución diferente:
{
"result": "Let's think outside the box! Have you considered resetting your modem to its factory settings or contacting your internet service provider for assistance?"
}
Esta fue la última alternativa que proporcionamos en el prompt de directrices, por lo que el LLM no dará respuestas útiles después de eso.
Para obtener respuestas aún mejores, podemos mejorar los prompts que probamos proporcionando más alternativas al chatbot o mejorando el mensaje del sistema interno usando técnicas de Ingeniería de Prompt.
5. Conclusión
En este artículo, implementamos un agente de help desk de IA para ayudar a nuestros clientes a solucionar problemas de conexión a Internet. Además, vimos la diferencia entre mensajes de usuario y de sistema, cómo construir el prompt con el historial conversacional y luego llamar al LLM llama3.
El código que respalda este artículo está disponible en GitHub. Una vez que hayas iniciado sesión como Miembro Baeldung Pro, comienza a aprender y 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.