Lección 233 · 25 min · Gratis

Agentes autoevolutivos en la documentación sanitaria

Descripción general

Los sistemas de agentes a menudo alcanzan una meseta después de la prueba de concepto porque dependen de los humanos para diagnosticar casos extremos y corregir fallos. Este manual presenta un bucle de reentrenamiento repetible que captura esos problemas, aprende de la retroalimentación y promueve mejoras en los flujos de trabajo de producción. Basamos el enfoque en una tarea de documentación sanitaria regulada, pero los patrones se generalizan a cualquier dominio que exija precisión, auditabilidad e iteración rápida.

Lo que aprenderás

  • Diagnosticar por qué un agente autónomo no alcanza la preparación para la producción e instrumentarlo con señales de retroalimentación medibles.
  • Comparar tres estrategias de optimización de prompt —desde la iteración manual rápida hasta los bucles totalmente automatizados— y comprender cuándo usar cada una.
  • Ensamblar un flujo de trabajo de autorreparación que combine la revisión humana, las evaluaciones de LLM-as-judge y el refinamiento iterativo de prompt.

Para quién es este notebook

  • Ingenieros de ML/IA y arquitectos de soluciones que necesitan ir más allá de las demostraciones de juguete.
  • Equipos de producto y entrega que buscan artefactos ejecutables que puedan adaptar a herramientas internas o pipelines de producción.

Cómo trabajar con este notebook

  1. Comienza con la Sección 1 para comprender el caso de uso sanitario, el agente de referencia y la arquitectura del sistema.
  2. Usa la Sección 2 para practicar la optimización de prompt dentro de la interfaz de OpenAI Evals y recopilar retroalimentación estructurada.
  3. Ejecuta la Sección 3 para automatizar el bucle de optimización con calificadores, evaluaciones y lógica de reentrenamiento.
  4. Consulta el apéndice para ver prompts, configuraciones y plantillas de evaluación reutilizables a medida que adaptas el flujo de trabajo a tu entorno.

El notebook es modular; siéntete libre de ejecutar las secciones de forma independiente o secuencial a medida que adaptas el bucle de reentrenamiento a tus propios agentes.

1. Descripción general del caso de uso: Agentes autoevolutivos en la atención sanitaria

Definición del problema

Para este manual, nos centramos en un caso de uso del mundo real: la redacción de documentos regulatorios para empresas farmacéuticas. Estas organizaciones deben preparar y presentar una extensa documentación a las autoridades reguladoras (por ejemplo, la Administración de Alimentos y Medicamentos de EE. UU.) para obtener la aprobación de nuevos medicamentos. La precisión y la velocidad de estas presentaciones son fundamentales, ya que impactan directamente en la rapidez con la que los tratamientos que salvan vidas pueden llegar a los pacientes.

La redacción de documentos regulatorios es un proceso altamente complejo, iterativo y basado en la precisión que requiere una profunda experiencia científica, médica y de cumplimiento. A pesar de la disponibilidad de herramientas de autoría avanzadas, sigue siendo intensivo en mano de obra y propenso a errores humanos. Los sistemas de agentes ofrecen una ventaja sustancial al ayudar con la síntesis de investigación, la generación de contenido y la estructuración de documentos, pero aún se necesitan expertos humanos para garantizar la precisión de los hechos y el cumplimiento normativo.

El desafío clave es diseñar un bucle de retroalimentación que permita a estos sistemas de agentes aprender de forma iterativa y refinar el comportamiento del modelo con el tiempo. Un sistema así puede cambiar gradualmente el esfuerzo humano de la corrección detallada a la supervisión de alto nivel, mejorando la eficiencia y manteniendo los rigurosos estándares requeridos para las presentaciones regulatorias.

Agente autoevolutivo

El siguiente diagrama ilustra el proceso iterativo para mejorar continuamente un agente de IA a través de la retroalimentación, el meta prompting y la evaluación. El bucle combina el juicio humano o la retroalimentación automatizada utilizando un LLM-as-a-judge para mejorar iterativamente el rendimiento.

Self-evolving loop
Figura 1 - Diagrama que muestra el bucle autoevolutivo para la mejora automatizada de agentes.

El proceso consta de los siguientes pasos:

  1. Agente de referencia
    El proceso comienza con un agente de referencia. En este notebook, usamos un ejemplo deliberadamente simple (un agente que resume secciones de un documento) para ilustrar el bucle de mejora iterativa. En entornos empresariales o del mundo real, el agente de referencia podría ser mucho más complejo. Los resúmenes que produce sirven como punto de referencia inicial para la evaluación y el refinamiento posteriores.

  2. Retroalimentación humana (o LLM-as-a-judge)
    Las salidas del agente de referencia se evalúan luego por revisores humanos (por ejemplo, para entornos de producción) y/o por un sistema automatizado LLM-as-a-judge. Este paso recopila retroalimentación tanto cuantitativa como cualitativa que indica qué tan bien el agente cumple sus objetivos; por ejemplo, si estamos probando la longitud del resumen, la retroalimentación podría ser "el resumen es demasiado largo" o una puntuación numérica (generalmente entre 0 y 1) generada por eval al evaluar si el resumen tiene menos de 500 palabras.

  3. Evaluaciones y puntuación agregada
    Basándose en la retroalimentación recopilada, se generan nuevos prompts y se prueban a través de evaluaciones (Evals). Estas pruebas miden el rendimiento según criterios predefinidos, y los resultados se combinan en una puntuación agregada que refleja el rendimiento general. El bucle continúa hasta que la puntuación supera un umbral objetivo (por ejemplo, 0.8) o se alcanza el número máximo de reintentos (por ejemplo, max_retry = 10). Si se alcanza el límite de reintentos, se alerta a los ingenieros de que se requieren mejoras manuales.

  4. Agente de referencia actualizado
    Una vez que una versión mejorada logra el rendimiento objetivo, reemplaza al agente de referencia original. Este agente actualizado se convierte en la base para la siguiente iteración, apoyando un ciclo continuo de aprendizaje, retroalimentación y optimización.

Descripción general del conjunto de datos

El conjunto de datos utilizado para la evaluación comprende ~70 secciones extraídas de la Sección CMC de muestra para inyección de piruvato hiperpolarizado (13C), disponible públicamente aquí. Este conjunto de datos proporciona contenido realista y específico del dominio adecuado para probar tanto la capacidad de resumen científico como el comportamiento de cumplimiento normativo.

Descripción general del agente de referencia

Para mantener este manual autónomo y fácilmente reproducible, simplificamos el caso de uso de redacción regulatoria manteniendo su complejidad esencial. En producción, un agente de autoría regulatoria típico comprende múltiples subagentes especializados responsables de tareas como la redacción, el análisis de datos, la verificación de cumplimiento, la generación de citas y la verificación de hechos.

Para esta guía, reducimos el alcance del agente de autoría regulatoria para centrarnos en el aspecto de autorreparación del sistema. Nuestro agente de autoría regulatoria consta de dos subagentes:

  • Un resumidor que crea resúmenes científicos y concisos.
  • Un verificador de cumplimiento: que evalúa cada resumen según los requisitos regulatorios clave (por ejemplo, FDA 21 CFR Parte 11).
Baseline Agent
Figura 2 - El agente de referencia tal como se creó en la interfaz de usuario de AgentBuilder.

Para el resto de este manual, implementamos una versión simplificada del agente Summarizer (consulta la sección Configuración del agente a continuación). Alternativamente, puedes reutilizar el código del agente creado con AgentBuilder. Si deseas reproducir el agente directamente desde la interfaz de usuario de AgentBuilder, aquí están los prompts y parámetros clave utilizados:

  • Agente Summarizer: Este agente utilizó la herramienta de búsqueda de archivos, donde el PDF de CMC se cargó en el almacén de vectores.

Prompt: "Resume la sección {{workflow.input_as_text}} de {{state.cmc_pdf}} cargada en el almacén de vectores."

  • Agente Compliance Checker:

Prompt: "Verifica que el siguiente resumen cumpla con la FDA 21 CFR Parte 11: {{input.output_text}}. Si el resumen cumple, devuelve Compliant. De lo contrario, devuelve This section needs to be manually summarized."

Ambos agentes se configuraron con los parámetros predeterminados: usando GPT-5, bajo esfuerzo de razonamiento y texto como formato de salida.

Enfoque de evaluación

Para evaluar el agente de referencia, existen dos enfoques principales:

  1. Recopilación de retroalimentación humana. Este enfoque implica recopilar retroalimentación de usuarios humanos a través de la plataforma OpenAI Evals (o una interfaz de usuario personalizada creada para una aplicación específica). Es más adecuado para entornos de producción o cuando se pilota una herramienta donde los expertos en la materia (SME) interactúan con la herramienta en escenarios del mundo real. Este método ayuda a descubrir casos extremos que pueden no haber sido identificados durante el desarrollo. En la plataforma Evals, los usuarios pueden proporcionar calificaciones de "pulgar hacia arriba" o "pulgar hacia abajo" y compartir retroalimentación cualitativa sobre los resúmenes.

  2. Uso de un LLM-as-a-Judge. Esta opción se usa típicamente durante la fase de desarrollo, lo que permite bucles de retroalimentación rápidos sin requerir el tiempo de los SME. Un LLM-as-a-judge usa un LLM para evaluar y calificar automáticamente las salidas del agente basándose en criterios predefinidos. También se puede usar para monitorear la deriva del modelo (por ejemplo, en producción) o validar cambios entre versiones del modelo (por ejemplo, cambiar entre gpt-5 y gpt-5-mini).

