Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Stream de salida LLM con Server-Sent Events (SSE)
El streaming de salida LLM mejora la latencia percibida — los usuarios ven tokens a medida que se generan en lugar de esperar la respuesta completa. Server-Sent Events (SSE) es el protocolo estandar para streaming del servidor al cliente sobre HTTP. A continuacion: streaming SSE con FastAPI, la API de streaming de OpenAI y async generators.
Cuando Usar Esto
-
For alternatives, see Complete Guide to OpenAI API Mastery.
-
Interfaces de chat donde los usuarios esperan respuestas en tiempo real
-
Generacion de texto largo donde esperar la salida completa se siente lento
-
Cualquier aplicacion LLM donde la latencia percibida importa
Requisitos Previos
- Python 3.10+
- Paquetes
fastapi,uvicorn,openai - Una API key de OpenAI
Solucion
1. Instalar dependencias
pip install fastapi uvicorn openai sse-starlette
2. Endpoint de streaming SSE con FastAPI
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from openai import AsyncOpenAI
import json
import asyncio
app = FastAPI()
client = AsyncOpenAI()
class ChatRequest(BaseModel):
message: str
model: str = "gpt-4o-mini"
async def stream_openai_response(message: str, model: str):
"""Async generator that yields SSE-formatted chunks from OpenAI."""
stream = await client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": message},
],
stream=True,
)
async for chunk in stream:
if chunk.choices[0].delta.content is not None:
data = json.dumps({"token": chunk.choices[0].delta.content})
yield f"data: {data}\n\n"
# Enviar evento de fin
yield f"data: {json.dumps({'done': True})}\n\n"
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest):
"""SSE endpoint for streaming LLM responses."""
return StreamingResponse(
stream_openai_response(request.message, request.model),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Deshabilitar buffering de Nginx
},
)
3. JavaScript del cliente (EventSource)
<!DOCTYPE html>
<html>
<body>
<div id="output"></div>
<script>
const eventSource = new EventSource("/api/chat/stream");
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.done) {
eventSource.close();
return;
}
document.getElementById("output").textContent += data.token;
};
eventSource.onerror = (error) => {
console.error("SSE error:", error);
eventSource.close();
};
</script>
</body>
</html>
4. SSE basado en POST con fetch (para cuerpos de peticion)
@app.post("/api/chat/stream-post")
async def chat_stream_post(request: ChatRequest):
"""SSE endpoint that accepts POST body."""
return StreamingResponse(
stream_openai_response(request.message, request.model),
media_type="text/event-stream",
)
// SSE basado en fetch del lado del cliente
async function streamChat(message) {
const response = await fetch("/api/chat/stream-post", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop(); // Mantener chunk incompleto en buffer
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
if (data.done) return;
console.log(data.token);
}
}
}
}
5. Streaming con historial de conversacion
class ChatHistoryRequest(BaseModel):
messages: list[dict]
model: str = "gpt-4o-mini"
async def stream_with_history(messages: list[dict], model: str):
"""Stream response with full conversation history."""
stream = await client.chat.completions.create(
model=model,
messages=messages,
stream=True,
)
async for chunk in stream:
content = chunk.choices[0].delta.content
if content is not None:
yield f"data: {json.dumps({'token': content})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
@app.post("/api/chat/conversation")
async def chat_conversation(request: ChatHistoryRequest):
return StreamingResponse(
stream_with_history(request.messages, request.model),
media_type="text/event-stream",
)
6. Manejo de errores en stream
async def stream_with_error_handling(message: str, model: str):
"""Stream with proper error handling."""
try:
stream = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": message}],
stream=True,
)
async for chunk in stream:
content = chunk.choices[0].delta.content
if content is not None:
yield f"data: {json.dumps({'token': content})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': str(e)})}\n\n"
Como Funciona
stream=Truele dice a la API de OpenAI que retorne chunks a medida que se generan en lugar de esperar la respuesta completa. Cada chunk contiene undeltacon los nuevos tokens.- Async generator (
async def+yield) produce chunks uno a la vez.StreamingResponsede FastAPI consume el generator y envia cada chunk al cliente inmediatamente. - Formato SSE — cada evento es
data: {json}\n\n. El cliente parsea estas lineas y procesa el payload JSON. El doble newline marca el fin de un evento. EventSourcees el cliente SSE integrado del navegador. Se reconecta automaticamente ante desconexion y parsea el stream de eventos.- Evento
donesenala al cliente que el stream esta completo, permitiendole cerrar la conexion y limpiar.
Variantes
Streaming con LangChain
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
async def stream_langchain(message: str):
"""Stream using LangChain LCEL."""
chain = (
ChatPromptTemplate.from_template("{question}")
| ChatOpenAI(model="gpt-4o-mini", streaming=True)
| StrOutputParser()
)
async for chunk in chain.astream({"question": message}):
yield f"data: {json.dumps({'token': chunk})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
Streaming con Ollama (LLM local)
import httpx
async def stream_ollama(message: str, model: str = "llama3"):
"""Stream from a local Ollama instance."""
async with httpx.AsyncClient() as http_client:
async with http_client.stream(
"POST",
"http://localhost:11434/api/generate",
json={"model": model, "prompt": message, "stream": True},
) as response:
async for line in response.aiter_lines():
data = json.loads(line)
if data.get("response"):
yield f"data: {json.dumps({'token': data['response']})}\n\n"
if data.get("done"):
yield f"data: {json.dumps({'done': True})}\n\n"
break
Manejo de backpressure
async def stream_with_backpressure(message: str, model: str):
"""Stream with backpressure — slow down if client can't keep up."""
queue = asyncio.Queue(maxsize=10)
producer_done = asyncio.Event()
async def producer():
stream = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": message}],
stream=True,
)
async for chunk in stream:
content = chunk.choices[0].delta.content
if content:
await queue.put(content) # Bloquea si la cola esta llena
producer_done.set()
asyncio.create_task(producer())
while not (producer_done.is_set() and queue.empty()):
try:
token = await asyncio.wait_for(queue.get(), timeout=30)
yield f"data: {json.dumps({'token': token})}\n\n"
except asyncio.TimeoutError:
yield f"data: {json.dumps({'error': 'timeout'})}\n\n"
break
yield f"data: {json.dumps({'done': True})}\n\n"
Mejores Practicas
- Establece
X-Accel-Buffering: no— previene que Nginx buffere el stream, lo que rompe la entrega en tiempo real - Maneja la desconexion del cliente — si el cliente cierra la conexion, el generator debe dejar de consumir el stream de OpenAI
- Envia un evento
done— le permite al cliente saber que el stream esta completo vs. un error - Usa POST para cuerpos de peticion —
EventSourcesolo soporta GET; usafetchconReadableStreampara POST
Errores Comunes
- No establecer
media_type="text/event-stream"— el navegador no parseara SSE sin el content type correcto - Buffering en un reverse proxy — Nginx y Cloudflare bufferean respuestas por defecto; deshabilita el buffering para endpoints SSE
- No manejar
delta.contentsiendoNone— el primer y ultimo chunk pueden tener contenidoNone(rol y finish reason) - Usar
EventSourcepara POST —EventSourcesolo soporta GET; usafetchcon streaming para peticiones POST
FAQ
Q: SSE vs. WebSocket — cual debo usar para streaming LLM? A: SSE es mas simple y suficiente para streaming servidor-a-cliente (que es todo lo que el streaming LLM necesita). Usa WebSocket si necesitas comunicacion bidireccional.
Q: Reduce el streaming la latencia total? A: No — el tiempo total para generar la respuesta completa es el mismo. El streaming reduce la latencia percibida mostrando tokens a medida que llegan.
Q: Puedo hacer stream con function calling? A: Si. OpenAI hace stream de los argumentos de function call como deltas. Necesitas acumular los chunks y parsear el JSON completo al final.
Q: Como manejo rate limits durante el streaming? A: Las respuestas de streaming cuentan como una llamada API. Implementa reintento con exponential backoff en errores 429 antes de iniciar el stream.
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Compose LCEL Chains in LangChain for Multi-Step LLM
Build composable LLM pipelines with LangChain Expression Language (LCEL) using pipes, parallel execution, and custom runnable components
RecipeStructured JSON Output from OpenAI Function Calling
Use OpenAI function calling and structured outputs to get reliable JSON from LLMs with Pydantic validation and error handling
RecipeEvaluate RAG Quality with RAGAS Metrics
Measure RAG pipeline quality using RAGAS framework metrics — faithfulness, answer relevancy, context precision, and context recall for objective evaluation
RecipeExtract Data from HTML Pages with Python and BeautifulSoup
Parse HTML and extract data using BeautifulSoup. Covers CSS selectors, navigation, tables, pagination, and respectful scraping with rate limiting.
RecipeBuild Stateful AI Agents with LangGraph State Machines
Create multi-step AI agents with LangGraph using state machines, conditional edges, tool calling, and human-in-the-loop checkpoints for production workflows
RecipeRun LLMs Locally with Ollama for Private Inference
Install and use Ollama to run open-source LLMs locally with Python, including streaming, embeddings, function calling, and model management without API costs
RecipeConfigure HTTP Security Headers with Helmet in Node.js
Set security HTTP headers in Express apps with Helmet — CSP, HSTS, X-Frame-Options, X-Content-Type-Options, and CORS for OWASP-compliant web security