cover

Streaming en LlamaIndex TypeScript: Una Guía Honesta para Principiantes


Escuchar este post

Selecciona una voz y genera audio para escuchar este post

Introducción: Lo Que Realmente Necesitas Saber

Si estás empezando con LlamaIndex TypeScript y el streaming te parece confuso, no estás solo. La documentación puede ser dispersa y los ejemplos a veces no muestran el panorama completo. Esta guía te explica qué métodos existen realmente, cuáles usar, y por qué.

Lo que encontrarás aquí:

  • Métodos de streaming que realmente funcionan
  • Cuándo usar cada uno (con ejemplos prácticos)
  • Los problemas comunes que encontrarás
  • Soluciones que funcionan en el mundo real

Los Métodos Que Realmente Existen

1. Streaming Básico con Callbacks (Funciona, Pero No Es Ideal)

// Método tradicional - funciona pero tiene limitaciones const queryEngine = index.asQueryEngine({ streaming: true }); queryEngine.query("¿Cuáles son los temas recurrentes en los cuentos de Cortázar?", { onToken: (token: string) => { process.stdout.write(token); // Escribe token por token }, onComplete: () => { console.log("\nRespuesta completa"); } });

Realidad: Este método funciona, pero es difícil de manejar en aplicaciones complejas. Los callbacks se vuelven difíciles de seguir cuando necesitas hacer más cosas.

2. Async Generators - El Patrón Moderno Recomendado

// Patrón moderno que realmente funciona bien async function* streamChat(message: string) { const events = agent.runStream(message); for await (const event of events) { if (event.type === 'text-delta') { yield { type: 'content', text: event.delta, timestamp: Date.now() }; } if (event.type === 'progress') { yield { type: 'progress', percentage: event.progress }; } } } // Uso simple y claro for await (const chunk of streamChat("¿Qué elementos surreales aparecen en los cuentos de Cortázar?")) { if (chunk.type === 'content') { console.log(chunk.text); } else if (chunk.type === 'progress') { console.log(`Progreso: ${chunk.percentage}%`); } }

Por qué es mejor:

  • Más fácil de leer y entender
  • Se integra naturalmente con TypeScript
  • Puedes usar for await que es intuitivo
  • Manejo de errores más simple con try/catch

3. Workflow Context - Para Casos Avanzados

// Para workflows complejos con múltiples pasos async function* workflowStream(query: string) { const { stream, sendEvent } = workflow.createContext(); // Iniciar el proceso sendEvent({ type: 'start', data: { query } }); for await (const event of stream) { // Diferentes tipos de eventos según tu workflow if (event.type === 'agent-response') { yield { type: 'response', content: event.data.content }; } if (event.type === 'tool-call') { yield { type: 'activity', message: `Ejecutando: ${event.data.toolName}` }; } // Condición de parada if (event.type === 'complete') { break; } } }

Cuándo usarlo: Cuando tienes workflows con múltiples herramientas o pasos complejos que necesitas monitorear.

Problema Importante: Filtrado de Tool Calls

El Problema Real

Cuando usas herramientas (tools) en tu agente, NO quieres enviar todos los eventos al cliente. Algunos eventos contienen información interna que no debería ser visible.

// ❌ PROBLEMÁTICO - Envía todo al cliente async function* unsafeStream(message: string) { const events = agent.runStream(message); for await (const event of events) { yield event; // Esto puede incluir detalles internos de herramientas } }

La Solución Práctica

// ✅ SEGURO - Filtra qué enviar al cliente async function* safeStream(message: string) { const events = agent.runStream(message); for await (const event of events) { // Solo envía texto al usuario if (event.type === 'text-delta') { yield { type: 'content', text: event.delta }; } // Envía feedback de progreso if (event.type === 'progress') { yield { type: 'progress', percentage: event.progress }; } // Para tool calls, solo envía feedback genérico if (event.type === 'tool-call') { yield { type: 'activity', message: 'Analizando información...' }; // NO envías los detalles de la herramienta } } }

Por qué es importante: Protege información sensible y da una mejor experiencia de usuario sin detalles técnicos confusos.

Integración con APIs Web (Caso Real)

Para Crear un Endpoint de Streaming

// Ejemplo real para un endpoint Next.js o similar export async function POST(request: Request) { const { message } = await request.json(); // Crear ReadableStream desde tu async generator const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); try { for await (const chunk of safeStream(message)) { // Formato Server-Sent Events const data = `data: ${JSON.stringify(chunk)}\n\n`; controller.enqueue(encoder.encode(data)); } } catch (error) { // Manejo de errores const errorData = `data: ${JSON.stringify({ type: 'error', message: 'Error al analizar el texto literario' })}\n\n`; controller.enqueue(encoder.encode(errorData)); } finally { controller.close(); } } }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' } }); }

