Durante años, se daba por hecho que las funcionalidades de IA se construían en Python y se llamaban desde Java por HTTP. Para muchos equipos empresariales eso suponía otro servicio, otro pipeline de despliegue y otro lenguaje que mantener. Spring AI cambia eso: lleva el acceso a modelos, la recuperación, las llamadas a herramientas y la observabilidad al modelo de programación de Spring Boot, con la autoconfiguración, la inyección de dependencias y el soporte de testing que los equipos Java ya conocen. Esta guía muestra cómo construyo funcionalidades de IA con Spring AI en producción, con ejemplos en Kotlin (las versiones en Java son casi idénticas).

Qué es Spring AI

Spring AI es un proyecto de Spring que ofrece abstracciones portables para proveedores de IA y los bloques básicos de las aplicaciones con LLM (documentación de referencia):

  • ChatClient: una API fluida para prompts, mensajes de sistema, opciones, streaming y salida estructurada.
  • Abstracciones de modelos para chat, embeddings, imágenes y audio con distintos proveedores: Anthropic, OpenAI, Azure OpenAI, Amazon Bedrock, Google Vertex AI, Mistral, Ollama y otros.
  • Advisors: interceptores alrededor de las llamadas al modelo para memoria, recuperación, registros y barandillas.
  • Abstracción VectorStore con implementaciones para PGvector, Qdrant, Weaviate, Milvus, Redis, Elasticsearch y más.
  • Llamadas a herramientas mediante métodos anotados.
  • Observabilidad a través de Micrometer, además de ayudas para evaluación y soporte de cliente y servidor MCP.

El valor no es magia; es la coherencia. Cambiar de un proveedor de modelos a otro, o de un modelo alojado a uno self-hosted mediante Ollama o vLLM, se convierte en un cambio de configuración.

Configuración

Usa el BOM de Spring AI y el starter de tu proveedor y de tu almacén vectorial:

// build.gradle.kts
dependencies {
    implementation(platform("org.springframework.ai:spring-ai-bom:1.0.0")) // use the current release
    implementation("org.springframework.ai:spring-ai-starter-model-anthropic")
    implementation("org.springframework.ai:spring-ai-starter-vector-store-pgvector")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
}
# application.yml
spring:
  ai:
    anthropic:
      api-key: ${ANTHROPIC_API_KEY}
      chat:
        options:
          model: ${CHAT_MODEL}
          max-tokens: 1024
    vectorstore:
      pgvector:
        initialize-schema: false   # manage schema with Flyway/Liquibase instead
        dimensions: 1024
        index-type: HNSW
        distance-type: COSINE_DISTANCE

Mantén el nombre del modelo en la configuración para poder cambiar de modelo por entorno sin tocar el código; consulta cómo elegir un LLM.

Lo básico de ChatClient

@RestController
@RequestMapping("/api/assistant")
class AssistantController(builder: ChatClient.Builder) {

    private val chat = builder
        .defaultSystem(
            """
            You are an internal assistant for ACME's sales team.
            Answer in the user's language. If you are not sure, say so.
            """.trimIndent(),
        )
        .build()

    @PostMapping("/ask")
    fun ask(@RequestBody req: AskRequest): String =
        chat.prompt()
            .user(req.question)
            .call()
            .content() ?: ""
}

ChatClient.Builder se autoconfigura para el proveedor que haya en el classpath. Construye un cliente por caso de uso con sus propios valores por defecto —prompt de sistema, advisors, herramientas— en lugar de un único cliente global. Los prompts de producción merecen el mismo rigor que el código; consulta prompt engineering para desarrolladores.

Salida estructurada en data classes de Kotlin

Spring AI puede mapear las respuestas directamente a objetos tipados, generando instrucciones de formato y un esquema JSON a partir de la clase de destino:

data class LeadQualification(
    val companyName: String?,
    val budgetEur: Int?,
    val timeline: Timeline,
    val fitScore: Int,          // 1..5
    val reasoning: String,
)
enum class Timeline { NOW, THIS_QUARTER, LATER, UNKNOWN }

