Saltar al contenido principal

Ingeniería automática de prompts (APE)

Definición

La ingeniería automática de prompts (APE) es la práctica de utilizar un modelo de lenguaje para generar y optimizar instrucciones de prompts en lugar de escribirlas a mano. Introducida por Zhou et al. (2022) en el artículo Large Language Models Are Human-Level Prompt Engineers, la APE enmarca el diseño de prompts como un problema de síntesis de programas: dado un conjunto de pares de demostración entrada-salida, encontrar la instrucción en lenguaje natural que, cuando se antepone a un prompt, maximiza el rendimiento de la tarea en un conjunto de evaluación reservado. La búsqueda, puntuación y refinamiento de instrucciones candidatas se realizan de forma programática —el rol del humano pasa de ser autor de prompts a definidor de tareas y diseñador de métricas.

La motivación para automatizar el diseño de prompts es práctica. La ingeniería manual de prompts es laboriosa, frágil y sesgada por las intuiciones del ingeniero humano sobre cómo los modelos de lenguaje procesan el texto. Pequeños cambios en la redacción —"Piensa paso a paso" vs "Pensemos cuidadosamente paso a paso"— producen diferencias de precisión medibles que son imposibles de predecir sin pruebas empíricas. La APE reemplaza estas conjeturas con una búsqueda sistemática: generar un gran grupo de instrucciones candidatas, evaluar cada una en un benchmark y conservar los mejores resultados. Esta es la misma filosofía de diseño detrás de la búsqueda de hiperparámetros en ML clásico —los humanos especifican el objetivo, las máquinas hacen la búsqueda.

La APE se distingue del ajuste suave de prompts (que optimiza incrustaciones continuas de tokens mediante descenso de gradiente) y del ajuste fino (que actualiza los pesos del modelo). La APE opera completamente en el espacio del lenguaje natural usando modelos congelados. Esto la hace agnóstica al modelo, interpretable —se puede leer y entender la instrucción ganadora— y desplegable sin ninguna infraestructura de entrenamiento. La compensación es que el espacio de búsqueda discreto del lenguaje natural es vasto y no diferenciable, por lo que la APE se basa en muestreo, heurísticas de puntuación y refinamiento iterativo en lugar de la optimización basada en gradientes.

Cómo funciona

Generación de candidatos

El bucle APE comienza con un conjunto de ejemplos de demostración —pares entrada-salida que ilustran la tarea objetivo. Estos ejemplos se pasan a un meta-LLM (el mismo u otro modelo) con un meta-prompt que le pide inferir la instrucción que produciría las salidas dadas a partir de las entradas dadas. Los meta-prompts típicos se ven así: "Aquí hay pares entrada-salida. ¿Cuál es la instrucción que produce estas salidas? Genera 10 instrucciones candidatas diversas." Al muestrear con temperatura > 0, el meta-LLM produce un grupo diverso de instrucciones candidatas que difieren en redacción, encuadre y especificidad. La calidad y diversidad de este grupo inicial determina directamente el techo de la optimización.

Puntuación

Cada instrucción candidata se instancia como un prefijo en el prompt (o como el mensaje del sistema) y se evalúa contra un benchmark reservado. La función de puntuación es específica de la tarea: precisión para clasificación, corrección de ejecución para generación de código, ROUGE o BERTScore para resumen, o un juez LLM secundario para tareas de extremo abierto. La decisión de diseño clave es si la puntuación se calcula con el propio meta-LLM (usando estimaciones de log-probabilidad de las salidas correctas) o con un evaluador separado específico de la tarea. La puntuación por log-probabilidad es más rápida pero puede sobreajustarse a la calibración del meta-LLM. La puntuación con un evaluador separado es más fiable pero requiere datos etiquetados.

Refinamiento iterativo

Después de la puntuación inicial, las instrucciones candidatas top-K se seleccionan para refinamiento. El meta-LLM recibe un prompt para parafrasear, extender o combinar los mejores candidatos, produciendo un nuevo grupo de variantes que están semánticamente relacionadas pero textualmente distintas. Este bucle de refinamiento se ejecuta durante un número fijo de iteraciones o hasta que se alcanza un umbral de puntuación objetivo. Cada iteración estrecha la búsqueda alrededor de regiones prometedoras del espacio de instrucciones, análogo a la búsqueda evolutiva o escalada de colinas sobre un paisaje discreto. En la práctica, una o dos rondas de refinamiento después de un gran grupo inicial (N ≥ 50) tiende a recuperar la mayor parte de la ganancia alcanzable.

