Construcción de un Sistema de Base de Conocimiento RAG con Spring AI Alibaba y Qdrant

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:

  1. Ingesta de Datos: El servicio KnowledgeLoader lee los archivos Markdown, los divide en fragmentos y genera sus embeddings (vectores) usando el modelo text-embedding-v2, almacenándolos en la colección de Qdrant.
  2. Consulta del Usuario: Una pregunta recibida por la API es pasada al agente ReactAgent.
  3. Recuperación Híbrida: El agente, siguiendo sus instrucciones, invoca la herramienta consulta_base_conocimiento. Esto activa al RetrievalEngine, que realiza una búsqueda por similitud en Qdrant y luego aplica un re-ranking para filtrar los resultados más relevantes.
  4. Generación de Respuesta: El contexto recuperado (fragmentos relevantes con sus fuentes) se proporciona al modelo de lenguaje qwen-plus como parte del prompt. El LLM genera una respuesta natural, citando la información proporcionada, sin inventar datos adicionales.
  5. 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.

Etiquetas: Spring AI Alibaba RAG Qdrant DashScope java

Publicado el 8-4 19:30