Volver a artículos
Inteligencia Artificial

Large Language Models (LLMs) en Producción

Domina el despliegue de LLMs en producción: arquitectura, RAG, fine-tuning, prompting avanzado, optimización de costos y mejores prácticas.

Academia Tooldata
30 de agosto de 2026
5 min de lectura
llmsgptragfine-tuningproducción

La Revolución de los LLMs en Producción

Los Large Language Models (LLMs) como GPT-4, Claude, Llama han transformado

lo posible en aplicaciones de IA. Sin embargo, pasar de un prototipo impresionante a un sistema en producción robusto, escalable y rentable presenta desafíos únicos.

En este artículo avanzado, cubriremos:

  • Arquitecturas de sistemas con LLMs
  • Técnicas de optimización (RAG, fine-tuning, prompt engineering)
  • Gestión de costos y latencia
  • Monitoring y evaluación
  • Consideraciones de seguridad y ética

Audiencia: Este artículo asume familiaridad con conceptos de ML, APIs y arquitectura de software.

Arquitectura de Sistemas con LLMs

Patrón 1: API Directa (Simplest)

import openai

def generar_respuesta(prompt):
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "Eres un asistente experto en análisis de datos."},
            {"role": "user", "content": prompt}
        ],
        temperature=0.7,
        max_tokens=500
    )
    return response.choices[0].message.content

Pros: Rápido de implementar, sin infraestructura adicional Contras: Costoso a escala, sin conocimiento específico del dominio, latencia variable

Patrón 2: RAG (Retrieval-Augmented Generation)

RAG combina retrieval de información relevante con generación del LLM.

Arquitectura:

Usuario Query
    ↓
Query Embedding (text-embedding-ada-002)
    ↓
Vector Search (Pinecone/Weaviate/Qdrant)
    ↓
Top-K documentos relevantes
    ↓
Construir prompt con contexto
    ↓
LLM genera respuesta
    ↓
Respuesta al usuario

Implementación:

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Pinecone
from langchain.llms import OpenAI
from langchain.chains import RetrievalQA
import pinecone

# 1. Inicializar vector store
pinecone.init(api_key="YOUR_API_KEY", environment="us-east1-gcp")

embeddings = OpenAIEmbeddings()
vectorstore = Pinecone.from_existing_index(
    index_name="knowledge-base",
    embedding=embeddings
)

# 2. Crear chain de QA con retrieval
llm = OpenAI(temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vectorstore.as_retriever(search_kwargs={"k": 4}),
    return_source_documents=True
)

# 3. Usar
def responder_con_contexto(query):
    result = qa_chain({"query": query})
    respuesta = result['result']
    fuentes = result['source_documents']

    return {
        'respuesta': respuesta,
        'fuentes': [doc.metadata for doc in fuentes]
    }

# Ejemplo
resultado = responder_con_contexto(
    "¿Cuáles son las políticas de devolución de la empresa?"
)

Ventajas de RAG:

  • Conocimiento actualizado sin reentrenamiento
  • Respuestas basadas en datos propietarios
  • Trazabilidad (citas de fuentes)
  • Menor costo que fine-tuning para muchos casos

Optimizaciones para RAG:

# Hybrid search: combinando búsqueda semántica y keyword
from langchain.retrievers import EnsembleRetriever
from langchain.retrievers import BM25Retriever

# Retriever semántico (vectores)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# Retriever de keywords
bm25_retriever = BM25Retriever.from_documents(documentos)
bm25_retriever.k = 5

# Combinar ambos
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.6, 0.4]  # 60% semántico, 40% keywords
)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=ensemble_retriever
)

Patrón 3: Fine-Tuning

Fine-tuning adapta el modelo a tu dominio específico.

Cuándo hacer fine-tuning:

  • Necesitas estilo/tono muy específico
  • Dominio técnico especializado
  • Volumen alto de requests (el costo se amortiza)
  • Necesitas modelo más pequeño/rápido

Proceso de fine-tuning con OpenAI:

import openai

