La inteligencia artificial (IA) busca que las máquinas emulen capacidades humanas. A diferencia de un programa tradicional con reglas predefinidas, un sistema de IA aprende de datos para tomar decisiones o predecir resultados.
La rama más destacada actualmente es la IA Generativa (AIGC), enfocada en la creación de contenido digital como texto, código o imágenes, en contraste con la IA tradicional orientada al análisis de datos.
Conceptos Fundamentales de IA
Modelos (Model)
Un modelo es el núcleo de un sistema de IA. Se entrena sobre datos utilizando algoritmos, funcionando como una función matemática que procesa entradas para generar salidas. Llamar a una IA implica usar su modelo subyacente, cuyo tamaño varía considerablemente.
Imagínalo como un "cerebro virtual" que adquiere habilidades al ser expuesto a grandes volúmenes de información.
Modelos de Lenguaje Grandes (LLM)
Los LLM son modelos de aprendizaje profundo entrenados con cantidades masivas de texto. Su propósito es comprender y generar lenguaje humano, siendo la base de la actual revolución de la IA Generativa. Su denominación "grande" se refiere a la escala de los datos de entrenamiento y la cantidad de parámetros.
Piensa en ellos como "cerebros expertos" hiperentrenados que, tras procesar vasta información textual, dominan la gramática, el conocimiento factual y la lógica contextual, permitiéndoles abordar una amplia gama de temas.
Instrucciones (Prompt)
Una instrucción es la directiva, pregunta o contexto que un usuario proporcioan a un modelo de IA. La calidad de la respuesta del modelo depende directamente de la claridad y especificidad de la instrucción. El diseño y optimización de estas instrucciones se conoce como "ingeniería de instrucciones".
Las instrucciones son como "pedidos de trabajo" para la IA. Un pedido claro resulta en un trabajo de mayor calidad.
- Ejemplo simple: "¿Cuál es la capital de Francia?" → Modelo responde: "París."
- Ejemplo complejo (rol): "Actúa como un nutricionista experimentado y diseña una dieta semanal saludable para un oficinista sedentario." → El modelo proporcionará una receta detallada con el tono de un nutricionista.
Tokens
Los tokens son las unidades básicas de texto que un modelo procesa y comprende. No siempre corresponden a una palabra completa ni a un carácter individual. El texto se divide en tokens antes de ser procesado, y los tokens se utilizan para la facturación y para medir la longitud del procesamiento.
Por ejemplo, la palabra "increíble" podría dividirse en los tokens ["in", "creíble"]. Una frase como "Me gusta programar" podría fragmentarse en ["Yo", "gusto", "programar"]. Las reglas de tokenización varía entre modelos.
Spring AI: Simplificando la Integración de IA
Spring AI es un framework de código abierto dentro del ecosistema Spring, diseñado para facilitar la integración de funcionalidades de IA en aplicaciones Java. Su objetivo es permitir a los desarrolladores Java construir aplicaciones de IA Generativa de manera eficiente.
Ofrece abstracciones con múltiples implementaciones, permitiendo cambiar componentes con mínimo esfuerzo. Soporta la interacción con diversos proveedores de modelos de IA (OpenAI, Microsoft, Google, etc.), tanto en la nube como localmente (ej. Ollama). Destaca por su integración con bases de datos vectoriales (Chroma, Pinecone, Redis) y su sinergia con otros proyectos Spring como Spring Boot.
Además, facilita la invocación de funciones definidas por el cliente para obtener información en tiempo real o ejecutar acciones específicas.
Primeros Pasos con Spring AI
Requisitos del Entorno
- JDK: Versión 17 o superior (recomendado 21).
- Spring Boot: Versión 3.2 o superior.
- Credenciales de IA: Una clave API válida de un proveedor de IA (ej. OpenAI, DeepSeek).
Para este ejemplo, utilizaremos DeepSeek.
Obtención de una Clave API (DeepSeek)
- Visita el sitio web de DeepSeek y accede a la plataforma de API.
- Crea una nueva clave API. Nota: la clave solo es visible durante la creación.
- Realiza una pequeña recarga de saldo (se requiere verificación de identidad) para poder usar la API.
Creación del Proyecto
Utiliza Spring Initializr para crear un proyecto Maven con las versiones de JDK y Spring Boot adecuadas. Añade la siguiente dependencia para la integración con modelos compatibles con la API de OpenAI:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
Configura tus credenciales y URL en application.yml:
spring:
ai:
openai:
# DeepSeek configuration
api-key: TU_API_KEY_DEEPSEEK
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
temperature: 0.7 # Controla la aleatoriedad (0.0 conservador, 2.0 imaginativo)
Desarrollo de la Interfaz de Usuario
Crea un controlador REST para interactuar con el modelo:
@RestController
@RequestMapping("/deepseek")
public class DeepSeekChatController {
@Autowired
private OpenAiChatModel deepSeekChatModel; // Spring AI proporciona implementaciones específicas
@GetMapping("/chat")
public String generate(String message) {
// Simple call to the model
return deepSeekChatModel.call(message);
}
}
Ejecuta la aplicación y accede a http://localhost:8080/deepseek/chat?message=¿Quién eres? para probar la comunicación.
Interfaces Clave en Spring AI
ChatModel
ChatModel es una interfaz de bajo nivel que interactúa directamente con los modelos de IA subyacentes. Maneja las solicitudes y respuestas crudas, ofreciendo gran flexibilidad.
@Service
public class ChatService {
@Autowired
private ChatModel chatModel; // Una implementación como OpenAiChatModel
public String askQuestion(String question) {
// 1. Construct the message
UserMessage userMessage = new UserMessage(question);
// 2. Create a Prompt
Prompt prompt = new Prompt(List.of(userMessage));
// 3. Call the model and get the full response
ChatResponse response = chatModel.call(prompt);
// 4. Extract content from the response
return response.getResult().getOutput().getContent();
}
}
ChatClient
ChatClient proporciona una API fluida y de alto nivel sobre ChatModel, simplificando los patrones de uso comunes. Es una capa de abstracción más conveniente.
@Service
public class ChatService {
@Autowired
private ChatClient chatClient;
public String askQuestion(String question) {
// Simplified call using ChatClient
return chatClient.call(question);
}
}
Comparación: ChatModel ofrece control detallado y devuelve objetos de respuesta complejos, mientras que ChatClient es más conciso, devolviendo directamente el contenido o permitiendo un uso en modo streaming.
Tipos de Mensajes
Todos los tipos de mensajes en Spring AI implementan la interfaz Message, simulando las interacciones entre participantes en una conversación.
SystemMessage
Se utiliza para definir el rol, personalidad, reglas de comportamiento y directrices generales del asistente de IA. Generalmente se coloca al inicio de la conversación.
@RestController
public class ChatClientController {
private final ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
// Setting a system message to define the AI's persona
this.chatClient = chatClientBuilder
.defaultSystem("Eres un asistente de IA profesional llamado 'Pequeño Pez', experto en Java y Python, responde amablemente.")
.build();
}
@GetMapping("/call")
public String generation(String userInput) {
return this.chatClient.prompt()
.user(userInput) // User's input
.call() // Invoke the API
.content(); // Get the response content
}
}
El método defaultSystem en el builder establece las instrucciones iniciales para la IA.
UserMessage
Representa la entrada específica del usuario (preguntas, comandos).
AssistantMessage
Contiene la respuesta generada por el modelo de IA. Es crucial para mantener la coherencia en conversaciones multi-turno, ya que las respuestas del asistente se pueden guardar y enviar como contexto en futuras interacciones.
Formato de Salida
Salida Estructurada
Spring AI permite obtener respuestas estructuradas modificando el tipo de retorno de los métodos de ChatModel/ChatClient a un tipo de entidad personalizado usando el método entity().
@RestController
public class ChatClientController {
private final ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
// Define a simple record for structured output
record Recipe(String description, List<string> ingredients) {}
@GetMapping("/entity")
public String entity(String userInput) {
Recipe recipe = this.chatClient.prompt()
.user("Genera una receta para " + userInput)
.call()
.entity(Recipe.class); // Specify the target entity type
return recipe.toString();
}
}
</string>
Salida en Streaming
La salida en streaming permite recibir la respuesta del modelo a medida que se genera, en lugar de esperar a que se complete. Esto mejora la experiencia del usuario al mostrar resultados de forma incremental.
Spring AI utiliza programación reactiva (Reactor) y el método stream() para generar un Flux<String>.
@RestController
public class ChatClientController {
private final ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
// Use produces to set the correct Content-Type for streaming
@GetMapping(value = "/stream", produces = "text/event-stream;charset=utf-8")
public Flux<string> stream(String userInput) {
return this.chatClient.prompt()
.user(userInput)
.stream() // Enable streaming
.content(); // Get the stream of content
}
record Recipe(String description, List<string> ingredients) {}
}
</string></string>
Protocolo SSE (Server-Sent Events)
Para lograr la transmisión de datos en tiempo real desde el servidor al cliente a través de HTTP, se utiliza el protocolo SSE. Permite al servidor enviar flujos de datos al navegador de forma continua sin que el cliente tenga que solicitar cada actualización.
- Características: Comunicación unidireccional (servidor a cliente), basada en HTTP, mantiene conexiones largas, transmite flujos de texto y soporta reconexión automática.
- Formato de Datos SSE: Los mensajes se componen de campos como
data(contenido del mensaje),event(tipo de evento),id(identificador único) yretry(tiempo de reintento). Cada mensaje termina con una línea vacía.
Ejemplo de Uso de SSE
Backend (Java con Spring WebFlux):
@Slf4j
@RequestMapping("/sse")
@RestController
public class SseController {
@RequestMapping("/events")
public void streamEvents(HttpServletResponse response) throws IOException, InterruptedException {
response.setContentType("text/event-stream;charset=utf-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
PrintWriter writer = response.getWriter();
for (int i = 0; i < 5; i++) {
// Sending a custom event named 'message'
writer.write("event: message\n");
writer.write("data: Current time: " + new Date() + "\n\n");
writer.flush();
Thread.sleep(1000L); // Simulate work
}
// Signal the end of the stream
writer.write("event: end\ndata: Stream finished\n\n");
writer.flush();
}
}
Frontend (HTML/JavaScript):
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE Example</title>
</head>
<body>
<div id="sse-output"></div>
<script>
const eventSource = new EventSource("/sse/events");
eventSource.addEventListener("message", function(event) {
const outputDiv = document.getElementById("sse-output");
const p = document.createElement("p");
p.textContent = event.data;
outputDiv.appendChild(p);
});
eventSource.addEventListener("end", function(event) {
console.log("Stream ended:", event.data);
eventSource.close();
});
eventSource.onerror = function(err) {
console.error("EventSource failed:", err);
eventSource.close();
};
</script>
</body>
</html>
Flux en Reactor (WebFlux)
Flux es un componente central en Project Reactor (usado por Spring WebFlux) para manejar flujos de datos asíncronos y reactivos. Su ciclo de vida implica creación, transformación, filtrado y consumo.
Creación de Flux
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
// From fixed values
Flux<String> fixedFlux = Flux.just("A", "B", "C");
// From an iterable
List<String> dataList = Arrays.asList("X", "Y", "Z");
Flux<String> fromCollection = Flux.fromIterable(dataList);
// Range of numbers
Flux<Integer> rangeFlux = Flux.range(1, 5); // Emits 1, 2, 3, 4, 5
// Interval emissions
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(3); // Emits every second, 3 times
// From an array
Flux<String> arrayFlux = Flux.fromArray(new String[]{"One", "Two"});
// Empty Flux
Flux<String> emptyFlux = Flux.empty();
// From a Future (adapting traditional async APIs)
CompletableFuture<String> futureResult = CompletableFuture.supplyAsync(() -> "Future Value");
Flux<String> futureFlux = Flux.fromFuture(futureResult);
Operaciones de Transformación
Flux<String> source = Flux.just("apple", "banana");
// Map: Apply a function to each element (one-to-one)
Flux<String> uppercased = source.map(String::toUpperCase); // APPLE, BANANA
// FlatMap: Apply a function that returns a Flux, then flatten (one-to-many, async)
Flux<String> words = Flux.just("hello world", "spring ai");
Flux<String> splitWords = words.flatMap(phrase -> Flux.fromArray(phrase.split(" "))); // hello, world, spring, ai
// Scan: Cumulative operations
Flux<Integer> numbers = Flux.range(1, 4);
Flux<Integer> cumulativeSum = numbers.scan((accumulator, current) -> accumulator + current); // 1, 3, 6, 10
Operaciones de Filtrado
Flux<Integer> numbers = Flux.range(1, 10);
// Filter: Keep elements that satisfy a predicate
Flux<Integer> evenNumbers = numbers.filter(n -> n % 2 == 0); // 2, 4, 6, 8, 10
// Distinct: Remove duplicate elements
Flux<String> duplicates = Flux.just("A", "B", "A", "C");
Flux<String> unique = duplicates.distinct(); // A, B, C
// Take: Limit the number of elements
Flux<String> firstTwo = source.take(2); // apple, banana
// Skip: Skip a number of initial elements
Flux<String> skipFirst = source.skip(1); // banana
// TakeWhile / SkipWhile: Based on a condition
Flux<Integer> sequence = Flux.range(1, 100);
Flux<Integer> belowTen = sequence.takeWhile(n -> n < 10); // 1 to 9
Operaciones de Consumo
Flux<String> dataFlux = Flux.just("one", "two");
// Subscribe: The fundamental way to consume a Flux
dataFlux.subscribe(
element -> System.out.println("Received: " + element), // onNext
error -> System.err.println("Error: " + error), // onError
() -> System.out.println("Completed!") // onComplete
);
// CollectList: Gather all elements into a Mono<List<T>>
Mono<List<String>> listMono = dataFlux.collectList();
// Reduce: Combine elements into a single result
Mono<Integer> sum = Flux.range(1, 5).reduce(0, Integer::sum); // 15
Advisors en Spring AI
Advisors son un mecanismo de interceptación en Spring AI que permite inyectar lógica personalizada en puntos específicos de la cadena de llamadas a la IA.
- Before Call: Se ejecuta antes de enviar la solicitud al modelo, útil para modificar
prompts. - After Call: Se ejecuta después de recibir la respuesta del modelo, antes de devolverla al cliente.
El flujo es: Entrada del Usuario → Advisor N.before() → ... → Modelo IA → ... → Advisor N.after() → Respuesta Final.
Spring AI incluye Advisors predefinidos, como SimpleLoggerAdvisor, que registra las interacciones. Simplemente añádelo a la cadena de Advisors.
@RestController
public class ChatClientController {
private final ChatClient chatClient;
public ChatClientController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("Eres un asistente experto en Java y Python.")
.build();
}
@GetMapping("/advisor")
public String advisorCall(String userInput) {
return this.chatClient.prompt()
.advisors(new SimpleLoggerAdvisor()) // Add the logging advisor
.user(userInput)
.call()
.content();
}
record Recipe(String description, List<string> ingredients) {}
}
</string>
Configura el nivel de log a DEBUG para ver los detalles de las llamadas interceptadas.
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG
Esto mostrará los logs de las solicitudes y respuestas procesadas por el SimpleLoggerAdvisor.