Ingeniería de IA

Mejores Prácticas de Ingeniería de Prompts para LLMs en Producción

Resumen

La ingeniería de prompts en producción requiere formatos estructurados, ejemplos few-shot, restricciones explícitas y evaluación continua. Avanza más allá de prompts ad-hoc hacia pipelines sistemáticos con control de versiones y pruebas A/B.

25 de enero, 20267 min de lectura
Ingeniería de PromptsLLMGPT-4ClaudeIA en ProducciónNLP

La ingeniería de prompts ha evolucionado de una curiosidad a una disciplina de ingeniería crítica. En sistemas de producción, la diferencia entre un buen y mal prompt puede significar la diferencia entre 95% de precisión y 60% de precisión. Esta guía comparte patrones prácticos que funcionan.

La Mentalidad de Producción

La mayoría de tutoriales de ingeniería de prompts se enfocan en escenarios de chat interactivo. La producción es diferente. Necesitas:

  • Consistencia: La misma entrada debe producir salidas similares
  • Medibilidad: Debes poder evaluar la calidad a escala
  • Mantenibilidad: Los prompts evolucionan y necesitan control de versiones

Insight Clave

Trata los prompts como código. Necesitan control de versiones, pruebas, documentación y procesos de revisión como cualquier otro artefacto de producción.

Formatos de Salida Estructurados

Los sistemas de producción necesitan salidas parseables. Siempre especifica el formato exacto que esperas.

Modo JSON

SYSTEM_PROMPT = """Eres un asistente de extracción de datos médicos.
 
Extrae la siguiente información de la nota clínica y devuelve SOLO JSON válido.
 
Campos requeridos:
- patient_age: entero o null
- chief_complaint: string
- medications: array de strings
- allergies: array de strings
 
Ejemplo de salida:
{
  "patient_age": 45,
  "chief_complaint": "dolor de pecho",
  "medications": ["aspirina", "lisinopril"],
  "allergies": ["penicilina"]
}
"""

Capa de Validación

Nunca confíes ciegamente en las salidas del LLM:

from pydantic import BaseModel, validator
from typing import Optional
 
class ClinicalExtraction(BaseModel):
    patient_age: Optional[int]
    chief_complaint: str
    medications: list[str]
    allergies: list[str]
 
    @validator('patient_age')
    def age_must_be_reasonable(cls, v):
        if v is not None and (v < 0 or v > 150):
            raise ValueError('La edad debe estar entre 0 y 150')
        return v
 
def extract_clinical_data(note: str) -> ClinicalExtraction:
    response = llm.invoke(SYSTEM_PROMPT, note)
    return ClinicalExtraction.model_validate_json(response)

Patrones de Aprendizaje Few-Shot

La investigación de Brown et al. (2020) demostró que el prompting few-shot mejora dramáticamente el rendimiento en tareas. Así es como implementarlo efectivamente:

Selección Dinámica de Ejemplos

from sentence_transformers import SentenceTransformer
import numpy as np
 
class FewShotSelector:
    def __init__(self, examples: list[dict]):
        self.examples = examples
        self.encoder = SentenceTransformer('all-MiniLM-L6-v2')
        self.embeddings = self.encoder.encode([e['input'] for e in examples])
 
    def select_examples(self, query: str, k: int = 3) -> list[dict]:
        """Selecciona los k ejemplos más similares a la consulta."""
        query_embedding = self.encoder.encode(query)
        similarities = np.dot(self.embeddings, query_embedding)
        top_indices = np.argsort(similarities)[-k:][::-1]
        return [self.examples[i] for i in top_indices]

Error Común

Los ejemplos few-shot estáticos pueden desorientar al modelo cuando la consulta es diferente. Siempre usa similitud semántica para seleccionar ejemplos relevantes dinámicamente.

Prompting de Cadena de Pensamiento

Wei et al. (2022) mostraron que pedir a los modelos que muestren su razonamiento mejora significativamente la precisión en tareas complejas:

REASONING_PROMPT = """
Analiza el siguiente ticket de soporte al cliente y determina la acción apropiada.
 
Piensa en esto paso a paso:
1. Primero, identifica el problema central del cliente
2. Luego, evalúa el nivel de urgencia (bajo, medio, alto, crítico)
3. Después, determina el departamento apropiado
4. Finalmente, sugiere la respuesta inicial
 
Ticket: {ticket_text}
 
Déjame trabajar esto paso a paso:
"""

Cadena de Pensamiento Estructurada

Para producción, captura el razonamiento en un formato estructurado:

class TicketAnalysis(BaseModel):
    reasoning_steps: list[str]
    core_issue: str
    urgency: Literal["bajo", "medio", "alto", "crítico"]
    department: str
    suggested_response: str
 
COT_PROMPT = """
Analiza el ticket y devuelve tu análisis como JSON.
 
{
  "reasoning_steps": ["Paso 1: ...", "Paso 2: ...", ...],
  "core_issue": "...",
  "urgency": "bajo|medio|alto|crítico",
  "department": "...",
  "suggested_response": "..."
}
"""

Plantillas de Prompts y Control de Versiones

Mantén los prompts en un formato estructurado y versionado:

# prompts/clinical_extraction_v2.yaml
name: clinical_extraction
version: "2.1.0"
description: "Extraer datos estructurados de notas clínicas"
model: gpt-4-turbo
temperature: 0
max_tokens: 1000
 
system_prompt: |
  Eres un asistente de extracción de datos médicos especializado en...
 
user_template: |
  Extrae información de la siguiente nota clínica:
 
  {note_content}
 
  Devuelve solo JSON válido que coincida con el esquema.
 
schema:
  type: object
  required: [patient_age, chief_complaint]
  properties:
    patient_age:
      type: integer
      minimum: 0
      maximum: 150

Evaluación y Optimización

Construyendo Suites de Pruebas

class PromptTestSuite:
    def __init__(self, prompt_template: str):
        self.prompt = prompt_template
        self.test_cases = []
 
    def add_test(self, input_data: dict, expected: dict, tags: list[str] = []):
        self.test_cases.append({
            "input": input_data,
            "expected": expected,
            "tags": tags
        })
 
    def run_evaluation(self) -> dict:
        results = []
        for case in self.test_cases:
            response = self.invoke(case["input"])
            score = self.evaluate(response, case["expected"])
            results.append({"case": case, "response": response, "score": score})
 
        return {
            "total": len(results),
            "passed": sum(1 for r in results if r["score"] >= 0.9),
            "average_score": sum(r["score"] for r in results) / len(results),
            "by_tag": self._aggregate_by_tag(results)
        }

Pruebas A/B de Prompts

class PromptABTest:
    def __init__(self, prompt_a: str, prompt_b: str, split: float = 0.5):
        self.prompts = {"A": prompt_a, "B": prompt_b}
        self.split = split
        self.results = {"A": [], "B": []}
 
    def get_prompt(self, request_id: str) -> tuple[str, str]:
        """Asignación determinista basada en ID de solicitud."""
        variant = "A" if hash(request_id) % 100 < self.split * 100 else "B"
        return variant, self.prompts[variant]
 
    def record_result(self, variant: str, success: bool, latency: float):
        self.results[variant].append({"success": success, "latency": latency})

Guardarraíles de Producción

Validación de Entrada

def validate_input(text: str, max_tokens: int = 4000) -> str:
    """Sanitiza y valida la entrada antes de enviar al LLM."""
    # Eliminar intentos potenciales de inyección
    text = re.sub(r'<\|.*?\|>', '', text)
 
    # Truncar si es muy largo
    tokens = tokenizer.encode(text)
    if len(tokens) > max_tokens:
        text = tokenizer.decode(tokens[:max_tokens])
 
    return text