Este manual demuestra ambos enfoques:

  • La Sección 2 muestra el enfoque de la interfaz de usuario de la plataforma para la optimización manual de prompts.
  • La Sección 3 implementa el enfoque de API totalmente automatizado usando LLM-as-a-judge.

Nota: La plataforma Evals aún no proporciona una API para recuperar la retroalimentación del usuario programáticamente.

2. Uso de la plataforma OpenAI Evals

La plataforma OpenAI Evals proporciona una interfaz intuitiva para la optimización y evaluación de prompts. Esta sección demuestra el flujo de trabajo completo, desde la carga del conjunto de datos hasta la mejora iterativa de prompts, mostrando cómo puedes aprovechar la interfaz visual de la plataforma para optimizar tus prompts antes de implementar soluciones automatizadas.

Paso 1: Cargar conjunto de datos

Para comenzar a usar la plataforma de evaluación de OpenAI, primero deberás cargar tu conjunto de datos:

  1. Haz clic en el botón + Crear
  2. Define el nombre del conjunto de datos
  3. Carga un archivo CSV y selecciona las columnas a mantener
  4. Cargar

Tu conjunto de datos debe contener los documentos o secciones de documentos que deben resumirse. Cada fila representa una entrada que será procesada por tu sistema.

Paso 2: Explora tus datos

Una vez cargado, puedes explorar tu conjunto de datos. Haz clic en el nombre del conjunto de datos para explorar los datos cargados. Esto te permite verificar que tus datos estén formateados correctamente y contengan el contenido esperado antes de continuar con la configuración del prompt.

Paso 3: Configurar el prompt inicial

Aquí es donde defines tu prompt de sistema inicial y configuras cómo fluyen los datos a través de tu modelo.

Platform Prompt Configuration
Figura 3 - La interfaz "Nuevo prompt" de la plataforma que muestra la configuración del modelo, las variables y los ajustes del mensaje del sistema.

Pasos de configuración

  1. Prompt del sistema: Agrega el mensaje del sistema que define la tarea y el comportamiento del modelo (este prompt se optimizará).
  2. Plantilla de prompt del usuario: Agrega la plantilla de mensaje de prompt para los mensajes del usuario, usando variables como {{<column_name>}} que se reemplazan con datos reales de tu conjunto de datos.
  3. Selección del modelo: Elige el modelo para la generación (por ejemplo, gpt-4.1, gpt-5).
  4. Temperatura: Configura la creatividad frente al determinismo.

Puedes comenzar con un prompt muy simple para demostrar el poder del proceso de optimización. Por ejemplo, comenzar con solo "resumir" muestra cómo el sistema puede evolucionar desde un punto de partida mínimo.

Paso 4: Generar salidas

Una vez que tu prompt esté configurado, estás listo para generar salidas en tu conjunto de datos. El prompt se ejecutará una vez por fila y la salida se generará en una nueva columna de salida.

  1. Haz clic en "Generar salida"
  2. La plataforma ejecuta tu prompt contra todas las muestras
  3. Los resultados aparecen en una nueva columna de Salida

La plataforma procesará cada fila de tu conjunto de datos, reemplazando las variables de la plantilla con valores reales y llamando al modelo con tu prompt del sistema. Esto crea una línea base de salidas que puedes evaluar.

Paso 5: Revisar y evaluar

La evaluación es donde proporcionas retroalimentación estructurada para guiar la mejora del prompt.

Revisar salidas

  1. Agrega columnas de evaluación si no se agregaron automáticamente - Haz clic en "Columnas" → "Anotaciones" → "Agregar":

    • Calificación - Calificaciones binarias (bueno/malo) o numéricas.
    • Retroalimentación - Texto que describe lo que necesita mejorar.
  2. Proporciona calificación y retroalimentación - Agrega tu evaluación para cada salida.

    Dependiendo de la calidad de la salida, puedes seleccionar una calificación buena o mala y explicar tu puntuación basándote en cómo te gustaría que se mejorara la respuesta. Por ejemplo:

    (Calificación) | Retroalimentación

    • (Bueno) Bueno, pero solo se debe proporcionar la respuesta. La salida no debe incluir encabezados ni ningún texto que no sea la respuesta.
    • (Malo) La información es buena, pero debe presentarse como viñetas.
    • (Bueno) Buen resumen; es claro.
    • (Malo) Usa viñetas al responder para mejorar la legibilidad. Resume cada subsección individualmente.
  3. Guardar anotaciones - Tu retroalimentación se guarda con la ejecución de la evaluación.

Platform Evaluation Interface
Figura 4 - La interfaz de evaluación que muestra las salidas generadas con columnas de calificación y retroalimentación para la anotación.

Esta retroalimentación estructurada se convierte en la base para la optimización automática de prompts.

Paso 6: Optimizar prompt

Después de recopilar retroalimentación, la plataforma puede generar automáticamente un prompt mejorado.

  1. Haz clic en "Optimizar"
  2. Se genera una nueva versión del prompt en una nueva pestaña
  3. Haz clic en "Ver prompt" para ver la versión mejorada
Platform Optimized Prompt
Figura 5 - El prompt mejorado generado por la plataforma, que muestra instrucciones y requisitos detallados.

Paso 7: Iterar y comparar

Con tu prompt mejorado listo, inicia una nueva iteración para medir la mejora.

  1. Haz clic en "Generar salida"
  2. Revisa los nuevos resultados y proporciona retroalimentación sobre cualquier problema restante
  3. Haz clic en "Optimizar" de nuevo si es necesario
  4. Repite hasta que estés satisfecho

La estructura de pestañas de la plataforma te permite comparar el rendimiento entre iteraciones. Puedes ver fácilmente cómo evolucionaron las salidas desde tu prompt inicial hasta las versiones optimizadas.

Platform Updated Prompt Feedback
Figura 6 - Resultados de retroalimentación y evaluación para el prompt optimizado, mostrando mejoras en la calidad de la salida.

Cuándo detener la iteración

Continúa el ciclo de optimización hasta que:

  • Se alcance el umbral de calidad: >80% de las salidas reciben retroalimentación positiva.
  • Rendimientos decrecientes: Las nuevas iteraciones muestran una mejora mínima.
  • Problemas específicos resueltos: Se abordan todos los modos de fallo identificados.

Este enfoque basado en la plataforma proporciona una excelente base para comprender la optimización de prompts antes de pasar a implementaciones automatizadas. La interfaz visual facilita ver el impacto de los cambios y comprender el proceso de optimización.

3. Bucle autoevolutivo con LLM-as-a-Judge

Esta sección presenta un flujo de trabajo de evaluación totalmente automatizado utilizando un LLM-as-a-Judge a través de la API de OpenAI, eliminando la necesidad de cualquier interfaz de usuario. Este enfoque permite una evaluación escalable y programática del rendimiento del agente, lo que permite una rápida iteración y un monitoreo continuo del modelo en producción.

# gepa and litellm are only required for the Section 4.b (prompt optimization with GEPA)
%pip install --upgrade openai openai-agents pydantic pandas gepa litellm python-dotenv -qqq 
%load_ext dotenv
%dotenv

# Place your API key in a file called .env
# OPENAI_API_KEY=sk-...

Creación de evaluación

Para evaluar el agente de resumen de referencia, utilizamos cuatro calificadores complementarios que equilibran las comprobaciones deterministas con el juicio semántico.

Calificador Tipo Umbral de aprobación Qué verifica Por qué
Nombre de cadena química python 0.8 Si algún nombre químico exacto en la sección aparece en el resumen. Fuerza la preservación de entidades de dominio críticas para que los resúmenes no omitan términos químicamente significativos.
Longitud del resumen python 0.85 Desviación inversa de una longitud esperada de 100 palabras. Mantiene los resúmenes concisos y comparables, reduciendo la verbosidad que puede enmascarar contenido deficiente.
Similitud de coseno text_similarity 0.85 Similitud de coseno entre el texto de la sección y el resumen. Garantiza que el resumen permanezca anclado al contenido de origen en lugar de desviarse semánticamente.
LLM-as-a-judge score_model 0.85 Una puntuación basada en rúbricas de un modelo que actúa como evaluador. Captura señales de calidad matizadas que las métricas basadas en reglas omiten, mejorando la robustez general.

Notas

  • Los dos calificadores de Python detectan la fidelidad del dominio y la disciplina de longitud temprano, lo que estabiliza la optimización antes del ajuste semántico.
  • La similitud de texto protege contra el parafraseo superficial que se desvía de la fuente.
  • El juez LLM proporciona una protección holística cuando los casos extremos se escapan de las comprobaciones deterministas.
import os
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

data_source_config = {
    "type": "custom",
    "item_schema": {
        "type": "object",
        "properties": {"section": {"type": "string"}, "summary": {"type": "string"}},
        "required": ["section", "summary"],
    },
    "include_sample_schema": False,
}

