Configuración del Entorno y Dependencias
Para desarrollar un sistema de Recuperación Aumentada por Generación (RAG) robusto, es fundamental definir una pila tecnológica coherente. Esta implementación se basa en el marco de trabajo Spring AI Alibaba, integrado con los servicios de inteligencia artificial de Alibaba Cloud y la base de datos vectorial Qdrant.
Dependencias Principales (pom.xml)
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
<alibaba-cloud-ai.version>1.0.0</alibaba-cloud-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>alibaba-cloud-ai-spring-boot-bom</artifactId>
<version>${alibaba-cloud-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>alibaba-cloud-ai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Parámetros de Conexión y Modelos
El archivo de configuración centraliza los parámetros necesarios para conectar con los servicios de nube y la base de datos vectorial.
spring:
application:
name: knowledge-base-app
ai:
dashscope:
api-key: ${DASHSCOPE_KEY:sk-ejemplo}
chat:
options:
model: qwen-plus
embedding:
options:
model: text-embedding-v2
vectorstore:
qdrant:
host: localhost
port: 6334
collection-name: documentos_empresa
initialize-schema: true
rag:
fuente-documentos: classpath:/corpus/
re-ranking:
modelo: bge-reranker-v2-m3
resultados-finales: 3
busqueda:
resultados-iniciales: 8
Implementación de los Componentes del Sistema
Carga y Procesamiento de Documentos
El servicio de documentos se encarga de ingestar la información, fragmentarla en trozos manejables y almacenar sus representaciones vectoriales en Qdrant.
package com.example.rag.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Service
public class KnowledgeLoader {
private static final Logger log = LoggerFactory.getLogger(KnowledgeLoader.class);
private final VectorStore almacenVectores;
private final ResourcePatternResolver resolvedorRecursos;
private final String rutaDocumentos;
public KnowledgeLoader(VectorStore almacenVectores,
ResourcePatternResolver resolvedorRecursos,
@Value("${rag.fuente-documentos}") String rutaDocumentos) {
this.almacenVectores = almacenVectores;
this.resolvedorRecursos = resolvedorRecursos;
this.rutaDocumentos = rutaDocumentos;
}
public int recargarDocumentos() throws IOException {
List<document> documentosOriginales = extraerDocumentos();
if (documentosOriginales.isEmpty()) {
log.warn("No se encontraron documentos en: {}", rutaDocumentos);
return 0;
}
List<document> fragmentos = fragmentarDocumentos(documentosOriginales);
almacenVectores.add(fragmentos);
log.info("Proceso completado. {} fragmentos nuevos almacenados.", fragmentos.size());
return fragmentos.size();
}
private List<document> extraerDocumentos() throws IOException {
String patron = rutaDocumentos.endsWith("/") ? rutaDocumentos + "*.md" : rutaDocumentos + "/*.md";
Resource[] recursos = resolvedorRecursos.getResources(patron);
List<document> documentos = new ArrayList<>();
for (Resource recurso : recursos) {
try {
String contenido = new String(recurso.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
Map<String, Object> metadatos = Map.of("archivo", recurso.getFilename());
documentos.add(new Document(contenido, metadatos));
log.debug("Archivo cargado: {}", recurso.getFilename());
} catch (IOException e) {
log.error("Error leyendo el archivo: {}", recurso.getFilename(), e);
}
}
return documentos;
}
private List<Document> fragmentarDocumentos(List<Document> documentos) {
TokenTextSplitter cortador = new TokenTextSplitter(800, 200, 10, 1000, true);
return cortador.apply(documentos);
}
}
</document></document></document></document>
Motor de Recuperación y Re-ranking
Este componente encapsula la lógica de búsqueda semántica y la re-ordenación de resultados para mejorar la relevancia.
package com.example.rag.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class RetrievalEngine {
private static final Logger log = LoggerFactory.getLogger(RetrievalEngine.class);
private final VectorStore almacenVectores;
private final RestTemplate restTemplate;
private final int maxResultadosBusqueda;
private final int maxResultadosFinales;
private final String modeloReRanking;
public RetrievalEngine(VectorStore almacenVectores,
RestTemplate restTemplate,
@Value("${rag.busqueda.resultados-iniciales}") int maxResultadosBusqueda,
@Value("${rag.re-ranking.resultados-finales}") int maxResultadosFinales,
@Value("${rag.re-ranking.modelo}") String modeloReRanking) {
this.almacenVectores = almacenVectores;
this.restTemplate = restTemplate;
this.maxResultadosBusqueda = maxResultadosBusqueda;
this.maxResultadosFinales = maxResultadosFinales;
this.modeloReRanking = modeloReRanking;
}
public String recuperarContextoRelevante(String consulta) {
List<document> candidatosIniciales = buscarEnVectorStore(consulta);
if (candidatosIniciales.isEmpty()) {
return "No se encontró información relevante en la base de conocimiento.";
}
List<document> resultadosReRanking = aplicarReRanking(consulta, candidatosIniciales);
return formatearResultados(resultadosReRanking);
}
private List<document> buscarEnVectorStore(String consulta) {
SearchRequest solicitud = SearchRequest.builder()
.query(consulta)
.topK(maxResultadosBusqueda)
.build();
List<document> resultados = almacenVectores.similaritySearch(solicitud);
log.debug("Búsqueda vectorial inicial devolvió {} resultados.", resultados.size());
return resultados != null ? resultados : Collections.emptyList();
}
private List<document> aplicarReRanking(String consulta, List<document> documentos) {
// Implementación simplificada del re-ranking
// En producción, se llamaría a un servicio externo como DashScope Re-Ranker
log.info("Simulando re-ranking para {} documentos.", documentos.size());
return documentos.stream()
.limit(maxResultadosFinales)
.collect(Collectors.toList());
}
private String formatearResultados(List<document> documentos) {
StringBuilder contexto = new StringBuilder();
for (int i = 0; i < documentos.size(); i++) {
Document doc = documentos.get(i);
String fuente = (String) doc.getMetadata().getOrDefault("archivo", "origen_desconocido");
contexto.append("--- Fragmento ").append(i + 1).append(" (Fuente: ").append(fuente).append(") ---\n");
contexto.append(doc.getText()).append("\n\n");
}
return contexto.toString().trim();
}
}
</document></document></document></document></document></document></document>
Exposición del Agente Inteligente como Servicio
Se configura un agante basado en React que utiliza la herramienta de búsqueda de la base de conocimiento para responder preguntas.
package com.example.rag.agent;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.example.rag.service.RetrievalEngine;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AgenteConfiguracion {
private static final String INSTRUCCIONES_SISTEMA = """
Eres un asistente especializado en responder preguntas basándote estrictamente en la información de una base de conocimiento corporativa.
Reglas estrictas:
1. Debes usar la herramienta `consulta_base_conocimiento` antes de generar cualquier respuesta.
2. Solo puedes utilizar la información devuelta por la herramienta. No uses tu conocimiento general.
3. Si la herramienta no devuelve información relevante, indica que no tienes datos para responder.
4. Cita siempre la fuente del fragmento de información que utilizas.
5. Proporciona respuestas concisas y precisas.
""";
@Bean
public ReactAgent crearAgenteRAG(ChatModel modeloChat, RetrievalEngine motorRecuperacion) {
ToolCallback herramientaConsulta = FunctionToolCallback
.builder("consulta_base_conocimiento", (input) -> motorRecuperacion.recuperarContextoRelevante((String) input))
.description("Busca información en la base de conocimiento corporativa. Debes usarla para toda pregunta.")
.inputType(String.class)
.build();
return ReactAgent.builder()
.name("agente_conocimiento")
.model(modeloChat)
.systemPrompt(INSTRUCCIONES_SISTEMA)
.tools(herramientaConsulta)
.saver(new MemorySaver())
.build();
}
}
Explicación del Flujo del Sistema
El proceso completo se ejecuta en las siguientes etapas clave:
- Ingesta de Datos: El servicio
KnowledgeLoaderlee los archivos Markdown, los divide en fragmentos y genera sus embeddings (vectores) usando el modelotext-embedding-v2, almacenándolos en la colección de Qdrant. - Consulta del Usuario: Una pregunta recibida por la API es pasada al agente
ReactAgent. - Recuperación Híbrida: El agente, siguiendo sus instrucciones, invoca la herramienta
consulta_base_conocimiento. Esto activa alRetrievalEngine, que realiza una búsqueda por similitud en Qdrant y luego aplica un re-ranking para filtrar los resultados más relevantes. - Generación de Respuesta: El contexto recuperado (fragmentos relevantes con sus fuentes) se proporciona al modelo de lenguaje
qwen-pluscomo parte del prompt. El LLM genera una respuesta natural, citando la información proporcionada, sin inventar datos adicionales. - Respuesta al Cliente: La respuesta final se devuelve al usuario. El sistema está diseñado para manejar la falta de información de manera explícita.