Saltar al contenido principal

El patrón evaluador-optimizador en profundidad: un bucle de crítica real en Python

básico 8 min lectura
Implementa el patrón evaluador-optimizador en Python: un crítico con salida estructurada, un reescritor que corrige lo señalado y un límite de rondas.

En este capítulo cerramos el bloque de patrones implementando el evaluador-optimizador: nota, aprobación y problemas concretos como salida estructurada del crítico, y un límite de rondas que evita que el bucle se alargue indefinidamente. Al terminar este capítulo serás capaz de:

  1. Entender en qué consiste el patrón evaluador-optimizador y por qué conviene poner límite a las rondas.
  2. Forzar que el crítico devuelva una evaluación estructurada: nota, aprobado y una lista de problemas concretos.
  3. Escribir la función crítico y la función reescritor, cada una con un rol distinto sobre el mismo texto.
  4. Implementar el bucle completo, con un número máximo de rondas y una nota mínima para aprobar.
  5. Ejecutarlo sobre un texto real y ver cómo mejora ronda a ronda hasta cumplir la rúbrica.

El patrón evaluador-optimizador

Un agente crítico evalúa un texto contra una rúbrica y señala problemas concretos; un agente reescritor corrige únicamente esos problemas, conservando lo que ya funcionaba. El ciclo se repite hasta que el crítico aprueba o se alcanza un número máximo de rondas. Lo veremos con un ejemplo concreto: mejorar la descripción de una mochila para un e-commerce.

reescribir()criticar()mejorar_con_critica()reescribir()criticar()mejorar_con_critica()alt[aprobado y nota >= nota_minima][no aprobado]loop[ronda en 1..max_rondas]criticar(texto, rubrica)Critica(nota, aprobado, problemas)return texto (aprobado=True)reescribir(texto, problemas)texto corregido

Preparar el proyecto

Si vienes siguiendo los capítulos anteriores, el proyecto ya tiene lo necesario. Si empiezas directamente aquí, sigue primero Preparar el proyecto: herramientas y entorno y luego instala lo que usa este capítulo:

uv add openai pydantic

La crítica también necesita salida estructurada

El reescritor necesita saber exactamente qué corregir, no una opinión vaga. Por eso el crítico responde siempre con la misma forma: una nota, si aprueba o no, y una lista de problemas accionables.

import os

from openai import OpenAI
from pydantic import BaseModel, Field

DEFAULT_MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.1")


class Critica(BaseModel):
    nota: float = Field(ge=0, le=10)
    aprobado: bool
    problemas: list[str]

El crítico: evalúa contra una rúbrica

def criticar(client: OpenAI, texto: str, rubrica: list[str]) -> Critica:
    respuesta = client.responses.parse(
        model=DEFAULT_MODEL,
        instructions=(
            "Eres un editor muy exigente. Evalúa el texto SOLO contra "
            "la rúbrica y lista problemas concretos y accionables."
        ),
        input=f"Rúbrica: {rubrica}\n\nTexto:\n{texto}",
        text_format=Critica,
    )
    critica = respuesta.output_parsed
    if critica is None:
        raise ValueError("El modelo no devolvió una crítica válida")
    return critica

Fíjate en la instrucción “lista problemas concretos y accionables”: no basta con que el crítico diga que el texto “no es bueno”, porque eso no le da al reescritor nada sobre lo que actuar. Cada elemento de problemas tiene que ser algo que se pueda corregir directamente.


El reescritor: corrige solo lo señalado

def reescribir(client: OpenAI, texto: str, problemas: list[str]) -> str:
    respuesta = client.responses.create(
        model=DEFAULT_MODEL,
        instructions=(
            "Reescribe el texto corrigiendo ÚNICAMENTE los problemas "
            "indicados. Conserva todo lo que ya funciona."
        ),
        input=f"Problemas: {problemas}\n\nTexto:\n{texto}",
    )
    return respuesta.output_text

“Únicamente los problemas indicados” es la instrucción que evita que cada ronda sea una reescritura completa desde cero: el reescritor parte del texto anterior y solo toca lo que el crítico señaló.


El bucle: criticar → ¿aprobado? → reescribir → repetir