testing_criteria = [
    {
        "type": "python",
        "name": "chemical_name_grader",
        "image_tag": "2025-05-08",
        "pass_threshold": 0.8,
        "source": r"""def grade(sample: dict, item: dict) -> float:
    section = item["section"]
    summary = item["summary"]
    CHEMICALS_MASTER = ["[1-¹³C]Pyruvic acid","[1-¹³C]Pyruvate","¹²C Pyruvic acid","Sodium [1-¹³C]pyruvate","Sodium pyruvate (¹²C)","AH111501 (Trityl radical)","Tris{8-carboxyl-2,2,6,6-tetra[2-(1-methoxyethyl)]-benzo(1,2-d:4,5-d’)bis(1,3)dithiole-4-yl}methyl acid","AH111501 sodium salt","Methyl, tris[8-carboxy-2,2,6,6-tetrakis(2-methoxyethyl)benzo[1,2-d:4,5-d’]bis[1,3]dithiol-4-yl]-, trisodium salt","AH111501 trisodium salt","AH111576","2,2′,2″,2‴-(4,8-Dibromobenzo[1,2-d:4,5-d′]bis([1,3]dithiole)-2,2,6,6-tetrayl)tetraethanol","AH111586","4,8-Dibromo-2,2,6,6-tetrakis(2-methoxyethyl)benzo[1,2-d:4,5-d′]bis([1,3]dithiole)","AH111709","AH111743","AH112615","4,4-Bis-hydroxymethyl-2-methyl-oxazolidine-2-carboxylic acid","AH112623","Parapyruvate","2-Hydroxy-2-methyl-4-oxo-pentanedioic acid","AH113127","(4-Hydroxymethyl-oxazolidin-4-yl)-methanol","AH113462/E","Enol lactone","AH113462/K","Keto lactone","Acetyl bromide","Methanol","Dimethyl sulfoxide","DMSO","Tetrahydrofuran","THF","Acetonitrile","ACN","Diethyl ether","Et₂O","N,N-Dimethylacetamide","DMA","1,3-Dimethyl-2-imidazolidinone","DMI","Hydrochloric acid","HCl","Sodium hydroxide","NaOH","Disodium ethylenediaminetetraacetate","Na₂EDTA","Ethylenediaminetetraacetic acid","EDTA","Tris(hydroxymethyl)aminomethane","TRIS","Trometamol","Trifluoroacetic acid","TFA","Toluene","Heptane","Ethyl acetate","Ethanol","Water","H₂O","Sodium chloride","NaCl","Cuprous [1-¹³C]cyanide","Cu¹³CN","Gadolinium","Gd","Tin","Sn","Phosphorus","P","Carbon dioxide","CO₂","Sodium [1-13C]pyruvate","[1-13C]Pyruvic acid","1-13C pyruvate"]

    # Identify the chemicals present in the section
    present = [chem for chem in CHEMICALS_MASTER if chem in section]

    # If no chemicals present, consider it satisfied
    if not present:
        return 1.0

    correct = 0
    for chem in present:
        # Only count as correct if the exact chemical string appears in the summary
        if chem in summary:
            correct += 1

    return correct / len(present)""",
    },
    {
    "type": "python",
    "name": "word_length_deviation_grader",
    "image_tag": "2025-05-08",
    "pass_threshold": 0.85,
    "source": r"""
def grade(sample: dict, item: dict) -> float:
    summary = item["summary"]
    word_count = len(summary.split())
    
    expected_summary_length = 100
    tolerance = 0.2  # 20% band around target
    
    # relative deviation
    deviation = abs(word_count - expected_summary_length) / expected_summary_length
    
    # If within tolerance band → full score
    if deviation <= tolerance:
        return 1.0
    
    # Outside band → score decays linearly, capped at 0
    # e.g., deviation 0.3 → score 0.8, deviation 1.0+ → 0.0
    score = 1.0 - (deviation - tolerance)
    return max(0.0, score)
""",
},
    {
        "name": "cosine_similarity",
        "type": "text_similarity",
        "input": "{{ item.summary }}",
        "reference": "{{ item.section }}",
        "evaluation_metric": "cosine",
        "pass_threshold": 0.85,
    },
    {
        "name": "llm_as_judge",
        "type": "score_model",
        "model": "gpt-4.1",
        "input": [
            {
                "role": "system",
                "content": (
                    "You are an expert technical summarization evaluator. "
                    "Evaluate whether the summary captures and preserves the important technical facts and specific details from the section, allowing for occasional minor rewording or omissions of less important points, but not major technical inaccuracies or information loss.\n\n"
                    "Scoring Guidelines:\n"
                    "- Return a numerical score between 0 and 1 (with up to two decimal places).\n"
                    "- A score of 1 means the summary is almost flawless: it is comprehensive, highly faithful, and technically accurate, with virtually no important or meaningful details missing, and no significant misstatements or distortions.\n"
                    "- 0.75-0.99 indicates excellent work: all main facts are represented, but there may be trivial omissions or very minor rewording that do not materially affect understanding.\n"
                    "- 0.5-0.75 indicates good but imperfect: most technical information is retained and correctly presented, some less critical details might be missing or slightly rephrased, but overall fidelity is preserved.\n"
                    "- 0.3-0.5 means significant information is missing, or some technical inaccuracies are present, but the summary retains a reasonable portion of key facts.\n"
                    "- 0.0-0.3 means there are major omissions, misunderstandings, or a failure to capture the most important technical content.\n\n"
                    "Respond only with a single number between 0 and 1 indicating summary quality by these criteria."
                ),
            },
            {
                "role": "user",
                "content": (
                    "Section:\n{{item.section}}\n"
                    "Summary:\n{{sample.output_text}}"
                ),
            },
        ],
        "range": [0, 1],
        "pass_threshold": 0.85,
    },
]

eval = client.evals.create(
    name="self_evolving_eval",
    data_source_config=data_source_config,
    testing_criteria=testing_criteria,
)
print(f"Created Eval: {eval.id}")

Deberías ver un ID de evaluación en la salida, por ejemplo, eval_.... Este es el ID de la evaluación que acabamos de crear (como se muestra a continuación).

Platform Eval Configuration
Figura 7 - La interfaz de evaluación de la plataforma que muestra la configuración de la fuente de datos y los ajustes de los criterios de prueba.

Puntuación y análisis del calificador

A continuación, tendremos que ejecutar las evaluaciones en la salida del agente de resumen y analizar los resultados para obtener las puntuaciones del calificador de la evaluación. Para ello, utilizaremos algunas funciones auxiliares:

  • run_eval: Ejecutor simple para llamar a la API de evaluaciones con el formato adecuado.
  • poll_eval_run: Una utilidad de sondeo para esperar a que se complete la ejecución de la evaluación programada.
  • parse_eval_run_output: Analiza la ejecución de la evaluación y devuelve una salida estructurada para el bucle de retroalimentación.
import time
import json

def run_eval(eval_id: str, section: str, summary: str):
  """Creates a run of the eval with the input section and output summary."""
  return client.evals.runs.create(
    eval_id=eval_id,
    name="self-evolving-eval",
    data_source={
      "type": "jsonl",
      "source": {
        "type": "file_content",
        "content": [
          {
            "item": {
              "section": section,
              "summary": summary,
            }
          }
        ],
      },
    },
  )


def poll_eval_run(eval_id: str, run_id: str, max_polls = 10):
    """
    Polls the evaluation run until completion or timeout.

    This function exists to handle asynchronous behavior in the eval service by
    periodically checking run status. It balances responsiveness and resource use by
    polling at fixed intervals rather than blocking indefinitely. The retry limit
    prevents runaway loops in cases where the service never returns a completed status.
    """
    run = None
    for attempt in range(1, max_polls + 1):
        run = client.evals.runs.retrieve(eval_id=eval_id, run_id=run_id)
        if run.status == "completed":
            break
        if attempt == max_polls:
            print("Exceeded retries, aborting")
            break

        time.sleep(5)

    run_output_items = client.evals.runs.output_items.list(
        eval_id=eval_id, run_id=run_id
    )
    return run_output_items


def parse_eval_run_output(items):
    """Extract all grader scores and any available conclusion outputs."""
    all_results = []

    for item in items.data:
        for result in item.results:
            grader_name_full = result.name
            score = result.score
            passed = result.passed
            reasoning = None
            try:
                sample = result.sample
                if sample:
                    content = result.sample["output"][0]["content"]
                    content_json = json.loads(content)
                    steps = content_json["steps"]
                    reasoning = " ".join([step["conclusion"] for step in steps])
            except Exception:
                pass

            all_results.append(
                {
                    "grader_name": grader_name_full,
                    "score": score,
                    "passed": passed,
                    "reasoning": reasoning,
                }
            )

    return all_results

Ahora podemos usar el ID de evaluación creado anteriormente y ejecutar los calificadores contra una sección de entrada arbitraria y una salida de resumen. Esto forma la columna vertebral del bucle de retroalimentación que iniciará la rutina de optimización de prompts.

Ejecución de la evaluación

Probemos nuestras evaluaciones proporcionando directamente una sección y un resumen generado.

EVAL_ID = eval.id #Created eval ID from above cell
SECTION = "3.2.S.1 General Information ([1-13C]pyruvic acid) The active ingredient in Hyperpolarized Pyruvate (13C) Injection is hyperpolarized [1-13C]pyruvate. The drug substance is defined as [13C]pyruvic acid, which is neutralized to [1-13C]pyruvate during the compounding process. In several pre-clinical and clinical studies and during evaluation of stability, pyruvic acid has been used instead of [1-13C]pyruvic acid (see Sections 3.2.P.2.2.1 Formulation Development for Hyperpolarized Pyruvate (13C) Injection and Section 8.1 Introduction for Item 8 Pharmacology and Toxicology Info). In the Section 3.2.S Drug Substance, data are presented for both pyruvic acid and for [1-13C]pyruvic acid. For simplicity, the terminology used in headings and captions is [1-13C]pyruvic acid. Batches containing pyruvic acid are specified by footnotes. 3.2.S.1.1 Nomenclature ([1-13C]pyruvic acid) The drug substance used for compounding of Hyperpolarized Pyruvate (13C) Injection is [1-13C]pyruvic acid. Company code: W6578 Chemical name: [1-13C]pyruvic acid CAS registry number: 127-17-3 3.2.S.1.2 Structure ([1-13C]pyruvic acid) Figure 1 Structure of [1-13C]pyruvic acid Molecular formula: C H O 3 4 3 Molecular weight: 89.06 3.2.S.1.3 General Properties ([1-13C]pyruvic acid) Appearance: Colorless to yellow, clear, viscous liquid pKa:Ka:aranWater solubility: Complete The structure of [1-13C]pyruvic acid has been confirmed by spectroscopic analysis (see Section 3.2.S.3.1 Elucidation of Structure and other Characteristics)."
SUMMARY = "The active ingredient in Hyperpolarized Pyruvate (13C) Injection is hyperpolarized [1-13C]pyruvate, derived from [1-13C]pyruvic acid (neutralized during compounding). Both pyruvic acid and [1-13C]pyruvic acid were used in studies and stability evaluations, but the documentation refers to [1-13C]pyruvic acid unless otherwise noted. The drug substance ([1-13C]pyruvic acid, CAS 127-17-3) is a colorless to yellow, clear, viscous liquid with a molecular formula C3H4O3 and molecular weight 89.06. Its structure has been confirmed by spectroscopic analysis, and it is completely soluble in water."