Guardarraíles de Salida

class OutputGuardrails:
    def __init__(self):
        self.sensitive_patterns = [
            r'\b\d{3}-\d{2}-\d{4}\b',  # SSN
            r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',  # Email
        ]
 
    def check(self, output: str) -> tuple[bool, list[str]]:
        issues = []
        for pattern in self.sensitive_patterns:
            if re.search(pattern, output):
                issues.append(f"Patrón de datos sensibles detectado: {pattern}")
        return len(issues) == 0, issues

Optimización de Costos

Los costos de LLM en producción pueden escalar rápidamente. Implementa estas estrategias:

Capa de Caché

from functools import lru_cache
import hashlib
 
class PromptCache:
    def __init__(self, redis_client):
        self.redis = redis_client
        self.ttl = 3600  # 1 hora
 
    def get_cache_key(self, prompt: str, params: dict) -> str:
        content = f"{prompt}:{json.dumps(params, sort_keys=True)}"
        return hashlib.sha256(content.encode()).hexdigest()
 
    async def get_or_compute(self, prompt: str, params: dict, compute_fn):
        key = self.get_cache_key(prompt, params)
        cached = await self.redis.get(key)
        if cached:
            return json.loads(cached)
 
        result = await compute_fn(prompt, params)
        await self.redis.setex(key, self.ttl, json.dumps(result))
        return result

Monitoreo y Observabilidad

Rastrea estas métricas para prompts en producción:

class PromptMetrics:
    def record(self, prompt_id: str, response: dict, metadata: dict):
        metrics = {
            "prompt_id": prompt_id,
            "timestamp": datetime.utcnow(),
            "latency_ms": metadata["latency_ms"],
            "input_tokens": metadata["input_tokens"],
            "output_tokens": metadata["output_tokens"],
            "cost_usd": self.calculate_cost(metadata),
            "success": metadata.get("success", True),
            "model": metadata["model"]
        }
        self.emit(metrics)

Conclusión

La ingeniería de prompts en producción requiere tratar los prompts como artefactos de ingeniería de primera clase. Puntos clave:

  1. Estructura tus salidas - Usa esquemas JSON y validación
  2. Versiona los prompts - Rastrea cambios como código
  3. Construye suites de evaluación - Mide la calidad sistemáticamente
  4. Implementa guardarraíles - Valida entradas y salidas
  5. Optimiza costos - Cachea y minimiza tokens
  6. Monitorea todo - Rastrea latencia, costo y calidad

El campo continúa evolucionando rápidamente. Mantente actualizado con la investigación y evalúa continuamente nuevas técnicas contra tus líneas base de producción.


Referencias

Brown, T. B., Mann, B., Ryder, N., Subbiah, M., Kaplan, J., Dhariwal, P., Neelakantan, A., Shyam, P., Sastry, G., Askell, A., Agarwal, S., Herbert-Voss, A., Krueger, G., Henighan, T., Child, R., Ramesh, A., Ziegler, D. M., Wu, J., Winter, C., ... Amodei, D. (2020). Language models are few-shot learners. Advances in Neural Information Processing Systems, 33, 1877-1901. https://arxiv.org/abs/2005.14165

Wei, J., Wang, X., Schuurmans, D., Bosma, M., Ichter, B., Xia, F., Chi, E., Le, Q., & Zhou, D. (2022). Chain-of-thought prompting elicits reasoning in large language models. Advances in Neural Information Processing Systems, 35, 24824-24837. https://arxiv.org/abs/2201.11903

OpenAI. (2024). GPT-4 technical report. https://openai.com/research/gpt-4

Anthropic. (2024). Claude model card and prompt engineering guide. https://docs.anthropic.com/claude/docs/prompt-engineering


¿Quieres discutir estrategias de ingeniería de prompts? Contáctame o explora mis proyectos de ingeniería de IA.

Frequently Asked Questions

OR

Osvaldo Restrepo

Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.