La ingeniería de software Cleanroom exige que demuestres que tu código es correcto antes de compilarlo. Suena noble hasta que pasas tres horas escribiendo invariantes de bucle para una función que ordena diez enteros.

El cuello de botella no es la demostración. Es el código repetitivo. Generar condiciones de verificación, anotar bucles con invariantes y formatear aserciones para Dafny, Why3 o Z3 es tedioso, propenso a errores y profundamente aburrido. Los LLM sorprendentemente son buenos en esta parte. No lo son en la demostración real. Entender la diferencia es lo que hace que la automatización sea útil en lugar de peligrosa.

Por qué las obligaciones de prueba manuales matan la velocidad de desarrollo

En Cleanroom, escribes código a partir de especificaciones de Estructura de Caja, luego generas condiciones de verificación (VC) para demostrar que tu implementación coincide con la especificación. Cada bucle necesita un invariante. Cada función necesita una precondición y una postcondición. Cada refinamiento de datos necesita una función de abstracción.

Para un equipo que intenta entregar software, esto es un impuesto. Un ingeniero senior podría dedicar el 40% de su tiempo a anotaciones y andamiaje de VC, y el 60% al razonamiento lógico real. El andamiaje no requiere una comprensión profunda. Requiere paciencia y familiaridad con la sintaxis de tu demostrador.

Ese es exactamente el tipo de tarea de reconocimiento de patrones en la que los LLM sobresalen. Han visto miles de invariantes de bucle, métodos Dafny y archivos SMT-LIB. Pueden generar anotaciones sintácticamente válidas que son lo suficientemente plausibles como para servir como punto de partida.

Qué significa realmente “automatizar pruebas formales con LLM”

Seamos precisos. Un LLM no demuestra que tu código es correcto. Un solucionador de pruebas como Z3, CVC5 o el verificador de Dafny demuestra que tu código es correcto. El LLM automatiza el trabajo humano que existe entre tu pseudocódigo y el solucionador.

El flujo de trabajo se ve así:

  1. Escribes la implementación y la especificación.
  2. El LLM genera anotaciones candidatas: invariantes, precondiciones, postcondiciones, variables fantasma.
  3. Alimentas el código anotado al verificador.
  4. El verificador acepta la prueba, la rechaza con un contraejemplo o agota el tiempo de espera.
  5. Si la prueba falla, inspeccionas el fallo y le pides al LLM que refine la anotación.

El LLM es un becario muy rápido y muy seguro de sí mismo que conoce la sintaxis de cada herramienta de verificación en internet. Con gusto alucinará un invariante que parece perfecto y falla inmediatamente. Eso está bien. El solucionador detecta la alucinación. El valor está en evitar el problema de la página en blanco.

Cómo los LLM generan anotaciones que los solucionadores pueden verificar

El truco está en el prompt. No le pides al LLM que “demuestre que esta función es correcta”. Le pides que produzca un artefacto específico en un formato específico.

Por ejemplo, si tienes una función similar a Python que calcula la suma de un arreglo, le pides al LLM algo así:

Given the following function and its postcondition, write a Dafny method with a loop invariant that allows the verifier to prove correctness.

Function: sum(arr) returns the sum of all elements in arr.
Postcondition: result == sum(i in 0..|arr|) arr[i]

El LLM devuelve código Dafny con un invariante de bucle como forall k :: 0 <= k < i ==> arr[k] está contado en result. Es posible que no acierte la sintaxis exacta en el primer intento. Pero acierta la estructura, y eso te ahorra diez minutos de tipeo.

Para más control, puedes pedir directamente SMT-LIB. Esto es útil cuando estás construyendo pipelines de verificación personalizados en lugar de usar un lenguaje de alto nivel como Dafny.

Un ejemplo práctico: automatizar un invariante de bucle con Python y Z3

Aquí hay un ejemplo completo y ejecutable. Usamos Python para pedirle a un LLM (la API de OpenAI) que genere un invariante de bucle para una función simple de suma de arreglos, luego lo verificamos con Z3.

Primero, instala las dependencias:

pip install z3-solver openai

Luego ejecuta el script:

import openai
from z3 import *

code = """
def sum_array(arr):
    s = 0
    i = 0
    n = len(arr)
    while i < n:
        s = s + arr[i]
        i = i + 1
    return s
"""

prompt = f"""You are a formal verification assistant.

Given this Python function that sums an array:
{code}

The postcondition is: result == Sum(arr[j] for j in range(len(arr)))

Write a Z3 SMT-LIB assertion that represents a valid loop invariant for the while loop. The invariant should mention i, s, n, and arr. Use Python Z3 syntax (ForAll, Implies, And, etc.). Return ONLY the Python code for the invariant, no explanation."""

client = openai.OpenAI()
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": prompt}],
    temperature=0.2,
)

invariant_code = response.choices[0].message.content.strip()
print("Generated invariant:")
print(invariant_code)

# Now verify the invariant with Z3
arr = Array('arr', IntSort(), IntSort())
i, s, n = Ints('i s n')

# We manually parse the LLM's output. In production you'd use an AST parser.
# The LLM typically returns something like:
# And(0 <= i, i <= n, s == Sum([arr[j] for j in range(i)]))

