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

> 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.

- Capítulo: 0.4
- Dificultad: básico
- Fuente: https://book-ai-engineer.lucenyo.dev/es/lessons/fundamentos/patron-evaluador-optimizador-python/

---
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.

```mermaid
sequenceDiagram
    participant M as mejorar_con_critica()
    participant Cr as criticar()
    participant Re as reescribir()

    loop ronda en 1..max_rondas
        M->>Cr: criticar(texto, rubrica)
        Cr-->>M: Critica(nota, aprobado, problemas)
        alt aprobado y nota >= nota_minima
            M->>M: return texto (aprobado=True)
        else no aprobado
            M->>Re: reescribir(texto, problemas)
            Re-->>M: texto corregido
        end
    end
```

---

## 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](/es/lessons/preparacion/preparar-el-proyecto/) y luego instala lo que usa este capítulo:

```bash
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.

```python
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

```python
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

```python
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

Sin un número máximo de rondas, una rúbrica ambigua o un crítico demasiado exigente pueden generar un bucle que no termina nunca, y cada ronda es una llamada de pago a un LLM. `max_rondas` no es un detalle de implementación: es lo que mantiene el coste bajo control.

```python
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

```python
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.

= nota_minima antes de terminar?'
  opciones={[
    'Son la misma condición escrita dos veces por seguridad.',
    'Para cubrir el caso de que el crítico apruebe el texto pero con una nota mediocre, que no debería considerarse un resultado final válido.',
    'Porque Pydantic exige comprobar ambos campos siempre.',
    'Para poder calcular cuántas rondas han pasado.',
  ]}
  correcta={1}
  explicacion="aprobado y nota son campos independientes de la crítica. Comprobar ambos evita dar por bueno un texto que el crítico aprobó de forma laxa pero con una nota por debajo del umbral de calidad exigido."
  mostrarCabecera={false}
/>