eval_run = run_eval(EVAL_ID, section=SECTION, summary=SUMMARY)
run_output = poll_eval_run(eval_id=EVAL_ID, run_id=eval_run.id)

grader_scores = parse_eval_run_output(run_output)
print(grader_scores)

Deberías ver una lista de puntuaciones de calificadores en la salida, por ejemplo:

[{'grader_name': 'chemical_name_grader-<uuid>', 'score': 0.5, 'passed': False, 'reasoning': None}, {'grader_name': 'word_length_deviation_grader-<uuid>', 'score': 0.8, 'passed': True, 'reasoning': None}, {'grader_name': 'cosine_similarity-<uuid>', 'score': 0.9104484223477793, 'passed': True, 'reasoning': None}, {'grader_name': 'llm_as_judge-<uuid>', 'score': 0.8, 'passed': True, 'reasoning': 'The summary needs to include specific details from the section. Part of the essential information is captured. Key pieces of information are missing. Not all relevant structural information is included.'}]

Al ejecutar este script, podemos ver que la mayoría de nuestros calificadores están pasando, excepto el chemical_name_grader. A continuación, reconoceremos programáticamente esta oportunidad para mejorar el agente de resumen.

Nota: Cuando lo ejecutes localmente, es posible que otros calificadores además de chemical_name_grader fallen al principio. Esto es normal, ya que los calificadores pueden fallar inicialmente, pero los resultados deberían mejorar a través del bucle de retroalimentación. Los fallos tempranos simplemente reflejan que el modelo ajusta sus respuestas antes de converger en resultados más precisos.

Observabilidad del panel

Las ejecuciones y los resultados de las evaluaciones también se pueden ver en el Panel de control de OpenAI:

Eval dashboard
Figura 8 - Panel de control de evaluación que muestra las ejecuciones y los resultados de la evaluación.

También podemos profundizar en una ejecución de evaluación específica:
Eval results
Figura 9 - Resultados detallados de la ejecución de la evaluación que muestran las puntuaciones del calificador y las métricas de rendimiento.

Configuración del agente

Ahora que tenemos nuestras evaluaciones y calificadores configurados, podemos volver a nuestro agente de resumen. Para simplificar, proporcionaremos el código para un agente simple a continuación. También podrías usar AgentBuilder, como se muestra en la Figura 2, y exportar el código desde la interfaz de usuario.

También necesitaremos un agente de optimización de metaprompt, para optimizar nuestro prompt, así como algunas utilidades simples para manejar las versiones de prompt:

  • PromptVersionEntry: Un modelo pydantic utilizado para rastrear el prompt y los metadatos a medida que cambian en producción.
  • VersionedPrompt: Una clase de utilidad para rastrear las versiones de prompt, esto será importante en producción al analizar la evolución del prompt, así como para garantizar que haya un historial de respaldo en caso de una regresión.
from datetime import datetime
from typing import Any, Optional

from pydantic import BaseModel, Field, ConfigDict, field_validator