# 1. Preparar datos de entrenamiento
# Formato JSONL con conversaciones
training_data = [
    {
        "messages": [
            {"role": "system", "content": "Eres un asistente de soporte técnico para nuestra plataforma SaaS."},
            {"role": "user", "content": "No puedo subir archivos"},
            {"role": "assistant", "content": "Para resolver problemas de carga de archivos: 1) Verifica que el archivo sea menor a 50MB, 2) Formatos soportados son PDF, DOC, XLS..."}
        ]
    },
    # ... 100-1000+ ejemplos más
]

# 2. Subir archivo de entrenamiento
with open("training_data.jsonl", "w") as f:
    for item in training_data:
        f.write(json.dumps(item) + "\n")

file_response = openai.File.create(
    file=open("training_data.jsonl", "rb"),
    purpose='fine-tune'
)

# 3. Crear job de fine-tuning
fine_tune_response = openai.FineTuningJob.create(
    training_file=file_response.id,
    model="gpt-3.5-turbo",
    hyperparameters={
        "n_epochs": 3,
        "learning_rate_multiplier": 0.1
    }
)

# 4. Monitorear progreso
job_id = fine_tune_response.id
status = openai.FineTuningJob.retrieve(job_id)
print(f"Status: {status.status}")

# 5. Usar modelo fine-tuned
model_name = status.fine_tuned_model
response = openai.ChatCompletion.create(
    model=model_name,
    messages=[{"role": "user", "content": "Mi pregunta..."}]
)

Mejores prácticas para fine-tuning:

  1. Calidad sobre cantidad: 100 ejemplos excelentes > 1000 mediocres
  2. Diversidad: Cubrir variedad de casos, tonos, estructuras
  3. Validación rigurosa: Split train/validation, evitar overfitting
  4. Iteración: Versionar modelos, A/B test vs baseline

Patrón 4: Arquitectura Híbrida (Producción Real)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import redis
from prometheus_client import Counter, Histogram
import logging

app = FastAPI()

# Caching con Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0)
CACHE_TTL = 3600  # 1 hora

# Métricas
request_counter = Counter('llm_requests_total', 'Total LLM requests')
latency_histogram = Histogram('llm_request_duration_seconds', 'LLM request latency')

class Query(BaseModel):
    query: str
    user_id: str
    session_id: str

@app.post("/ask")
async def ask_question(query: Query):
    request_counter.inc()

    # 1. Check cache
    cache_key = f"query:{hash(query.query)}"
    cached_response = redis_client.get(cache_key)
    if cached_response:
        logging.info("Cache hit")
        return {"response": cached_response.decode(), "cached": True}

    # 2. Guardrails: validación de input
    if len(query.query) > 1000:
        raise HTTPException(status_code=400, detail="Query demasiado largo")

    if contiene_contenido_inapropiado(query.query):
        raise HTTPException(status_code=400, detail="Contenido inapropiado detectado")

    # 3. RAG: recuperar contexto relevante
    with latency_histogram.time():
        contexto = recuperar_contexto_relevante(query.query, top_k=3)

        # 4. Construir prompt optimizado
        prompt = construir_prompt(query.query, contexto)

        # 5. Llamada al LLM con retry y fallback
        try:
            respuesta = llamar_llm_con_retry(prompt, max_retries=3)
        except Exception as e:
            logging.error(f"LLM error: {e}")
            # Fallback a respuesta pre-definida
            respuesta = "Lo siento, tenemos problemas técnicos. Por favor intenta más tarde."

    # 6. Post-procesamiento y validación
    respuesta = post_procesar_respuesta(respuesta)

    # 7. Guardar en cache
    redis_client.setex(cache_key, CACHE_TTL, respuesta)

    # 8. Logging para análisis
    log_interaccion(query.user_id, query.query, respuesta)

    return {"response": respuesta, "cached": False}

def llamar_llm_con_retry(prompt, max_retries=3):
    """LLM call con exponential backoff"""
    for attempt in range(max_retries):
        try:
            response = openai.ChatCompletion.create(
                model="gpt-4",
                messages=[{"role": "user", "content": prompt}],
                temperature=0.7,
                max_tokens=500,
                timeout=10
            )
            return response.choices[0].message.content
        except openai.error.RateLimitError:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
            else:
                raise
        except Exception as e:
            logging.error(f"LLM call failed: {e}")
            raise

