Ingeniería en producción más allá de las demos: RAG, costos, protección de datos y uso responsable de herramientas de IA.
"La IA amplifica a quien piensa. No lo sustituye."
Cesar Zanis
01
El cambio de mentalidad (lee primero)
01
La IA amplifica el entendimiento
Generar código es cada vez más accesible. El valor de la ingeniería va más allá de escribir una aplicación CRUD.
Los profesionales necesitan entender el negocio, diseñar soluciones y coordinar herramientas para entregarlas. La IA amplifica el buen criterio; no lo sustituye.
La calidad de una respuesta de IA depende en gran medida del contexto que proporcionas.
02
Primero el problema, después la herramienta
Antes de abrir una herramienta de IA, analiza el problema de negocio. ¿Quién es el usuario? ¿Cuál es la necesidad real? ¿Qué pasa sin esta funcionalidad?
'Crea un sistema de registro' aporta poco contexto útil. Explicar que un inspector de campo usa un terminal Honeywell con internet inestable y necesita guardar formularios sin perder datos, incluso si se agota la batería, hace concretas las restricciones.
Tu experiencia aporta el contexto que vuelve útil el resultado.
03
IA para planificar ≠ IA para implementar
Usa modelos de razonamiento para explorar la arquitectura: comparar alternativas, analizar trade-offs, revisar ADR y cuestionar decisiones. La decisión final sigue siendo tuya.
Usa agentes integrados en el editor para implementar: boilerplate, CRUD, pruebas y refactorización. Pueden trabajar con los archivos reales del proyecto, ejecutar comandos y corregir errores.
No delegues en la IA la responsabilidad de la arquitectura. Propone; tú decides.
02
Patrones de producción
RAG: cuándo y cómo usarlo bien
La diferencia entre un chatbot genérico y un asistente útil.
RAG (Retrieval-Augmented Generation) recupera contexto relevante antes de generar una respuesta. Sin recuperación, el modelo depende de su conocimiento de entrenamiento. Un RAG bien diseñado aporta tu documentación, base de conocimiento e historial de decisiones.
Un error frecuente es usar solo búsqueda vectorial e ignorar la búsqueda léxica, como BM25 o búsqueda de texto completo. Evalúa la recuperación híbrida y el reranking con preguntas representativas.
Python
# Arquitectura RAG mínima funcional
# 1. Indexación (offline, cuando cambia el contenido)
def index_document(doc: str, doc_id: str):
# Chunking: bloques solapados para preservar el contexto
chunks = chunk_with_overlap(doc, size=800, overlap=100)
for i, chunk in enumerate(chunks):
embedding = embed(chunk) # mediante API: text-embedding-004
vector_store.upsert(
id=f"{doc_id}_{i}",
vector=embedding,
metadata={"text": chunk, "doc_id": doc_id}
)
# 2. Recuperación (en tiempo real, por consulta del usuario)
def retrieve(query: str, top_k: int = 5) -> list[str]:
query_embedding = embed(query)
# Búsqueda semántica (vectores)
semantic_results = vector_store.query(query_embedding, top_k=top_k)
# Búsqueda léxica (palabras exactas): no la ignores
lexical_results = full_text_search(query, top_k=top_k)
# Reordenamiento híbrido: RRF (Reciprocal Rank Fusion)
return reciprocal_rank_fusion([semantic_results, lexical_results])
# 3. Generación: contexto inyectado en el prompt
def answer(query: str) -> str:
context_chunks = retrieve(query)
context = "\n\n".join(context_chunks)
prompt = f"""Responda baseado APENAS no contexto abaixo.
Se a resposta não estiver no contexto, diga explicitamente.
CONTEXTO:
{context}
PERGUNTA: {query}"""
return llm.generate(prompt)
Costos de LLM más allá del playground
Mide por separado los costos de entrada, salida y contexto.
Los costos en producción incluyen instrucciones del sistema, historial de conversación, contexto recuperado y salida generada. Cada componente crece de forma distinta.
Resume el historial en lugar de reenviar todos los turnos. Dirige tareas sencillas a modelos pequeños y reserva los grandes para las que los necesitan. Evalúa la caché de respuestas cuando corresponda y establece límites explícitos de salida. Mide calidad y costo juntos.
Python
# Control de costos en producción
# 1. Comprime el historial: no envíes todo
def compress_history(history: list[dict], max_turns: int = 4) -> str:
if len(history) <= max_turns:
return format_history(history)
# Resume los turnos anteriores y conserva los últimos N mensajes completos
old_turns = history[:-max_turns]
recent_turns = history[-max_turns:]
summary = llm.generate(
f"Resuma em 2-3 frases os pontos principais desta conversa: {old_turns}",
config={"maxOutputTokens": 150} # resumen breve, modelo pequeño
)
return f"[Resumo anterior: {summary}]\n{format_history(recent_turns)}"
# 2. Enruta por complejidad: elige un modelo para cada tarea
def route_to_model(query: str) -> str:
# Clasificación rápida y económica
is_complex = classifier.predict(query) # modelo local o flash
return "gemini-3.8-flash" if is_complex else "gemini-3.5-flash-lite"
# 3. Límite explícito de salida
generation_config = {
"maxOutputTokens": 600, # suficiente para una respuesta útil
"temperature": 0.3, # ajusta la temperatura a la tarea y al modelo
}
LGPD + IA: requisitos de protección de datos
Define la base legal y envía solo los datos necesarios.
Antes de enviar datos personales a una API de IA, define la finalidad y la base legal aplicable, minimiza los datos y revisa la retención, el acceso y las transferencias internacionales. El consentimiento es una de las bases legales de la LGPD brasileña, no la única. Las políticas de entrenamiento varían según el producto y el contrato; por ejemplo, los datos de la API de OpenAI no se usan para entrenamiento por defecto. Evalúa anonimización, controles contractuales y procesamiento local según el caso. El procesamiento local también requiere acceso, registros y copias de seguridad seguros.
Python
# Enmascaramiento básico antes de enviar datos a un LLM externo
import re
def anonymize_for_llm(text: str) -> tuple[str, dict]:
"""Remove PII antes de enviar. Retorna texto limpo + mapa para substituição."""
replacements = {}
counter = {"cpf": 0, "email": 0, "phone": 0}
# CPF
def replace_cpf(m):
key = f"[CPF_{counter['cpf']}]"
replacements[key] = m.group(0)
counter["cpf"] += 1
return key
# Email
def replace_email(m):
key = f"[EMAIL_{counter['email']}]"
replacements[key] = m.group(0)
counter["email"] += 1
return key
text = re.sub(r'\d{3}\.\d{3}\.\d{3}-\d{2}', replace_cpf, text)
text = re.sub(r'[\w.-]+@[\w.-]+\.\w+', replace_email, text)
return text, replacements
# Uso
clean_text, pii_map = anonymize_for_llm(user_message)
llm_response = llm.generate(clean_text)
# Nunca registres pii_map: contiene los datos originales
# Úsalo solo si la respuesta necesita los valores originales
IA Local vs IA Cloud
Entiende dónde tiene sentido cada enfoque.
Herramientas locales como Ollama, vLLM y llama.cpp ejecutan modelos en infraestructura bajo tu control. Pueden servir para clasificación, extracción y cargas con requisitos estrictos sobre los datos. Revisa cada dependencia y ruta de telemetría en lugar de asumir que los datos nunca salen.
Plataformas cloud como Vertex AI, AWS Bedrock y Azure OpenAI ofrecen acceso gestionado a modelos sin operar tus propias GPU. Compara capacidad, privacidad, latencia y costo según la tarea.
Un enfoque híbrido puede usar un modelo local para el enrutamiento y otro cloud para solicitudes difíciles, cuando la política de datos lo permita.
Abstrae el proveedor mediante una interfaz si necesitas varias clouds. La inversión de dependencias funciona aquí como con las bases de datos.
Python
# Abstracción de proveedor: cambia de cloud sin modificar la lógica del negocio
from abc import ABC, abstractmethod
class LLMProvider(ABC):
@abstractmethod
def generate(self, prompt: str, config: dict) -> str: ...
# Google Cloud
class VertexProvider(LLMProvider):
def generate(self, prompt: str, config: dict) -> str:
from vertexai.generative_models import GenerativeModel
model = GenerativeModel(config.get("model", "gemini-3.5-flash-lite"))
response = model.generate_content(prompt)
return response.text
# AWS
class BedrockProvider(LLMProvider):
def generate(self, prompt: str, config: dict) -> str:
import boto3
client = boto3.client("bedrock-runtime")
# Claude via Bedrock
response = client.invoke_model(
modelId=config.get("model", "anthropic.claude-3-haiku"),
body={"prompt": prompt, "max_tokens": config.get("max_tokens", 600)}
)
return response["body"].read().decode()
# Local (Ollama)
class OllamaProvider(LLMProvider):
def generate(self, prompt: str, config: dict) -> str:
import requests
r = requests.post("http://localhost:11434/api/generate", json={
"model": config.get("model", "llama3.2"),
"prompt": prompt, "stream": False
})
return r.json()["response"]
# Enrutamiento de modelos
def get_provider(task_type: str) -> LLMProvider:
if task_type in ("classify", "extract", "moderate"):
return OllamaProvider() # ejecución local; protege el acceso y los logs
if task_type == "generate":
return VertexProvider() # cloud, modelo grande
return BedrockProvider() # alternativa multicloud
Un criterio práctico: si no puedes explicar en una frase lo que hace un agente, todavía no lo entiendes lo suficiente para operarlo en producción. La sencillez es una señal de claridad.