Streaming de respuestas de IA en tiempo real con NestJS y Vercel AI SDK
Hace un tiempo audité la arquitectura backend de una plataforma SaaS que ofrecía asistentes conversacionales para empresas. Su backend estaba construido en NestJS.
Cada vez que un usuario enviaba una consulta compleja, la pantalla mostraba un indicador de carga girando durante 7 u 8 segundos desesperantes. De repente, ¡pum!, la pantalla escupía el bloque entero de 600 palabras.
Los usuarios se quejaban de que la aplicación era "lenta e inestable".
El problema no era la velocidad de la API de Anthropic o OpenAI. El problema era que el backend esperaba a que el LLM generara la respuesta completa antes de enviársela al cliente. Al migrar el controlador de NestJS a un modelo de streaming de respuestas de IA en tiempo real utilizando Vercel AI SDK, redujimos el Time to First Token (TTFT) percibido por el usuario a menos de 250 milisegundos.
Por qué el Streaming es obligatorio en aplicaciones de IA
En productos basados en modelos de lenguaje, la percepción de velocidad lo es todo. Como analizamos en nuestra guía sobre agentes de voz en tiempo real, la latencia percibida es el producto.
Cuando una persona lee texto en pantalla a medida que se genera token a token:
- Siente que la aplicación responde de forma instantánea.
- Empieza a procesar la información de inmediato sin quedarse esperando a un spinner.
- El servidor no tiene que acumular buffers de memoria gigantescos antes de responder.
El desafío de NestJS: HTTP de respuesta continua vs. Controllers estándar
Por defecto, los controladores de NestJS están diseñados para devolver objetos JSON o promesas que se resuelven antes de cerrar la conexión HTTP.
Para emitir un stream continuo de datos desde un backend en NestJS hacia una aplicación frontend (React, Angular, Next.js, etc.), debemos aprovechar las capacidades de Server-Sent Events (SSE) o escribir directamente en la respuesta nativa Response del servidor.
1. Instalación de Vercel AI SDK en NestJS
npm install ai @ai-sdk/anthropic
2. Creación del Servicio de IA (ai.service.ts)
import { Injectable } from 39;@nestjs/common39;;
import { anthropic } from 39;@ai-sdk/anthropic39;;
import { streamText } from 39;ai39;;
@Injectable()
export class AiService {
async generarRespuestaStream(prompt: string) {
const result = await streamText({
model: anthropic(39;claude-3-5-sonnet-2024102239;),
system: 39;Eres un asistente técnico especializado en arquitectura de software.39;,
prompt,
});
// Retorna el stream directo de texto/tokens
return result.toDataStreamResponse();
}
}
3. El Controlador de NestJS con Streaming (ai.controller.ts)
import { Controller, Post, Body, Res } from 39;@nestjs/common39;;
import { Response } from 39;express39;;
import { AiService } from 39;./ai.service39;;
@Controller(39;api/chat39;)
export class AiController {
constructor(private readonly aiService: AiService) {}
@Post(39;stream39;)
async chatStream(@Body(39;prompt39;) prompt: string, @Res() res: Response) {
const aiResponse = await this.aiService.generarRespuestaStream(prompt);
// Copiamos los cabezales y pipeamos la respuesta directamente al cliente
res.setHeader(39;Content-Type39;, aiResponse.headers.get(39;Content-Type39;) || 39;text/plain; charset=utf-839;);
// Convertimos el ReadableStream a Node.js Stream para enviarlo
const reader = aiResponse.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
res.end();
}
}
Optimización de Tokens y Tipado Defensivo
Al implementar llamadas en streaming, debes tener en cuenta dos aspectos críticos de producción:
- Gestión del Tokenizador en Español: Como detallamos en nuestro artículo sobre el coste de los tokens en español, el texto en español consume ligeramente más tokens por palabra que el inglés. El streaming continuo evita que la latencia acumulada por esta diferencia afecte al usuario.
- Manejo Defensivo de Errores: Si la API del LLM falla a mitad del stream, el servidor no puede devolver un código HTTP 500 porque los encabezados HTTP 200 ya han sido enviados. Aplicando programación defensiva en TypeScript, debes emitir un evento de error formateado dentro del propio stream para que el cliente lo maneje elegantemente.
El streaming de respuestas convierte un backend estático en un motor dinámico en tiempo real que ofrece experiencias de usuario fluidas y profesionales.
Si quieres aprender a construir arquitecturas de backend modernas integradas con IA, explora los Cursos de Dominicode. Y si buscas desarrollar proyectos de alto impacto junto a desarrolladores senior, te esperamos en Dominicode Labs.
Preguntas frecuentes
¿Es mejor usar Server-Sent Events (SSE) o WebSockets para streaming de texto?
Para streaming unidireccional de texto desde el servidor hacia el cliente (como un chat de IA), Server-Sent Events (SSE) o HTTP Streaming es mucho más simple, eficiente y compatible con proxies que WebSockets, ya que reutiliza conexiones HTTP estándar.
¿Cómo consume este stream un cliente Frontend (React / Angular)?
Librerías como Vercel AI SDK ofrecen hooks cliente como useChat() o useCompletion() en React/Next.js que consumen el endpoint en streaming automáticamente. En Angular o vanilla JS, puedes usar la API nativa fetch() examinando response.body.getReader().
¿Se pueden enviar metadatos estructurados (como IDs o fuentes) junto al stream de texto?
Sí. Vercel AI SDK incluye soporte para StreamData, lo que te permite adjuntar JSONs con metadatos personalizados (ej. documentos de contexto RAG, fuentes o consumo de tokens) antes o durante la emisión del stream.
¿Cómo afecta el streaming al escalado de servidores en Kubernetes o Docker?
Dado que las conexiones HTTP permanecen abiertas durante la generación del texto (habitualmente unos pocos segundos), debes asegurarte de ajustar el timeout de tus proxies o balancadores de carga (como NGINX o Traefik) para no cortar prematuramente la respuesta.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