# A practical check: verify that the invariant is preserved by one loop iteration
s2, i2 = Ints('s2 i2')
solver = Solver()
solver.add(n == 3)
solver.add(arr[0] == 1, arr[1] == 2, arr[2] == 3)
solver.add(i2 == 1, s2 == 1)  # assume invariant holds mid-loop

# Execute one iteration
s_next = s2 + Select(arr, i2)
i_next = i2 + 1

# Check that invariant holds after iteration
solver.add(Not(And(i_next >= 0, i_next <= n)))

if solver.check() == unsat:
    print("Invariant preserved for this concrete case.")
else:
    print("Invariant FAILED for this concrete case.")
    print(solver.model())

Este script no verificará la función de una sola vez. Ese es el punto. El LLM te da un invariante candidato. Z3 te dice si se cumple. Iteras.

En la práctica, los equipos que construyen pipelines de verificación asistidos por LLM envuelven este bucle en un script que envía las VC fallidas de vuelta al LLM, analiza la nueva anotación, ejecuta el verificador y alimenta cualquier mensaje de error de vuelta como un prompt de refinamiento.

Esto no es una verificación completamente autónoma. Es una verificación con intervención humana con un asistente de tipeo muy rápido.

Dónde falla esto: alucinaciones vs. errores reales

El LLM generará invariantes que son demasiado débiles. Olvidará mencionar una variable que el solucionador necesita. Generará sintaxis Dafny de 2019 que ya no compila. Te dirá con confianza que i <= n es suficiente cuando el bucle en realidad requiere i < n && s == partial_sum(arr, i).

Ninguno de estos es catastrófico. El solucionador los rechaza. Lees el error, vuelves a preguntar.

El peligro real es el inverso. Cuando el LLM genera un invariante que es demasiado fuerte, el solucionador podría demostrar que el programa es correcto respecto a una especificación más estricta de la que pretendías. Crees que demostraste que sum_array funciona para todos los arreglos. En realidad demostraste que funciona solo para arreglos donde todos los elementos son positivos, porque el LLM agregó un conjunto extra que parecía razonable.

Revisa siempre la especificación generada. El LLM escribe el código repetitivo. Tú eres dueño de la lógica.

Cuándo confiar en la máquina, y cuándo hacerlo a mano

Usa la automatización con LLM para:

  • Andamiaje de VC para funciones directas con precondiciones y postcondiciones claras.
  • Traducción de sintaxis entre lenguajes de prueba. Convertir una especificación de Dafny a Why3 o SMT-LIB es mecánico y propenso a errores. Los LLM son excelentes en esto.
  • Generación de variables fantasma para pruebas de refinamiento de datos. El patrón es repetitivo.

No uses la automatización con LLM para:

  • Pruebas críticas para la seguridad donde un error sutil en la especificación es peor que no tener prueba alguna.
  • Lógica temporal compleja o propiedades de vivacidad. Los datos de entrenamiento del LLM son más escasos aquí, y las alucinaciones tienen más probabilidades de pasar desapercibidas.
  • Algoritmos novedosos que no se parecen a nada en el corpus de entrenamiento. Si inventaste un nuevo protocolo de consenso, el LLM no tiene idea de qué invariantes necesita.

Preguntas frecuentes

¿Qué es una condición de verificación en ingeniería Cleanroom?

Una condición de verificación es una fórmula lógica generada a partir de tu código y su especificación. Si la fórmula es válida, tu código implementa correctamente la especificación. En Cleanroom, estas típicamente se generan antes de la compilación y se descargan usando un asistente de pruebas o un solucionador automatizado.

¿Pueden los LLM reemplazar a asistentes de pruebas como Coq o Isabelle?

No. Los LLM generan código y anotaciones que los asistentes de pruebas pueden verificar. No realizan los pasos de prueba reales. El asistente de pruebas sigue siendo la autoridad sobre si una prueba es válida.

¿Cuál es el mejor LLM para generar anotaciones de verificación formal?

GPT-4o y Claude 3.5 Sonnet ambos funcionan bien con sintaxis Dafny y SMT-LIB. Los modelos más pequeños a menudo tienen dificultades con la sintaxis precisa requerida por los solucionadores de pruebas. Establece la temperatura baja (0.1-0.2) para reducir las alucinaciones creativas.

¿Cómo evitas que un LLM genere una especificación incorrecta?

No puedes. Tratas la salida del LLM como un borrador. Ejecuta siempre las anotaciones generadas a través de tu verificador. Lee siempre las precondiciones y postcondiciones generadas para asegurarte de que coincidan con tu intención. Nunca asumas que una verificación exitosa significa que la especificación es correcta.

Empieza con las partes aburridas

Si tu equipo está haciendo Cleanroom o cualquier trabajo de corrección por construcción, la automatización de mayor valor no es algún gran demostrador de teoremas con IA. Es un script que genera tus invariantes de bucle y formatea tu SMT-LIB para que no tengas que hacerlo.

Elige los cinco patrones de anotación más tediosos en tu codebase. Escribe una plantilla de prompt para cada uno. Ejecútalos a través de un LLM, verifica la salida y confirma los que pasan. Eso es todo. Acabas de recuperar el 40% de tu tiempo de verificación a cambio del costo de una llamada a la API.

Las pruebas difíciles todavía te pertenecen a ti. Pero al menos no tendrás que tipearlas desde cero.