def mejorar_con_critica(
    borrador_inicial: str,
    rubrica: list[str],
    client: OpenAI | None = None,
    max_rondas: int = 3,
    nota_minima: float = 8,
) -> dict:
    client = client or OpenAI()
    texto = borrador_inicial
    criticas: list[Critica] = []

    for ronda in range(1, max_rondas + 1):
        print(f"Ronda {ronda}: el crítico evalúa el texto…")
        critica = criticar(client, texto, rubrica)
        criticas.append(critica)

        sello = "✅" if critica.aprobado and critica.nota >= nota_minima else "❌"
        print(f"   Nota: {critica.nota}/10 {sello}")
        for problema in critica.problemas:
            print(f"   · {problema}")

        if critica.aprobado and critica.nota >= nota_minima:
            return {"texto": texto, "rondas": ronda, "criticas": criticas, "aprobado": True}

        print("El reescritor corrige los problemas señalados…")
        texto = reescribir(client, texto, critica.problemas)
        print(f"   {texto}")

    return {"texto": texto, "rondas": max_rondas, "criticas": criticas, "aprobado": False}

Nota los dos criterios de parada: critica.aprobado (el crítico está satisfecho) y critica.nota >= nota_minima (por si acaso el crítico aprueba con una nota mediocre). Si se agotan las rondas sin cumplir ambos, la función devuelve igualmente la mejor versión disponible, con aprobado=False para que quien la use sepa que no llegó al listón.


Ejecutarlo

if __name__ == "__main__":
    resultado = mejorar_con_critica(
        "Esta mochila es buena y tiene cosas útiles para llevar cosas.",
        rubrica=[
            "menciona un beneficio concreto",
            "evita palabras vacías como 'cosas'",
            "máximo 30 palabras",
        ],
    )
    print(f"\nTexto final ({resultado['rondas']} rondas)")
    print(resultado["texto"])

Ejecútalo con uv run main.py: verás la nota y los problemas de cada ronda, la reescritura correspondiente y, al final, el texto que superó la rúbrica —o la mejor versión conseguida si se agotaron las rondas.


Qué te llevas de este ejemplo

  • El crítico y el reescritor tienen roles opuestos. Uno solo evalúa y señala problemas; el otro solo corrige lo señalado. Ninguno hace el trabajo del otro.
  • La rúbrica es un parámetro, no código. Puedes ajustarla —añadir o quitar reglas— sin tocar ni criticar ni reescribir.
  • max_rondas y nota_minima son decisiones de negocio. Cuánto estás dispuesto a gastar en rondas de corrección y qué nivel de calidad exiges antes de aprobar son ajustes, no reescrituras de código.
  • Con esto se completan los cuatro patrones del bloque: pipeline, enrutado, planificador-ejecutor y evaluador-optimizador, cada uno con una implementación real en Python y sin ningún framework de agentes de por medio.

Resumen

  • El patrón evaluador-optimizador es un bucle entre dos roles: un crítico que evalúa contra una rúbrica y señala problemas concretos, y un reescritor que corrige únicamente esos problemas.
  • La salida estructurada del crítico (nota, aprobado, problemas) es lo que le da al reescritor algo concreto sobre lo que actuar, en vez de una opinión vaga.
  • El bucle necesita dos condiciones de parada: aprobación del crítico y un límite máximo de rondas, para mantener el coste bajo control.
  • La rúbrica, el número de rondas y la nota mínima son parámetros ajustables, no partes fijas del código.

En el siguiente capítulo damos el salto a un framework de agentes concreto: instalarás Strands Agents y verás, con código real, cómo funciona el agent loop por dentro.


Autoevaluación

Antes de continuar, comprueba que los conceptos clave han quedado claros.

Comprueba tu comprensión

1. ¿Por qué el crítico debe devolver una lista de problemas concretos y no solo una nota?

2. ¿Por qué es importante que el reescritor corrija "únicamente" los problemas señalados, y no reescriba el texto entero?

3. ¿Por qué el bucle comprueba tanto critica.aprobado como critica.nota >= nota_minima antes de terminar?

4. ¿Qué ocurre si el bucle alcanza max_rondas sin que el crítico apruebe el texto?

agentes patrones evaluator-optimizer python openai structured-outputs