class PromptVersionEntry(BaseModel):
    """Data model for a prompt and associated data for observability"""
    version: int = Field(
        ..., ge=0, description="Version number of the prompt (increments)"
    )
    model: str = Field(
        "gpt-5",
        min_length=1,
        description="The model version to use for this version of the prompt, defaults to gpt-5",
    )
    prompt: str = Field(
        ..., min_length=1, description="The prompt text for this version"
    )
    timestamp: datetime = Field(
        default_factory=datetime.utcnow,
        description="UTC timestamp when this version was created",
    )
    eval_id: Optional[str] = Field(
        None, description="ID of the evaluation associated with this prompt version"
    )
    run_id: Optional[str] = Field(
        None, description="ID of the run associated with this prompt version"
    )
    metadata: Optional[dict[str, Any]] = Field(
        None, description="Free-form metadata dict (e.g., section, summary)"
    )

    model_config = ConfigDict(
        str_strip_whitespace=True, validate_assignment=True, extra="forbid"
    )

    @field_validator("prompt")
    @classmethod
    def prompt_not_blank(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("prompt must not be blank or only whitespace")
        return v


class VersionedPrompt:
    """Manages a collection of prompt versions and provides controlled updates and rollbacks."""
    def __init__(
        self,
        initial_prompt: str,
        model: Optional[str] = "gpt-5",
        eval_id: Optional[str] = None,
        run_id: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
    ):
        if not initial_prompt or not initial_prompt.strip():
            raise ValueError("initial_prompt must be non-empty")
        self._versions: list[PromptVersionEntry] = []
        first_entry = PromptVersionEntry(
            version=0,
            prompt=initial_prompt,
            model=model,
            eval_id=eval_id,
            run_id=run_id,
            metadata=metadata,
        )
        self._versions.append(first_entry)

    def update(
        self,
        new_prompt: str,
        model: Optional[str] = "gpt-5",
        eval_id: Optional[str] = None,
        run_id: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
    ) -> PromptVersionEntry:
        if not new_prompt or not new_prompt.strip():
            raise ValueError("new_prompt must be non-empty")

        version = self.current().version + 1
        entry = PromptVersionEntry(
            version=version,
            prompt=new_prompt,
            model=model,
            eval_id=eval_id,
            run_id=run_id,
            metadata=metadata,
        )
        self._versions.append(entry)
        return entry

    def current(self) -> PromptVersionEntry:
        return self._versions[-1]

    def revert_to_version(self, version: int) -> PromptVersionEntry:
        idx = None
        for i, entry in enumerate(self._versions):
            if entry.version == version:
                idx = i
                break

        if idx is None:
            raise ValueError(f"No version found with version={version}")

        self._versions = self._versions[: idx + 1]
        return self._versions[-1]

A continuación, crearemos los agentes iniciales de resumen y optimización de prompts.

Nota: Creamos un wrapper para rastrear los cambios de prompt en el agente de resumen, ya que se espera que evolucione en producción; el prompt del agente de metaprompt permanecerá estático para los propósitos de este manual.


from agents import Agent

METAPROMPT_TEMPLATE = """
# Context:
## Original prompt:
{original_prompt}

## Section:
{section}

## Summary:
{summary}

## Reason to improve the prompt:
{reasoning}

# Task:
Write a new summarization prompt that is significantly improved and more specific than the original.  
The new prompt should instruct the model to produce concise yet comprehensive technical summaries that precisely preserve all explicit information from the source text. It should emphasize the inclusion of all named entities, quantities, compounds, and technical terminology without paraphrasing or omission. The resulting prompt should read like a clear, directive system message for a technical summarization assistant—structured, unambiguous, and generalizable across scientific or regulatory document sections.
"""

metaprompt_agent = Agent(
    name="MetapromptAgent", instructions="You are a prompt optimizer."
)

summarization_prompt = VersionedPrompt(
    initial_prompt="""You are a summarization assistant.
Given a section of text, produce a summary."""
)

def make_summarization_agent(prompt_entry: PromptVersionEntry) -> Agent:
    return Agent(
        name="SummarizationAgent",
        instructions=prompt_entry.prompt,
        model=prompt_entry.model,
    )

summarization_agent = make_summarization_agent(summarization_prompt.current())

# Cache eval results by section + summary so repeated attempts do not trigger redundant grader runs.
eval_cache: dict[tuple[str, str], list[dict[str, Any]]] = {}

# Track the highest-scoring candidate that also passes the lenient score threshold.
best_candidate: dict[str, Any] = {
    "score": float("-inf"),
    "prompt": summarization_prompt.current().prompt,
    "model": summarization_prompt.current().model,
    "summary": None,
    "metadata": None,
    "version": summarization_prompt.current().version,
    "passed_lenient": False,
    "total_score": float("-inf"),
}

# Aggregate per-version performance so we can pick the strongest total scorer at the end.
aggregate_prompt_stats: dict[int, dict[str, Any]] = {}

Orquestación y monitoreo

Esto es lo que hemos hecho hasta ahora: hemos creado:

  • Evaluaciones con 4 calificadores que evaluarán las salidas y producirán una puntuación para cada calificador.
  • Un agente de resumen con una clase de prompt versionada para rastrear los cambios en el prompt y el modelo.
  • Un agente de optimización de metaprompt que intentará actualizar el prompt basándose en un conjunto de razonamientos.

Ahora estas diferentes funcionalidades se pueden componer para orquestar el bucle autoevolutivo con el rastreo de agentes en el panel de control de OpenAI.

Ten en cuenta que este es un ejemplo simplificado. En un escenario del mundo real, querrías asegurarte de tener salvaguardias para los intentos de optimización y de que una alerta notifique a un humano cuando se active una salvaguardia.

Nota: Debido a las limitaciones prácticas del manual, estamos simulando un flujo de datos alimentando un conjunto de datos estático y usando declaraciones print en lugar de una verdadera observabilidad.

Utilidades de orquestación

Como en secciones anteriores, crearemos algunas utilidades para gestionar la lógica de orquestación del bucle de retroalimentación.

import asyncio
from typing import Any, Optional
from agents import Runner

LENIENT_PASS_RATIO = 0.75 # 75% of graders must pass (binary) 
LENIENT_AVERAGE_THRESHOLD = 0.85 # 85% average score across graders 

def reset_best_candidate() -> None:
    """Reset the best candidate tracker for a new optimization run."""
    global best_candidate

    current = summarization_prompt.current()
    best_candidate = {
        "score": float("-inf"),
        "prompt": current.prompt,
        "model": current.model,
        "summary": None,
        "metadata": None,
        "version": current.version,
    }

def reset_best_trackers() -> None:
    """Reset both the best-candidate tracker and aggregate stats."""
    reset_best_candidate()
    aggregate_prompt_stats.clear()


def update_best_candidate(
    *,
    average_score: Optional[float] = None,
    prompt_text: str,
    model_name: str,
    summary_text: str = None,
    metadata: dict[str, Any] = None,
    lenient_passed: bool = False,
    prompt_version: int = None,
    total_score: Optional[float] = None,
    score: Optional[float] = None,
) -> None:
    """Persist the best lenient-passing candidate."""
    global best_candidate

    if prompt_version is None:
        prompt_version = summarization_prompt.current().version

    if average_score is None:
        average_score = score

    if average_score is None:
        return

    if lenient_passed:
        best_candidate.update(
            {
                "score": average_score,
                "prompt": prompt_text,
                "model": model_name,
                "summary": summary_text,
                "metadata": metadata,
                "version": prompt_version,
                "total_score": total_score if total_score is not None else average_score,
            }
        )


def apply_best_candidate_if_needed() -> Agent:
    """Ensure summarization_prompt reflects the best prompt candidate."""
    if best_candidate["score"] > float("-inf"):
        current = summarization_prompt.current()
        target = best_candidate
        # Only update if different
        if (
            current.prompt != target["prompt"]
            or current.model != target["model"]
            or current.version != target.get("version")
        ):
            summarization_prompt.update(
                new_prompt=target["prompt"],
                model=target["model"],
                metadata=target.get("metadata"),
            )
            target["version"] = summarization_prompt.current().version
        return make_summarization_agent(summarization_prompt.current())

    return make_summarization_agent(summarization_prompt.current())


def record_aggregate_prompt_score(
    *,
    prompt_version: int,
    prompt_text: str,
    model_name: str,
    average_score: float,
    total_score: Optional[float] = None,
) -> None:
    """Accumulate per-version grader scores for aggregate selection."""
    stats = aggregate_prompt_stats.setdefault(
        prompt_version,
        {
            "version": prompt_version,
            "prompt": prompt_text,
            "model": model_name,
            "total_score": 0.0,
            "total_average": 0.0,
            "count": 0,
        },
    )
    stats["total_score"] += total_score if total_score is not None else average_score
    stats["total_average"] += average_score
    stats["count"] += 1
    stats["prompt"] = prompt_text
    stats["model"] = model_name


def select_best_aggregate_prompt() -> Optional[dict[str, Any]]:
    """Return the prompt version with the highest cumulative score."""
    if not aggregate_prompt_stats:
        return None
    return max(
        aggregate_prompt_stats.values(),
        key=lambda entry: (
            entry.get("total_score", float("-inf")),
            entry.get("version", -1),
        ),
    )


async def get_eval_grader_score(eval_id: str, section: str, summary: str):
    """Retrieve grader scores for a section-summary pair with caching."""
    cache_key = (section, summary)
    if cache_key in eval_cache:
        return eval_cache[cache_key]

    eval_run = run_eval(eval_id=eval_id, section=section, summary=summary)
    run_output = poll_eval_run(eval_id=eval_id, run_id=eval_run.id)
    results = parse_eval_run_output(run_output)
    eval_cache[cache_key] = results
    return results


def calculate_grader_score(grader_scores):
    """Simple average score of all graders from the eval."""
    if not grader_scores:
        return 0.0

    score_sum = 0.0
    for entry in grader_scores:
        score_sum += entry.get("score", 0.0)

    return score_sum / len(grader_scores)



def calculate_total_grader_score(grader_scores):
    """Sum of all grader scores for aggregate tracking."""
    if not grader_scores:
        return 0.0

    return sum(entry.get("score", 0.0) for entry in grader_scores)


DEFAULT_PASSING_FEEDBACK = (
    "All graders passed; tighten factual coverage, chemical completeness, and conciseness."
)


def is_lenient_pass(grader_scores, average_score: float) -> bool:
    if not grader_scores:
        return False

    passed_count = sum(1 for entry in grader_scores if entry.get("passed"))
    total_graders = len(grader_scores)

    if total_graders and (passed_count / total_graders) >= LENIENT_PASS_RATIO:
        return True
    return average_score >= LENIENT_AVERAGE_THRESHOLD


def collect_grader_feedback(grader_scores):
    """Consolidate grader reasoning into actionable feedback for the metaprompt agent."""
    feedback_lines = []

    for entry in grader_scores:
        grader = entry.get("grader_name", "")
        passed = entry.get("passed", False)
        reasoning = entry.get("reasoning")

        if not passed:
            if grader.startswith("chemical_name_grader"):
                feedback_lines.append(
                    "Not all chemical names in the input section were included in the summary."
                )
            elif grader.startswith("word_length_deviation_grader"):
                feedback_lines.append(
                    "The summary length deviates too much from the expected length."
                )
            elif grader.startswith("cosine_similarity"):
                feedback_lines.append(
                    "The summary is not sufficiently similar to the source section (cosine similarity too low)."
                )
            elif grader.startswith("llm_as_judge") and reasoning:
                feedback_lines.append(reasoning)

    if not feedback_lines:
        feedback_lines.append(DEFAULT_PASSING_FEEDBACK)

    return "".join(feedback_lines)

Bucle autoevolutivo

Ahora, para simular un flujo de solicitudes de resumen, introduciremos un conjunto de datos preparado y observaremos cómo la optimización evoluciona a partir de un prompt ingenuo.

El archivo dataset.csv referenciado se puede encontrar en el repositorio de Github.

import pandas as pd

from agents import Agent, trace

EVAL_ID = eval.id #Created eval ID from above cell
MAX_OPTIMIZATION_RETRIES = 3

async def self_evolving_loop(summarization_agent: Agent) -> Agent:
    print(f"Starting self-evolving loop | Initial prompt v{summarization_prompt.current().version}")
    print(f"Prompt:{summarization_prompt.current().prompt}")
    print("-" * 80)

    reset_best_trackers()
    df = pd.read_csv("data/dataset.csv")

    with trace("Self-evolving Optimization Workflow"):
        for _, row in df.head().iterrows():
            content = row.get("content")
            if pd.isna(content) or (isinstance(content, str) and not content.strip()):
                continue

            section_number = str(row["section_number"])
            section = str(content)
            current_version = summarization_prompt.current().version

            print(f"[Section {section_number}] Using prompt v{current_version}")

            optimization_success = False

            for attempt in range(1, MAX_OPTIMIZATION_RETRIES + 1):
                print(f"  Attempt {attempt}: evaluating summary...")

                summary_result = await Runner.run(summarization_agent, section)
                summary = summary_result.final_output

                grader_scores = await get_eval_grader_score(eval_id=EVAL_ID, summary=summary, section=section)
                average_score = calculate_grader_score(grader_scores)
                total_score = calculate_total_grader_score(grader_scores)
                lenient_passed = is_lenient_pass(grader_scores, average_score)
                print(
                    f"	Scores — avg={average_score:.3f}, total={total_score:.3f}, lenient_passed={lenient_passed}"
                )

                record_aggregate_prompt_score(
                    prompt_version=summarization_prompt.current().version,
                    prompt_text=summarization_prompt.current().prompt,
                    model_name=summarization_prompt.current().model,
                    average_score=average_score,
                    total_score=total_score,
                )

                update_best_candidate(
                    average_score=average_score,
                    prompt_text=summarization_prompt.current().prompt,
                    model_name=summarization_prompt.current().model,
                    summary_text=summary,
                    metadata={
                        "section": section_number,
                        "average_score": average_score,
                        "grader_results": grader_scores,
                        "prompt_version": summarization_prompt.current().version,
                    },
                    lenient_passed=lenient_passed,
                    prompt_version=summarization_prompt.current().version,
                )

                if lenient_passed:
                    optimization_success = True
                    print(f"	Passed with prompt v{summarization_prompt.current().version}")
                    break

                print("	Failed eval. Improving prompt...")
                eval_feedback = collect_grader_feedback(grader_scores)

                metaprompt_result = await Runner.run(
                    metaprompt_agent,
                    input=METAPROMPT_TEMPLATE.format(
                        original_prompt=summarization_prompt.current().prompt,
                        section=section,
                        summary=summary,
                        reasoning=eval_feedback,
                    ),
                )
                improved_prompt = metaprompt_result.final_output
                summarization_prompt.update(
                    new_prompt=improved_prompt,
                    metadata={"section": section, "summary": summary},
                )
                summarization_agent = make_summarization_agent(summarization_prompt.current())

                print(f"	Prompt improved → v{summarization_prompt.current().version}")

            if not optimization_success:
                print(
                    "	All attempts failed; keeping latest prompt version "
                    f"v{summarization_prompt.current().version} for the next section."
                )

    summarization_agent = apply_best_candidate_if_needed()

    print("" + "-" * 80)
    print("Completed optimization loop.")
    print(f"Final prompt version: v{summarization_prompt.current().version}")
    if best_candidate["score"] > float("-inf"):
        print(
            f"Best lenient prompt: v{best_candidate.get('version')} (avg={best_candidate['score']:.3f})"
        )

    aggregate_best = select_best_aggregate_prompt()
    if aggregate_best:
        per_section = (
            aggregate_best.get("total_average", 0.0) / aggregate_best.get("count", 1)
            if aggregate_best.get("count")
            else 0.0
        )
        print(
            f"Aggregate best prompt: v{aggregate_best.get('version')} "
            f"(total={aggregate_best.get('total_score', 0.0):.3f}, avg/section={per_section:.3f}, model={aggregate_best.get('model', 'unknown')})"
        )

    print(f"Final prompt:{summarization_prompt.current().prompt}")
    return summarization_agent

summarization_agent = await self_evolving_loop(summarization_agent)

Cómo se elige el prompt final

  • Cada evaluación registra la puntuación promedio del calificador, la puntuación total de todos los calificadores y si el intento pasó los criterios indulgentes.
  • best_candidate rastrea el pase indulgente más reciente (para mayor transparencia), pero la selección final utiliza los totales agregados para garantizar que mantengamos el prompt con el mejor rendimiento general.
  • Cuando el bucle termina, apply_best_candidate_if_needed restaura el prompt con la puntuación acumulada más alta del calificador (los empates favorecen la versión más reciente), garantizando que el prompt presentado sea el de mejor rendimiento observado.

Aquí tienes un ejemplo (abreviado) de salida para el código anterior.

Al inspeccionar la salida, se observa que el prompt autoevolutivo funcionó. Hay algunas consideraciones a tener en cuenta:

  1. La optimización no siempre tiene éxito, por lo que es importante poder revertir la versión del prompt.
  2. La fidelidad de la información de los calificadores es crucial para garantizar una optimización de calidad.

Registros y rastreo de agentes

Podemos ver las ejecuciones del flujo de trabajo de optimización en el panel de control, en los registros:

Agent log traces
Figura 10 - Rastros de registro del agente que muestran las ejecuciones del flujo de trabajo de optimización en el panel de control.

Y profundizar en las diferentes llamadas del agente:

Agent trace details
Figura 11 - Traza detallada del agente que muestra las llamadas individuales del agente y el flujo de ejecución.

Monitoreo continuo

Una vez que el ciclo de evaluación esté completo, el sistema debe continuar monitoreando los nuevos datos entrantes y reevaluar periódicamente el rendimiento del modelo en conjuntos de datos ciegos. Esto asegura que el modelo se mantenga preciso y conforme a medida que la distribución de los datos evoluciona.

Para habilitar el monitoreo continuo, puedes integrar un cron job o un ciclo de programador ligero que verifique periódicamente las actualizaciones en tu fuente de datos (por ejemplo, nuevas cargas de PDF o entradas de bases de datos). Cuando se detectan nuevos datos, el sistema activa automáticamente el ciclo de evaluación y optimización descrito anteriormente.

Por ejemplo (pseudocódigo):

# this cell is pseudo-code and not meant to be run as-is

import time

def continuous_monitoring(interval_hours=24):
    """Periodically check for new data and trigger the evaluation loop."""
    while True:
        print("Checking for new data...")
        if new_data_detected():
            print("New data found — running evaluation and optimization loop.")
            self_evolving_loop()
        else:
            print("No new data. Sleeping until next cycle.")
        time.sleep(interval_hours * 3600)

continuous_monitoring(interval_hours=24)

Este enfoque permite que el modelo aprenda y se adapte continuamente, mejorando con el tiempo a medida que procesa datos frescos, un requisito clave para mantener un rendimiento de alta calidad en el mundo real.

4. Para ir más allá

a. Evaluación del modelo

Ahora tenemos un ciclo completamente automatizado que mejora nuestro prompt con evals y acepta el nuevo prompt cuando la calificación supera el umbral definido.

En producción, podrías usar un marco similar para monitorear el rendimiento de tus agentes a medida que llegan nuevas solicitudes de usuarios. Como se mencionó anteriormente, este es un ejemplo simplificado, y en un escenario del mundo real querrías tener salvaguardas adicionales y un enfoque de "humano en el ciclo" para aprobar nuevos prompts.

Llevando este concepto más allá, también podemos usar evals para probar diferentes candidatos de parámetros del modelo, como la versión del modelo, la verbosidad y el razonamiento. Para ver el conjunto completo de parámetros disponibles que podrían considerarse, consulta la clase ModelSettings en el SDK de Agents

La función compare_model_candidates es un ejemplo de cómo:

  1. Optimizar el prompt
  2. Generar resultados candidatos a partir del prompt optimizado usando dos o más modelos diferentes
  3. Usar evals para calificar los resultados candidatos y seleccionar el mejor candidato

Se puede integrar en la función self_evolving_loop con una refactorización mínima.

NOTA: Las pruebas de producción de las versiones del modelo deben limitarse a versiones dentro de la misma familia de versiones (por ejemplo, gpt-5, gpt-5-mini, gpt-5-nano). Se recomienda realizar la selección de versiones entre familias antes de la implementación en producción.

Y el self_evolving_loop final con código de comparación de modelos:

from agents import Agent, Runner

async def eval_agent_candidate(agent: Agent, section: str, prompt_text: str, model_name: str):
    summary_result = await Runner.run(agent, section)
    summary = summary_result.final_output

    scores = await get_eval_grader_score(
        eval_id=EVAL_ID, summary=summary, section=section
    )
    average = calculate_grader_score(scores)
    lenient_passed = is_lenient_pass(scores, average)
    passed = all(entry.get("passed") is True for entry in scores)

    update_best_candidate(
        average_score=average,
        prompt_text=prompt_text,
        model_name=model_name,
        summary_text=summary,
        metadata={
            "section": section,
            "average_score": average,
            "grader_results": scores,
        },
        lenient_passed=lenient_passed,
    )

    return {"summary": summary, "scores": scores, "average": average, "passed": passed}

async def compare_model_candidates(
    summarization_prompt,
    eval_feedback: str,
    section: str,
    summary: str,
    model_candidates=None,
):
    """Improve the prompt, evaluate it across candidate models, and adopt the top performer."""
    if model_candidates is None:
        model_candidates = ["gpt-5", "gpt-5-mini"]

    metaprompt_result = await Runner.run(
        metaprompt_agent,
        input=METAPROMPT_TEMPLATE.format(
            original_prompt=summarization_prompt.current().prompt,
            section=section,
            summary=summary,
            reasoning=eval_feedback,
        ),
    )
    improved_prompt = metaprompt_result.final_output

    async def evaluate_model(model_name: str):
        candidate_agent = Agent(
            name=f"SummarizationAgent:{model_name}",
            instructions=improved_prompt,
            model=model_name,
        )
        result = await eval_agent_candidate(candidate_agent, section, improved_prompt, model_name)
        return model_name, candidate_agent, result

    best = {
        "average": float("-inf"),
        "passed": False,
        "agent": None,
        "model": None,
        "summary": None,
    }

    tasks = [asyncio.create_task(evaluate_model(model_name)) for model_name in model_candidates]
    for task in asyncio.as_completed(tasks):
        model_name, candidate_agent, result = await task
        print(
            f"Candidate average — {model_name}: {result['average']:.4f} "
            f"(passed={result.get('passed', False)})"
        )
        if result["average"] > best["average"]:
            best.update(
                {
                    "average": result["average"],
                    "model": model_name,
                    "summary": result.get("summary"),
                    "agent": candidate_agent,
                    "passed": result.get("passed", False),
                }
            )

    for task in tasks:
        if not task.done():
            task.cancel()

    if best["passed"] and best["model"]:
        summarization_prompt.update(
            new_prompt=improved_prompt,
            model=best["model"],
            metadata={"section": section, "summary": best["summary"]},
        )
        print(f"Updated summarization_prompt with passing model: {best['model']}")
        return make_summarization_agent(summarization_prompt.current())

    print(
        f"No passing models. Best candidate (model={best['model']}, "
        f"avg={best['average']:.4f}) did not pass. Prompt not updated."
    )
    return None

async def self_evolving_loop_with_model_comparison(summarization_agent: Agent) -> Agent:
    print(
        f"Starting self-evolving loop | Initial prompt v{summarization_prompt.current().version}"
    )
    print(f"Prompt: {summarization_prompt.current().prompt}")
    print(f"Model: {summarization_prompt.current().model}")
    print("-" * 80)

    reset_best_trackers()
    df = pd.read_csv("data/dataset.csv")

    with trace("Self-evolving Optimization Workflow: model comparison"):
        for _, row in df.head(5).iterrows():
            content = row.get("content")
            if pd.isna(content) or (isinstance(content, str) and not content.strip()):
                continue

            section_number = str(row["section_number"])
            section = str(content)
            current_version = summarization_prompt.current().version

            print(f"[Section {section_number}] Using prompt v{current_version}")

            summary_passed = False

            for attempt in range(1, MAX_OPTIMIZATION_RETRIES + 1):
                print(f"\tAttempt {attempt}: evaluating summary...")

                summary_result = await Runner.run(summarization_agent, section)
                summary = summary_result.final_output

                grader_scores = await get_eval_grader_score(
                    eval_id=EVAL_ID, summary=summary, section=section
                )
                average_score = calculate_grader_score(grader_scores)
                total_score = calculate_total_grader_score(grader_scores)
                lenient_passed = is_lenient_pass(grader_scores, average_score)
                print(
                    f"\tScores — avg={average_score:.3f}, total={total_score:.3f}, lenient_passed={lenient_passed}"
                )

                record_aggregate_prompt_score(
                    prompt_version=summarization_prompt.current().version,
                    prompt_text=summarization_prompt.current().prompt,
                    model_name=summarization_prompt.current().model,
                    average_score=average_score,
                    total_score=total_score,
                )

                update_best_candidate(
                    average_score=average_score,
                    total_score=total_score,
                    prompt_text=summarization_prompt.current().prompt,
                    model_name=summarization_prompt.current().model,
                    summary_text=summary,
                    metadata={
                        "section": section_number,
                        "average_score": average_score,
                        "grader_results": grader_scores,
                        "prompt_version": summarization_prompt.current().version,
                    },
                    lenient_passed=lenient_passed,
                    prompt_version=summarization_prompt.current().version,
                )

                if lenient_passed:
                    summary_passed = True
                    print(
                        f"\tPassed with prompt v{summarization_prompt.current().version} (model={summarization_prompt.current().model})"
                    )
                    break

                print("\tFailed eval. Improving prompt...")
                eval_feedback = collect_grader_feedback(grader_scores)

                new_agent = await compare_model_candidates(
                    summarization_prompt=summarization_prompt,
                    eval_feedback=eval_feedback,
                    section=section,
                    summary=summary,
                    # model_candidates could be given as an argument if you want to expand options.
                )

                if new_agent is None:
                    print(
                        "\tNo passing model found. Optimization failed for this section."
                    )
                    summary_passed = False
                else:
                    summarization_agent = new_agent
                    summary_passed = True
                    print(
                        f"\tPrompt improved → v{summarization_prompt.current().version} "
                        f"(model={summarization_prompt.current().model})"
                    )
                    break

            if not summary_passed:
                print(
                    "\tAll attempts failed; keeping latest prompt version "
                    f"v{summarization_prompt.current().version} (model={summarization_prompt.current().model}) for the next section."
                )

    summarization_agent = apply_best_candidate_if_needed()

    print("" + "-" * 80)
    print("Completed optimization loop.")
    print(f"Final prompt version: v{summarization_prompt.current().version}")
    print(f"Final model: {summarization_prompt.current().model}")
    aggregate_best = select_best_aggregate_prompt()
    if best_candidate["score"] > float("-inf"):
        print(
            f"Best lenient prompt: v{best_candidate.get('version')} (avg={best_candidate['score']:.3f}, model={best_candidate.get('model', 'unknown')})"
        )
    if aggregate_best:
        per_section = (
            aggregate_best.get("total_average", 0.0) / aggregate_best.get("count", 1)
            if aggregate_best.get("count")
            else 0.0
        )
        print(
            f"Aggregate best prompt: v{aggregate_best.get('version')} "
            f"(total={aggregate_best.get('total_score', 0.0):.3f}, avg/section={per_section:.3f}, model={aggregate_best.get('model', 'unknown')})"
        )
    print(f"Final prompt: {summarization_prompt.current().prompt}")
    print(f"Final model: {summarization_prompt.current().model}")
    return summarization_agent

summarization_agent = await self_evolving_loop_with_model_comparison(summarization_agent)

Aquí podemos ver una salida muy similar con información adicional sobre las puntuaciones de la versión del modelo:

b. Optimización de prompts con Genético-Pareto (GEPA)

Hemos demostrado que el ciclo autoevolutivo funciona y que un prompt puede mejorarse de forma autónoma usando Evals. Sin embargo, nos basamos en un metaprompt estático relativamente sencillo para mejorar nuestro prompt del sistema. En esta sección, exploramos un método más dinámico y reflexivo usando Genético-Pareto (GEPA) [1], un marco que muestrea trayectorias de agentes, reflexiona sobre ellas en lenguaje natural, propone revisiones de prompts y evoluciona el sistema a través de ciclos de retroalimentación iterativos.

El método GEPA, descrito en el artículo disponible aquí, ofrece un plan convincente para la optimización continua y auto-mejorable de prompts. El código a continuación se basa generosamente en el repositorio de GitHub de GEPA disponible aquí.

import pandas as pd
import gepa
from gepa import EvaluationBatch

# Extract sections from dataset
def read_csv_content(file_path: str) -> list[dict]:
    """Read csv and return section to summarize."""
    df = pd.read_csv(file_path)
    return [{'content': content} for content in df['content'].tolist()]

# Split dataset into training and validation sets
trainset = read_csv_content("data/dataset.csv")
val_cut = max(1, int(0.1 * len(trainset)))
valset = trainset[:val_cut] if len(trainset) > 1 else trainset

Reutilizaremos nuestros calificadores y funciones de ayuda agregando un pequeño adaptador para que nuestra configuración funcione con GEPA. El GEPAAdapter de GEPA facilita la conexión a nuestro marco de evaluación. Definimos tres hooks

  • evaluate: ejecuta el resumen y califica con los calificadores definidos en la sección anterior (es decir, chemical_name_grader, word_length_deviation_grader, cosine_similarity, llm_as_judge).
  • get_components_to_update: obtiene los campos de texto que GEPA debe evolucionar (aquí, system_prompt).
  • make_reflective_dataset: empaqueta entradas, salidas y retroalimentación para la reflexión.
class EvalsBackedSummarizationAdapter:
    """
    Minimal adapter for GEPA:
      - evaluate(...) -> EvaluationBatch (scores + outputs + feedback-rich trajectories)
      - get_components_to_update(...) returns the prompt to update
      - make_reflective_dataset(...) packages examples for reflection
    """
    propose_new_texts = None  # use GEPA's default reflection flow

    def __init__(self, client, eval_id: str, gen_model: str = "gpt-5", user_prefix: str | None = None):
        self.client = client
        self.eval_id = eval_id
        self.gen_model = gen_model
        self.user_prefix = user_prefix or "Summarize:\n\n"

    # Same summarization agent as in the previous section
    def _summarize(self, system_prompt: str, section: str) -> str:
        resp = self.client.chat.completions.create(
            model=self.gen_model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": f"{self.user_prefix}{section}"},
            ],
        )
        return resp.choices[0].message.content.strip()

    # Required by GEPA: run eval minibatch
    def evaluate(self, inputs: list[dict], candidate: dict, capture_traces: bool = True) -> EvaluationBatch:
        system_prompt = candidate["system_prompt"]

        scores: list[float] = []
        outputs: list[str] = []
        trajectories: list[dict] = []

        for item in inputs:
            section = item["content"]

            # 1) Generate with the candidate prompt
            summary = self._summarize(system_prompt, section)
            outputs.append(summary)

            # 2) Grade using previous evals pipeline
            run = run_eval(eval_id=self.eval_id, section=section, summary=summary)
            out_items = poll_eval_run(eval_id=self.eval_id, run_id=run.id)
            grader_scores = parse_eval_run_output(out_items)

            # 3) Score + actionable feedback
            scalar = calculate_grader_score(grader_scores)
            feedback = collect_grader_feedback(grader_scores) or "All graders passed; keep precision and coverage."

            scores.append(float(scalar))
            trajectories.append(
                {
                    "inputs": {"section": section},
                    "generated_output": summary,
                    "metrics": {
                        "combined": float(scalar),
                        "by_grader": grader_scores,  # keeping for analysis if needed
                    },
                    "feedback": feedback,
                }
            )

        return EvaluationBatch(scores=scores, outputs=outputs, trajectories=trajectories)

    # Required by GEPA: text field to evolve
    def get_components_to_update(self, candidate: dict) -> list[str]:
        return ["system_prompt"]

    # Required by GEPA: build the reflective dataset the reflection LM will read
    def make_reflective_dataset(self, candidate: dict, eval_batch: EvaluationBatch, components_to_update: list[str]) -> dict:
        examples = []
        for traj in (eval_batch.trajectories or []):
            examples.append(
                {
                    "Inputs": {"section": traj["inputs"]["section"]},
                    "Generated Outputs": traj["generated_output"],
                    "Feedback": traj["feedback"],
                }
            )
        return {"system_prompt": examples}

