Optimizando un agente Terraform con DSPy: Construir el verificador antes que el optimizador
Los agentes de Terraform inventan esquemas. Construya un verificador determinista, establezca una línea de base para el agente y luego utilice DSPy y GEPA para optimizar su archivo de habilidades, obteniendo resultados precisos.
Fuentes: Your Terraform Agent Is Probably Wrong Half the Time
Un agente que escribe código de infraestructura probablemente haya creado un módulo como este en algún momento: parece completamente razonable, se lee como cualquier módulo que un revisor haya aprobado alguna vez, y luego falla en terraform validate por una razón que nadie podría detectar a simple vista.
También se mide esto. IaC-Eval es una prueba de rendimiento que incluye 458 escenarios de Terraform seleccionados cuidadosamente, y se dice que GPT-4 solo logra un 19.36% de éxitos@1 en ella, frente al 86.6% del mismo modelo en pruebas de rendimiento de Python más estrictas. Un estudio más reciente sobre la generación de IaC por agentes indica un 14.0% para Qwen2.5-Coder 7B, un 41.4% para GPT-4o y un 62.4% para GPT-5.4.
¿Por qué el código de infraestructura es mucho más difícil que Python? Una configuración correcta debe satisfacer cuatro requisitos al mismo tiempo: sintaxis válida, compatibilidad con el esquema de la versión del proveedor en uso, un grafo de dependencias coherente y cumplimiento con la política propia de la organización. Si falta uno de ellos, todo falla. Un código en Python que esté 90% correcto suele seguir funcionando, mientras que un código en Terraform que esté 90% correcto no se aplica.
La investigación también arroja resultados que hacen que las soluciones habituales parezcan dudosas. La inducción con pocos ejemplos, el razonamiento en cadena y la interacción multietapa solo ayudaron un poco. Pero aproximadamente 79% de los fallos en la aplicación de políticas se debieron a información faltante en la instrucción, y no a las capacidades del modelo. El modelo era lo suficientemente capaz; simplemente no se le habían proporcionado las informaciones adecuadas.
Eso es un problema de optimización. Por lo tanto, el plan aquí es tomar un agente Terraform funcional, crear la evaluación necesaria para saber si está mejorando, y luego ejecutar todo el flujo de trabajo de DSPy contra esa evaluación: programar, evaluar, optimizar. Todo es ejecutable. El proyecto complementario se divide en tres carpetas, una por etapa:
agent/ the deepagents Terraform agent, its tools, prompts, and skills
eval/ the verifier, 38 tasks with Rego policies, and the benchmark
optimizer/ the DSPy program, metric, and GEPA compile
El agente inicial
Mantenga esta sección breve, ya que no es la parte interesante y cualquier agente puede sustituirla. Este agente busca el esquema real, escribe los archivos que se van a verificar y los valida antes de responder.
Tres herramientas se encuentran en agent/tools.py. El agente lee el esquema real del proveedor antes de escribir. Los archivos que genera se formatean y se comprueban sintácticamente a medida que se crean. Luego valida toda la configuración y corrige cualquier problema que señale el compilador, y solo después responde.
Está basado en deepagents, el framework de LangChain para agentes con herramientas y habilidades, aunque el framework apenas tiene importancia. Lo que realmente cuenta es la estructura:
from deepagents import create_deep_agent
agent = create_deep_agent(
model="openrouter:openai/gpt-5.6-luna",
tools=[provider_schema, write_terraform, validate_config],
system_prompt=SYSTEM_PROMPT,
skills=["./agent/skills"], # SKILL.md — the thing we'll optimize
)
El directorio de habilidades merece una atención más detallada, ya que todo lo demás aquí lo optimiza. Una habilidad es un archivo en formato Markdown con instrucciones que un agente carga junto a su prompt del sistema, en un formato llamado Agent Skills que se está popularizando rápidamente: Claude, GitHub Copilot y deepagents leen todos el mismo esquema SKILL.md. Este formato separa claramente las responsabilidades. El prompt del sistema se encarga únicamente de aspectos básicos como cómo llamar a las herramientas o cómo formatear la salida, mientras que la habilidad alberga conocimientos específicos del dominio, como las convenciones de Terraform, los valores predeterminados de seguridad y las particularidades de los proveedores. Dado que el mismo archivo funciona en varios agentes, las habilidades también son portátiles.
La mayoría de las explicaciones sobre DSPy dirigen al optimizador hacia la entrada principal de un programa. La pregunta que se plantea aquí es un poco diferente: ¿puede optimizar una habilidad? Los equipos reales dedican la mayor parte de su ajuste manual a dicha habilidad, ya que contiene la experiencia necesaria y se modifica sin cesar durante las revisiones. En este proyecto está escrita a mano, se encuentra en git y pasa por revisión de código como cualquier otro archivo. Además, nunca se ha medido, y ese es el verdadero problema; DSPy está diseñado para abordarlo, porque cada edición en ese archivo modifica el comportamiento en producción sin realizar pruebas alguna.
Así es en realidad un fallo
Comencemos con un artefacto real obtenido antes de aplicar cualquier corrección. Fue generado durante la ejecución del benchmark de referencia, donde el agente no contaba con herramientas y escribía directamente el HCL en su respuesta. La solicitud pedía un bucket de estado que utilizara cifrado gestionado por el cliente:
encryption {
kms_key_name = var.encryption_key_name
}
lifecycle_rules {
...
Parece estar bien, pero al ejecutar el validador sobre él se obtiene un resultado diferente:
$ tofu validate
Error: Missing required argument
on main.tf line 18, in resource "google_storage_bucket" "terraform_state":
The argument "default_kms_key_name" is required, but no definition was found.
Error: Unsupported argument
on main.tf line 19, in resource "google_storage_bucket" "terraform_state":
An argument named "kms_key_name" is not expected here.
Error: Unsupported block type
on main.tf line 22, in resource "google_storage_bucket" "terraform_state":
Blocks of type "lifecycle_rules" are not expected here. Did you mean
"lifecycle_rule"?
Tres errores provinieron de un único recurso. El argumento requerido es default_kms_key_name, mientras que kms_key_name pertenece a otros recursos de Google, y por eso parece correcto. El nombre de un bloque también se había pluralizado convirtiéndolo en algo que no existe.
Cuando los fallos básicos se clasificaron por tipo, el 80% de los errores de validación correspondían a esquemas inventados: argumentos, bloques o atributos que parecen reales en HCL pero que no existen en el recurso en cuestión. El proveedor google cuenta con aproximadamente mil tipos de recursos que cambian con cada versión, por lo que el modelo recita de memoria una API versionada, y la memoria genera ficciones plausibles.
Las tres herramientas abordan directamente ese diagnóstico. Por sí solas sumaron 27.7 puntos (la tasa de aprobación aumentó del 41.7% al 69.4% con el conjunto descrito a continuación). Los errores de formato se redujeron a cero porque write_terraform ejecuta fmt en lugar de esperar que el modelo gestione el espacio en blanco. Los errores de esquema también se redujeron a cero, ya que el agente podía finalmente ver lo que ve el compilador. Lo que quedaba consistía casi en su totalidad en la intención, es decir, si el agente construía lo solicitado, y ahí es donde entra en juego DSPy.
Aquí está la misma tarea ejecutada por el agente equipado con herramientas tal como funciona hoy en día:
$ uv run python -m agent.run --task eval/tasks/backend-var-interpolation.json
task backend-var-interpolation (opentofu)
model openai/gpt-5.6-luna engine=deepagents
files main.tf, variables.tf
tools {'write_terraform': 2, 'validate_config': 1, 'validate_pass': 1}
elapsed 56602ms
Hubo dos escrituras y una validación, y el agente responde solo una vez que el compilador está satisfecho. El archivo de tarea y el objetivo opentofu provienen del conjunto de evaluación, que es hacia donde se dirigen las cosas a continuación.
Un evaluador antes que un framework
Juzgar si algo de esto es útil implica responder “¿es buena la salida?” con algo más concreto que una simple sensación. La mayoría de los campos utilizan un evaluador basado en LLM, que compara la opinión de un modelo con la de otro sin tener una verdad objetiva, y luego incluso el propio evaluador necesita ser verificado.
Terraform no tiene ese problema, porque la cadena de herramientas que ya se ejecuta en CI es el evaluador:
tofu fmt -checkdetecta problemas de formato.tofu validateencuentra errores de sintaxis, esquema y referencias.tflintseñala argumentos obsoletos y tipos de máquina inválidos.tofu testconmock_providerverifica la semántica sin necesidad de credenciales en la nube.conftestcon OPA Rego comprueba la intención, determinando si el agente construyó lo que se solicitó.
Esos cinco filtros deterministas están implementados en eval/verifier/run.py. No hay participación de ningún “juez”, la evaluación no cuesta nada en términos de llamadas a la API, y ningún paso puede generar un recurso sujeto a facturación, ya que el proyecto no contiene ninguna instrucción apply en ningún lugar, intencionadamente.
La mayoría de las personas omite el último filtro, aunque es el que más importancia tiene. validate indica que la configuración se compila correctamente. Rego verifica que el bucket realmente tenga la encriptación activada, lo cual es lo que se solicitó y lo que hace que sea correcto y no simplemente válido:
package main
import rego.v1
deny contains "bucket must use a customer-managed KMS key" if {
some name
bucket := input.resource.google_storage_bucket[name][_]
not bucket.encryption
}
Un equipo que ya aplica políticas a través de OPA probablemente tenga muchas políticas similares en Terraform Cloud o en su pipeline de CI, y estas pueden reutilizarse sin cambios. Se trata de un enfoque de tipo “shift-left” aplicado a los agentes. Terraform Cloud rechaza un plan defectuoso en el momento de su aplicación, pero para entonces el código ya existe, un revisor lo ha leído y se ha programado su ejecución. Al ejecutar el mismo Rego cuando se genera el código, se detectan las infracciones antes de que se invierta todo ese esfuerzo.
Pruebe al probador. Este es el paso que más vale replicar. Una política que aprueba todo es peor que ninguna, ya que otorga a cada versión del agente la misma puntuación e informa silenciosamente que no hay cambios. Por lo tanto, cada política en eval/tasks/policy/ viene acompañada de un caso de prueba que cumple con los requisitos y otro que los incumple, y un script verifica ambos escenarios:
$ cd eval/tasks && ./check_policies.sh
...
✓ vpc-subnet-firewall good=0 bad=2
✅ 38/38 policies verified in both directions
Se redactaron treinta y ocho tareas de esta manera. Abordan los valores predeterminados de seguridad, el cableado entre recursos, la amplitud en los esquemas de los proveedores, la orientación a dialectos específicos, las restricciones (¿el agente agrega elementos que nadie quería?) y la estructura de los archivos. Cada una combina una instrucción con una política Rego que captura su intención. Los dialectos reciben un trato especial: cada tarea indica si está dirigida a terraform o a opentofu, ya que ambos han ido evolucionando de forma independiente (OpenTofu ofrece cifrado de estado y la función for_each de los proveedores, mientras que Terraform ofrece Stacks); por lo tanto, un agente que se dirija silenciosamente al dialecto incorrecto fallará en la validación.
El diagrama de las cinco puertas también muestra un límite de medición que el agente no puede cruzar, y es fácil exagerar el significado de eso. En producción, el agente puede, y probablemente debería, contar con una herramienta OPA que verifique las políticas antes de responder. Ese es el mismo argumento de desplazamiento hacia la izquierda, lo que permite entregar código de mejor calidad.
El límite tiene un propósito diferente. Aquí esas mismas políticas sirven como criterio para comparar habilidades, modelos y ejecuciones del optimizador. Si el agente pudiera iterar contra la misma política que evalúa el experimento, todas las variantes tenderían a superar la evaluación y la comparación no diría nada. Este es el principio del conjunto de pruebas reservado, aplicado a las herramientas. La regla es que el agente puede ver cualquier tipo de verificación, excepto aquella que está evaluando actualmente el experimento. Si el agente de producción se lanzará con la herramienta de políticas, también se debe evaluar esa configuración, calificándola con políticas reservadas o contando las iteraciones de reparación necesarias en lugar de determinar si supera la evaluación.
Adoptar DSPy
DSPy se describe a sí mismo como un marco para programar modelos de lenguaje en lugar de darles instrucciones. Su documentación organiza el flujo de trabajo en tres fases: programación, evaluación y optimización, y el orden en sí mismo constituye la lección: el optimizador es lo último en ser modificado, y todo lo anterior es lo que hace que el optimizador sea fiable. El optimizador consume exactamente tres elementos: un programa, una métrica y datos.
Toda la funcionalidad de DSPy se encuentra en optimizer/, y su configuración depende de uv:
uv add dspy
uv sync
Fase 1: programación
Una firmas es un contrato tipado para una llamada de LM, compuesto por entradas, salidas y una documentación que describe la tarea. Esta se encuentra en optimizer/harness.py:
import dspy
class AuthorConfiguration(dspy.Signature):
"""Write a complete, valid Infrastructure-as-Code configuration."""
request: str = dspy.InputField(
desc="What the user wants built, in natural language."
)
target: str = dspy.InputField(
desc="Which dialect to target: 'terraform' or 'opentofu'. These have "
"diverged — code targeting the wrong one will fail validation."
)
config: str = dspy.OutputField(
desc="The complete configuration as fenced HCL blocks. Start each block "
"with a comment naming its file, e.g. '# main.tf'."
)
Las omisiones son precisamente el punto clave. No hay frases como “eres un ingeniero experto en Terraform”, ni ejemplos prácticos, ni súplicas relacionadas con el formato. Los nombres y descripciones de los campos transmiten el significado, y el optimizador se encarga de la redacción.
Cualquiera que considere esto mágico puede disipar esa ilusión con una sola línea:
dspy.inspect_history(n=1)
Imprime el comando real que el adaptador construyó a partir de la firma. La salida real de este es la siguiente:
System message:
Your input fields are:
1. `request` (str): What the user wants built, in natural language.
2. `target` (str): Which dialect to target: 'terraform' or 'opentofu'. ...
Your output fields are:
1. `config` (str): The complete configuration as fenced HCL blocks. ...
[[ ## request ## ]]
{request}
[[ ## target ## ]]
{target}
[[ ## config ## ]]
{config}
In adhering to this structure, your objective is:
Write a complete, valid Infrastructure-as-Code configuration ...
Nada está oculto. Se trata de una plantilla estructurada generada a partir de la firma, y esa es precisamente la razón por la cual un optimizador puede reescribirla sin riesgos, y también por qué debe ser el primer lugar al que recurrirse cuando el comportamiento es extraño.
Un módulo aplica una técnica a una firma, y ambos varían de forma independiente. Una firma puede ejecutarse como un único proceso, con razonamiento primero, o como el motor de un bucle de herramientas:
generate = dspy.Predict(AuthorConfiguration) # one shot
generate = dspy.ChainOfThought(AuthorConfiguration) # reason first
generate = dspy.ReAct(AuthorConfiguration, tools=[...]) # run a tool loop
Este agente utiliza herramientas, por lo que se replica con dspy.ReAct y se le proporcionan exactamente las funciones de las herramientas que llama el agente desplegado, importadas directamente del paquete del agente. El código se encuentra en optimizer/harness_deepagents.py:
from agent.tools import make_tools # the deployed agent's tools
class TerraformAuthoringAgent(dspy.Module):
def __init__(self, seed_instruction: str):
super().__init__()
# Seed the SIGNATURE before constructing ReAct, so DSPy appends its
# tool protocol to your instruction instead of replacing it.
seeded = AuthorConfiguration.with_instructions(seed_instruction)
lc_tools = make_tools(work_dir, binary="terraform", stats={})
self.react = dspy.ReAct(
seeded,
tools=[dspy.Tool(t.func, name=t.name, desc=t.description)
for t in lc_tools.values()],
max_iters=10,
)
def forward(self, request: str, target: str = "terraform"):
return self.react(request=request, target=target)
La decisión de diseño más importante en esta configuración es la siguiente: optimizar el mismo programa que se despliega. Un experimento inicial optimizó un generador de uso único y luego envió el resultado a un agente que utilizaba herramientas, lo que hizo que todos los números perdieran sentido, ya que las instrucciones estaban ajustadas para situaciones de fallo que el agente real ya no producía.
La instrucción inicial es el archivo SKILL.md existente, por lo que no hay un nuevo inicio, y la verdadera pregunta es si el texto escrito a mano puede mejorarse. DSPy no sabe que la cadena de texto representa una habilidad en lugar de una instrucción principal, ya que solo ve el texto de instrucción de la firma. Esto es lo que hace posible que un experimento con habilidades funcione: el resultado se guarda directamente en formato SKILL.md y funciona para cualquier agente que lea habilidades.
Phase 2: evaluación
El tipo de dato en DSPy es dspy.Example, un diccionario que sabe qué campos son entradas. El cargador en optimizer/dataset.py convierte directamente el conjunto de datos:
dataset = [
dspy.Example(
task_id=t["id"],
request=t["prompt"],
target=t["target"],
rego=t["rego"], # path to this task's policy
).with_inputs("request", "target")
for t in tasks
]
Los campos que no están marcados como entradas se transmiten como metadatos. La métrica utiliza rego para encontrar la política correspondiente, y el modelo nunca la ve.
Valdría la pena explicar cómo se seleccionan los ejemplos, ya que el conjunto de datos determina todo lo que puede aprender el optimizador. No se obtuvieron de ninguna prueba de rendimiento; imitan el formato de los tickets reales, como por ejemplo una solicitud para un bucket de estado en GCS encriptado con una clave gestionada por el cliente, o una cuenta de servicio autorizada para acceder a un bucket y nada más. La cobertura abarca entonces los recursos principales que maneja un equipo de plataforma: almacenamiento, redes, IAM, SQL y Pub/Sub, además de familias de tareas dirigidas a los modos de fallo ya observados: recursos con esquemas complejos en los que los modelos inventan argumentos, tareas de restricción que verifican que el agente no añada recursos que nadie solicitó, y las tareas de dialecto mencionadas anteriormente. Algunas son deliberadamente difíciles, ya que un conjunto que supere todo desde el primer día no podría demostrar mejoras.
Treinta y ocho ejemplos suena poco, y lo es. El objetivo es cubrir cada ejemplo, ya que cada uno tiene su propia política, además de realizar divisiones honestas. Realice la división antes de optimizar y mantenga el conjunto de prueba como algo sagrado: el conjunto de entrenamiento es de lo que aprende el optimizador, la validación es cómo elige a los candidatos, y el conjunto de prueba se evalúa una sola vez, al final. La mayoría de los enfoques de ingeniería de prompts omiten esta disciplina, y al hacerlo, la gente se convence a sí misma de que un cambio fue útil cuando en realidad no lo fue. Esto ocurrió dos veces en este proyecto, y en ambas ocasiones una victoria con una pequeña muestra de validación se invirtió una vez que la muestra creció.
La medida es el lugar donde interviene el verificador, y se trata de una función que realiza tres tareas (optimizer/metric.py):
def verify_metric(example, prediction, trace=None, pred_name=None, pred_trace=None):
"""Score with the SAME verifier that produces our benchmark numbers."""
result = verify(
config_dir=materialise(prediction.config),
policy_dir=example.rego,
target=example.target,
)
# Job 1 — bootstrapping (trace is set): a strict bool. Only outputs that
# FULLY pass may become worked examples.
if trace is not None:
return result.passed
# Job 2 — reflective optimization (pred_name is set): score AND feedback.
# GEPA reads the text to understand *why* a candidate failed.
if pred_name is not None:
return dspy.Prediction(score=result.score, feedback=format_failures(result))
# Job 3 — plain evaluation: a float.
return result.score
Manténgalo simple. Si la métrica vuelve a implementar alguna lógica de puntuación, el número que se está optimizando y el número que se publica se separan, y entonces no se está midiendo nada.
La rama de retroalimentación es donde este área destaca. Los optimizadores reflexivos toman texto que explica un fallo, y en este caso ese texto es libre y preciso: errores reales del compilador y denegaciones reales de políticas, como los mostrados anteriormente. Los campos que dependen de un juez basado en LLM pagan por cada evaluación y reciben una retroalimentación mucho peor, además de tener que validar también al juez.
Mida una línea de base antes que nada, utilizando dspy.Evaluate:
evaluate = dspy.Evaluate(devset=valset, metric=verify_metric,
num_threads=8, display_table=True)
evaluate(program)
La salida de una prueba de humo con 3 ejemplos se ve así, y el conjunto completo lleva más tiempo:
Average Metric: 0.59 / 3 (19.6%): 100%|██████████| 3/3 [01:16<00:00, 25.60s/it]
INFO dspy.evaluate.evaluate: Average Metric: 0.5882 / 3 (19.6%)
WARNING dspy.evaluate.evaluate: Skipping table display since `pandas` is not installed.
Se pueden extraer dos lecciones de ese fragmento. Cada “ejemplo” corresponde a un episodio completo del agente, no a una sola llamada a la API, por lo que hay que presupuestar en consecuencia. Además, display_table requiere silenciosamente pandas, así que instálalo para las tablas correspondientes a cada ejemplo.
Si hay un hábito que debe adoptarse de esto, es el de no optimizar nada sin contar con un punto de referencia. Esa cifra es lo único que da sentido a los resultados del optimizador.
Fase 3: optimización
Diferentes optimizadores ajustan cosas distintas:
BootstrapFewShotsintetiza ejemplos funcionales (demonstraciones).MIPROv2modifica las instrucciones y las demostraciones mediante búsqueda bayesiana.GEPAajusta las instrucciones analizando las huellas de los fallos.
El ajuste de instrucciones es la herramienta adecuada para el código sujeto a restricciones de esquema, ya que el estudio sobre IaC basado en agentes determinó que supera a la inyección de demostraciones, precisamente porque estas no solucionan la alucinación de esquema. Por lo tanto, GEPA es la opción preferida, según lo indicado en optimizer/compile.py:
gepa = dspy.GEPA(
metric=verify_metric,
max_metric_calls=150,
reflection_lm=dspy.LM("openrouter/openai/gpt-5.6-luna", max_tokens=16000),
num_threads=8,
track_stats=True,
)
compiled = gepa.compile(student=program, trainset=trainset, valset=valset)
compiled.save("compiled_state.json", save_program=False)
Los dos modelos realizan tareas diferentes. El modelo de tarea escribe el código en Terraform y es el que se envía; el modelo de reflexión analiza los fallos y elabora instrucciones preliminares. Se ejecuta unas 40 veces, frente a las aproximadamente 900 del modelo de tarea, por lo que invertir en un modelo más potente en este caso es viable.
Algunas observaciones prácticas obtenidas con esfuerzo:
- Establezca
num_threads. En una ejecución se olvidó establecerlo y la compilación tardó 69 minutos; las mediciones posteriores mostraron que la máquina estuvo inactiva el 94% del tiempo, esperando las llamadas a la API realizadas una tras otra. - “150 llamadas métricas” significa 150 episodios del agente, lo que, con un bucle de herramientas, equivale aproximadamente a 900 llamadas al modelo de lenguaje.
- Siempre llame a
save()después de compilar. Consave_program=Falseel resultado es JSON puro: el texto de las instrucciones optimizadas más cualquier demostración auxiliar. Ese mismo JSON permite prescindir por completo de DSPy, ya que unas veinte líneas (optimizer/to_skill_md.py) lo convierten nuevamente en el archivoSKILL.mdque consume el agente. En tiempo de ejecución, DSPy nunca se importa. El optimizador es solo un paso de compilación, y lo que produce llega en una solicitud de pull request como un diff que una persona puede leer y rechazar.
GEPA también detecta situaciones reales. En un modelo menos potente, el agente seguía abandonando su bucle de herramientas: una llamada a provider_schema y luego se detenía sin escribir nada. Nunca se le informó al agente al respecto. GEPA leyó las trazas de fallo y lo incluyó en la instrucción optimizada: no finalizar simplemente porque se haya escrito un archivo, ya que una escritura exitosa solo demuestra que el HCL se puede analizar, y es necesario utilizar validate_config para detectar errores en el esquema del proveedor y referencias inválidas. Se trata de una reflexión sobre las trazas reales de ejecución que genera indicaciones que hubieran requerido horas de depuración para formularse.
El flujo de trabajo sigue siendo eficaz tras la optimización
Otras dos partes de este mismo sistema reutilizan la métrica que ya existe.
La métrica también puede ejecutarse en el momento de la inferencia. BestOfN y Refine de DSPy envuelven cualquier módulo, lo ejecutan hasta N veces y devuelven el primer resultado cuya recompensa alcanza un umbral:
robust = dspy.Refine(module=program, N=3, reward_fn=verify_metric, threshold=1.0)
El patrón debería resultar familiar, ya que el bucle de validación y reparación del agente aplica la misma idea con el compilador como función de recompensa. La evidencia lo considera la herramienta más efectiva disponible: en el estudio de IaC, la reparación iterativa con retroalimentación del verificador elevó el rendimiento de Qwen 7B del 14,0 % al 62,9 %, mejor que cualquier técnica de inducción de respuestas probada. Sea cual sea la forma, se debe proporcionar al modelo su verificador en tiempo de ejecución.
Los cambios de modelo se convierten en una modificación de una sola línea, y ahora son medibles:
program.set_lm(dspy.LM("openrouter/qwen/qwen3-coder-30b-a3b-instruct"))
evaluate(program)
Esto fue más importante de lo esperado. Un cambio a mitad de proyecto hacia un modelo más económico redujo la calidad del 83,3 % al 37,5 %, y casi la mitad de las ejecuciones abandonaron el bucle de la herramienta sin generar ningún archivo. Nada en un flujo de trabajo manual revela esto, y la primera señal provendría de un cliente. Doce minutos de ejecución del conjunto de pruebas lo revelaron.
El resultado honesto
En este agente, la instrucción compilada no logró superar a la escrita a mano.
La comparación se realizó nueve veces con dos implementaciones de agentes y dos modelos, incluyendo un conjunto de pruebas aparte en el que ambos obtuvieron el mismo 92,9 %. Esa cifra corresponde al modelo más potente, mientras que los valores anteriores de 41,7 % a 69,4 % corresponden al modelo más económico en el mismo conjunto de pruebas. En dos ocasiones, un resultado pareció ser una clara victoria, se documentó, pero luego cambió al analizar una muestra más grande.
Esa es la lección más valiosa que se puede aprender, y eso no demuestra que DSPy fracase. Una razón específica y plausible podría explicar por qué no se aplicó. Las mejoras reportadas por GEPA provienen de flujos de trabajo modulares, donde cada paso del pipeline utiliza un prompt diferente, de modo que el optimizador puede atribuir el fallo a un módulo concreto y repararlo. Un agente, en cambio, utiliza el mismo prompt en cada paso. La crítica de Benjamin Anderson sostiene que esto elimina precisamente esa localidad de la que depende GEPA.
Las orientaciones prácticas son las siguientes:
- En un pipeline simple, es probable que las mejoras reportadas se mantengan.
- En un bucle de agentes, ejecute su propio conjunto de pruebas antes de asumir que también se aplican.
- En ambos casos, dirija los comentarios de verificación al propio agente; así cuenta con la mejor evidencia, tanto en trabajos publicados como en estas cifras.
Y esa prueba es posible porque el conjunto de pruebas existe.
Donde deja esto a las cosas
Los modelos de lenguaje encuentran difícil la infraestructura como código. El 19,36% obtenido por GPT-4 en IaC-Eval no es un problema que pueda resolverse con simples instrucciones o pensamientos optimistas, y los trucos habituales apenas sirven de ayuda. Aun así, la mayoría de los fallos en las políticas surgen por falta de información, no por falta de capacidad, por lo que aún existe margen de mejora, y este se encuentra en las instrucciones y las herramientas, no en los pesos del modelo.
El flujo de trabajo que ha demostrado su eficacia es el que enseña la propia documentación de DSPy, en el orden en que lo explica. Para la programación: firmas para el contrato, módulos que reflejen lo que se despliega, y inspect_history cuando haya dudas sobre lo que se está enviando. Para la evaluación: ejemplos con entradas explícitas, una métrica sencilla que también sirve como retroalimentación para el optimizador, un punto de referencia antes de cualquier cambio y un conjunto de pruebas que se revise una sola vez. Para la optimización, y solo entonces, se utiliza la misma métrica para guardar texto que pueda ser revisado en una solicitud de integración.
Aunque el optimizador no superó la eficacia de la solución manuscrita, la adopción de DSPy obligó a definir qué se considera “correcto” mediante código ejecutable. Ese enfoque siguió manteniéndose en uso. Detectó una regresión de 46 puntos tras el cambio de modelo, reveló que la solución se cargaba solo el 25 % de las veces (problema resuelto con una oración reescrita, lo que hizo que la tasa de carga alcanzara el 100 %), y en dos ocasiones impidió que un resultado defectuoso fuera registrado como victoria.
Nada de eso requiere necesariamente DSPy, y el verificador utilizado aquí no importa ninguna de sus funciones. Lo que sí aporta DSPy es obligar a que existan métricas reutilizables, de modo que una sola definición impulse el conjunto de pruebas de regresión, las comparaciones de modelos, los intentos de inferencia y el optimizador. Los equipos que ya cuentan con disciplina pueden no necesitar este framework, mientras que aquellos que no la tienen pueden adquirirla a partir de él. En cualquier caso, hay que dejar de publicar ediciones de prompts que no puedan medirse.
Leer más
- Controlar el comportamiento del agente Bedrock: etapas de prompts, analizadores e inferencia — Cómo sobrescribir los prompts de preprocesamiento, orquestación y posprocesamiento de Bedrock en Terraform, con un analizador Lambda, un contrato JSON y configuraciones de inferencia por etapa.
- Forma de la ontología vs tamaño del modelo: cuánto cuesta el diseño de un grafo a un agente de IA — Cómo tres formas sin pérdidas de un grafo de conocimiento de seguros cambiaron la precisión, las llamadas a herramientas, los tokens y el costo en modelos pequeños y grandes en Azure Cosmos DB.