Comparaciones

CriterioAPEIngeniería manual de promptsAjuste fino
Esfuerzo humanoBajo — definir tarea y métricaAlto — autoría iterativa y pruebasAlto — recolección de datos y ejecuciones de entrenamiento
Requiere datos etiquetadosSí — para puntuaciónNo — se puede hacer empíricamenteSí — típicamente miles de ejemplos
Pesos del modelo actualizadosNoNo
Salida interpretableSí — instrucción en lenguaje naturalNo — los cambios de pesos son opacos
Generaliza entre modelosSí — volver a ejecutar la búsqueda por modeloParcialmenteNo — vinculado al modelo base
Latencia en inferenciaNinguna — sin sobrecarga en tiempo de ejecuciónNingunaNinguna
CostoMedio — N × M llamadas de evaluaciónBajoAlto — tiempo de GPU
Mejor paraTareas con una métrica clara y ≥ 50 ejemplosTareas novedosas sin una métricaTareas de alto volumen donde las ganancias de precisión justifican el entrenamiento

Cuándo usar / Cuándo NO usar

Usar cuandoEvitar cuando
Tienes un conjunto de evaluación etiquetado y puedes definir una métrica de puntuación claraLa tarea no tiene una métrica automatizada fiable — la APE no puede buscar sin una señal
La iteración manual de prompts está tomando más de un día y la precisión sigue en mesetaNecesitas un resultado de inmediato — la APE requiere múltiples llamadas a la API del LLM para evaluación
Estás desplegando el mismo prompt para muchos usuarios y incluso ganancias de precisión del 1–2% importanTu grupo de demostración es demasiado pequeño (< 10 ejemplos) — la puntuación será ruidosa
Quieres auditar la instrucción mejor encontrada por seguridad antes del despliegueLa tarea requiere creatividad o juicio subjetivo donde una sola métrica es engañosa
Estás usando DSPy o un framework similar donde la optimización de prompts está integradaEl ajuste fino ya está planeado — la APE optimiza prompts, no pesos

Ejemplos de código

Bucle APE básico con OpenAI

# Minimal APE implementation: generate instructions, score, return best
# pip install openai

import os
import re
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# ----- Task definition --------------------------------------------------------
# Demonstrations: pairs of (input, expected_output)
DEMOS = [
("The movie was absolutely fantastic, I loved every minute.", "positive"),
("Terrible film, waste of time and money.", "negative"),
("It was okay, nothing special but not bad either.", "neutral"),
("A masterpiece of modern cinema.", "positive"),
("I walked out after 20 minutes.", "negative"),
]

# Held-out evaluation set for scoring
EVAL_SET = [
("A stunning visual experience with weak writing.", "positive"), # debatable but positive
("Boring, predictable, and too long.", "negative"),
("I enjoyed it more than I expected.", "positive"),
("Neither good nor bad — forgettable.", "neutral"),
("One of the best films of the decade.", "positive"),
]


# ----- Step 1: Generate candidate instructions --------------------------------
def generate_instructions(demos: list[tuple[str, str]], n: int = 10) -> list[str]:
"""Ask a meta-LLM to infer N candidate instructions from demo pairs."""
demo_text = "\n".join(f'Input: "{inp}"\nOutput: "{out}"' for inp, out in demos)
meta_prompt = (
f"Here are input-output example pairs for a text classification task:\n\n"
f"{demo_text}\n\n"
f"Generate {n} diverse natural-language instructions that, when prepended to "
f"an input text, would cause a language model to produce the correct output. "
f"Return one instruction per line, numbered."
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": meta_prompt}],
temperature=0.9,
max_tokens=800,
)
raw = resp.choices[0].message.content
lines = [re.sub(r"^\d+[\.\)]\s*", "", l).strip() for l in raw.splitlines()]
return [l for l in lines if len(l) > 20] # filter out empty / too-short lines