Prompt Engineering Avanzado

Técnicas de Prompting

1. Few-Shot Learning

def crear_prompt_few_shot(query, ejemplos):
    prompt = """Analiza el sentimiento del siguiente texto y clasifícalo como Positivo, Negativo o Neutral.

Ejemplos:

Texto: "Este producto superó mis expectativas, excelente calidad"
Sentimiento: Positivo

Texto: "Decepcionante, no funciona como prometen"
Sentimiento: Negativo

Texto: "Es un producto normal, cumple su función"
Sentimiento: Neutral

Ahora analiza este texto:

Texto: "{query}"
Sentimiento:"""

    return prompt.format(query=query)

2. Chain-of-Thought (CoT)

cot_prompt = """Resuelve el siguiente problema paso a paso:

Problema: Una empresa tiene 150 empleados. El 40% trabaja en ventas, 35% en ingeniería y el resto en administración. Si el salario promedio en ventas es $50k, en ingeniería $80k y en administración $45k, ¿cuál es la nómina total anual?

Razonamiento paso a paso:
1. Primero, calculemos cuántos empleados hay en cada departamento
2. Luego, calculemos la nómina de cada departamento
3. Finalmente, sumemos las nóminas

Solución:"""

response = openai.ChatCompletion.create(
    model="gpt-4",
    messages=[{"role": "user", "content": cot_prompt}],
    temperature=0
)

3. Self-Consistency

Generar múltiples respuestas y tomar la más común:

def respuesta_con_self_consistency(prompt, n=5):
    respuestas = []

    for _ in range(n):
        response = openai.ChatCompletion.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.8  # Mayor temperatura para diversidad
        )
        respuestas.append(response.choices[0].message.content)

    # Tomar la respuesta más común
    from collections import Counter
    respuesta_final = Counter(respuestas).most_common(1)[0][0]

    return respuesta_final

4. ReAct (Reasoning + Acting)

react_prompt = """Responde la siguiente pregunta usando este formato:

Pregunta: [pregunta original]
Pensamiento: [tu razonamiento sobre qué hacer]
Acción: [acción a tomar: Buscar[término] o Calcular[expresión]]
Observación: [resultado de la acción]
... (repetir Pensamiento/Acción/Observación si es necesario)
Pensamiento: [razonamiento final]
Respuesta: [respuesta final]

Pregunta: ¿Cuál es el PIB actual de México y cómo se compara con el de España?

Pensamiento:"""

# Este prompt permite al modelo "razonar" sobre qué acciones tomar
# y puede integrarse con herramientas externas (búsqueda, calculadora, APIs)

Optimización de Tokens y Costos

Estrategias:

def optimizar_prompt(texto, max_tokens=2000):
    """
    Reduce tokens manteniendo información relevante
    """
    # 1. Eliminar redundancias
    texto = eliminar_repeticiones(texto)

    # 2. Resumir si es muy largo
    if contar_tokens(texto) > max_tokens:
        texto = resumir_texto(texto, target_tokens=max_tokens)

    # 3. Usar formato eficiente
    texto = texto.strip()  # Eliminar espacios innecesarios

    return texto

def contar_tokens(texto):
    """Estima tokens (1 token ≈ 4 caracteres en inglés, ≈ 2 en español)"""
    import tiktoken
    encoding = tiktoken.encoding_for_model("gpt-4")
    return len(encoding.encode(texto))

# Ejemplo de optimización
texto_largo = "..." # 5000 tokens
texto_optimizado = optimizar_prompt(texto_largo, max_tokens=2000)

print(f"Original: {contar_tokens(texto_largo)} tokens")
print(f"Optimizado: {contar_tokens(texto_optimizado)} tokens")
# Ahorro de costos: 60%

Tabla de costos (enero 2025):

Modelo Input (1K tokens) Output (1K tokens) Casos de uso
GPT-4 Turbo $0.01 $0.03 Tareas complejas
GPT-3.5 Turbo $0.0005 $0.0015 Tareas simples
Claude 3 Opus $0.015 $0.075 Análisis profundo
Claude 3 Sonnet $0.003 $0.015 Balance costo/calidad
Llama 2 70B (self-hosted) $0 (infraestructura) $0 Alto volumen