Ahora que el adaptador está listo, podemos ejecutar GEPA usando el mismo prompt inicial ("You are a summarization assistant. Given a section of text, produce a summary.") y modelo (aquí, gpt-5) que en el ciclo autoevolutivo anterior para comparar. Proporcionamos nuestra instancia de adaptador, candidato semilla y conjuntos de entrenamiento/validación a gepa.optimize(...). Durante la optimización, GEPA invoca repetidamente el adaptador para calificar candidatos, reflexiona sobre la retroalimentación y, en última instancia, produce el mejor prompt evolucionado.

Nota: GEPA podría tardar entre 10 y 15 minutos en completarse.

seed_candidate = {"system_prompt": "You are a summarization assistant. Given a section of text, produce a summary."}

adapter = EvalsBackedSummarizationAdapter(
    client=client,
    eval_id=EVAL_ID,
    gen_model=summarization_prompt.current().model,  
)

# Keeping max_metric_calls small for the cookbook. 
# In practice, use a larger value to allow more optimization iterations.
result = gepa.optimize(
    seed_candidate=seed_candidate,
    trainset=trainset,
    valset=valset,
    adapter=adapter,
    reflection_lm="gpt-5",
    max_metric_calls=10,
    track_best_outputs=True,
    display_progress_bar=True
)

