Hoy en dia, cualquiera puede construir un demo de LLM con una sola llamada a la API.
Pero lograr que esa misma funcionalidad sobreviva a usuarios reales, datos reales y una factura real es un trabajo totalmente diferente.
Ese trabajo se resume en cinco habilidades que aparecen en casi todos los sistemas de produccion en los que he trabajado o revisado: retrieval, routing, guardrails, evals y agent loops.
Cada una responde a una pregunta que un modelo frontera por si solo no puede manejar.
- De donde provienen los datos de mi empresa?
- Por que la factura es tan alta?
- Que ocurre cuando alguien envia un prompt hostil?
- Mi ultimo cambio mejoro o empeoro las cosas?
- Como consigo que un modelo haga trabajo de multiples pasos sin fallar?
En este articulo vamos a repasar estas 5 habilidades con codigo que puedes ejecutar hoy. Cada fragmento usa una funcion call_llm(), de modo que puedes apuntarla al modelo que ya uses, ya sea alojado o local.
Hola, soy Sara Nóbrega, ingeniera de IA enfocada en desplegar sistemas de machine learning en produccion.
Un wrapper para que el codigo funcione en cualquier lugar
Rellena una de las ramas de esta funcion con el SDK que tengas.
El resto del articulo nunca toca un proveedor directamente, asi que cambiar de modelo significa editar esta funcion y nada mas.
def call_llm(prompt: str, system: str = "", model: str = "default") -> str:
"""Envia un prompt al modelo que tengas y devuelve la respuesta de texto."""
# --- Opcion A: Compatible con OpenAI (OpenAI, Together, un vLLM local, Ollama) ---
from openai import OpenAI
client = OpenAI() # lee OPENAI_API_KEY; pasa base_url=... para apuntar a un servidor local
resp = client.chat.completions.create(
model="gpt-4o-mini" if model == "default" else model,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": prompt},
],
)
return resp.choices[0].message.content
# --- Opcion B: Anthropic (borra la Opcion A de arriba si usas esta) ---
# from anthropic import Anthropic
# client = Anthropic() # lee ANTHROPIC_API_KEY
# resp = client.messages.create(
# model="claude-haiku-4-5" if model == "default" else model,
# max_tokens=1024,
# system=system,
# messages=[{"role": "user", "content": prompt}],
# )
# return resp.content[0].text
Todo lo que sigue usa call_llm. Cambia el cuerpo una vez y todos los ejemplos siguen funcionando.
Habilidad #1: RAG, anclar el modelo a tus propios datos
Un modelo base no conoce los documentos de tu empresa, y afinarlo cada vez que cambian es lento y costoso.
La generacion aumentada por recuperacion (RAG) obtiene los documentos relevantes en el momento de la consulta y se los pasa al modelo como contexto.
Sigue siendo el patron de LLM mas comun en produccion, y normalmente es lo primero que ensena un curriculo.
El trabajo interesante esta en la recuperacion (retrieval).
Si devuelves el fragmento correcto depende de como dividas los documentos (chunking), de si combinas busqueda por palabras clave y busqueda semantica (retrieval hibrido), y de si reordenas los resultados antes de que lleguen al modelo (reranking).
Aqui tienes un pequeno pipeline que se ejecuta en CPU sin ningun servicio externo:
# pip install sentence-transformers numpy
import numpy as np
from sentence_transformers import SentenceTransformer
embedder = SentenceTransformer("all-MiniLM-L6-v2") # pequeno, corre en una laptop
# En la vida real estos vienen de tus propios documentos, divididos en fragmentos.
docs = [
"Nuestra politica de reembolsos permite devoluciones dentro de los 30 dias posteriores a la compra.",
"El envio estandar tarda de 3 a 5 dias habiles.",
"El plan Pro cuesta 49 dolares al mes e incluye soporte prioritario.",
"El restablecimiento de contrasena se hace desde la pagina de configuracion de la cuenta.",
]
# Incrusta la base de conocimiento una sola vez, por adelantado.
doc_vectors = embedder.encode(docs, normalize_embeddings=True)
def retrieve(question: str, k: int = 2) -> list[str]:
"""Devuelve los k documentos mas cercanos en significado a la pregunta."""
q = embedder.encode([question], normalize_embeddings=True)[0]
scores = doc_vectors @ q # similitud por coseno (los vectores estan normalizados)
top = np.argsort(scores)[::-1][:k] # primero los de mayor puntaje
return [docs[i] for i in top]
def answer(question: str) -> str:
context = "\n".join(retrieve(question))
prompt = f"Responde usando solo el contexto siguiente.\n\nContexto:\n{context}\n\nPregunta: {question}"
return call_llm(
prompt,
system="Eres un asistente de soporte. Si el contexto no responde la pregunta, dimelo.",
)
Observa que la pregunta dice “enviar algo de vuelta” y el documento que coincide habla de “devoluciones”. La busqueda por palabras clave no detectaria esa coincidencia. La coincidencia por embeddings la captura porque ambas frases estan cerca en el espacio vectorial.
Como aprender RAG
Construyelo sobre tus propias notas y luego rompelo a proposito. Haz una pregunta que el recuperador responda mal y observa como la respuesta se degrada. Arreglalo cambiando el tamano del fragmento (chunk). Anade la busqueda por palabras clave BM25 junto a los embeddings (retrieval hibrido) y mide si las respuestas mejoran. Anade un reranker, como un cross-encoder o una API de reranking alojada, y compara los 3 mejores resultados antes y despues.
Cuando superes un array de numpy, pasa a una base de datos vectorial (FAISS para velocidad, Qdrant o Pinecone para filtrado) y a un framework como LlamaIndex o LangChain que gestiona el almacenamiento y la busqueda por ti. Para medir la calidad en lugar de fiarte de la intuicion, RAGAS te da metricas especificas de RAG: fidelidad, relevancia de la respuesta, precision del contexto y recuperacion del contexto.
Habilidad #2: Enrutamiento de modelos - pagar por el modelo que la tarea necesita
Enviar cada solicitud a un modelo frontera es la forma mas rapida de conseguir una factura que nadie puede explicar.
El enrutamiento (routing) envia las solicitudes faciles y de alto volumen a un modelo pequeno o local, y reserva el modelo caro para las dificiles.
Anadir una cache para los prompts repetidos se reporta que reduce entre 40% y 70% las facturas de inferencia en produccion, por eso el enrutamiento paso de ser un truco avanzado a una expectativa basica.
La habilidad consiste en puntuar una solicitud y elegir un nivel de servicio. Esta es la forma que tiene:
BARATO, MEDIO, FRONTERA = "local-pequeno", "nivel-medio", "frontera"
def route(tipo_tarea: str, tokens_entrada: int) -> str:
# Las tareas de alto volumen y estrechas van a un modelo pequeno, normalmente local.
if tipo_tarea in {"clasificar", "extraer", "etiquetar"} and tokens_entrada < 2000:
return BARATO
# El contexto largo o el razonamiento abierto merecen el modelo frontera.
if tipo_tarea in {"razonar", "planificar", "revisar-codigo"} or tokens_entrada > 8000:
return FRONTERA
return MEDIO # el valor predeterminado sensato en el medio
Un enrutador solo rinde si puedes ver cuanto gastas. Envuelve tus llamadas para que cada una quede registrada con su tipo de tarea, cantidad de tokens y costo.
import collections
# Precio aproximado por millon de tokens. Actualizalos para tu propio proveedor.
PRECIO = {"local-pequeno": 0.0, "nivel-medio": 0.60, "frontera": 12.0}
registro_llamadas = [] # en produccion esto va a una base de datos o a tu herramienta de tracing
def llamada_conseguida(prompt: str, tipo_tarea: str) -> str:
nivel_modelo = route(tipo_tarea, tokens_entrada=len(prompt.split()))
tokens = len(prompt.split()) * 1.3 # estimacion burda; usa un tokenizador real en produccion
costo = tokens / 1_000_000 * PRECIO[nivel_modelo]
registro_llamadas.append({"tarea": tipo_tarea, "nivel": nivel_modelo, "costo": costo})
return call_llm(prompt, model=nivel_modelo)
# Despues de un lote de trabajo, ve a donde fue a parar el dinero:
gasto = collections.Counter()
for c in registro_llamadas:
gasto[c["nivel"]] += c["costo"]
print(dict(gasto))
Como aprender el enrutamiento de modelos
Registra cada llamada de LLM en un proyecto que ya tengas y luego observa a donde va el dinero. Toma la tarea de mayor volumen y mas simple y enviala a un modelo mas barato, y mide honestamente la caida de calidad. Anade una cache para los prompts repetidos. Cuando el codigo manual se vuelva tedioso, prueba una libreria de enrutamiento como RouteLLM o un gateway como LiteLLM que se situa frente a muchos proveedores. Anota el costo antes y despues.
Habilidad #3: Guardarrailes - entrada y salida que puedas defender
Los guardarrailes pasaron de ser un extra a algo que los entrevistadores esperan que tengas.
La inyeccion de prompts ha sido el riesgo numero 1 en el OWASP Top 10 para Aplicaciones LLM durante 2 ediciones seguidas.
La razon por la que funciona es simple: un LLM lee las instrucciones y los datos en el mismo canal, asi que un atacante puede escribir una entrada que el modelo trate como una nueva instruccion y la siga, porque no puede distinguir entre ambas.
El trabajo abarca la validacion de entrada y salida, la deteccion de inyecciones y jailbreaks, el enmascaramiento de los datos personales (PII) antes de que lleguen al modelo o a tus registros, y mantener el modelo dentro de su ambito permitido.
Aqui tienes un filtro inicial que puedes ejecutar sin dependencias.
import re
PATRONES_INYECCION = [
r"ignore (all|the|previous|above) instructions",
r"disregard .*(system|prompt)",
r"you are now",
r"reveal your (system )?prompt",
]
def parece_inyeccion(texto: str) -> bool:
return any(re.search(p, texto, re.IGNORECASE) for p in PATRONES_INYECCION)
PATRONES_PII = {
"correo": r"[\w.+-]+@[\w-]+\.[\w.-]+",
"telefono": r"\+?\d[\d\s().-]{7,}\d",
}
def enmascarar(texto: str) -> str:
for etiqueta, patron in PATRONES_PII.items():
texto = re.sub(patron, f"[{etiqueta}]", texto)
return texto
def respuesta_protegida(entrada_usuario: str) -> str:
if parece_inyeccion(entrada_usuario):
return "Rechazado: esto parece un intento de cambiar mis instrucciones."
limpio = enmascarar(entrada_usuario)
return call_llm(
limpio,
system="Responde solo preguntas sobre nuestro producto. Rechaza cualquier otra cosa.",
)
Una advertencia: trata las expresiones regulares como un primer filtro solamente. Un atacante decidido sortea una lista de patrones. La propia orientacion de OWASP es la defensa en profundidad: validacion de entrada mas filtrado de salida, herramientas con minimos privilegios, aprobacion humana para acciones sensibles y pruebas adversarias periodicas. Los sistemas de produccion recurren a Microsoft Presidio para la PII y a Guardrails AI o NeMo Guardrails para la validacion, en lugar del regex anterior.
Como aprender guardarrailes
Lee el OWASP Top 10 para Aplicaciones LLM una vez, de principio a fin. Luego toma el prompt del sistema de tu propio proyecto e intenta romperlo. Consigue que el modelo ignore sus instrucciones y luego tapona el agujero. Anade validacion de entrada y salida con una de las librerias anteriores. Anade deteccion de PII antes de que algo llegue al modelo o a los registros. La pregunta de entrevista suele ser “realiza un red teaming sobre tu propio prompt del sistema y nombra los vectores de inyeccion”, asi que practica escribir un ataque y su defensa de memoria.
Habilidad #4: Evals y observabilidad - saber si funciona
Cambiar un prompt sin un eval es adivinar. Cuando un agente de multiples pasos falla sin observabilidad, ves la respuesta final equivocada y ninguno de los pasos que la produjeron.
Empieza con un conjunto de eval pequeno. No necesitas un framework para empezar. Necesitas de 20 a 50 entradas reales con las respuestas que aceptarias. Incluso 8 ejemplos te muestran la forma.
conjunto_eval = [
{"pregunta": "Cual es la capital de Francia?", "esperado": "Paris"},
{"pregunta": "Cuantos dias tiene un anio bisiesto?", "esperado": "366"},
{"pregunta": "Que significa CPU?", "esperado": "unidad central de procesamiento"},
# ... amplia esto a 20-50 casos reales de tu propio uso
]
def puntuacion_ok(salida: str, esperado: str) -> bool:
"""Una comprobacion flexible: la respuesta aceptada aparecio en la respuesta?"""
return esperado.lower() in salida.lower()
def ejecutar_eval(prompt_sistema: str) -> float:
aprobados = 0
for fila in conjunto_eval:
salida = call_llm(fila["pregunta"], system=prompt_sistema)
aprobados += puntuacion_ok(salida, fila["esperado"])
puntaje = aprobados / len(conjunto_eval)
print(f"aprobados {aprobados}/{len(conjunto_eval)} = {puntaje:.0%}")
return puntaje
# Ahora puedes comparar dos prompts con un numero en lugar de con intuiciones:
ejecutar_eval("Responde en una sola palabra.")
ejecutar_eval("Responde solo con el dato, sin oracion.")
La primera vez que cambias un prompt y puedes demostrar que el puntaje se mantuvo, esto deja de sentirse como un trabajo pesado.
Calificar respuestas abiertas con un juez LLM
La coincidencia exacta falla con las respuestas abiertas donde la redaccion varia. La solucion habitual es que un segundo modelo califique al primero. La orientacion de DeepEval sobre los jueces destaca un punto que vale la pena recordar: un APROBADO o REPROBADO binario es mas fiable que pedirle a un juez una puntuacion de 0 a 100. Asi que mantengamos el veredicto binario.
PROMPT_JUEZ = """Calificas la respuesta de un asistente frente a una referencia.
Responde exactamente con APROBADO o REPROBADO en la primera linea, luego una razon corta.
Pregunta: {q}
Respuesta de referencia: {ref}
Respuesta del asistente: {ans}
La respuesta APROBADO si es factualmente coherente con la referencia,
incluso cuando la redaccion es diferente."""
def juez(pregunta: str, referencia: str, respuesta_asistente: str) -> bool:
veredicto = call_llm(PROMPT_JUEZ.format(q=pregunta, ref=referencia, ans=respuesta_asistente))
return veredicto.strip().upper().startswith("APROBADO")
Comprueba al juez contra ti mismo
Un juez sin comprobar es un numero en el que no puedes confiar. Los jueces LLM muestran sesgos conocidos: favorecen respuestas mas largas, favorecen la respuesta que aparece primero y tienden a ser demasiado indulgentes. Asi que antes de confiar en los numeros del juez, etiqueta unas 20 respuestas tu mismo y mide con que frecuencia el juez esta de acuerdo contigo.
# Tu calificas estas 20 a mano: True = buena respuesta, False = mala respuesta.
etiquetas_humanas = [True, True, False, True, False, True] # ... tus etiquetas
etiquetas_juez = [juez(r["q"], r["ref"], r["ans"]) for r in respuestas_calificadas]
acuerdo = sum(h == j for h, j in zip(etiquetas_humanas, etiquetas_juez)) / len(etiquetas_humanas)
print(f"el juez coincide conmigo {acuerdo:.0%} de las veces")
# Por debajo de ~80%? Reescribe la rubrica del juez antes de confiar en una sola puntuacion.
Ver el interior de una ejecucion fallida
La observabilidad es la otra mitad. Cuando algo se rompe, quieres toda la secuencia de pasos que produjo la respuesta. Un pequeño decorador te permitira empezar: registra cada paso, su estado y cuanto tiempo tardo.
import time, functools
traza = []
def trazado(fn):
@functools.wraps(fn)
def envoltorio(*args, **kwargs):
inicio = time.time()
estado = "ok"
try:
return fn(*args, **kwargs)
except Exception as e:
estado = f"error: {e}"
raise
finally:
traza.append({
"paso": fn.__name__,
"estado": estado,
"segundos": round(time.time() - inicio, 3),
})
return envoltorio
@trazado
def paso_recuperar(q): return retrieve(q)
@trazado
def paso_responder(q): return answer(q)
paso_responder("Cuanto tiempo tengo para devolver algo?")
print(traza) # -> una fila por paso, con tiempos y estado
En produccion no haces esto a mano. La industria se decanto por las convenciones semanticas GenAI de OpenTelemetry como el esquema estandar para las trazas de LLM, y OpenTelemetry se graduo en la CNCF en 2026, lo que la convierte en una eleccion segura a largo plazo. Herramientas como Langfuse, LangSmith, Arize Phoenix y Braintrust se conectan a ella y te dan una interfaz sobre cada paso.
Como aprender evals
Empieza escribiendo a mano el conjunto de 20 ejemplos anterior para un proyecto que ya tengas. Hacerlo a mano te obliga a decidir que es una buena respuesta, una decision que ninguna herramienta tomara por ti. Cuando quieras mas que una coincidencia exacta, elige un framework:
- DeepEval es nativo de pytest, con licencia MIT, y corre localmente sin cuenta, asi que encaja en un pipeline de CI. Buena primera opcion para un ingeniero solitario.
- promptfoo se configura en YAML y es potente para comparar modelos lado a lado y para hacer red-teaming, si la seguridad es tu preocupacion cercana.
- RAGAS esta disenado especificamente para RAG, con las metricas de fidelidad y contexto mencionadas antes.
Para el monitoreo en produccion, anade encima una de Langfuse, LangSmith, Phoenix o Braintrust.
Habilidad 5: Bucles agenicos - trabajo de multiples pasos que se recupera
Esta habilidad tiene la mayor hype y la mayor rotacion, asi que aprendela con los ojos bien abiertos.
La encuesta del Q1 2026 de Gartner encontro que solo el 17% de las organizaciones han desplegado agentes de IA, aunque mas del 60% espera hacerlo en dos anios. La advertencia que te mantiene creible: Gartner tambien espera que mas del 40% de los proyectos agenicos de IA sean descartados para 2027 por costo, valor poco claro o gobernanza debil.
La adopcion avanza por delante de la fiabilidad.
Un agente es un modelo mas herramientas mas un bucle que sigue llamando herramientas hasta que la tarea se completa. Construyelo a mano una vez para sentir donde se vuelve fragil.
import json
def herramienta_busqueda(consulta: str) -> str:
"""Sustituto de una llamada real de busqueda o base de datos."""
conocimiento = {"clima en lisboa": "22C y soleado"}
return conocimiento.get(consulta.lower().strip(), "") # devuelve "" cuando no encuentra nada
HERRAMIENTAS = {"buscar": herramienta_busqueda}
SISTEMA_AGENTE = """Resuelves una tarea usando herramientas.
Para llamar a una herramienta, responde con JSON: {"tool": "buscar", "input": "tu consulta"}
Cuando tengas la respuesta final, responde con JSON: {"answer": "tu respuesta"}
Responde solo con JSON, nada mas."""
def ejecutar_agente(tarea: str, max_pasos: int = 5) -> str:
historial = f"Tarea: {tarea}"
for _ in range(max_pasos):
respuesta = call_llm(historial, system=SISTEMA_AGENTE)
try:
accion = json.loads(respuesta)
except json.JSONDecodeError:
historial += "\nEso no era JSON valido. Responde solo con JSON."
continue
if "answer" in accion:
return accion["answer"]
herramienta = HERRAMIENTAS.get(accion.get("tool"))
resultado = herramienta(accion["input"]) if herramienta else ""
if resultado:
historial += f"\nResultado de la herramienta: {resultado}"
else:
# La herramienta devolvio basura. Informa al agente en lugar de reventar.
historial += "\nLa herramienta no devolvio nada util. Prueba otra consulta o responde con lo que sepas."
return "Detenido tras demasiados pasos."
Ejecutalo unas cuantas veces y veras con que facilidad se descarrila: el modelo devuelve prosa en lugar de JSON, una herramienta regresa vacia, el bucle gira. Esa fragilidad es precisamente la razon por la que existen los frameworks. LangGraph y CrewAI te dan maquinas de estado y puntos de control (checkpointing), de modo que una ejecucion se reanuda desde el paso que fallo en lugar de reiniciar toda la tarea.
Como aprender bucles agenicos
Construye el bucle anterior a mano y siente donde se rompe. Reconstruye lo mismo en LangGraph o CrewAI y nota que es lo que el framework maneja por ti: estado, reintentos, puntos de control. Fuerza un fallo y haz que el agente se recupere. Dale una herramienta real y maneja el caso en que la herramienta devuelve basura. Luego lee un post-mortem de un proyecto de agentes que fue cancelado. Las lecciones de costo y gobernanza son la parte que ningun tutorial ensena, y conocerlas te convierte en la persona que pregunta si la tarea necesita un agente o basta con una buena llamada de funcion.
Conclusion
Aprende estas cinco habilidades y podras anclar un modelo en los datos de la empresa, reducir buena parte de la factura, evitar que el sistema filtra informacion o se le haga jailbreak, demostrar con numeros que funciona, y lograr que haga trabajo de multiples pasos sin colapsar.
No necesitas las cinco a la vez. Intentar aprenderlas en paralelo es como la gente se agota y no aprende ninguna. Elige la que toca tu trabajo actual y profundiza.
- Para la mayoria de la gente es RAG o evals.
- Evals es por donde yo empezaria si nada mas te atrae.
- Bucles agenicos es la que trataria con mas cuidado: la hype es ruidosa y la tasa de cancelacion es real.
Nada de esto requiere un titulo nuevo. Solo requiere elegir una habilidad, construir algo pequeno que se rompa y arreglarlo. Luego la siguiente.
Abre un proyecto que ya tengas y anade el patron que le falta: retrieval si no puede ver tus datos, routing si la factura te asusta, evals si cambias prompts por corazonada.
Gracias por leer!
Mi nombre es Sara Nóbrega. Soy ingeniera de IA con formacion en fisica.
Fuente original: 5 AI Skills That Will Keep Data Scientists Relevant in 2027