Evaluación y Monitoring

Métricas de Calidad

from sklearn.metrics import accuracy_score, f1_score
import pandas as pd

class LLMEvaluator:
    def __init__(self, test_set):
        """
        test_set: Lista de (query, expected_response) o (query, expected_label)
        """
        self.test_set = test_set

    def evaluar_exactitud(self, modelo_fn):
        """Para tareas de clasificación"""
        predicciones = []
        referencias = []

        for query, expected in self.test_set:
            pred = modelo_fn(query)
            predicciones.append(pred)
            referencias.append(expected)

        accuracy = accuracy_score(referencias, predicciones)
        f1 = f1_score(referencias, predicciones, average='weighted')

        return {'accuracy': accuracy, 'f1': f1}

    def evaluar_similitud(self, modelo_fn):
        """Para tareas de generación - usa embeddings"""
        from sklearn.metrics.pairwise import cosine_similarity
        from openai.embeddings_utils import get_embedding

        similitudes = []

        for query, expected in self.test_set:
            pred = modelo_fn(query)

            pred_emb = get_embedding(pred)
            expected_emb = get_embedding(expected)

            sim = cosine_similarity([pred_emb], [expected_emb])[0][0]
            similitudes.append(sim)

        return {
            'similitud_promedio': sum(similitudes) / len(similitudes),
            'similitud_min': min(similitudes)
        }

    def evaluar_con_llm(self, modelo_fn, criterios):
        """Evaluación usando otro LLM como juez"""
        scores = []

        for query, expected in self.test_set:
            pred = modelo_fn(query)

            eval_prompt = f"""Evalúa la siguiente respuesta según estos criterios: {criterios}

Pregunta: {query}
Respuesta esperada: {expected}
Respuesta generada: {pred}

Califica del 1-10 y justifica:"""

            eval_response = openai.ChatCompletion.create(
                model="gpt-4",
                messages=[{"role": "user", "content": eval_prompt}],
                temperature=0
            )

            # Extraer score del response
            score = extraer_score(eval_response.choices[0].message.content)
            scores.append(score)

        return {
            'score_promedio': sum(scores) / len(scores),
            'scores': scores
        }

# Uso
test_set = [
    ("¿Cuál es la capital de Francia?", "París"),
    ("¿Qué es la fotosíntesis?", "Proceso donde plantas convierten luz en energía"),
    # ... más ejemplos
]

evaluator = LLMEvaluator(test_set)
resultados = evaluator.evaluar_similitud(mi_modelo_fn)
print(f"Similitud promedio: {resultados['similitud_promedio']:.2f}")

Monitoring en Producción

import prometheus_client
from datadog import initialize, statsd
import structlog

logger = structlog.get_logger()

class LLMMonitor:
    def __init__(self):
        # Métricas de Prometheus
        self.request_duration = prometheus_client.Histogram(
            'llm_request_duration_seconds',
            'Duración de requests al LLM'
        )
        self.token_usage = prometheus_client.Counter(
            'llm_tokens_used_total',
            'Tokens totales usados',
            ['model', 'type']  # type: input/output
        )
        self.error_count = prometheus_client.Counter(
            'llm_errors_total',
            'Errores totales',
            ['error_type']
        )

    def track_request(self, model, input_text, output_text, latency, error=None):
        # Duración
        self.request_duration.observe(latency)

        # Tokens
        input_tokens = contar_tokens(input_text)
        output_tokens = contar_tokens(output_text)
        self.token_usage.labels(model=model, type='input').inc(input_tokens)
        self.token_usage.labels(model=model, type='output').inc(output_tokens)

        # Errores
        if error:
            self.error_count.labels(error_type=type(error).__name__).inc()
            logger.error("llm_error", error=str(error), model=model)

        # Logging estructurado
        logger.info(
            "llm_request",
            model=model,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            latency=latency,
            success=(error is None)
        )

        # Alertas basadas en umbrales
        if latency > 10:  # > 10 segundos
            enviar_alerta("Latencia alta en LLM", latency=latency)

        costo = calcular_costo(model, input_tokens, output_tokens)
        if costo > 1:  # > $1 por request
            enviar_alerta("Costo alto por request", costo=costo)