best_prompt = result.best_candidate["system_prompt"]
print("\n=== Best evolved instruction ===\n")
print(best_prompt)

Aquí tienes un ejemplo (abreviado) de la salida del código anterior:

En este recetario, exploramos tres enfoques distintos para la optimización de prompts:

  • Optimizador de la plataforma OpenAI: usando el botón Optimizar con un conjunto de datos que contiene retroalimentación humana ingresada manualmente (pulgares arriba/abajo y comentarios textuales), produjimos rápidamente un prompt sólido con una configuración mínima. Este método destaca por su rápida iteración, pero no proporciona la automatización necesaria para entornos de producción.

  • Optimización usando un metaprompt estático: Nuestro ciclo, que incorpora cuatro calificadores diferentes, permitió la exploración automatizada y la auto-mejora iterativa sin intervención manual. Sin embargo, su espacio de exploración estaba limitado por un único metaprompt estático, y la evaluación se realizó sección por sección. En consecuencia, este enfoque corría el riesgo de sobreajustarse a la retroalimentación inmediata del calificador en lugar de lograr una generalización más amplia.

  • Optimización GEPA: Ofreciendo un proceso de búsqueda más estructurado, las actualizaciones reflexivas se basaron tanto en puntuaciones cuantitativas como en retroalimentación textual, mientras que los candidatos se entrenaron en un conjunto de datos y se validaron en otro. Este método produjo un prompt más robusto y generalizado y proporcionó una evidencia empírica más clara de su rendimiento.

