Cómo separar una PoC de IA de un sistema que puedes mantener
Qué cambia cuando una demo con IA se convierte en producto real: logs, costes, evaluación, permisos, fallback y mantenimiento.

La semana pasada vi una demo de un producto con IA que hacía algo impresionante: tomaba un documento legal de 40 páginas, lo analizaba y generaba un resumen ejecutivo con los puntos clave. En la demo funcionaba perfecto. Cinco minutos después, el fundador me dijo que llevaban tres meses intentando ponerlo en producción y no lo conseguían. Los resúmenes eran inconsistentes, los costes se habían multiplicado por seis respecto a la estimación inicial y no tenían forma de saber si el LLM estaba generando resúmenes correctos sin que un abogado los revisara uno por uno.
Esa historia me la he encontrado más veces de las que me gustaría. Y cada vez el patrón es el mismo: alguien monta una PoC con un LLM, funciona bien en condiciones controladas, la empresa se entusiasma y de repente hay que convertir eso en un producto real. Ahí es donde todo se complica.
Porque lo que separa una PoC de un sistema en producción no es la calidad del prompt ni la potencia del modelo. Es todo lo que rodea al modelo: logs, costes, evaluación, fallback, permisos, versionado y la capacidad de mantener el sistema funcionando cuando nadie está mirando.
PoC vs producción: lo que realmente cambia
La diferencia entre una PoC y un sistema en producción no es una cuestión de grado. Es una diferencia de naturaleza. En una PoC demuestras que algo es posible. En producción demuestras que algo es fiable, sostenible y mantenible a lo largo del tiempo.
| Aspecto | PoC | Producción |
|---|---|---|
| Datos de entrada | Controlados, seleccionados a mano | Cualquier cosa que llegue |
| Volumen | Decenas de peticiones | Miles o millones |
| Costes | ”Ya veremos” | Partida presupuestaria real |
| Errores del modelo | ”A veces falla, pero mola” | Cada error es un ticket de soporte |
| Latencia | Aceptable si es < 30s | El usuario se va si pasa de 3s |
| Evaluación | Vistazo manual, “se ve bien” | Métricas automatizadas, benchmarks |
| Logs | print() en la consola | Observabilidad estructurada |
| Prompts | Un string en el código | Versionados, testeados, documentados |
| Fallback | No existe | Obligatorio |
| Permisos y seguridad | Localhost con tu API key | Multi-tenant, RBAC, data isolation |
| Dependencia del proveedor | ”Usamos OpenAI y ya” | Plan B si la API se cae o sube precios |
| Human-in-the-loop | El propio desarrollador | Flujo diseñado para usuarios no técnicos |
Esta tabla no es un ejercicio teórico. Es la lista de cosas que me he encontrado al llevar funcionalidades de IA de Rolsfera de “funciona en mi máquina” a “funciona todos los días sin que yo esté pendiente”.
Costes reales: la conversación que nadie quiere tener
En una PoC, los costes de API son un número pequeño que nadie cuestiona. En producción, ese número se convierte en una línea del presupuesto que alguien tiene que justificar.
Hagamos números reales. Supongamos un servicio que procesa documentos con un LLM:
# Estimación de costes para procesamiento con LLM
# Modelo: GPT-4o (ejemplo de precios a mayo 2026)
# Input: ~5.00$ / 1M tokens
# Output: ~15.00$ / 1M tokens
tokens_por_documento_input = 4000 # documento de ~3000 palabras
tokens_por_documento_output = 800 # resumen + clasificación
documentos_por_dia = 500
coste_input_diario = (tokens_por_documento_input * documentos_por_dia / 1_000_000) * 5.00
coste_output_diario = (tokens_por_documento_output * documentos_por_dia / 1_000_000) * 15.00
coste_diario = coste_input_diario + coste_output_diario
coste_mensual = coste_diario * 30
print(f"Coste diario: ${coste_diario:.2f}") # ~$16.00
print(f"Coste mensual: ${coste_mensual:.2f}") # ~$480.00480 dólares al mes para 500 documentos diarios. Parece manejable. Pero ahora añade:
- Reintentos por errores. Un 5% de peticiones fallan y se reintentan. Eso es un 5% más de coste.
- Evaluación y testing. Cada vez que cambias un prompt, necesitas probarlo contra un conjunto de datos de referencia. Eso son peticiones adicionales.
- Crecimiento. Si el producto tiene éxito, el volumen se multiplica. 500 documentos pasan a 5000 y los costes se van a 4800$/mes.
- Modelo más potente. El cliente pide mayor precisión. Subes de modelo y los costes se duplican.
El coste de la PoC multiplicado por el volumen real no da el coste de producción. Da una estimación optimista. Los costes reales siempre incluyen reintentos, evaluación, picos de tráfico y la tentación inevitable de usar un modelo más caro.
En Rolsfera aprendí esto rápido. Mi primera versión procesaba todos los artículos con el modelo más potente disponible. Cuando vi la factura del primer mes, reestructuré el pipeline para usar modelos pequeños en clasificación y reservar los grandes solo para resúmenes de artículos ya aprobados. El coste bajó un 60%.
# Estrategia de costes: modelo según la tarea
TASK_MODELS = {
"classification": "gpt-4o-mini", # barato, suficiente para categorizar
"relevance_scoring": "gpt-4o-mini", # no necesita el modelo grande
"summary": "gpt-4o", # aquí sí importa la calidad
"entity_extraction": "gpt-4o-mini", # tarea estructurada, modelo pequeño basta
}
def get_model_for_task(task: str) -> str:
return TASK_MODELS.get(task, "gpt-4o-mini") # default al baratoEvaluación: cómo saber si el LLM funciona bien
Este es probablemente el problema más difícil de pasar de PoC a producción. En una PoC, la evaluación es “lo miro yo y parece correcto”. En producción, necesitas algo más riguroso.
El problema de evaluar outputs de LLM
Un LLM no es determinista (o al menos no lo es en la práctica con temperature > 0). El mismo input puede generar outputs ligeramente diferentes. Y “ligeramente diferente” a veces es “sutilmente incorrecto”.
Para una tarea de clasificación, la evaluación es relativamente fácil: comparas la categoría asignada con la correcta y calculas precisión. Para una tarea de generación de texto (resúmenes, respuestas), la evaluación es mucho más compleja.
Cómo lo hago en Rolsfera
Mi sistema de evaluación tiene tres niveles:
Nivel 1: Validación estructural. El output tiene el formato esperado. Si pido JSON, ¿es JSON válido? Si pido un resumen de 2-3 frases, ¿tiene entre 1 y 5 frases? Si pido una puntuación de 1-10, ¿está en ese rango?
def validate_llm_output(output: dict, task: str) -> list[str]:
issues = []
if task == "classification":
valid_categories = ["tech", "politics", "economy", "science", "other"]
if output.get("category") not in valid_categories:
issues.append(f"Categoría inválida: {output.get('category')}")
if task == "summary":
summary = output.get("summary", "")
sentences = summary.split(".")
if len(sentences) < 1 or len(sentences) > 6:
issues.append(f"Resumen con {len(sentences)} frases (esperado: 1-5)")
if len(summary) < 50:
issues.append("Resumen demasiado corto")
if len(summary) > 1000:
issues.append("Resumen demasiado largo")
if task == "relevance_scoring":
score = output.get("relevance_score", 0)
if not isinstance(score, (int, float)) or score < 1 or score > 10:
issues.append(f"Puntuación fuera de rango: {score}")
return issuesNivel 2: Evaluación con dataset de referencia. Mantengo un conjunto de 50 artículos manualmente clasificados y resumidos. Cuando cambio un prompt, ejecuto el pipeline contra ese dataset y comparo resultados.
def evaluate_against_reference(
pipeline_fn,
reference_data: list[dict],
) -> dict:
correct = 0
total = len(reference_data)
for item in reference_data:
result = pipeline_fn(item["article"])
if result["category"] == item["expected_category"]:
correct += 1
accuracy = correct / total if total > 0 else 0
return {
"accuracy": accuracy,
"correct": correct,
"total": total,
"threshold": 0.85, # mínimo aceptable
"passed": accuracy >= 0.85,
}Nivel 3: Feedback del proceso editorial. Cuando reviso artículos en Rolsfera y rechazo uno que la IA había puntuado como relevante, o cuando edito un resumen porque era impreciso, eso queda registrado. Con el tiempo, esos datos me dan una medida real de la calidad del pipeline de IA.
Versionado de prompts
Los prompts son código. O deberían tratarse como tal. En una PoC, el prompt es un string que editas cuando quieres. En producción, necesitas saber qué prompt generó qué output, cuándo cambió y por qué.
# prompts/classification_v3.py
PROMPT_VERSION = "3.2.1"
PROMPT_DATE = "2026-05-15"
PROMPT_CHANGELOG = """
3.2.1 - Añadida categoría 'devops', ajustado umbral de relevancia
3.2.0 - Simplificado formato de salida, eliminado campo 'confidence'
3.1.0 - Añadido ejemplo few-shot para mejorar precisión en categoría 'science'
3.0.0 - Reescritura completa del prompt de clasificación
"""
CLASSIFICATION_PROMPT = """Clasifica el siguiente artículo en una de estas categorías:
- tech: tecnología, programación, software, hardware
- politics: política nacional o internacional
- economy: economía, finanzas, mercados
- science: ciencia, investigación, medio ambiente
- devops: infraestructura, CI/CD, cloud, monitorización
- other: no encaja en ninguna anterior
Artículo:
Título: {title}
Contenido: {content}
Responde SOLO con un JSON:
{{"category": "...", "relevance_score": 1-10}}
Ejemplos:
- "Kubernetes 1.30 introduces sidecar containers" → {{"category": "devops", "relevance_score": 8}}
- "New study links sleep to cognitive performance" → {{"category": "science", "relevance_score": 4}}
"""Cada vez que el prompt cambia, se incrementa la versión. Los outputs generados se almacenan con la versión del prompt que los generó. Así, si detecto una degradación de calidad, puedo rastrear exactamente qué cambio de prompt la causó.
def process_with_tracking(article: dict, prompt_template: str, prompt_version: str) -> dict:
filled_prompt = prompt_template.format(
title=article["title"],
content=article["content"][:2500],
)
response = call_llm(filled_prompt)
result = json.loads(response)
# Guardar metadata de trazabilidad
result["_meta"] = {
"prompt_version": prompt_version,
"model": "gpt-4o-mini",
"timestamp": datetime.utcnow().isoformat(),
"input_tokens": count_tokens(filled_prompt),
"output_tokens": count_tokens(response),
}
return resultRiesgos técnicos que no aparecen en la PoC
Alucinaciones
En una PoC, si el LLM inventa un dato, lo detectas porque estás mirando el output. En producción, las alucinaciones se mezclan con miles de outputs correctos y pasan desapercibidas.
Mi estrategia para mitigar alucinaciones en Rolsfera:
- Tareas acotadas. No pido al LLM que genere información nueva. Le pido que clasifique, resuma o extraiga información del texto que le paso. Eso reduce el espacio para inventar.
- Validación cruzada. Si el resumen menciona un dato que no aparece en el texto original, es sospechoso. Tengo una validación básica que compara entidades del resumen con entidades del texto fuente.
- Outputs estructurados. Pedir JSON con campos específicos es mejor que pedir texto libre. El formato acotado reduce la superficie de alucinación.
def check_hallucination_risk(original_text: str, summary: str) -> float:
"""Heurística simple: qué proporción de entidades del resumen
aparecen en el texto original."""
import re
# Extraer palabras capitalizadas como proxy de entidades
summary_entities = set(re.findall(r'\b[A-Z][a-z]+(?:\s[A-Z][a-z]+)*\b', summary))
original_lower = original_text.lower()
if not summary_entities:
return 0.0
found = sum(1 for e in summary_entities if e.lower() in original_lower)
return 1.0 - (found / len(summary_entities))
# 0.0 = todas las entidades están en el original (bien)
# 1.0 = ninguna entidad está en el original (probable alucinación)No es un sistema perfecto. Pero detecta los casos más flagrantes.
Latencia
Un LLM tarda entre 1 y 15 segundos en responder, dependiendo del modelo, el tamaño del input y la carga del servidor. En una PoC, esperas. En producción, si el usuario espera 10 segundos sin feedback, se va.
Soluciones que aplico:
- Procesamiento asíncrono. El usuario no espera al LLM. Lanza la petición y recibe el resultado cuando está listo (notificación, polling, websocket).
- Cache de resultados. Si el mismo input ya fue procesado, devuelvo el resultado cacheado. En Rolsfera, si un artículo ya fue clasificado, no lo vuelvo a pasar por el LLM.
- Timeouts estrictos. Si la API del LLM no responde en 20 segundos, la petición falla y se encola para reintento. Nunca bloqueo el sistema esperando una respuesta que puede no llegar.
Dependencia del proveedor
Si tu sistema depende de una sola API de LLM, tienes un single point of failure. La API puede caerse, puede subir precios, puede cambiar sus términos de servicio o puede deprecar el modelo que usas.
# Patrón de multi-proveedor con fallback
LLM_PROVIDERS = [
{
"name": "openai",
"model": "gpt-4o-mini",
"endpoint": "https://api.openai.com/v1/chat/completions",
"priority": 1,
},
{
"name": "anthropic",
"model": "claude-sonnet",
"endpoint": "https://api.anthropic.com/v1/messages",
"priority": 2,
},
]
def call_llm_with_fallback(prompt: str) -> str:
providers = sorted(LLM_PROVIDERS, key=lambda p: p["priority"])
for provider in providers:
try:
return call_provider(provider, prompt)
except (APIError, TimeoutError) as e:
logger.warning(
f"Proveedor {provider['name']} falló: {e}. "
f"Intentando siguiente..."
)
raise AllProvidersFailedError("Todos los proveedores de LLM han fallado")Human-in-the-loop: el patrón que nadie quiere implementar
En la PoC, el humano está implícito: eres tú mirando los resultados. En producción, el human-in-the-loop tiene que ser un flujo diseñado, no un accidente.
En Rolsfera, la revisión humana es parte del diseño del sistema, no un parche. El LLM procesa y sugiere; yo reviso y decido. Eso tiene implicaciones arquitectónicas:
Cola de revisión. Los artículos procesados por IA no se publican automáticamente. Llegan a una cola donde yo los apruebo, edito o descarto. La cola está priorizada: artículos de fuentes de alta confianza con puntuación de relevancia alta aparecen primero.
Feedback loop. Mis decisiones de aprobación/rechazo se almacenan y sirven para evaluar la calidad del pipeline. Si empiezo a rechazar más artículos de lo habitual, algo ha cambiado (en las fuentes, en el prompt o en el modelo).
Escalación. Si el LLM produce un output que no pasa la validación estructural, el artículo se marca para revisión obligatoria en lugar de descartarse. A veces el problema no es el artículo, sino el prompt.
# Lógica de decisión: cuándo publicar automáticamente vs. pedir revisión
def decide_workflow(article: dict) -> str:
ai_meta = article.get("ai_metadata", {})
source_trust = get_source_trust_score(article["source_name"])
relevance = ai_meta.get("relevance_score", 0)
validation_issues = ai_meta.get("validation_issues", [])
# Si hay problemas de validación, siempre revisión humana
if validation_issues:
return "manual_review"
# Fuente de alta confianza + alta relevancia = puede ir automático
if source_trust > 0.9 and relevance >= 8:
return "auto_publish"
# El resto, a la cola de revisión
return "manual_review"En la práctica, menos del 10% de los artículos pasan por auto-publicación. Y eso está bien. Prefiero revisar más de lo necesario que publicar algo que no debería haber salido.
Observabilidad: logs que sirven para algo
Los print() de la PoC tienen que convertirse en logs estructurados que puedas consultar, filtrar y agregar.
import structlog
logger = structlog.get_logger()
def process_article_with_logging(article: dict) -> dict:
log = logger.bind(article_url=article["url"], source=article["source_name"])
log.info("processing_started")
try:
result = call_llm(article)
log.info("processing_completed",
category=result.get("category"),
tokens_used=result.get("_meta", {}).get("input_tokens", 0),
latency_ms=result.get("_meta", {}).get("latency_ms", 0))
return result
except TimeoutError:
log.error("llm_timeout", timeout_seconds=20)
raise
except Exception as e:
log.error("processing_failed", error=str(e), exc_info=True)
raiseLo que me interesa de los logs en producción:
- Latencia por petición. Si las peticiones al LLM empiezan a tardar más de lo habitual, quiero saberlo antes de que los usuarios se quejen.
- Tokens consumidos. Para controlar costes en tiempo real, no al final del mes.
- Tasa de errores. Si el porcentaje de peticiones fallidas sube por encima del 5%, algo va mal.
- Distribución de categorías. Si de repente el 80% de los artículos se clasifican como “other”, probablemente el prompt necesita ajustes.
La checklist que uso antes de llevar IA a producción
Después de varias iteraciones, me he quedado con esta lista de verificación. No es exhaustiva, pero cubre lo que más problemas me ha dado:
| Área | Preguntas clave |
|---|---|
| Costes | Estimación mensual con volumen real. Modelos por tarea. Alertas de gasto. Plan B si suben precios. |
| Evaluación | Dataset de referencia. Validación estructural. Métricas de calidad. Proceso de actualización. |
| Prompts | Versionado. Changelog. Tests al cambiar prompt. Trazabilidad output-prompt. |
| Resiliencia | Timeouts. Reintentos con backoff. Fallback multi-proveedor. Comportamiento sin IA. |
| Observabilidad | Logs estructurados. Métricas (latencia, tokens, errores). Alertas. Dashboard. |
| Human-in-the-loop | Flujo de revisión. Feedback loop. Escalación. Docs para revisores no técnicos. |
Reflexión
La PoC demuestra lo que es posible. La producción demuestra lo que es sostenible. Y entre una y otra hay un abismo de trabajo que no se ve en demos ni en hilos de Twitter.
No escribo esto para desanimar a nadie. La IA es útil de verdad, y los LLMs abren posibilidades que hace dos años eran ciencia ficción. Pero la conversación honesta es esta: montar una demo con un LLM cuesta un día. Llevar esa demo a producción con fiabilidad, costes controlados y mantenimiento razonable cuesta meses.
Lo que he aprendido con Rolsfera es que el éxito de un sistema con IA no depende de elegir el modelo correcto. Depende de construir todo lo que rodea al modelo: la evaluación que te dice si funciona, los logs que te dicen cuándo falla, los costes que te dicen si es sostenible y el human-in-the-loop que te dice si el output tiene sentido en el mundo real.
Si estás en la fase de PoC y te están pidiendo pasar a producción, esta checklist es un buen punto de partida. Y si alguien te dice que es cuestión de “simplemente subir el prompt a un endpoint”, ahora sabes por qué eso no es suficiente.