fun qualify(emailText: String): LeadQualification =
    chat.prompt()
        .user { it.text("Qualify this inbound lead email:\n<email>{email}</email>").param("email", emailText) }
        .call()
        .entity(LeadQualification::class.java)
        ?: error("Model returned no result")

Usa campos que admitan nulos y un valor UNKNOWN en los enums para la información que puede faltar, y valida las reglas de negocio después del mapeo (Bean Validation funciona bien). Cuando el proveedor admita salida nativa restringida por esquema, actívala. Los principios generales están en salidas estructuradas de LLM.

RAG con advisors y pgvector

Los advisors envuelven las llamadas al modelo con comportamiento adicional. Dos de los más útiles son la memoria de chat y la recuperación:

@Configuration
class AssistantConfig {
    @Bean
    fun supportChat(builder: ChatClient.Builder, vectorStore: VectorStore, chatMemory: ChatMemory): ChatClient =
        builder
            .defaultSystem(SUPPORT_SYSTEM_PROMPT)
            .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(chatMemory).build(),
                QuestionAnswerAdvisor.builder(vectorStore)
                    .searchRequest(SearchRequest.builder().topK(6).similarityThreshold(0.5).build())
                    .build(),
            )
            .build()
}

@Service
class SupportAssistant(private val supportChat: ChatClient) {
    fun answer(userId: Long, conversationId: String, question: String): String =
        supportChat.prompt()
            .user(question)
            .advisors { it.param(ChatMemory.CONVERSATION_ID, "$userId:$conversationId") }
            .call()
            .content() ?: ""
}

La ingesta usa los componentes de estilo ETL de Spring AI: DocumentReader, TextSplitter y VectorStore.add(). Para una calidad de producción, ve más allá de los valores por defecto: chunking consciente de la estructura, metadatos para filtrar por cliente y nivel de acceso, búsqueda híbrida, reranking. Son las decisiones que más importan; consulta RAG para empresas, embeddings y chunking y la comparativa de bases de datos vectoriales. Usar PGvector mantiene los vectores en el mismo PostgreSQL del que ya haces copias de seguridad y que ya monitorizas.

Control de acceso: aplica filtros de metadatos basados en el usuario autenticado en el SearchRequest (expresión de filtro); nunca confíes en el prompt para ocultar documentos.

Llamadas a herramientas

Anota los métodos con @Tool y pasa las instancias al prompt:

class OrderTools(private val orders: OrderService, private val customerId: Long) {

    @Tool(description = "Get status, items and delivery estimate of one order of the current customer.")
    fun getOrderStatus(
        @ToolParam(description = "Order number, digits only, e.g. 482193") orderNumber: String,
    ): OrderStatusDto =
        orders.findForCustomer(customerId, orderNumber)
            ?.toDto()
            ?: OrderStatusDto.notFound(orderNumber)
}

fun answerWithTools(customerId: Long, question: String): String =
    chat.prompt()
        .user(question)
        .tools(OrderTools(orderService, customerId))
        .call()
        .content() ?: ""

El ID del cliente viene del contexto de seguridad, no del modelo. Devuelve DTOs compactos en lugar de entidades JPA: las relaciones con carga perezosa y las entidades de 40 campos desperdician tokens y filtran datos. Los principios de diseño de herramientas están en diseño de herramientas para agentes LLM, y el modelo de seguridad en seguridad de agentes de IA.

Para exponer tus servicios Spring a clientes de IA externos —Claude Desktop, asistentes del IDE, otros agentes—, Spring AI también ofrece soporte de servidor MCP; consulta MCP para empresas.

Streaming

Para interfaces de chat, envía los tokens en streaming como server-sent events:

@GetMapping("/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun stream(@RequestParam q: String): Flux<String> =
    chat.prompt().user(q).stream().content()

Funciona tanto en Spring MVC como en WebFlux. Comprueba que los proxies inversos no almacenen la respuesta en búfer. En el frontend sirve cualquier cliente SSE; para un frontend en React/Next.js, consulta cómo crear un chat de IA en Next.js.

Concurrencia: llamadas bloqueantes y virtual threads

Las llamadas a un LLM son E/S lenta: segundos, no milisegundos. Con los servidores clásicos de un hilo por petición, una ráfaga de peticiones de IA puede agotar el pool de hilos. En Java 21+ con Spring Boot 3.2+, activar los virtual threads (spring.threads.virtual.enabled=true) permite que el código bloqueante con call() escale sin reescribirlo de forma reactiva. Vigila el pinning en bloques synchronized y los límites de los recursos posteriores.

Añade también timeouts, reintentos con backoff ante respuestas 429/5xx y fallbacks entre proveedores; Spring Retry y los ajustes de reintento a nivel de proveedor de Spring AI ayudan; consulta fiabilidad de las APIs de LLM.

Observabilidad

Spring AI emite observaciones de Micrometer para las llamadas de ChatClient, las peticiones al modelo, los advisors, las llamadas a herramientas y las operaciones del almacén vectorial, incluido el uso de tokens. Con Spring Boot Actuator y un exportador de OpenTelemetry, aparecen como métricas y trazas junto al resto de tu aplicación:

  • Sigue el uso de tokens por endpoint y por cliente para controlar el coste; consulta optimización de costes de LLM.
  • Traza las peticiones RAG de extremo a extremo: latencia de recuperación, número de documentos, latencia del modelo.
  • Decide de forma deliberada si registras el contenido de prompts y respuestas: suele venir desactivado por defecto por privacidad; actívalo solo donde la política lo permita.

El enfoque general está en observabilidad de LLM.

Testing

  • Tests unitarios: simula ChatModel o envuelve el uso de ChatClient en tu propia interfaz para probar de forma determinista la lógica que lo rodea, las herramientas y las comprobaciones de permisos.
  • Tests de integración: Testcontainers para PostgreSQL con pgvector y, si alojas tú el modelo, Ollama con un modelo pequeño.
  • Evaluación: Spring AI incluye evaluadores, como los de relevancia y comprobación de hechos, que usan un modelo como juez. Trátalos como parte de una batería de evals con un conjunto de referencia, ejecutada en CI con cada cambio de prompt o de modelo; consulta evals de LLM.

Spring AI frente a LangChain4j

LangChain4j es la otra gran opción en la JVM. Ambas son sólidas:

Aspecto Spring AI LangChain4j
Estilo de integración Autoconfiguración de Spring Boot, modismos de Spring Independiente del framework; integraciones para Spring, Quarkus, Micronaut
API de alto nivel ChatClient + advisors AI Services (interfaces anotadas)
Observabilidad Micrometer integrado Listeners; las integraciones varían
Ecosistema Parte del portfolio de Spring Gran comunidad, muchas integraciones

Para los equipos que ya usan Spring Boot, Spring AI es la opción natural por defecto. Para Quarkus o librerías independientes del framework, LangChain4j encaja mejor.

Preguntas frecuentes

¿Spring AI está listo para producción? Sí, desde su versión 1.0 GA de 2025. Como con cualquier librería que evoluciona rápido, fija las versiones, lee las notas de versión y mantén los tests de integración.

¿Podemos usar Spring AI con modelos self-hosted? Sí, mediante el starter de Ollama o el starter de OpenAI apuntando a un servidor compatible con OpenAI como vLLM.

¿Funciona con las corrutinas de Kotlin? La API bloqueante funciona bien sobre virtual threads; la API de streaming devuelve un Flux de Reactor, que se convierte en un Flow de Kotlin con asFlow().

¿La lógica de IA debería vivir en un microservicio aparte? No por defecto. Un módulo dentro de tu servicio actual —por ejemplo, en una arquitectura de monolito modular— es más sencillo hasta que el escalado o los límites entre equipos justifiquen separarlo.

Fuentes

  1. Spring. Spring AI project y documentación de referencia.
  2. Spring AI. ChatClient API.
  3. Spring AI. Tool calling.
  4. Spring AI. Vector databases.
  5. Documentación de LangChain4j.
  6. pgvector.