monitor = LLMMonitor()

def mi_funcion_llm(query):
    start_time = time.time()
    error = None
    output = ""

    try:
        output = llamar_llm(query)
    except Exception as e:
        error = e
        raise
    finally:
        latency = time.time() - start_time
        monitor.track_request(
            model="gpt-4",
            input_text=query,
            output_text=output,
            latency=latency,
            error=error
        )

    return output

Seguridad y Guardrails

Input Validation

import re
from typing import List

class InputValidator:
    def __init__(self):
        self.palabras_prohibidas = cargar_lista_palabras_prohibidas()
        self.patrones_sospechosos = [
            r'ignore previous instructions',
            r'system prompt',
            r'\/\*.*\*\/',  # Comentarios de código
        ]

    def validar(self, input_text: str) -> dict:
        errores = []

        # 1. Longitud
        if len(input_text) > 5000:
            errores.append("Input demasiado largo")

        # 2. Palabras prohibidas
        for palabra in self.palabras_prohibidas:
            if palabra.lower() in input_text.lower():
                errores.append(f"Contenido inapropiado detectado")
                break

        # 3. Prompt injection
        for patron in self.patrones_sospechosos:
            if re.search(patron, input_text, re.IGNORECASE):
                errores.append("Posible intento de prompt injection")
                break

        # 4. PII (Personal Identifiable Information)
        if contiene_pii(input_text):
            errores.append("Datos personales detectados - se anonimizarán")
            input_text = anonimizar_pii(input_text)

        return {
            'valido': len(errores) == 0,
            'errores': errores,
            'input_sanitizado': input_text
        }

