Gestión de Prompts en Producción
Prompts como Código
En producción, los prompts no son strings ad-hoc — son artefactos de ingeniería que necesitan versionado, testing, revisión y deployment controlado.
Prompt Registry
import json
import hashlib
from datetime import datetime
from pathlib import Path
class PromptRegistry:
"""Registro centralizado de prompts con versionado."""
def __init__(self, storage_path: str = "prompts/"):
self.storage = Path(storage_path)
self.storage.mkdir(exist_ok=True)
def register(self, name: str, template: str, metadata: dict = None) -> str:
version = hashlib.sha256(template.encode()).hexdigest()[:8]
prompt_data = {
"name": name,
"version": version,
"template": template,
"metadata": metadata or {},
"created_at": datetime.utcnow().isoformat(),
}
path = self.storage / f"{name}_v{version}.json"
path.write_text(json.dumps(prompt_data, indent=2))
# Actualizar latest
latest = self.storage / f"{name}_latest.json"
latest.write_text(json.dumps(prompt_data, indent=2))
return version
def get(self, name: str, version: str = "latest") -> dict:
path = self.storage / f"{name}_{version}.json"
if not path.exists():
path = self.storage / f"{name}_latest.json"
return json.loads(path.read_text())
def render(self, name: str, version: str = "latest", **kwargs) -> str:
prompt = self.get(name, version)
template = prompt["template"]
for key, value in kwargs.items():
template = template.replace(f"{{{key}}}", str(value))
return template
# Uso
registry = PromptRegistry()
registry.register(
"classify_ticket",
"Clasifica el ticket en: {categories}\n\nTicket: {text}\n\nCategoría:",
metadata={"model": "gpt-4o", "temperature": 0}
)
A/B Testing de Prompts
import random
from dataclasses import dataclass
@dataclass
class PromptVariant:
name: str
template: str
weight: float = 0.5
class PromptABTest:
def __init__(self, test_name: str, variants: list[PromptVariant]):
self.test_name = test_name
self.variants = variants
self.results = {v.name: {"calls": 0, "scores": []} for v in variants}
def select_variant(self) -> PromptVariant:
"""Seleccionar variante basada en pesos."""
weights = [v.weight for v in self.variants]
selected = random.choices(self.variants, weights=weights, k=1)[0]
self.results[selected.name]["calls"] += 1
return selected
def record_score(self, variant_name: str, score: float):
self.results[variant_name]["scores"].append(score)
def get_stats(self) -> dict:
stats = {}
for name, data in self.results.items():
scores = data["scores"]
stats[name] = {
"calls": data["calls"],
"avg_score": sum(scores) / len(scores) if scores else 0,
"min_score": min(scores) if scores else 0,
"max_score": max(scores) if scores else 0,
}
return stats
# Uso
test = PromptABTest("summarize_v2", [
PromptVariant("concise", "Resume en 2 oraciones:\n{text}"),
PromptVariant("detailed", "Resume manteniendo puntos clave:\n{text}"),
])
Prompt Versioning con Git
prompts/
├── classify/
│ ├── v1.0.yaml
│ ├── v1.1.yaml
│ └── v2.0.yaml # breaking change
├── summarize/
│ ├── v1.0.yaml
│ └── v1.1.yaml
└── extract/
└── v1.0.yaml
Formato YAML para Prompts
# prompts/classify/v2.0.yaml
name: classify_support_ticket
version: "2.0"
description: "Clasifica tickets de soporte en categorías"
model: gpt-4o
temperature: 0
max_tokens: 50
system: |
Eres un clasificador de tickets de soporte.
Responde SOLO con la categoría, sin explicación.
user_template: |
Categorías disponibles: {categories}
Ticket del cliente:
{ticket_text}
Categoría:
variables:
- name: categories
required: true
description: "Lista de categorías separadas por coma"
- name: ticket_text
required: true
description: "Texto del ticket a clasificar"
test_cases:
- input:
categories: "bug, feature, question, billing"
ticket_text: "No puedo iniciar sesión desde ayer"
expected_output: "bug"
- input:
categories: "bug, feature, question, billing"
ticket_text: "¿Cuánto cuesta el plan premium?"
expected_output: "billing"
changelog:
- version: "2.0"
date: "2025-12-01"
changes: "Añadido few-shot examples, mejorada precisión en billing"
- version: "1.0"
date: "2025-10-15"
changes: "Versión inicial"
Prompt Evaluation Pipeline
import yaml
from dataclasses import dataclass
@dataclass
class EvalResult:
test_name: str
passed: bool
expected: str
actual: str
latency_ms: float
class PromptEvaluator:
def __init__(self, llm_client):
self.client = llm_client
def load_prompt(self, path: str) -> dict:
with open(path) as f:
return yaml.safe_load(f)
def evaluate(self, prompt_path: str) -> list[EvalResult]:
prompt_config = self.load_prompt(prompt_path)
results = []
for i, test in enumerate(prompt_config.get("test_cases", [])):
# Renderizar template
user_msg = prompt_config["user_template"]
for key, value in test["input"].items():
user_msg = user_msg.replace(f"{{{key}}}", value)
# Llamar al LLM
import time
start = time.time()
response = self.client.chat.completions.create(
model=prompt_config["model"],
messages=[
{"role": "system", "content": prompt_config["system"]},
{"role": "user", "content": user_msg},
],
temperature=prompt_config["temperature"],
max_tokens=prompt_config["max_tokens"],
)
latency = (time.time() - start) * 1000
actual = response.choices[0].message.content.strip()
passed = actual.lower() == test["expected_output"].lower()
results.append(EvalResult(
test_name=f"test_{i+1}",
passed=passed,
expected=test["expected_output"],
actual=actual,
latency_ms=latency,
))
return results
Mejores Prácticas
- Versionar prompts igual que código (git, semantic versioning)
- Testear cada cambio de prompt con test cases
- Monitorear performance de prompts en producción
- Rollback rápido a versiones anteriores si hay regresión
- Documentar el propósito y uso esperado de cada prompt
- Separar configuración (model, temperature) del contenido
Resumen
Gestionar prompts como código — con versionado, testing automatizado, A/B testing y rollback — es fundamental para mantener la calidad y confiabilidad de aplicaciones LLM en producción.