Nota: Ejemplos de prompts generados por cada método están disponibles en el Apéndice.

Dependiendo de tu caso de uso, puedes priorizar la velocidad (optimizador de OpenAI), la automatización ligera (metaprompt estático) o la generalización sistemática (GEPA). En la práctica, combinar estos métodos, comenzando con una iteración rápida y progresando hacia la optimización reflexiva, puede ofrecer tanto agilidad como rendimiento.

¡Feliz codificación!

Colaboradores

Este recetario se basa en una colaboración conjunta entre Bain y OpenAI.

Calvin Maguranis
Fanny Perraudeau
Giorgio Saladino
Shikhar Kwatra
Valentina Frenkel

Citas

[1] GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning por Lakshya A Agrawal, Shangyin Tan, Dilara Soylu, Noah Ziems, Rishi Khare, Krista Opsahl-Ong, Arnav Singhvi, Herumb Shandilya, Michael J Ryan, Meng Jiang, Christopher Potts, Koushik Sen, Alexandros G. Dimakis, Ion Stoica, Dan Klein, Matei Zaharia, Omar Khattab - https://arxiv.org/abs/2507.19457

Apéndice

Ejemplos de prompts de salida:

  • Prompt inicial:
You are a summarization assistant. Given a section of text, produce a summary.
  • Optimizador de la plataforma OpenAI:
You are a summarization assistant.
Task: Summarize the provided text concisely and accurately.
Output requirements:
- Output only the summary. Do not add titles, labels (e.g.,
"Summary:"), prefaces, or commentary.
- Preserve the document's structure. If multiple sections/subsections appear, summarize each one.
- Use a numbered list for sections/subsections (use their numbers/titles when present).
- Under each, use short dash bullets for key points.
- If there is only a single short section, return a brief bullet list or 1-2 concise sentences.
- Split any inline lists into separate bullets.
- Use plain, simple language. Keep bullets tight (ideally one line each). Remove redundancy.
- Include important quantitative details (values, units, conditions) and constraints. Do not invent information.
- Keep formatting simple: plain text, "1." numbering and "-" bullets only. No tables or special markup.
- Retain exact technical terms/notation from the source (e.g., chemical names, isotopic labels).
- If a section is explicitly marked "Not applicable," include that status; otherwise do not add it.
  • Metaprompt estático:
You are a technical summarization assistant for scientific and regulatory documentation. Your task is to generate a concise, comprehensive, and fully detailed summary of any scientific, technical, or regulatory text provided. Strictly adhere to the following instructions:

---

**1. Complete and Exact Information Inclusion**  
- Capture *every* explicit fact, technical value, specification, quantity, measurement, regulatory reference, entity, process, site, and contextual detail verbatim from the source text.
- Do not omit or generalize any explicit information, no matter how minor.

**2. Precise Terminology and Named Entity Retention**  
- Reproduce all names of chemicals, drugs, mixtures, buffer components, devices, companies, institutions, regulatory standards, section numbers, and procedural labels *exactly as stated*.
- Report all quantities, measurements, concentrations, ratios, masses, volumes, compositions, pH values, and units precisely as given.
- Do not paraphrase, rename, substitute, or simplify any term or value.

**3. All Procedural Details and Justifications**  
- Explicitly include all described procedures, technical processes (e.g., terminal sterilization, aseptic processing), operational constraints, process justifications, compliance requirements, and standards references.
- Clearly state all reasons provided for choosing or omitting particular methods or processes.

**4. Regulatory and Compliance References**  
- Accurately cite all regulations, standards (e.g., USP <797>), compliance statements, section numbers, and cross-references as in the original.
- Include all explicit mentions of compliance, applicability, and site location details.

**5. Explicit Statements of Absence, Limitations, and Applicability**  
- Clearly state any declarations of absence, inapplicability (“Not applicable”), or limitations exactly as written in the source.

**6. Structural and Organizational Fidelity**  
- Precisely reflect the original document’s section and subsection hierarchy, using clear section labels and indentation.
- Present all enumerations, lists, and tabulated data in structured bullet-point or numbered format, organized in accordance with the source document’s arrangement.

**7. No Paraphrasing, Summarizing, or Reinterpretation**  
- Do *not* paraphrase, summarize contextually, reinterpret, or alter the meaning or sequence of any content.
- Remove only literal repetitions or redundant phrasing; otherwise, preserve all explicit statements, technical details, and contextual notes.

---

**Summary Output Objective:**  
Produce a summary that delivers the full technical, factual, and regulatory content and structure of the original text, reformatted by eliminating only redundant language. The summary must enable audit, regulatory review, or peer reference without loss of any explicit information or terminology from the source.

---

*Apply these instructions rigorously to every provided document section to ensure scientific and regulatory accuracy and completeness.*
  • Optimizador GEPA:
You are a domain-aware summarization assistant for technical pharmaceutical texts. Given a “section” of text, produce a concise, single-paragraph summary that preserves key technical facts and exact nomenclature.

Length and format
- Write 1–3 sentences totaling about 45–70 words (target ~60; never exceed 90).
- Use one paragraph; no bullets, headings, tables, or heavy formatting.

Exact names and notation
- Include every chemical name that appears in the section at least once, using the exact original spelling, capitalization, punctuation, isotopic labels, brackets, hyphens, salts, buffer names, and parenthetical qualifiers. Treat distinct case/format variants as distinct names (e.g., [1-13C]pyruvic acid and [1-13C]Pyruvic acid are separate and each must appear once).
- Examples you must preserve verbatim when present: Hyperpolarized Pyruvate (13C) Injection; non-polarized Pyruvate Injection; Pyruvate (13C) Injection; hyperpolarized [1-13C]pyruvate; Mixture of [1-13C]pyruvic acid and 15 mM AH111501 sodium salt; TRIS/EDTA buffer solution; TRIS; NaOH; Na2EDTA; [1-13C]pyruvic acid; AH111501 sodium salt.
- Also preserve exact study identifiers, batch codes, section numbers, regulatory citations, and instrument parameters as written (e.g., GE-101-001, GE-101-003, USP <797>, 3.2.P.5.2.5, FFF106/140-806, FFF106/142-806, 3T MRI, 5 degree RF pulse, TR=3s, 90 degree pulse, 64 averages, TR=10s, 10 μl Gd/ml solution).

Content prioritization (if space is tight)
1) What the section is about (topic/purpose).
2) All named chemical entities and compositions (list all chemical names at least once; include concentrations/amounts if given).
3) Critical process/handling facts (e.g., aseptic processing vs terminal sterilization; ISO classifications; filtration specs; compounding/filling steps; temperatures/times/volumes; storage/administration limits).
4) Container/packaging specifics (e.g., cryovials, “sterile fluid path”).
5) Microbiological/testing/regulatory details (e.g., sterility/pyrogenicity testing timing; USP <797>; state board compliance; site/manufacturer if stated).
6) Overages/single-dose formulas and key quantities.

Numerical fidelity
- Preserve all critical numbers and units exactly (e.g., 1.44 g, 27.7 mg, 15 mM, 18 mL, 1.47 g, two 0.2 μm filters, ISO 7, ISO 5, 38 mL).
- Include testing/analysis parameters when present (e.g., polarization/relaxation time (T1); number of spectra; pulse angles; TR values; MRI location relative to clean room).

Style and compression
- Be neutral and factual; do not infer unstated information.
- Consolidate repeated statements; compress lists with commas/semicolons to save words.
- Mention tables/figures only to convey key data; do not reproduce them.
- If many chemicals are present, ensure each distinct name appears once; group them succinctly.
- Avoid symbols or special formatting not in the source text.

Common domain cues to include when present
- Aseptic processing vs terminal sterilization and the rationale/timing (e.g., “tested for sterility and pyrogenicity subsequent to patient administration”).
- Environmental/processing controls (ISO 7/ISO 5; LAF unit; filtration; filling/weight targets per cryovial).
- Site/regulatory context (e.g., USP <797>; California State Board of Pharmacy; University of California, San Francisco Department of Clinical Pharmacy).
- Study/kit equivalence statements (e.g., equivalence to GE-101-001/GE-101-003 formulations).
- QC/measurement methods (e.g., capacitive threshold at Administration syringe nominal 38 mL).

Self-check before finalizing
- Does the paragraph contain every distinct chemical name exactly as written in the section (including case and notation variants)?
- Is the summary 45–70 words (≤90), in a single paragraph?
- Are the most critical process/regulatory/testing details and all key numbers preserved without unnecessary verbosity?`
Lección del curso «OpenAI Cookbook» de OpenAI, publicado con licencia MIT. Traducción y adaptación al español de IA con Clase. IA con Clase no está afiliado a OpenAI. Ver el original · Licencia
Esta lección es gratuita. El resto del curso se abre con la Membresía de IA con Clase, que incluye todos los cursos del catálogo. Ver precios