def contiene_pii(text):
    """Detecta SSN, tarjetas de crédito, etc"""
    # SSN pattern
    if re.search(r'\b\d{3}-\d{2}-\d{4}\b', text):
        return True
    # Credit card
    if re.search(r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b', text):
        return True
    return False

Output Validation

class OutputValidator:
    def validar_respuesta(self, respuesta: str, contexto: dict) -> dict:
        problemas = []

        # 1. Fact-checking básico
        if not self.es_factualmente_consistente(respuesta, contexto):
            problemas.append("Posible inconsistencia factual")

        # 2. Detectar alucinaciones
        if self.contiene_alucinaciones(respuesta, contexto):
            problemas.append("Posible alucinación detectada")

        # 3. Toxicidad
        toxicidad_score = self.detectar_toxicidad(respuesta)
        if toxicidad_score > 0.7:
            problemas.append("Contenido potencialmente tóxico")

        # 4. Bias
        bias_score = self.detectar_sesgo(respuesta)
        if bias_score > 0.6:
            problemas.append("Posible sesgo detectado")

        return {
            'segura': len(problemas) == 0,
            'problemas': problemas,
            'scores': {
                'toxicidad': toxicidad_score,
                'bias': bias_score
            }
        }

    def detectar_toxicidad(self, texto):
        """Usa modelo de clasificación de toxicidad"""
        from transformers import pipeline
        classifier = pipeline("text-classification",
                            model="unitary/toxic-bert")
        result = classifier(texto)[0]
        return result['score'] if result['label'] == 'toxic' else 0

Optimización de Costos

Estrategias

class CostOptimizer:
    def seleccionar_modelo(self, query, complejidad_threshold=0.7):
        """
        Enruta queries simples a modelos más baratos
        """
        complejidad = self.estimar_complejidad(query)

        if complejidad < complejidad_threshold:
            return "gpt-3.5-turbo"  # 10x más barato
        else:
            return "gpt-4"

    def estimar_complejidad(self, query):
        """
        Estima complejidad de query (0-1)
        """
        factores = {
            'longitud': min(len(query) / 500, 1.0) * 0.3,
            'palabras_tecnicas': self.contar_palabras_tecnicas(query) / 10 * 0.4,
            'requiere_razonamiento': self.detectar_razonamiento(query) * 0.3
        }
        return sum(factores.values())

    def cache_strategy(self, query):
        """
        Determina TTL de cache basado en tipo de query
        """
        if self.es_query_estatica(query):
            return 86400  # 24 horas
        elif self.es_query_dinamica(query):
            return 3600  # 1 hora
        else:
            return 300  # 5 minutos

# Implementación
optimizer = CostOptimizer()

def procesar_query_optimizado(query):
    # 1. Check cache
    cached = check_cache(query)
    if cached:
        return cached

    # 2. Seleccionar modelo óptimo
    modelo = optimizer.seleccionar_modelo(query)

    # 3. Procesar
    respuesta = llamar_llm(query, model=modelo)

    # 4. Cache con TTL apropiado
    ttl = optimizer.cache_strategy(query)
    save_to_cache(query, respuesta, ttl=ttl)

    return respuesta

Cálculo de ROI

def calcular_roi_llm(metricas_mes):
    """
    Calcula ROI de implementación de LLM
    """
    # Costos
    costo_llm_api = metricas_mes['tokens_usados'] * PRECIO_POR_TOKEN
    costo_infraestructura = 500  # Vector DB, Redis, etc
    costo_desarrollo = 10000 / 12  # Amortizado
    costo_total = costo_llm_api + costo_infraestructura + costo_desarrollo

    # Beneficios
    horas_ahorradas = metricas_mes['tickets_automatizados'] * 0.25  # 15min/ticket
    ahorro_soporte = horas_ahorradas * 30  # $30/hora

    mejora_conversion = metricas_mes['conversiones_adicionales'] * metricas_mes['valor_promedio_venta']

    beneficio_total = ahorro_soporte + mejora_conversion

    roi = (beneficio_total - costo_total) / costo_total * 100

    return {
        'costo_total': costo_total,
        'beneficio_total': beneficio_total,
        'roi_porcentaje': roi,
        'payback_meses': costo_desarrollo / (beneficio_total - costo_llm_api - costo_infraestructura)
    }

Próximos Pasos

Checklist para Producción

  • Implementar sistema de logging estructurado
  • Configurar monitoring y alertas
  • Establecer rate limiting y quotas
  • Implementar caching inteligente
  • Configurar circuit breakers para fallbacks
  • Realizar pruebas de carga y estrés
  • Documentar costos y ROI esperado
  • Establecer proceso de evaluación continua
  • Implementar A/B testing de prompts
  • Configurar respaldos y recuperación ante desastres

Recursos Avanzados

  • Papers: "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (Lewis et al.)
  • Frameworks: LangChain, LlamaIndex, Haystack
  • Herramientas de evaluación: PromptLayer, Helicone, LangSmith
  • Comunidades: r/LocalLLaMA, OpenAI Developer Forum

Experimentación Continua

# Framework de experimentación
class ExperimentoLLM:
    def __init__(self, nombre, variante_a, variante_b):
        self.nombre = nombre
        self.variante_a = variante_a
        self.variante_b = variante_b
        self.resultados_a = []
        self.resultados_b = []

    def ejecutar(self, query):
        # Asignar aleatoriamente
        if random.random() < 0.5:
            resultado = self.variante_a(query)
            self.resultados_a.append(resultado)
            return resultado
        else:
            resultado = self.variante_b(query)
            self.resultados_b.append(resultado)
            return resultado

    def analizar(self):
        # Comparar métricas
        return {
            'variante_a_promedio': calcular_metricas(self.resultados_a),
            'variante_b_promedio': calcular_metricas(self.resultados_b),
            'ganador': determinar_ganador(self.resultados_a, self.resultados_b)
        }

Reflexión final: Los LLMs en producción requieren ingeniería rigurosa. No es suficiente con hacer funcionar un prototipo - necesitas arquitectura robusta, monitoring exhaustivo, optimización de costos y mejora continua. Trata tu sistema de LLM como cualquier otro servicio crítico de tu infraestructura.

¿Te gustó este artículo?

Explora más contenido educativo en nuestras categorías

Explorar más contenido