# ----- Step 2: Score an instruction on the eval set --------------------------
def score_instruction(instruction: str, eval_set: list[tuple[str, str]]) -> float:
"""Return accuracy of the instruction on the eval set."""
correct = 0
for text, expected in eval_set:
prompt = f"{instruction}\n\nText: {text}\nLabel:"
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=5,
)
prediction = resp.choices[0].message.content.strip().lower()
if expected.lower() in prediction:
correct += 1
return correct / len(eval_set)


# ----- Step 3: Iterative refinement of top-K instructions --------------------
def refine_instructions(top_instructions: list[str], n_variants: int = 5) -> list[str]:
"""Ask the meta-LLM to paraphrase the top instructions to get variants."""
instr_text = "\n".join(f"- {i}" for i in top_instructions)
refine_prompt = (
f"Here are high-performing instructions for a sentiment classification task:\n"
f"{instr_text}\n\n"
f"Generate {n_variants} new instructions that paraphrase or combine the above "
f"to potentially improve performance. Return one per line."
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": refine_prompt}],
temperature=0.7,
max_tokens=500,
)
raw = resp.choices[0].message.content
lines = [l.strip().lstrip("- ") for l in raw.splitlines()]
return [l for l in lines if len(l) > 20]


# ----- APE main loop ---------------------------------------------------------
def run_ape(
demos: list[tuple[str, str]],
eval_set: list[tuple[str, str]],
n_candidates: int = 10,
top_k: int = 3,
n_refinement_rounds: int = 1,
) -> dict:
print("=== APE: Generating initial candidates ===")
candidates = generate_instructions(demos, n=n_candidates)
print(f"Generated {len(candidates)} candidates.\n")

all_scored: list[tuple[str, float]] = []

for round_num in range(n_refinement_rounds + 1):
print(f"--- Round {round_num + 1}: Scoring {len(candidates)} instructions ---")
round_scores = []
for instr in candidates:
score = score_instruction(instr, eval_set)
round_scores.append((instr, score))
print(f" [{score:.0%}] {instr[:80]}{'...' if len(instr) > 80 else ''}")
all_scored.extend(round_scores)

if round_num < n_refinement_rounds:
top = [i for i, _ in sorted(round_scores, key=lambda x: -x[1])[:top_k]]
candidates = refine_instructions(top, n_variants=n_candidates // 2)
print()

best_instr, best_score = max(all_scored, key=lambda x: x[1])
return {"instruction": best_instr, "score": best_score, "all_scored": all_scored}


if __name__ == "__main__":
result = run_ape(DEMOS, EVAL_SET, n_candidates=8, top_k=3, n_refinement_rounds=1)
print(f"\n=== Best instruction (accuracy {result['score']:.0%}) ===")
print(result["instruction"])

Uso de DSPy para APE estructurada

# DSPy provides a higher-level abstraction for automatic prompt optimization.
# pip install dspy-ai

import dspy

# Configure DSPy with your LLM backend
lm = dspy.LM("openai/gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"])
dspy.configure(lm=lm)


# Define the task as a DSPy signature
class SentimentClassifier(dspy.Signature):
"""Classify the sentiment of a movie review as positive, negative, or neutral."""
review: str = dspy.InputField(desc="A movie review text")
sentiment: str = dspy.OutputField(desc="One of: positive, negative, neutral")


# Wrap in a module
class SentimentModule(dspy.Module):
def __init__(self):
self.classify = dspy.Predict(SentimentClassifier)

def forward(self, review: str) -> dspy.Prediction:
return self.classify(review=review)


# Training examples
trainset = [
dspy.Example(review=inp, sentiment=out).with_inputs("review")
for inp, out in [
("Absolutely loved it!", "positive"),
("Worst movie ever.", "negative"),
("It was fine, nothing memorable.", "neutral"),
]
]


# Use MIPROv2 optimizer to automatically engineer the prompt
def optimize_with_dspy():
module = SentimentModule()
optimizer = dspy.MIPROv2(metric=dspy.evaluate.answer_exact_match, auto="light")
optimized = optimizer.compile(module, trainset=trainset)
print(optimized.classify.extended_signature) # shows the optimized instruction
return optimized


if __name__ == "__main__":
optimized_module = optimize_with_dspy()
result = optimized_module(review="A surprisingly moving and well-acted drama.")
print(result.sentiment)

Recursos prácticos

Ver también