¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
dev
Simón Rodríguez Hace 18 minutos Cargando comentarios…
Como ya hemos comentado, los LLMs son modelos preentrenados hasta una cierta fecha y con datos públicos (se supone que no tienen acceso a datos de una empresa o de nuestros dispositivos, por ejemplo). También es popular la importancia de los tokens en aspectos como la facturación o la context-window (no podemos enviar todo lo que queramos en el input ni el output, aunque hay LLMs que tienen context-window de millones de tokens como Gemini).
El patrón RAG se enfoca en ayudarnos con estas limitaciones de los LLMs de cara a tener unos mejores resultados, con menos alucinaciones y reduciendo el coste por uso.
Antes de entrar en detalle, puedes echarle un vistazo al resto de contenidos de la serie de Spring AI en los siguientes enlaces por si te has perdido alguno de los artículos anteriores:
Algunas de las bases de RAG ya las hemos visto en el post de los Advisors, Tool-Calling o el tema de aumentar el contexto, pero en este post nos centraremos más en la esencia del propio RAG y en qué tecnologías y abstracciones está apoyado.
Retrieval Augmented Generation (RAG) es una técnica muy empleada para abordar las limitaciones que existen en los modelos lingüísticos con contenidos grandes, precisión y conocimiento del contexto. El enfoque se basa en un procesamiento batch en donde primero se leen datos desestructurados desde los documentos (u otros orígenes de datos), se transforman y luego se escriben en una Vector Database (a grandes rasgos es un proceso ETL).
Una de las transformaciones en este flujo es la división del documento original (u origen de datos) en piezas más pequeñas, existiendo dos pasos clave:
La siguiente fase en RAG ya sería el procesamiento del input del usuario, empleando la búsqueda por similitud para enriquecer el contexto con documentos similares a enviar al modelo.
Spring AI soporta RAG proporcionando una arquitectura modular que permite crear flujos RAG personalizados o usar los creados a través del uso del API de Advisor.
String rag() {
return ChatClient.builder(chatModel)
.build().prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore))
.user("que me puedes decir de los aranceles de EEUU en el año 2025")
.call()
.content();
}
En el ejemplo, QuestionAnswerAdvisor realizará una búsqueda por similitud sobre los documentos en la base de datos, pudiendo filtrar los documentos por los que se busque con la clase SearchRequest (también en tiempo de ejecución).
Spring AI implementa una arquitectura modular RAG basada en el documento Modular RAG: Transforming RAG Systems into LEGO-like Reconfigurable Frameworks. Esto todavía está en fase experimental, por lo que es susceptible a cambios. Los módulos que se pueden encontrar son:
Query query = new Query("Hvad er Danmarks hovedstad?");
QueryTransformer queryTransformer = TranslationQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.targetLanguage("english")
.build();
Query transformedQuery = queryTransformer.transform(query);
Map<Query, List<List<Document>>> documentsForQuery = ...
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
List<Document> documents = documentJoiner.join(documentsForQuery);
Los embeddings son representaciones numéricas de texto, imágenes o vídeos que capturan la relación entre los datos de entrada. Funcionan transformando texto, imágenes o vídeos en arrays de números llamados vectores, que están diseñados para capturar el significado de estos recursos. Esto se hace calculando la distancia numérica entre dos vectores de, por ejemplo, dos textos, pudiendo así determinar su similitud.
La interfaz EmbeddingModel está diseñada para la integración con los modelos, cuya función principal es convertir los recursos en vectores numéricos que son usados para análisis semántico y clasificación de textos, entre otros. La interfaz se centra principalmente en:
La interfaz EmbeddingModel extiende de la interfaz Model, así como EmbeddingRequest y EmbeddingResponse de sus correspondientes ModelRequest y ModelResponse. En la siguiente imagen se puede observar las relaciones entre el Embedding API, la Model API y los Embedding Models:
Ya más en profundidad:
public interface EmbeddingModel extends Model<EmbeddingRequest, EmbeddingResponse> {
@Override
EmbeddingResponse call(EmbeddingRequest request);
float[] embed(Document document);
default float[] embed(String text) {...}
default List<float[]> embed(List<String> texts) {...}
default EmbeddingResponse embedForResponse(List<String> texts) {...}
default int dimensions() {...}
}
public class EmbeddingRequest implements ModelRequest<List<String>> {
private final List<String> inputs;
private final EmbeddingOptions options;
...
}
public class EmbeddingResponse implements ModelResponse<Embedding> {
private List<Embedding> embeddings;
private EmbeddingResponseMetadata metadata = new EmbeddingResponseMetadata();
...
}
public class Embedding implements ModelResult<float[]> {
private float[] embedding;
private Integer index;
private EmbeddingResultMetadata metadata;
...
}
En esta página puedes encontrar las implementaciones disponibles. Siguiendo las premisas de posts anteriores nos centraremos en la implementación de Ollama.
De cara a la autoconfiguración, se proporcionan una serie de propiedades muy similares a las propiedades existentes en el modelo de chat, habilitando la autoconfiguración con la propiedad:
spring.ai.model.embedding
A continuación mostramos distintos ejemplos de cómo hacer uso del OllamaEmbeddingModel y EmbeddingModel:
@Autowired
private EmbeddingModel autoEmbeddingModel;
...
@GetMapping("/auto")
public EmbeddingResponse embed() {
return autoEmbeddingModel.embedForResponse(List.of("Tendencias tecnologicas 2025", "Tendencia tecnologica IA", "Tendencia tecnologica accesibilidad"));
}
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
</dependency>
Para, posteriormente, crear la instancia correspondiente:
private OllamaApi ollamaApi;
private OllamaEmbeddingModel embeddingModel;
public EmbeddingController() {
ollamaApi = new OllamaApi();
embeddingModel =
OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(OllamaOptions.builder().model(OllamaModel.LLAMA3_2_1B).build()).build();
}
@GetMapping("/plain")
public EmbeddingResponse embedd() {
return embeddingModel.call(new EmbeddingRequest(List.of("Tendencias tecnologicas 2025", "Tendencia tecnologica IA", "Tendencia tecnologica accesibilidad"),
OllamaOptions.builder()
.model(OllamaModel.LLAMA3_2_1B)
.truncate(false)
.build()));
}
Son un tipo de bases de datos especializadas que, en vez de realizar coincidencias exactas, se apoyan en las búsquedas por similitud, es decir, cuando se indica como entrada un vector, el resultado son vectores “similares”. En primer lugar, se cargan los datos en la base de datos vectorial. Posteriormente, cuando se envía una petición al modelo, primero se recuperan unos vectores similares que luego son usados como contexto para la pregunta del usuario y enviados también al modelo.
Para trabajar con las bases de datos vectoriales se hace uso de la interfaz Vector Store:
public interface VectorStore extends DocumentWriter {
default String getName() {...}
void add(List<Document> documents);
void delete(List<String> idList);
void delete(Filter.Expression filterExpression);
default void delete(String filterExpression) {...};
List<Document> similaritySearch(String query);
List<Document> similaritySearch(SearchRequest request);
default <T> Optional<T> getNativeClient() {...}
}
Además de la clase SearchRequest:
public class SearchRequest {
public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0;
public static final int DEFAULT_TOP_K = 4;
private String query = "";
private int topK = DEFAULT_TOP_K;
private double similarityThreshold = SIMILARITY_THRESHOLD_ACCEPT_ALL;
@Nullable
private Filter.Expression filterExpression;
public static Builder from(SearchRequest originalSearchRequest) {...}
public static class Builder {...}
public String getQuery() {...}
public int getTopK() {...}
public double getSimilarityThreshold() {...}
public Filter.Expression getFilterExpression() {...}
}
Para insertar datos en la base de datos vectorial es necesario encapsular dichos datos en un objeto Document. Al insertarse en la bbdd, el texto se transforma en un array de números conocido como embedding vector (la función de la bbdd vectorial es realizar búsquedas por similitud, no para generar los embeddings en sí). Los métodos similaritySearch permiten obtener documentos similares a una petición, pudiendo ajustarse con los siguientes parámetros:
FilterExpressionBuilder builder = new FilterExpressionBuilder();
Expression expression = builder.eq("tendencia", "cloud").build();
Algunas bases de datos necesitan que se inicialice el esquema antes de usarlo. Con Spring Boot se puede configurar la propiedad …initialize-schema a true, aunque lo adecuado es contrastar esta información con cada una de las implementaciones.
Es común cuando se trabaja con este tipo de bbdd tener que embeber muchos documentos.
Aunque la primera idea que se nos ocurre es intentar embeber todos estos documentos al mismo tiempo, puede llevar a ciertos problemas. Esto se debe sobre todo a los límites de tokens en los modelos (window size), lo que provocaría errores o embedings truncados.
Para eso precisamente se usa la estrategia batch, en donde se dividen conjuntos grandes de documentos en conjuntos más pequeños que cuadren con la window size. Esto no solo soluciona el problema de los límites de tokens, sino que además puede mejorar el rendimiento y los límites de peticiones existentes en las distintas APIs.
Spring ofrece esta funcionalidad a través de la interfaz BatchingStrategy:
public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}
La implementación por defecto es TokenCountBatchingStrategy que basa la división según el número de tokens existentes en cada batch, asegurándose que no se supere el límite de input de tokens. Los puntos importantes de esta implementación serían:
La estrategia estima el número de tokens por documento, los agrupa en batch sin exceder el límite de tokens, lanzando una excepción si alguno de los documentos lo excediera. También se puede personalizar la estrategia creando una nueva instancia a través de una clase @Configuration:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customTokenCountBatchingStrategy() {
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE, // Specify the encoding type
8000, // Set the maximum input token count
0.1 // Set the reserve percentage
);
}
}
Una vez definido este bean, será usado automáticamente por las implementaciones del EmbeddingModel en vez de la estrategia por defecto. Adicionalmente, se permite incluir las implementaciones propias de TokenCountEstimator (clase que calcula los tokens del documento), además de parámetros para formatear contenido y metadatos o incluso nuestra propia implementación custom. Y, como en otras ocasiones, además se permite crear la implementación completamente personalizada:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}
En esta página se pueden encontrar las implementaciones disponibles. En este caso, nos centraremos en la implementación PGvector para bases de datos PostgreSQL, que no es más que una extensión open source para PostgreSQL que permite guardar y buscar sobre embeddings.
En primer lugar, se necesita acceso a PostgreSQL con las extensiones vector, hstore y uuid-ossp. Al iniciarse PgVectorStore intentará instalar las extensiones necesarias sobre la bbdd y crear la tabla vector_store con un índice. Se puede hacer lo mismo de forma manual con:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS hstore;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS vector_store (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
content text,
metadata json,
embedding vector(1536) // 1536 is the default embedding dimension
);
CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);
Empezamos por incluir la dependencia correspondiente:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
La implementación de vector store puede inicializar el schema automáticamente, pero se debe indicar el initializeSchema en el constructor correspondiente o mediante la propiedad …initialize-schema=true.
Evidentemente, también será necesario un EmbeddingModel, debiendo incluir su correspondiente dependencia en el proyecto. Y, como en muchas ocasiones, también habrá que indicar los valores para la conexión a través de properties:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1024
max-document-batch-size: 10000
initialize-schema: true
Además, existen otras properties para mayor personalización de la Vector Store. Recordemos que tal y como nos tiene acostumbrados Spring, también se puede crear una configuración manual.
Se puede ejecutar una instancia de PGVector a través del siguiente comando Docker:
docker run -it --rm --name postgres -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg16
Y conectándose a la instancia con el comando:
psql -U postgres -h localhost -p 5432
Como es de esperar, se debe seleccionar un modelo embedding en conjunción con el modelo general empleado. Un ejemplo de carga de Documents en la Vector Store (esta operación de carga realmente se ejecutaría de forma batch) sería:
Document document1 = new Document("Tendencia tecnologica 2025: La voz y los vídeos en la IA", Map.of("tendencia", "ia"));
...
List<Document> documents = Arrays.asList(document1, document2, document3, document4, document5, document6, document7, document8, document9, document10);
vectorStore.add(documents);
Más tarde, cuando un usuario envíe una pregunta, se realizará una búsqueda por similitud para obtener documentos similares que se pasarán como contexto del prompt.
List<Document> similarDocuments = vectorStore.similaritySearch("Dime las tendencias tecnologicas actuales");
Se proporcionan múltiples métodos para eliminar documentos:
Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, new Filter.Key("tendencia"), new Filter.Value("dragdrop"));
vectorStore.delete(filterExpression);
FilterExpressionBuilder builder = new FilterExpressionBuilder();
Expression expression = builder.eq("tendencia", "cloud").build();
try {
vectorStore.delete(filterExpression);
vectorStore.delete(expression);
} catch (Exception e) {
log.error("Invalid filter expression", e);
}
Nota: según la documentación, se recomienda envolver estas llamadas de delete en bloques try-catch.
Además, también hay que tener en cuenta las siguientes consideraciones en cuanto a rendimiento:
Para ver en acción los componentes que hemos visto en este post, creamos una app. Una vez iniciamos la aplicación con la propiedad …initialize-schema: true se puede ver cómo se ha creado la tabla vector_store con un cliente de base de datos:
Para comprobar la funcionalidad contamos con los siguientes endpoints:
En este enlace se puede descargar el código de la aplicación de ejemplo.
En esta ocasión hemos visto en qué consiste y cómo se implementa el patrón RAG en Spring AI, además de las funcionalidades que lo soporta (embeddings y las bbdd vectoriales), así como los ejemplos de las implementaciones correspondientes (Ollama y PGVector).
En el siguiente capítulo abordaremos la parte offline del patrón RAG (fase ETL) además de centrarnos en el auge de MCP y cómo lo resuelve Spring AI. ¡Te leo en comentarios!
Referencias
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.