En el Frontend (React)

function ChatComponent() { const [response, setResponse] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleSendMessage = async (message: string) => { setIsLoading(true); setResponse(''); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) }); const reader = response.body?.getReader(); if (!reader) return; while (true) { const { done, value } = await reader.read(); if (done) break; const text = new TextDecoder().decode(value); const lines = text.split('\n').filter(line => line.startsWith('data: ')); for (const line of lines) { const data = JSON.parse(line.slice(6)); if (data.type === 'content') { setResponse(prev => prev + data.text); } } } } finally { setIsLoading(false); } }; return ( <div> {/* Tu UI aquí */} <div>{response}</div> {isLoading && <div>Pensando...</div>} </div> ); }

Errores Comunes y Cómo Evitarlos

1. No Manejar Errores en el Stream

// ❌ Sin manejo de errores async function* badStream(message: string) { const events = agent.runStream(message); for await (const event of events) { yield event; // Si algo falla, tu app se rompe } } // ✅ Con manejo de errores async function* goodStream(message: string) { try { const events = agent.runStream(message); for await (const event of events) { yield { type: 'content', data: event.delta }; } } catch (error) { yield { type: 'error', message: 'Error procesando el análisis literario, intenta de nuevo' }; } }

2. Olvidar el Cleanup

// ✅ Con cleanup apropiado async function* streamWithCleanup(message: string, signal?: AbortSignal) { const events = agent.runStream(message); try { for await (const event of events) { // Verificar si se canceló if (signal?.aborted) { yield { type: 'cancelled' }; return; } yield { type: 'content', data: event.delta }; } } finally { // Cleanup si es necesario console.log('Stream terminado'); } }

3. Chunking Ineficiente

// ✅ Chunking inteligente para mejor UX async function* efficientStream(message: string) { const events = agent.runStream(message); let buffer = ''; for await (const event of events) { if (event.type === 'text-delta') { buffer += event.delta; // Envía chunks cuando tiene sentido if (buffer.includes('.') || buffer.includes('\n') || buffer.length > 50) { yield { type: 'content', text: buffer }; buffer = ''; } } } // No olvides el buffer final if (buffer) { yield { type: 'content', text: buffer }; } }

Cuándo Usar Cada Método

Usa Callbacks Si:

  • Estás haciendo una prueba rápida
  • Tu caso de uso es muy simple
  • Estás migrando código legacy gradualmente

Usa Async Generators Si:

  • Estás construyendo una aplicación real
  • Necesitas integrar con APIs web
  • Quieres código mantenible y fácil de entender
  • Esta es la recomendación para la mayoría de casos

Usa Workflow Context Si:

  • Tienes workflows complejos con múltiples pasos
  • Necesitas control granular sobre el estado
  • Estás construyendo sistemas de agentes avanzados

Consejos Prácticos

1. Empezar Simple

// Comienza con algo básico que funcione async function* myFirstStream(message: string) { const events = agent.runStream(message); for await (const event of events) { if (event.type === 'text-delta') { yield event.delta; } } } // Luego añade complejidad gradualmente for await (const text of myFirstStream("Analiza el estilo narrativo de Ribeyro")) { console.log(text); }

2. Debugging

// Añade logs para entender qué está pasando async function* debugStream(message: string) { console.log('Iniciando stream para:', message); const events = agent.runStream(message); for await (const event of events) { console.log('Evento recibido:', event.type); if (event.type === 'text-delta') { yield event.delta; } } console.log('Stream terminado'); }

3. Testing

// Los async generators son fáciles de testear async function testStream() { const results = []; for await (const chunk of myStream("Compara el realismo urbano de Ribeyro con el fantástico de Cortázar")) { results.push(chunk); } console.log('Resultados:', results); // Verifica que tengas lo que esperas }

Conclusión Práctica

Para empezar: Usa async generators. Son el balance perfecto entre simplicidad y funcionalidad.

Para producción: Async generators + filtrado adecuado + manejo de errores.

Para casos avanzados: Workflow context cuando realmente lo necesites.

Lo más importante: Empieza simple, prueba que funcione, y luego añade complejidad gradualmente.

No te sientas abrumado por todas las opciones. El 90% de los casos se resuelven bien con async generators básicos. Una vez que domines eso, puedes explorar las características más avanzadas.


Recursos Útiles

Los async generators pueden parecer intimidantes al principio, pero una vez que los pruebes, verás que son mucho más fáciles que los callbacks

Abrazo. bliss. 🤓

meta cover

Los 5 MCPs Más Populares en Claude Code: Guía Completa para Principiantes 🚀

Checa este otro Post

meta cover

Los MCPs Más Populares de la Comunidad 🤖

Checa este otro Post

¡Nuevo curso!

Animaciones web con React + Motion 🧙🏻