Capítulo 4. Estrategias avanzadas de integración del LLM con OpenAI

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Ahora que ya conoces los fundamentos de los LLM y la API de OpenAI, ha llegado el momento de llevar tus habilidades al siguiente nivel. Este capítulo abarca potentes estrategias que te permitirán aprovechar el verdadero potencial de los modelos de OpenAI. Desde la ingeniería de prompts y el ajuste fino de los modelos para tareas específicas hasta la implementación del diseño RAG en tus datos propios, este capítulo te proporcionará todos los conocimientos que necesitas para ir un paso más allá en el uso de los LLM con OpenAI. También cubriremos algunas estrategias para superar los escollos específicos que te encontrarás mientras trabajas en la integración de los LLM en tus proyectos.

Ingeniería de prompts

Antes de que se sumerja en la ingeniería de prompts, repasemos brevemente la función completion del modelo de chat, ya que esta sección la utilizará ampliamente. Para que el código sea más compacto, definimos la función como sigue:

client = OpenAI()

def chat_completion(prompt, model="gpt-4", temperature=0, response_format=None):
    res = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        temperature=temperature,
        response_format=response_format 
    )
    return res.choices[0].message.content

Esta función recibe un aviso y muestra el resultado de la finalización en el terminal. El modelo y la temperatura son dos características opcionales establecidas por defecto, respectivamente, en GPT-4 y 0. Se puede utilizar response_format si se necesita un formato de salida JSON.

Para demostrar la ingeniería de prompts en, volveremos al ejemplo de la frase Como dijo Descartes, pienso por tanto. Si se pasa esta entrada a GPT-4, es natural que el modelo complete la frase añadiendo iterativamente los tokens más probables:

chat_completion("As Descartes said, I think therefore")

Como resultado, obtenemos el siguiente mensaje de salida:

I am. This famous philosophical statement, also known as "Cogito, ergo sum,"
emphasizes the existence of the self through the act of thinking or doubting.
Descartes used this statement as a foundational principle in his philosophy,
arguing that one's own existence is the most certain and indubitable fact
that can be known.

La ingeniería de prompts es una disciplina centrada en el desarrollo de buenas prácticas para crear entradas óptimas para los LLM con el fin de producir resultados deseables de la forma más programática posible. Como ingeniero de prompts, debes saber cómo interactuar con la IA para obtener resultados explotables para tus aplicaciones, cómo formular las preguntas adecuadas y cómo escribir prompts de calidad; todos ellos temas que trataremos en esta sección.

En hay que tener en cuenta que la ingeniería de prompts puede afectar al coste del uso de la API de OpenAI. La cantidad de dinero que pagarás por utilizar la API es proporcional al número de tokens que envíes y recibas de OpenAI. Como se menciona en el Capítulo 2, el uso del parámetro max_tokens es muy recomendable para evitar sorpresas desagradables en tus facturas.

Ten en cuenta también que debes considerar los distintos parámetros que puedes utilizar en los métodos openai, ya que puedes obtener resultados significativamente distintos con la misma consulta si utilizas parámetros como temperature, top_p y max_tokens.

OpenAI ha publicado una guía dedicada a la ingeniería de prompts, cuya lectura te recomendamos encarecidamente. Como complemento, en las siguientes secciones encontrarás diferentes estrategias para mejorar el diseño de tus prompt.

Diseñar estímulos eficaces con funciones, contextos y tareas

En se ha demostrado empíricamente que detallar tus avisos con una función, un contexto y una tarea, como se muestra en la Figura 4-1, mejora significativamente la eficacia de los avisos para tareas complejas.

Figura 4-1. Un aviso eficaz

Los tres elementos no siempre son necesarios, y su orden puede cambiarse, pero si tu estímulo está bien construido y los elementos están bien definidos, deberías obtener buenos resultados. Ten en cuenta que, incluso cuando se utilicen estos tres elementos, para tareas complejas puede que necesites utilizar técnicas más avanzadas, como el aprendizaje de cero disparos, el aprendizaje de pocos disparos y el ajuste fino. Estas técnicas avanzadas se tratarán más adelante en este capítulo.

El contexto

El primer elemento esencial en un texto de entrada es el contexto. Cuando escribas un texto de entrada para un LLM, debes detallar el contexto tanto como sea posible. Para ilustrarlo, supongamos que quieres utilizar la GPT-4 para crear una aplicación que te sugiera el plato principal para el almuerzo. A continuación, compararemos los resultados obtenidos con dos contextos diferentes. El primer contexto tendrá pocos detalles, y el segundo tendrá más detalles.

Con un breve mensaje de entrada como

chat_completion("Give me a suggestion for a main course for today's lunch.")

obtenemos el siguiente mensaje de salida:

Grilled chicken with roasted vegetables and quinoa.

Ahora, con más detalles sobre el contexto en el mensaje de entrada:

prompt = """
Context: I do 2 hours of sport a day. I am vegetarian, and I don't like green
vegetables. I am conscientious about eating healthily.
Task: Give me a suggestion for a main course for today's lunch.
"""
chat_completion(prompt)

obtenemos el siguiente mensaje de salida:

For today's lunch, you can try a Chickpea and Sweet Potato Curry served with
Quinoa. This dish is packed with protein, fiber, and essential nutrients,
while also being vegetarian and not relying on green vegetables. The curry
can be made with chickpeas, sweet potatoes, tomatoes, onions, garlic, and a
blend of spices like turmeric, cumin, and coriander. Cook the quinoa
separately and serve it alongside the curry for a delicious and healthy meal.

En el segundo ejemplo, la respuesta es más detallada porque el modelo tiene más contexto; sugiere un plato vegetariano sano y lleno de proteínas.

El contexto guía el "pensamiento" de GPT para añadir valor a su respuesta. Construir un estímulo con un buen contexto es un proceso iterativo, que a menudo requiere pasos de ensayo y error. Identificar los puntos esenciales que debe contener el contexto es a veces complicado. Para ayudarte a determinar posibles mejoras, puedes pedir sugerencias a GPT-4. En el siguiente ejemplo, cambiamos la sugerencia: en lugar de pedir al modelo que responda a nuestra pregunta, le indicamos que haga preguntas sobre el contexto en un esfuerzo por conseguir una mejor finalización.

Con el siguiente mensaje de entrada:

prompt = """
Context: I do 2 hours of sport a day. I am vegetarian, and I don't like green
vegetables. I am conscientious about eating healthily.
Task: Give me a suggestion for a main course for today's lunch?
Do not perform the requested task! Instead, can you ask me questions about the context so that when I answer, you can perform the requested task more
efficiently?
"""
chat_completion(prompt)

obtenemos las siguientes preguntas del modelo GPT-4:

Sure, I have a few questions to help me suggest a suitable main course for you:
1. What type of sport do you do, and do you have any specific nutritional
  requirements related to it?
2. Are there any other vegetables or ingredients that you particularly enjoy
  or dislike?
3. Do you prefer a hot or cold meal for lunch?
4. Are you open to trying new recipes or do you prefer sticking to
  familiar dishes?

Las preguntas que hace el modelo suelen ser relevantes. Basándote en estas preguntas, puedes actualizar el contexto en tu prompt. Por lo tanto, te aconsejamos que utilices este consejo a menudo cuando construyas tu prompt.

La tarea

La tarea define tu uso del modelo GPT-4 y debe estar bien definida y ser específica. Debes proporcionar información suficiente para la tarea y utilizar frases adecuadas en la indicación para guiar al modelo hacia el resultado deseado.

En la sección anterior, la tarea consistía en que el modelo sugiriera un plato principal para el almuerzo. Podemos añadir detalles a esa tarea para aclarar lo que queremos. Aquí, pedimos al modelo que añada una tabla con los ingredientes y las cantidades necesarias de cada uno en gramos:

prompt = """
Context: I do 2 hours of sport a day. I am vegetarian, and I don't like green
vegetables. I am conscientious about eating healthily.
Task: Give me a suggestion for a main course for today's lunch.
With this suggestion, I also want a table with two columns where each row
contains an ingredient from the main course.
The first column in the table is the name of the ingredient.
The second column of the table is the number of grams of that ingredient needed
for one person. Do not give the recipe for preparing the main course.
"""
chat_completion(prompt)

El modelo nos da el siguiente resultado:

Suggestion for today's lunch: Stuffed Bell Peppers with Quinoa and Black Beans
| Ingredient          | Grams per person |
|---------------------|------------------|
| Bell peppers        | 200g             |
| Quinoa              | 60g              |
| Black beans         | 75g              |
| Onion               | 50g              |
| Garlic              | 5g               |
| Olive oil           | 10g              |
| Tomato sauce        | 50g              |
| Cumin               | 1g               |
| Chili powder        | 1g               |
| Salt                | 1g               |
| Pepper              | 1g               |
| Shredded cheese     | 30g              |

En la página de ejemplos de la API OpenAI hay una lista con múltiples ejemplos de tareas que pueden realizar los modelos GPT, siempre con la indicación asociada y una demostración. Los ejemplos ilustran muy bien cómo asignar una tarea a los modelos de OpenAI. No los repasaremos todos aquí, pero aquí tienes algunos:

Corrección gramatical
Corrige las frases de al inglés estándar.

Prompt:

Correct this to standard English: She no went to the market.
Resumir para un alumno de segundo curso
Traduce texto complejo en conceptos más sencillos.

Prompt:

Summarize this for a second-grade student: Jupiter is the fifth planet [...]
Explica el código
Explica un código complejo en un lenguaje comprensible.

Prompt:

You will be provided with a piece of code, and your task is to explain it in a concise way.
Calcula la complejidad temporal
Encuentra en la complejidad temporal de una función.

Prompt:

# Python 3
def hello(x, n):
     for i in range(n):
        print('hello '+str(x))
# The time complexity of this function is
Corrector de errores de Python
Corrige el código de que contenía un error.

Prompt:

### Buggy Python
def hello(x, n):
     for i in rang(n):
        print('hello '+str(x))
### Fixed Python
Convierte el lenguaje natural en consultas SQL
Dada una definición de tablas, genera consultas SQL a partir del lenguaje natural.

Prompt:

Given the following SQL tables, your job is to write queries given a user’s request.
Resumidor de notas de reunión
Resume las notas de una reunión.

Prompt:

Convert my shorthand into a first-hand account of the meeting:
Tom: Profits up 50%
Jane: New servers are online
Kjel: Need more time to fix software
Creador de hojas de cálculo
Crea conjuntos de datos en formato CSV.

Prompt:

Create a two-column CSV of top science fiction movies along with the year of release.

El papel

Una última forma de influir en el modelo mientras escribes la petición es darle un papel. Por supuesto, el papel y el contexto pueden utilizarse de forma independiente, pero el uso de ambos aumenta tu control sobre el resultado del modelo. En el siguiente ejemplo, ponemos al modelo en el papel de un experto en nutrición deportiva:

prompt = """
Role: You are a nutritionist designing healthy diets for high-performance
athletes. You take into account the nutrition needed for a good recovery.
Context: I do 2 hours of sport a day. I am vegetarian, and I don't like green
vegetables. I am conscientious about eating healthily.
Task: Based on your expertise, give me a suggestion for a main course
for today's lunch. With this suggestion, I also want a table with
two columns where each row in the table contains an ingredient from the main
course. The first column in the table is the name of the ingredient.
The second column of the table is the number of grams of that ingredient needed
for one person. Do not give the recipe for preparing the main course.
"""
chat_completion(prompt)

Y obtenemos el siguiente resultado:

Main Course Suggestion: Quinoa and Chickpea Stuffed Bell Peppers
Ingredient Table:
| Ingredient          | Grams per Person |
|---------------------|------------------|
| Quinoa              | 60               |
| Chickpeas           | 100              |
| Bell Peppers        | 200              |
| Olive Oil           | 10               |
| Onion               | 50               |
| Garlic              | 5                |
| Tomato              | 100              |
| Feta Cheese         | 30               |
| Kalamata Olives     | 20               |
| Fresh Basil         | 5                |
| Salt                | 2                |
| Black Pepper        | 1                |

Como has visto, las indicaciones pueden utilizarse para condicionar el conjunto de distribuciones probabilísticas de los LLM, como los modelos GPT. Pueden verse como una guía que dirige al modelo para que produzca un tipo concreto de resultado. Aunque no existe una estructura definitiva para el diseño de las instrucciones, un marco útil a tener en cuenta es la combinación de contexto, función y tarea.

Nota

Es importante comprender que éste es sólo un enfoque, y que se pueden crear avisos sin definir explícitamente estos elementos. Algunas instrucciones pueden beneficiarse de una estructura diferente o pueden requerir un enfoque más creativo basado en las necesidades específicas de tu aplicación. Por lo tanto, este marco contexto-función-tarea no debe limitar tu pensamiento, sino que debe ser una herramienta que te ayude a diseñar eficazmente tus prompts cuando sea apropiado.

Pensar paso a paso

Como sabemos en, la GPT-4 no sirve para calcular. No puede calcular 369 × 1.235, por ejemplo:

prompt = "How much is 369 * 1235?"
chat_completion(prompt)

Esta pregunta produce la siguiente respuesta: 454965.

La respuesta correcta es 455.715. ¿No puede la GPT-4 resolver problemas matemáticos complejos? Recuerda que el modelo formula esta respuesta prediciendo cada ficha de la respuesta secuencialmente, empezando por la izquierda. Esto significa que GPT-4 genera primero el dígito situado más a la izquierda, luego lo utiliza como parte del contexto para generar el siguiente dígito, y así sucesivamente, hasta formar la respuesta completa. El reto aquí es que cada número se predice independientemente del valor correcto final. GPT-4 considera los números como fichas; no hay lógica matemática.

En el Capítulo 5, exploraremos cómo OpenAI ha enriquecido la GPT-4 con plug-ins. Un ejemplo es un complemento de calculadora para proporcionar soluciones matemáticas precisas.

Nota

Cada nueva versión de GPT-4 es capaz de realizar multiplicaciones cada vez más complejas, pero el problema sigue siendo el mismo: la forma en que calcula prediciendo la siguiente ficha no es óptima.

Existe un truco para aumentar la capacidad de razonamiento de los modelos lingüísticos. Por ejemplo, cuando pedimos al modelo que resuelva 369 × 1.235, podemos ver que intenta responder directamente de un tirón. Piensa que probablemente tú tampoco serías capaz de resolver esta multiplicación sin la ayuda de un lápiz y una hoja de papel para hacer los cálculos. Es posible animar al modelo a hacer razonamientos intermedios mediante la indicación. Y al igual que tú con tu lápiz y tu papel, el modelo puede resolver problemas más complejos si le das tiempo para razonar.

Se ha demostrado empíricamente que añadir Pensemos paso a paso al final de la indicación permite al modelo resolver problemas de razonamiento más complicados. Esta técnica, denominada estrategia zero-shot-CoT, se introdujo en el artículo científico "Large Language Models Are Zero-Shot Reasoners" de Kojima et al., publicado en 2022.

CoT significa cadena de pensamiento; se refiere aquí a la técnica de utilizar indicaciones que animan al modelo a imitar el razonamiento paso a paso. El término zero-shot significa que el modelo no depende de ejemplos de tareas específicas para realizar este razonamiento; está preparado para manejar nuevas tareas basándose en su entrenamiento general. A diferencia de técnicas como el aprendizaje de pocos disparos (que examinaremos en breve), que requieren que el modelo haya visto ejemplos de la tarea, el aprendizaje de disparo cero intenta generalizar sin necesitar ejemplos específicos de la tarea.

Como veremos, con esta frase añadida al prompt, el modelo empieza a razonar dividiendo el problema en subproblemas. Como resultado, puede tardar en razonar, lo que le permite encontrar soluciones a problemas que antes no podía resolver de una sola vez. Veamos qué ocurre si cambiamos la instrucción de entrada:

prompt = "How much is 369 * 1235 ? Let's think step by step."
chat_completion(prompt)

Obtenemos el siguiente resultado:

Step 1: Multiply 369 by 5 (the last digit of 1235).
369 * 5 = 1845
Step 2: Multiply 369 by 30 (the second last digit of 1235 is 3, so multiply by
   3 and add a 0 at the end).
369 * 30 = 11070
Step 3: Multiply 369 by 200 (the third digit of 1235 is 2, so multiply by 2 and
   add two 0s at the end).
369 * 200 = 73800
Step 4: Multiply 369 by 1000 (the first digit of 1235 is 1, so multiply by 1 and
   add three 0s at the end).
369 * 1000 = 369000
Step 5: Add the results from steps 1 to 4.
1845 + 11070 + 73800 + 369000 = 455715
So, 369 * 1235 = 455,715.

Se ha producido un cambio notable en el enfoque del modelo. Ahora descompone el problema principal en pasos más pequeños y manejables, en lugar de intentar abordar el problema de frente. Y, al menos en este caso, encuentra la respuesta correcta.

Advertencia

A pesar de indicarle al modelo que "piense paso a paso", debes evaluar cuidadosamente sus respuestas, ya que la GPT-4 no es infalible. Para un cálculo más complejo, como 3.695 × 123.548, el LLM no es capaz de encontrar la solución correcta (en el momento de escribir esto) ni siquiera con este truco.

Por supuesto, es difícil saber a partir de un solo ejemplo si este truco funciona en general o si simplemente hemos tenido suerte. En puntos de referencia con diversos problemas matemáticos, los experimentos empíricos han demostrado que este truco aumentaba significativamente la precisión de los modelos GPT. Aunque el truco funciona bien para la mayoría de los problemas matemáticos, no es práctico para todas las situaciones. Los autores de "Large Language Models Are Zero-Shot Reasoners" descubrieron que era más beneficioso para los problemas aritméticos de varios pasos, los problemas que implican razonamiento simbólico, los problemas que implican estrategia y otros problemas que implican razonamiento. No lo encontraron útil para problemas de sentido común.

Implantar el Aprendizaje de Pocos Tiros

El aprendizaje de pocos intentos, también conocido en como "few-shot prompting", es un paradigma en el que el modelo recibe, a través de una indicación, unos cuantos ejemplos de la tarea que quieres que realice el modelo, como se ilustra en la Figura 4-2. Estos ejemplos guían al modelo para procesar el formato de salida deseado. El aprendizaje de pocos ejemplos se introdujo en "Los modelos lingüísticos son aprendices de pocos ejemplos" de Brown et al. y se refiere a la capacidad del LLM para generalizar y producir resultados valiosos con sólo unos pocos ejemplos en el prompt.

Figura 4-2. Un aviso con algunos ejemplos

En este ejemplo, pedimos a la LLM que convierta palabras concretas en emojis. Es difícil imaginar las instrucciones que hay que poner en un prompt para realizar esta tarea. Pero con el aprendizaje de pocos intentos, es fácil. Dale ejemplos, y el modelo intentará reproducirlos automáticamente:

prompt = """
I go home -->  go 
my dog is sad --> my  is 
I run fast -->  run 
I love my wife -->   my wife
the girl plays with the ball --> the   with the 
The boy writes a letter to a girl --> 
"""
chat_completion(prompt)

Del ejemplo anterior, obtenemos como salida el siguiente mensaje:

The   a  to a 

La técnica de aprendizaje de pocos disparos proporciona ejemplos de entradas con las salidas deseadas. Luego, en la última línea, proporcionamos la instrucción para la que queremos una finalización. Este mensaje tiene la misma forma que los ejemplos anteriores. Naturalmente, el modelo lingüístico realizará una operación de compleción teniendo en cuenta el patrón de los ejemplos dados.

Podemos ver que con sólo unos pocos ejemplos, el modelo puede reproducir las instrucciones. Aprovechando el amplio conocimiento que han adquirido en su fase de entrenamiento, los LLM pueden adaptarse rápidamente y generar respuestas precisas basándose sólo en unos pocos ejemplos.

Nota

El aprendizaje en pocos pasos es un aspecto poderoso de los LLM, porque les permite ser muy flexibles y adaptables, y sólo necesitan una cantidad limitada de información adicional para realizar diversas tareas.

Cuando proporciones ejemplos en la instrucción, es esencial que te asegures de que el contexto es claro y relevante. Los ejemplos claros mejoran la capacidad del modelo para ajustarse al formato de salida deseado y ejecutar el proceso de resolución de problemas. Por el contrario, los ejemplos inadecuados o ambiguos pueden conducir a resultados inesperados o incorrectos. Por lo tanto, redactar los ejemplos con cuidado y asegurarse de que transmiten la información correcta puede influir significativamente en la capacidad del modelo para realizar la tarea con precisión.

Otro enfoque para guiar a los LLM es aprendizaje de un solo ejemplo. Como su nombre indica, en este caso sólo proporcionas un ejemplo para ayudar al modelo a ejecutar la tarea. Aunque este enfoque proporciona menos orientación que el aprendizaje de pocos intentos, puede ser eficaz para tareas más sencillas o cuando el LLM ya tiene un conocimiento previo sustancial sobre el tema. Las ventajas del aprendizaje de una sola vez son la sencillez, la generación más rápida de instrucciones y el menor coste computacional y, por tanto, de API. Sin embargo, para tareas o situaciones complejas que requieren un conocimiento más profundo del resultado deseado, el aprendizaje de pocos intentos podría ser un enfoque más adecuado para garantizar resultados precisos.

Consejo

La ingeniería de prompts se ha convertido en un tema de moda, y encontrarás muchos recursos online para profundizar en el tema. Por ejemplo, este repositorio de GitHub contiene una lista de avisos eficaces aportados por más de 70 usuarios diferentes.

Aunque en esta sección se han explorado varias técnicas de ingeniería de prompts que puedes utilizar individualmente, ten en cuenta que puedes combinarlas para obtener resultados aún mejores. Como desarrollador, tu trabajo consiste en encontrar el indicador más eficaz para tu problema concreto. Recuerda que la ingeniería de prompts es un proceso iterativo de experimentación de prueba y error.

Perfeccionamiento iterativo con la opinión del usuario

La técnica de refinamiento iterativo consiste en mejorar progresivamente un prompt de entrada inicial mediante iteraciones repetidas. En este método, se utiliza el LLM para reescribir el prompt inicial, con el objetivo de producir una versión mejor en cada iteración. Al pedir continuamente al LLM que optimice la instrucción, este proceso puede mejorar significativamente el rendimiento del modelo.

En este libro, presentamos un sistema creador de avisos diseñado sobre el refinamiento iterativo, que incorpora la retroalimentación del usuario combinada con un multiagente en el que los LLM se comportan como agentes. Este sistema, que llamaremos Promptor, se caracteriza por su capacidad para evaluar y mejorar los avisos mediante este proceso cíclico. En Promptor, el proceso está dirigido por tres agentes: el revisor, el interrogador y el creador de avisos.

Nota

Un agente se crea utilizando un LLM con una indicación específica, dándole un objetivo, y pidiéndole que alcance su objetivo realizando diferentes acciones y pasos. Los agentes se detallan en el Capítulo 5.

La Figura 4-3 proporciona un diagrama visual paso a paso de cómo interactúan estos agentes. El aviso inicial se coloca primero en la variable de aviso actual. Esta pregunta actual se envía al agente revisor (marcado con 1 en la Figura 4-3), que evalúa la calidad de la pregunta actual e identifica formas de mejorarla. A continuación, el agente interrogador (marcado con 2) utiliza la pregunta actual y la revisión de la misma para generar preguntas que se presentan al usuario final. La salida generada por este agente incluye las preguntas y las respuestas del usuario. Por último, el aviso actual, la revisión y las preguntas/respuestas del usuario se envían al agente creador de avisos (marcado con 3). Este agente utiliza toda esta información, así como su conocimiento de las buenas prácticas de ingeniería de prompts, para generar el mejor nuevo prompt posible. Este nuevo aviso se presenta entonces al usuario final, y o bien está satisfecho con el resultado o puede iterar de nuevo para generar un nuevo aviso.

Figura 4-3. Tres agentes actúan para construir una pronta mejor

El siguiente código muestra la función principal de Promptor. La variable prompt_​ini⁠tialization es una cadena que contiene un prompt compartido con los tres agentes, en el que posicionamos a nuestro LLM como experto en ingeniería de prompts. Observa que le decimos a nuestro modelo que se llame a sí mismo Promptor, no GPT. Lo hacemos para evitar ambigüedades, ya que el aviso que genera puede contener la palabra "GPT", y tenemos que evitar que el Promptor piense que ese aviso habla de sí mismo. Por eso decimos claramente Promptor y GPT son entidades separadas y distintas. Tras definir la variable de cadena prompt_​initiali⁠zation, esta función hace un bucle de iteración entre los tres agentes: the_reviewer, the_questioner, y the_prompt_maker. Antes de iniciar una nueva iteración, la función pregunta al usuario si está de acuerdo con el indicador actual. Si no es así, se inicia una nueva iteración:

def promptor(initial_prompt, max_nb_iter=3):

    print(f"Your initial prompt: {initial_prompt}")

    prompt_initialization = """
    Your name is now 'Promptor' and that is how I will address you from now on. 
    Promptor and GPT are separate and distinct entities.
    You are an expert in prompt engineering and large language models. 
    A good prompt should assign one or many roles to GPT, define a clear context
    and task, and clarify expected output. You know and use many prompt
    techniques such as Few-Shot Learning, Prompt Chaining, Shadow Prompting, ...
    I want you to be my personal prompt creator expert. 
    You, Promptor, are responsible for creating good prompts for GPT.
    """

    current_prompt = initial_prompt
    questions_answers = ""
    for i in range(max_nb_iter):

        print(f"Loop {i+1}")
        reviews = the_reviewer(prompt_initialization, current_prompt)
        questions_answers = the_questioner(
            prompt_initialization, current_prompt, reviews, questions_answers)
        current_prompt = the_prompt_maker(
            prompt_initialization, current_prompt, reviews, questions_answers)

        print(f"\nNew current prompt: {current_prompt}\n\n")
        keep = input(f"Do you want to keep this prompt (y/n)? ")
        if keep == 'y':
            break

    return current_prompt

A continuación definiremos los tres agentes.

El primer agente es el revisor, que realiza una crítica a una solicitud de entrada dada. Esta función utiliza el LLM para devolver un texto que contiene una puntuación y una crítica de la pregunta:

def the_reviewer(prompt_initialization, current_prompt):

    prompt_reviewer = prompt_initialization + "\n\n"
    prompt_reviewer += f"This is my prompt: {current_prompt}\n\n"
    prompt_reviewer += """
    Task: Provide a detailed, rigorous critique of my prompt.
    To do this, first start by giving my prompt a score from 0 to 5 
    (0 for poor, 5 for very optimal), and then write a short paragraph 
    detailing improvements that would make my prompt a perfect prompt 
    with a score of 5."""

    reviews = chat_completion(prompt_reviewer)

    return reviews

El cuestionador identifica las áreas clave de mejora y plantea preguntas al usuario final. Observa que response_format se utiliza para forzar al modelo a devolver el texto en formato JSON en la variable questions_json. Utilizamos el modelo gpt-4-1106-preview porque los anteriores no admiten muy bien el formato de salida JSON. A continuación, las preguntas se extraen de questions_json y se plantean al usuario final. Los nuevos pares pregunta/respuesta se añaden a la variable questions_answers. Ten en cuenta que pasamos esta variable como entrada a la función porque en ella se almacenan los pares pregunta/respuesta de bucles anteriores:

def the_questioner(
        prompt_initialization, current_prompt, reviews, questions_answers):

    prompt_questioner = prompt_initialization + "\n\n"
    prompt_questioner += f"This is my prompt: {current_prompt}\n\n"
    prompt_questioner += f"A critical review of my prompt:{reviews}\n\n"
    prompt_questioner += """Task: Compile a list of maximum 4 short questions 
    whose answers are indispensable for improving my prompt (also give examples 
    of answers in baskets.). 
    Output format: In JSON format. The output must be accepted by json.loads. 
    The json format should be similar to: 
    {'Questions': ['Question 1','Question 2','Question 3','Question 4']}"""

    questions_json = chat_completion(
        prompt_questioner, model="gpt-4-1106-preview",
        response_format={"type": "json_object"})

    try:
        questions = json.loads(questions_json).get('Questions', [])
    except json.JSONDecodeError:
        print("Failed to decode questions from the model's response.")
        questions = []

    for i, question in enumerate(questions, start=1):
        answer = input(f"Question {i}: {question} ")
        questions_answers = questions_answers + \
            f"Question: {question}\nAnswer:{answer}\n\n"

    return questions_answers

El último agente es el hacedor de avisos, que revisa los avisos basándose en la información y los comentarios de los otros dos agentes:

def the_prompt_maker(
        prompt_initialization, current_prompt, reviews, questions_answers):

    prompt = prompt_initialization + "\n\n"
    prompt += f"This is my current prompt: {current_prompt}\n\n"
    prompt += f"This is critical review of my current prompt:{reviews}\n\n"
    prompt += f"Some questions and answers for improving my current prompt:
    {questions_answers}\n\n"
    prompt += """Task: With all of this information, use all of your prompt 
    engineering expertise to rewrite my current prompt in the best possible 
    way to create a perfect prompt for GPT with a score of 5. All the 
    information contained in the questions and answers must be included in 
    the new prompt. Start the prompt by assigning one or many roles to GPT, 
    defining the context, and the task.
    Output: It's very important that you only return the new prompt for GPT 
    that you've created, and nothing else."""

    new_prompt = chat_completion(prompt)
    return (new_prompt)

El código siguiente muestra un ejemplo en el que se utiliza esta función de Promptor, en el que la consulta inicial es Dame una sugerencia para el plato principal de la comida de hoy. Al final, la nueva sugerencia mejorada generada por Promptor se envía a chat_completion para obtener el resultado:

prompt = promptor(
    "Give me a suggestion for the main course for today's lunch.",
    max_nb_iter=3)
res = chat_completion(prompt)
print(res)

A continuación, mostramos la ejecución con dos iteraciones:

Your initial prompt: Give me a suggestion for the main course for today's lunch.
Loop 1
Question 1: Do you have any dietary restrictions or preferences? With protein
Question 2: What type of cuisine are you in the mood for today? Belgian
Question 3: Are there any specific ingredients you would like to use or avoid in your meal? no
Question 4: How much time are you willing to spend on preparing the lunch? It can be complex, I have time to cook

New current prompt: "GPT, as a knowledgeable culinary advisor with expertise in Belgian cuisine, I need your help. I'm looking for a main course for today's lunch that is rich in protein. I have no specific ingredients to avoid and I'm open to complex recipes as I have ample time to cook. Please suggest a protein-rich, Belgian-inspired main course that I can prepare for lunch today."

Do you want to keep this prompt (y/n)? n
Loop 2
Question 1: What is your cooking skill level? Expert
Question 2: Do you have any specific cooking equipment or appliances? no
Question 3: Are there any types of protein you prefer or would like to feature in your meal? Chicken, beef, or pork
Question 4: Do you have a preference for any specific Belgian dishes or ingredients? no

New current prompt: "GPT, don your chef's hat and prepare to whisk me away on a culinary journey to Belgium! As an expert in Belgian cuisine, I'm relying on your knowledge to craft a protein-packed main course for my lunch today. I'm an expert cook, so feel free to suggest complex recipes. I have no specific cooking equipment or appliances, but I have ample time to prepare the meal. I'm particularly interested in dishes featuring chicken, beef, or pork, but I'm open to other protein sources as well. I don't have any specific Belgian dishes or ingredients in mind, so surprise me! Remember, I have no dietary restrictions or ingredients to avoid. Let's create a Belgian-inspired, protein-rich culinary masterpiece together!"

Do you want to keep this prompt (y/n)? y

Este sistema Promptor demuestra que el perfeccionamiento iterativo con los comentarios de los usuarios y un enfoque multiagente potencian la colaboración entre los LLM y los usuarios para mejorar la ingeniería de prompts. Al utilizar las distintas capacidades de los agentes revisor, interrogador y creador de avisos, esta metodología facilita un proceso dinámico y centrado en el usuario para optimizar los avisos.

Consejo

Todo este código está disponible en el repositorio GitHub del libro.

Mejorar la eficacia de los avisos

En hemos visto varias técnicas de ingeniería de prompts que nos permiten influir en el comportamiento de los modelos GPT para obtener mejores resultados que satisfagan nuestras necesidades. Terminaremos esta sección con algunos consejos y trucos más que puedes utilizar en distintas situaciones al escribir prompts para modelos GPT.

Indica al modelo que haga más preguntas

Terminar los avisos preguntando al modelo si ha entendido la pregunta e indicándole que haga más preguntas es una técnica eficaz si estás construyendo una solución basada en un chatbot. Puedes añadir un texto como éste al final de tus indicaciones:

Did you understand my request clearly? If you do not fully understand my request,
ask me questions about the context so that when I answer, you can
perform the requested task more efficiently.

Formatear la salida

A veces querrás utilizar la salida LLM en un proceso más largo; en esos casos, el formato de salida importa. Por ejemplo, si quieres una salida JSON, el modelo tiende a escribir la salida antes y después del bloque JSON. Si añades la indicación La salida debe ser aceptada por json.loads, tiende a funcionar mejor. Este tipo de truco puede utilizarse en muchas situaciones. Como vimos en el Capítulo 2, puedes utilizar la opción response_format en los parámetros de client.chat.completions.create() para forzar al modelo a producir JSON.

Para obtener la salida en el formato solicitado, debes hacer ambas cosas: pedir un formato de salida JSON en el prompt, y especificar el tipo de salida con la opción response_format. Esto es lo que hacemos en el siguiente script. Ten en cuenta también que para utilizar este response_​for⁠mat, necesitas utilizar un modelo de la familia GPT-4 Turbo, así que cambiamos el modelo a gpt-4-1106-preview.

Por ejemplo, con este script:

prompt = """
Give a JSON output with 5 names of animals. The output must be accepted
by json.loads.
"""
chat_completion(prompt, model='gpt-4-1106-preview', response_format={"type": "json_object"})

obtenemos el siguiente bloque de código JSON:

{
  "animals": [
    "lion",
    "tiger",
    "elephant",
    "giraffe",
    "zebra"
  ]
}

Repite las instrucciones

Se ha comprobado empíricamente que repetir instrucciones da buenos resultados, sobre todo cuando la instrucción es larga. La idea es añadir varias veces la misma instrucción al prompt, pero formularla de forma diferente cada vez.

Esto también puede hacerse con indicaciones negativas.

Utiliza indicaciones negativas

Indicaciones negativas en el contexto de la generación de texto son una forma de guiar al modelo especificando lo que no quieres ver en la salida. Actúan como restricciones o directrices para filtrar determinados tipos de respuestas. Esta técnica es especialmente útil cuando la tarea es complicada: los modelos tienden a seguir las instrucciones con mayor precisión cuando las tareas se repiten varias veces de distintas formas.

Siguiendo con el ejemplo anterior, podemos insistir en el formato de salida con indicación negativa añadiendo No añadir nada antes ni después del texto json.

En el capítulo 3, utilizamos indicaciones negativas en el tercer proyecto:

Extract the keywords from the following question: {user_question}. Do not answer
anything else, only the keywords.

Sin este añadido a la indicación, el modelo tendía a no seguir las instrucciones.

Añadir restricciones de longitud

Una restricción de longitud suele ser una buena idea; si esperas una respuesta de una sola palabra o 10 frases, añádelo a tu pregunta. Esto es lo que hicimos en el Capítulo 3 en el primer proyecto: especificamos LONGITUD: 100 palabras para generar un artículo de noticias adecuado. En el cuarto proyecto, nuestra pregunta también tenía una instrucción de longitud: Si puedes responder a la pregunta CONTESTA, si necesitas más información MÁS, si no puedes responder: OTRO. Responde sólo con una palabra. Sin esta última frase, el modelo tendería a formular frases en lugar de seguir las instrucciones. Ten en cuenta que no hay garantía de que el modelo respete con precisión esta instrucción, sobre todo si tu pregunta es larga y compleja; no confíes exclusivamente en esta técnica para la gestión de costes.

Encadenamiento de avisos

Divide las tareas en sus subtareas. A continuación, se solicita al LLM una subtarea, y su respuesta se utiliza como entrada para otra solicitud de la siguiente subtarea. Creas una cadena de operaciones de consulta.

Por ejemplo, considera un escenario de planificación de un viaje multidestino. La primera pregunta podría pedir sugerencias de destinos. La siguiente pregunta podría utilizar el destino elegido para preguntar sobre opciones de alojamiento. La última pregunta pide ideas para el itinerario.

Si es necesario, pide al modelo GPT que te ayude a identificar la subtarea con la siguiente pregunta:

You are a prompt engineering expert with a deep knowledge of prompt chaining techniques. Break the following task into its subtasks. List the subtasks in short bullet points only; no more text.
The task:[...]

Sombra incitadora

En, no indicas directamente las tareas, sino que incrustas sutilmente pistas en las indicaciones para guiar a los modelos hacia los resultados deseados. En algunos casos, esto puede fomentar la creatividad. En lugar de decir Escribe un cuento de miedo para niños, una indicación en la sombra podría ser: Imagina que estamos alrededor de una hoguera en un bosque oscuro y misterioso. ¿Qué cuento contarías para provocar un escalofrío a tu joven audiencia? Este enfoque empuja al modelo hacia la elaboración de una historia de miedo atractiva y apropiada para su edad.

Las técnicas de incitación parecen interminables, y puede ser difícil trabajar con todos estos consejos y trucos. Trabajar en el perfeccionamiento iterativo mientras se evalúan los resultados es siempre el mejor enfoque. Algunas herramientas y marcos de trabajo pueden ayudarte a crear, gestionar y realizar un seguimiento de tus estímulos. En el momento de escribir esto, recomendamos echar un vistazo a promptfoo, una CLI y una biblioteca para el desarrollo de LLM dirigidos por pruebas, y a DSPy, un marco que pretende gestionar el proceso de optimización de las indicaciones para que los desarrolladores interactúen con los LLM con código en lugar de con indicaciones.

Ajuste fino

OpenAI proporciona muchos modelos GPT listos para usar. Aunque estos modelos destacan en un amplio abanico de tareas, afinarlos para tareas o contextos específicos puede mejorar aún más su rendimiento.

Se ha demostrado que el ajuste fino funciona bien para:

  • Ajustar el tono y el estilo del LLM

  • Ayudar al LLM a centrarse en los conocimientos específicos del ámbito que ya posee

  • Mejorar la fiabilidad y limitar las alucinaciones

  • Realizar tareas complejas que son difíciles de describir en un aviso

  • Cambiar el formato de salida del modelo (por ejemplo, de lenguaje natural a JSON)

En los próximos párrafos, nos sumergiremos en un escenario de ejemplo y recorreremos todos los pasos necesarios para construir y utilizar un modelo afinado.

Cómo empezar

Imaginemos que quieres crear un generador de respuestas de correo electrónico para tu empresa. Como tu empresa trabaja en un sector específico con un vocabulario concreto, quieres que las respuestas de correo electrónico generadas mantengan su estilo de escritura actual. Hay dos estrategias para conseguirlo: o bien puedes utilizar las técnicas de ingeniería de prompts introducidas anteriormente para forzar al modelo a producir el texto que deseas, o bien puedes ajustar un modelo existente. Esta sección explora la segunda técnica.

Para este ejemplo, debes recopilar un gran número de correos electrónicos que contengan datos sobre tu ámbito empresarial concreto, consultas de clientes y respuestas a esas consultas. A continuación, puedes utilizar estos datos para afinar un modelo existente con el fin de aprender los patrones lingüísticos y el vocabulario específicos de tu empresa. El modelo ajustado es esencialmente un nuevo modelo construido a partir de uno de los modelos originales proporcionados por OpenAI, en el que los pesos internos del modelo se ajustan para adaptarse a tu problema específico, de modo que el nuevo modelo aumente su precisión en tareas similares a los ejemplos que vio en el conjunto de datos proporcionado para el ajuste. Afinando un LLM existente, es posible crear un generador de respuestas de correo electrónico altamente personalizado y especializado, adaptado explícitamente a los patrones lingüísticos y a las palabras utilizadas en tu negocio concreto.

La Figura 4-4 ilustra el proceso de ajuste fino en el que se utiliza un conjunto de datos de un dominio específico para actualizar los pesos internos de un modelo GPT existente. El objetivo es que el nuevo modelo afinado haga mejores predicciones en el dominio concreto que el modelo GPT original. Hay que destacar que se trata de un nuevo modelo. Este nuevo modelo está en los servidores de OpenAI; como antes, debes utilizar la API de OpenAI para utilizarlo, ya que no se puede acceder a él localmente.

Figura 4-4. El proceso de ajuste
Advertencia

El ajuste fino suele ser un método más complejo y costoso que la ingeniería de prompts. Para elegir la estrategia correcta, consulta la sección "Elegir entre estrategias".

Incluso después de haber afinado un LLM con tus propios datos específicos, el nuevo modelo permanece en los servidores de OpenAI. Interactuarás con él a través de las API de OpenAI, no localmente.

Consejo

Los LLM de código abierto te permiten ejecutar modelos localmente. Esto te permite utilizar modelos de código abierto que hayas afinado localmente.

Adaptar los modelos GPT a las necesidades específicas de cada dominio

OpenAI añade y elimina regularmente modelos de la lista de ajuste fino. Actualmente, el ajuste fino está disponible para gpt-3.5-turbo-0125, gpt-3.5-turbo-1106, gpt-3.5-turbo-0613, babbage-002, davinci-002 y gpt-4-0613.

babbage-002 y davinci-002 pertenecen a la familia GPT de modelos básicos. Estos modelos han sido entrenados para predecir qué ficha vendrá a continuación en una instrucción, pero no han realizado el aprendizaje por refuerzo a partir de la retroalimentación humana (RLHF), presentado en el Capítulo 1, para aprender a seguir instrucciones. Cuando OpenAI propuso por primera vez el ajuste fino, sólo los modelos de esta familia estaban disponibles para el ajuste fino. Comparado con el davinci-002, el modelo babbage-002 es más pequeño, tiene menos parámetros y es más rápido. Estos modelos básicos están disponibles más bien con fines históricos, y no recomendamos utilizarlos para nuevos proyectos de ajuste fino.

gpt-3.5-turbo-0125, gpt-3.5-turbo-1106, y gpt-3.5-turbo-0613 son tres modelos de la familia GPT-3.5. gpt-3.5-turbo-0125 es el modelo que OpenAI recomienda que utilices para tus proyectos de ajuste.

gpt-4-0613 es el único modelo GPT-4 disponible para el ajuste fino, pero aún se considera experimental y debe utilizarse con precaución.

Nota

También puedes afinar un modelo que ya ha sido afinado. Esto puede ser útil si recibes nuevos datos y quieres actualizar tu modelo con ellos.

Ajuste fino frente a aprendizaje de pocos disparos

Ajuste fino es un proceso de reentrenamiento de un modelo existente en un conjunto de datos de una tarea específica para mejorar su rendimiento y hacer que sus respuestas sean más precisas. En el ajuste fino, actualizas los parámetros internos del modelo. Como vimos antes, el aprendizaje de pocos disparos proporciona al modelo un número limitado de buenos ejemplos a través de su indicador de entrada, que guía al modelo para que produzca los resultados deseados basándose en estos pocos ejemplos. Con el aprendizaje de pocos disparos, los parámetros internos del modelo no se modifican.

Tanto el ajuste fino como el aprendizaje de pocos disparos pueden servir para mejorar los modelos GPT. El ajuste fino produce un modelo altamente especializado que puede proporcionar resultados más precisos y contextualmente relevantes para una tarea determinada. Esto lo convierte en una opción ideal para casos en los que se dispone de una gran cantidad de datos. Esta personalización garantiza que el contenido generado se ajuste más a los patrones lingüísticos, el vocabulario y el tono específicos del dominio de destino.

El aprendizaje de pocos ejemplos es un enfoque más flexible y eficiente en cuanto a datos, porque no requiere volver a entrenar el modelo. Esta técnica es beneficiosa cuando se dispone de ejemplos limitados o se necesita una adaptación rápida a diferentes tareas. Para tareas sencillas, suelen bastar entre 5 y 10 ejemplos. El aprendizaje de pocos ejemplos permite a los desarrolladores crear rápidamente prototipos y experimentar con diversas tareas, lo que lo convierte en una opción versátil y práctica para muchos casos de uso. Otro criterio esencial para elegir entre los dos métodos es que utilizar y entrenar un modelo que utiliza el ajuste fino es más caro.

Los métodos de ajuste fino requieren un conjunto de datos de buena calidad. La falta de ejemplos disponibles suele limitar el uso de este tipo de técnicas. Para que te hagas una idea de la cantidad de datos necesarios para el ajuste fino, puedes suponer que para tareas relativamente sencillas o cuando sólo se requieren pequeños ajustes, puedes conseguir buenos resultados de ajuste fino con unos cientos de ejemplos de indicaciones de entrada y sus correspondientes terminaciones deseadas. Este enfoque funciona cuando el modelo GPT preentrenado ya funciona razonablemente bien en la tarea, pero necesita ligeros refinamientos para alinearse mejor con el dominio objetivo. Sin embargo, para tareas más complejas o en situaciones en las que tu aplicación necesita más personalización, puede que tu modelo necesite utilizar muchos miles de ejemplos para el entrenamiento. Esto puede corresponder, por ejemplo, al caso de uso que propusimos antes, con la respuesta automática a un correo electrónico que respeta tu estilo de escritura. También puedes hacer un ajuste fino para tareas muy especializadas, para las que tu modelo puede necesitar cientos de miles o incluso millones de ejemplos. Esta escala de ajuste fino puede dar lugar a mejoras significativas del rendimiento y a una mejor adaptación del modelo al dominio específico.

Dado que el ajuste fino puede resultar caro rápidamente, se recomienda probar el modelo con un pequeño conjunto de ejemplos antes de ajustarlo con grandes cantidades de datos. Si el modelo no mejora tras el ajuste fino en este pequeño conjunto de datos, puede que sea necesario reconsiderar la tarea o la estructura de datos antes de escalar más allá de estos ejemplos limitados.

Nota

El aprendizaje por transferencia aplica los conocimientos aprendidos en un dominio a un entorno diferente pero relacionado. Por eso, a veces oirás el término aprendizaje por transferencia en relación con el ajuste.

Ajuste fino con la API de OpenAI

Esta sección de te guía a través del proceso de ajuste de un LLM utilizando la API de OpenAI. Te explicaremos cómo preparar tus datos, cargar conjuntos de datos y crear un modelo afinado utilizando la API.

Preparar tus datos

Para actualizar un modelo LLM, es necesario proporcionar un conjunto de datos con ejemplos. El conjunto de datos debe estar en un archivo JSONL. Este tipo de archivo es un archivo de texto, en el que cada línea representa un único objeto JSON. Puedes utilizarlo para almacenar grandes cantidades de datos de forma eficiente.

Hay dos casos: o bien afinas un modelo de finalización, como babbage-002 o davinci-002, o bien afinas un modelo de chat.

En el primer caso, cada fila del conjunto de datos debe corresponder a un par de prompts y compleciones:

{"prompt": "<prompt text>", "completion": "<completion text>"}
{"prompt": "<prompt text>", "completion": "<completion text>"}
{"prompt": "<prompt text>", "completion": "<completion text>"}
…

En el segundo caso, cuando se afina un modelo de chat, cada fila del conjunto de datos debe corresponder a una lista de mensajes en la que cada elemento de la lista de mensajes es un diccionario con dos claves: un rol y un contenido. Aunque no es obligatorio, el primer mensaje de la lista suele tener el rol system, y los demás mensajes que le siguen son sucesiones del rol user seguidas del rol assistant:

{"messages": [{"role": "<role text>", "content": "<content text>"}, {"role": "<role text>", "content": "<content text>"}, …]}

{"messages": [{"role": "<role text>", "content": "<content text>"}, {"role": "<role text>", "content": "<content text>"}, …]}

{"messages": [{"role": "<role text>", "content": "<content text>"}, {"role": "<role text>", "content": "<content text>"}, …]}

…

En el resto de esta sección, nos centraremos en el segundo enfoque, ya que es el que recomienda ahora OpenAI.

Si tienes suficientes datos, es una buena práctica dividirlos en conjuntos de entrenamiento y de validación. El algoritmo utilizará los datos de entrenamiento para cambiar los parámetros del modelo durante el ajuste fino. El conjunto de validación puede utilizarse entonces para medir el rendimiento del modelo en un conjunto de datos que no se ha utilizado para actualizar los parámetros.

Consejo

El ajuste fino de un LLM se beneficia del uso de ejemplos de alta calidad, idealmente revisados por expertos. Cuando afines con conjuntos de datos preexistentes, asegúrate de que los datos se examinan en busca de contenido ofensivo o inexacto; examina muestras aleatorias si el conjunto de datos es demasiado grande para revisar todas las entradas manualmente.

Hacer que tus datos estén disponibles

Una vez preparado tu conjunto de datos con los ejemplos de entrenamiento, tienes que subirlo a los servidores de OpenAI. La API de OpenAI proporciona diferentes funciones para manipular archivos. Aquí tienes las más importantes:

  • Subir un archivo:

    client.files.create(
      file=open('training.jsonl', 'rb'),
      purpose='fine-tune'
    )

    Hay dos parámetros obligatorios: file y purpose. Establece purpose en fine-tune. Esto valida el formato del archivo descargado para su ajuste. La salida de esta función es un diccionario en el que puedes recuperar el file_id en el campo id. Actualmente, el tamaño total del archivo puede ser de hasta 1 GB. Para más información, debes ponerte en contacto con OpenAI.

  • Borrar un archivo:

    client.files.delete('file-z5mGg(...)')

    Un parámetro es obligatorio: file_id.

  • Listar todos los archivos subidos:

    client.files.list()

    Puede ser útil recuperar el ID de un archivo, por ejemplo, cuando inicies el proceso de ajuste.

  • Recuperar el contenido de un archivo:

    client.files.content('file-z5mGg(...)')

    Un parámetro es obligatorio: file_id. Recibimos una respuesta binaria de esta función. Suponiendo que content_bin es una variable con la respuesta binaria de la función, para ver este contenido en Python, debes convertir este contenido binario en una cadena: content_str = content_bin.read().decode('utf-8').

Crear un modelo afinado

Afinar con un archivo cargado es un proceso sencillo. El método cli⁠ent​.fine_tuning.jobs.create crea un trabajo en los servidores de OpenAI para afinar un modelo especificado a partir de un conjunto de datos dado. La respuesta de esta función contiene los detalles del trabajo en cola, incluido el estado del trabajo, el fine_tuning_job_id.

Los principales parámetros de entrada se describen en la Tabla 4-1.

Tabla 4-1. Parámetros para client.fine_tuning.jobs.create
Nombre del campo Tipo Descripción
train⁠ing_​file Cadena Éste es el único parámetro obligatorio que contiene la dirección file_id del archivo cargado. Tu conjunto de datos debe tener formato de archivo JSONL. Cada ejemplo de entrenamiento es un objeto JSON.
model Cadena En el momento de escribir esto, puedes seleccionar gpt-3.5-turbo-0125 (recomendado), gpt-3.5-turbo-1106, gpt-3.5-turbo-0613, babbage-002, davinci-002, o gpt-4-0613 (experimental).
valida⁠tion_​file Cadena Contiene la dirección file_id del archivo cargado con los datos de validación. Si proporcionas este archivo, los datos se utilizarán para generar métricas de validación periódicamente durante el ajuste fino.
suffix Cadena Es una cadena de hasta 40 caracteres que se añade al nombre personalizado de tu modelo.

Listado de trabajos de ajuste

Es posible obtener una lista de todos tus trabajos de ajuste en los servidores de OpenAI mediante la siguiente función:

client.fine_tuning.jobs.list()

El resultado es un diccionario que contiene información sobre todos los modelos afinados.

Cancelar un trabajo de ajuste

Es posible interrumpir inmediatamente un trabajo que se esté ejecutando en los servidores OpenAI mediante la siguiente función:

client.fine_tuning.jobs.cancel('ftjob-(...)')

Esta función sólo tiene un parámetro obligatorio: fine_tuning_job_id. El parámetro fine_tuning_job_id es una cadena que empieza por ftjob-; por ejemplo, ftjob-Re12otqdRaJ(...). Se obtiene tras la creación de tu trabajo con la función client.fine_tuning.jobs.create(). Si has perdido tu fine_​tun⁠ing_job_id, puedes recuperarlo con client.fine_tuning.jobs.list().

Obtener actualizaciones de estado para un trabajo de ajuste

La siguiente función es útil para seguir el progreso de un trabajo. Por ejemplo, puedes averiguar cuántas iteraciones totales tiene que completar el proceso de ajuste fino y cuántas iteraciones ha completado ya el proceso:

client.fine_tuning.jobs.list_events('ftjob-(...)')

El único parámetro necesario es fine_tuning_job_id.

Obtener información sobre un trabajo de ajuste

La siguiente función se puede utilizar para obtener información de un trabajo:

client.fine_tuning.jobs.retrieve('ftjob-(...)')

Esta función sólo tiene un parámetro obligatorio: fine_tuning_job_id. Una vez finalizado el proceso, puedes utilizar esta función para recuperar el nombre del modelo final en el campo fine_tuned_model u obtener información sobre el proceso de entrenamiento a través del campo result_files.

Ajuste fino con la interfaz web de OpenAI

En es posible adaptar un modelo a tus necesidades sin escribir código Python; en lugar de eso, pasas por una interfaz web proporcionada por OpenAI. El inconveniente es que no puedes automatizar el proceso, pero puedes obtener buenos resultados de forma rápida y sencilla.

A continuación te explicamos cómo acceder a la interfaz web para realizar ajustes:

  1. Ve a la página de la plataforma para desarrolladores OpenAI y haz clic en Iniciar sesión en la parte superior derecha de la pantalla.

  2. Una vez que te hayas identificado, selecciona API en la siguiente pantalla; entonces verás el enlace para realizar el ajuste fino en la parte superior izquierda de la página web(Figura 4-5).

Cuando estés en la página de ajuste, haz clic en el botón verde "+ Crear" de la parte superior derecha y verás una pantalla similar a la que se muestra en la Figura 4-6:

  • La lista desplegable Modelo base contiene todos los modelos ajustables. Esta lista contiene todos los modelos proporcionados por OpenAI, así como tus modelos previamente afinados. Puede ser interesante volver a actualizar un modelo de ajuste fino existente si, por ejemplo, se dispone de nuevos datos.

  • La sección Datos de entrenamiento define el conjunto de datos que OpenAI utilizará para actualizar el modelo. Puedes subir un archivo directamente a través de esta interfaz o seleccionar un archivo que ya esté en el servidor por su ID de archivo.

  • Como opción recomendada, también es posible proporcionar un archivo de validación con ejemplos diferentes que no estén en el conjunto de datos de entrenamiento.

Figura 4-6. La interfaz web del proceso de ajuste fino

Los archivos del servidor también se pueden gestionar a través de una interfaz web, haciendo clic en el enlace correspondiente de la interfaz(Figura 4-7).

Esta interfaz te permite listar, cargar, descargar y eliminar fácilmente archivos en el servidor OpenAI.

Aplicaciones de ajuste fino

El ajuste fino ofrece una forma eficaz de mejorar el rendimiento de los modelos en diversas aplicaciones. Esta sección examina varios casos de uso en los que se ha implementado eficazmente el ajuste fino. ¡Inspírate en estos ejemplos! Quizás tengas el mismo tipo de problema en tus casos de uso. Una vez más, recuerda que el ajuste fino es más caro que otras técnicas basadas en la ingeniería de prompts, y por tanto no será necesario en la mayoría de tus situaciones. Pero cuando lo sea, esta técnica puede mejorar significativamente tus resultados.

Revisión automatizada del código

En este caso de uso, el ajuste puede ayudar al modelo a analizar y sugerir mejoras para el código escrito por el desarrollador. Para ello es necesario entrenar el modelo con un conjunto de datos que incluya fragmentos de código y comentarios, lo que permitirá al modelo procesar la sintaxis, la semántica y las buenas prácticas específicas del lenguaje. Ten en cuenta que este caso de uso es similar a lo que hace GitHub con su herramienta Copilot, que está diseñada para ayudar a los desarrolladores a escribir código proporcionando sugerencias de código y funciones completas en tiempo real. Puedes utilizar el ajuste fino para entrenar al modelo en una tarea específica, como la revisión de código, y construir un proyecto para tu propio código que tenga normas específicas o esté dedicado a un marco de programación concreto.

Como ejemplo de archivo de entrada para este caso de uso, tu archivo JSONL podría contener pares de código y sus correspondientes comentarios de revisión. Esto ayudaría al modelo a aprender a proporcionar comentarios precisos y relevantes sobre la calidad del código. Una línea del archivo JSONL podría ser algo así

{'messages':[{'role':'system', 'content':'You are an expert in reviewing Python code.'}, {'role':'user', 'content':"def sum(a, b): return a + b\nresult = sum(5, '5')"}, {'role':'assistant', 'content':"Type error: The 'sum' function is adding an integer and a string. Consider converting the string to an integer using int() before passing it to the function."}]}

Integración de documentos financieros

En síntesis de documentos financieros, un LLM afinado puede generar resúmenes concisos y precisos de documentos financieros, como informes de ganancias, estados financieros e informes de analistas. Al afinar un modelo lingüístico en un conjunto de datos relacionados explícitamente con los registros financieros, el modelo resultante puede ser más preciso a la hora de comprender la terminología y el contexto de estos documentos. Por ejemplo, el modelo podría tomar un informe detallado sobre un aumento de los tipos de interés por parte de un banco central y condensarlo en un resumen breve pero informativo.

Para ajustar un modelo existente para obtener mejores resúmenes de documentos financieros, necesitas disponer ya de un gran número de resúmenes de muestra. Entonces tienes que proporcionar al modelo un archivo JSONL que contenga pares de documentos financieros y sus resúmenes ideales. Por ejemplo

{'messages':[{'role':'user', 'content':"The central bank announced a 0.25% increase in the benchmark interest rate, citing concerns about inflationary pressures. This move is expected to affect borrowing costs for consumers and businesses."}, {'role':'assistant', 'content':"Bank increases rate 0.25% for inflation worry. May affect people, business borrowing costs."}]}

Traducción de documentos técnicos

Utilizar el ajuste fino de para traducir documentos técnicos puede mejorar significativamente el rendimiento de un modelo base en comparación con el que puedes obtener con el aprendizaje de pocos disparos. La razón principal es que los documentos técnicos suelen contener vocabulario especializado y estructuras de frases complejas que el aprendizaje de pocos disparos no puede manejar con eficacia. Los modelos base son modelos GPT-3 que no se han ajustado, como los modelos InstructGPT-3, con técnicas RLHF. Para poder utilizarlos, es necesario ajustarlos. Para adaptar un modelo base existente, tienes que preparar un archivo JSONL que contenga tus datos de entrenamiento. Para el caso de uso de la traducción de documentos técnicos, el contenido de este archivo incluiría traducciones de textos técnicos a la lengua de destino.

Generación de artículos de noticias para temas especializados

En este caso de uso, un modelo afinado podría generar artículos de noticias de alta calidad, atractivos y contextualmente relevantes para temas muy especializados que el modelo base probablemente no habría visto lo suficiente como para ser preciso. Como en todos los demás casos de uso, necesitas crear un conjunto de datos de entrenamiento para especializar tu modelo en la redacción de artículos. Para ello, necesitas tener a tu disposición muchos artículos escritos sobre ese tema nicho específico. Estos datos se utilizarán para crear el archivo JSONL que contiene los pares pregunta-completado. Aquí tienes un pequeño ejemplo:

{'messages':[{'role':'user', 'content':"Write an introductory article about a new environmentally friendly cryptocurrency: 'EcoCoin: The Green Cryptocurrency Taking the Market by Storm'"}, {'role':'assistant', 'content':"As concerns over the environmental impact of cryptocurrency mining (...) mining process and commitment to sustainability."}]}

Generar y ajustar datos sintéticos para una campaña de marketing por correo electrónico

En este ejemplo, crearemos una herramienta de generación de texto para una agencia de marketing por correo electrónico que utiliza contenido específico para crear campañas de correo electrónico personalizadas para empresas. Los correos electrónicos están diseñados para atraer al público y promocionar productos o servicios.

Supongamos que nuestra agencia tiene un cliente del sector de procesamiento de pagos que le ha pedido ayuda para realizar una campaña de marketing directo por correo electrónico para ofrecer a las tiendas un nuevo servicio de pago para el comercio electrónico. La agencia de email marketing decide utilizar técnicas de ajuste fino para este proyecto. La agencia de email marketing necesitará una gran cantidad de datos para realizar este ajuste.

En nuestro caso, necesitaremos generar los datos sintéticamente con fines de demostración, como verás en el siguiente subapartado. Normalmente, los mejores resultados se obtienen con datos de expertos humanos, pero en algunos casos, la generación de datos sintéticos puede ser una solución útil.

Crear un conjunto de datos sintéticos

En el siguiente ejemplo, crearemos datos artificiales a partir de GPT-3.5 Turbo. Para ello, especificaremos en un aviso que queremos frases promocionales para vender el servicio de comercio electrónico a un comerciante concreto. El comerciante se caracteriza por un sector de actividad, la ciudad en la que se encuentra y el tamaño de la tienda. Obtendremos frases promocionales enviando las prompts a GPT-3.5 Turbo mediante la función chat_​comp⁠le⁠tion, definida anteriormente.

Comenzamos nuestro script definiendo tres listas que corresponden respectivamente al tipo de tienda, a las ciudades en las que se encuentran las tiendas y al tamaño de las tiendas:

l_sector = [
    'Grocery Stores', 'Restaurants', 'Fast Food Restaurants', 'Pharmacies',
    'Service Stations (Fuel)', 'Electronics Stores', 'Taxi service']
l_city = ['Brussels', 'Paris', 'Bujumbura', 'Berlin', 'Santiago']
l_size = ['small', 'medium', 'large']

A continuación, definimos el primer indicador en una cadena. En este prompt, el papel, el contexto y la tarea están bien definidos, ya que se construyeron utilizando las técnicas de ingeniería de prompts descritas anteriormente en este capítulo. En esta cadena, los tres valores entre llaves se sustituyen por los valores correspondientes más adelante en el código. Este primer mensaje se utiliza para generar los datos sintéticos:

f_prompt = """
Role: You are an expert content writer with extensive direct marketing
experience. You have strong writing skills, creativity, adaptability to
different tones and styles, and a deep understanding of audience needs and
preferences for effective direct campaigns.
Context: You have to write a short message in no more than 2 sentences for a
direct marketing campaign to sell a new ecommerce payment service to stores.
The target stores have the following three characteristics:
- The sector of activity: {sector}
- The city where the stores are located: {city}
- The size of the stores: {size}
Task: Write a short message for the direct marketing campaign. Use the skills
defined in your role to write this message! It is important that the message
you create takes into account the product you are selling and the
characteristics of the store you are writing to.
"""

La consulta siguiente sólo contiene los valores de las tres variables, separados por comas; no se utiliza para crear los datos sintéticos, sino sólo para el ajuste fino:

f_sub_prompt = "{sector}, {city}, {size}"

A continuación viene la parte principal del código, que itera sobre las tres listas de valores que definimos antes. Podemos ver que el código del bloque del bucle es sencillo. Sustituimos los valores entre llaves de las dos indicaciones por los valores adecuados. La variable prompt se utiliza con la función chat_completion para generar un anuncio guardado en response_txt. A continuación, las variables sub_prompt y response_txt se añaden al archivo out_openai_completion.csv, nuestro conjunto de entrenamiento para el ajuste fino:

res = []
for sector in l_sector:
    for city in l_city:
        for size in l_size:
            for i in range(nb_rep):  # 'nb_rep' times each example
                prompt = f_prompt.format(
                    sector=sector, city=city, size=size)
                sub_prompt = f_sub_prompt.format(
                    sector=sector, city=city, size=size)

                response_txt = chat_completion(
                    prompt, model='gpt-3.5-turbo', temperature=1)
                response_txt = response_txt.replace('"', '')
                print(response_txt)

                new_row = {
                    'prompt': sub_prompt,
                    'completion': response_txt}

                new_row = {'messages':
                           [{'role': 'user', 'content': sub_prompt},
                            {'role': 'assistant', 'content': response_txt}]}
                res.append(new_row)

Al final de estos bucles, la variable res es una lista que contiene diccionarios. Cada diccionario tiene la siguiente estructura {'messages':[{'role':'user', 'con⁠tent':​sub_prompt}, {'role':'assistant', 'content':response_txt}]}. Cada diccionario tiene una única clave messages que contiene una lista de dos diccionarios. El primero representa la consulta del usuario con el sector de destino, la ciudad y el tamaño de la tienda. El segundo diccionario representa la finalización generada por el asistente con el contenido que proporciona el mensaje de marketing sintético basado en las características especificadas.

Observa que hemos fijado el valor de nb_rep en 3; por tanto, para cada combinación de características, producimos tres ejemplos. Para maximizar la creatividad del modelo, fijamos la temperatura en 1. Para almacenar estos ejemplos en un archivo JSONL, hacemos lo siguiente:

with open('training.jsonl', 'w') as file:
    for entry in res:
        json_str = json.dumps(entry)
        file.write(json_str + '\n')

Este archivo contiene 315 observaciones. Aquí tienes las dos primeras líneas de este fichero:

{"messages": [{"role": "user", "content": "Grocery Stores, Brussels, small"}, {"role": "assistant", "content": "Boost your grocery store sales with our new ecommerce payment service designed for small stores in Brussels."}]}
{"messages": [{"role": "user", "content": "Grocery Stores, Brussels, small"}, {"role": "assistant", "content": "Boost efficiency and convenience in your small Brussels grocery store with our new ecommerce payment service."}]}

Para evaluar la calidad del proceso de ajuste del modelo, también creamos un conjunto de validación. Es importante que esta evaluación se base en observaciones que no estaban disponibles cuando se actualizó el modelo. Así que tenemos que construir un conjunto de datos completamente independiente para el paso de validación. Para ello, repetimos el proceso descrito anteriormente, pero con nuevos datos de entrada:

l_sector = ['Florist', 'Toy store', 'Pizza restaurant']
l_city = ['Rome', 'New York', 'Rio de Janeiro']
l_size = ['small', 'medium', 'large']

Se trata de sectores empresariales y ciudades completamente distintos de los utilizados para construir el conjunto anterior. Para construir este conjunto, hemos puesto el valor de nb_rep en 1; por tanto, el conjunto de validación contiene 27 ejemplos.

Afinar un modelo con el conjunto de datos sintéticos

El código siguiente carga los archivos de entrenamiento y validación y realiza el ajuste fino. En este ejemplo, utilizamos el modelo gpt-3.5-turbo:

tr_file = client.files.create(
    file=open('training.jsonl', 'rb'),
    purpose='fine-tune'
)

vl_file = client.files.create(
    file=open('validation.jsonl', 'rb'),
    purpose='fine-tune'
)

fine_tuning_job = client.fine_tuning.jobs.create(
    training_file=tr_file.id,
    validation_file=vl_file.id,
    model='gpt-3.5-turbo'
)

Esto iniciará el proceso de actualización del modelo gpt-3.5-turbo con nuestros datos. Este proceso de ajuste fino puede llevar algún tiempo, pero cuando termine, tendrás un nuevo modelo adaptado a tu tarea. El tiempo necesario para este ajuste fino depende principalmente del número de ejemplos disponibles en tu conjunto de datos, del número de tokens de tus ejemplos y del modelo base que hayas elegido. Para que te hagas una idea del tiempo necesario para el ajuste fino, en nuestro ejemplo tardamos menos de cinco minutos.

Para obtener el estado de un trabajo de ajuste fino en ejecución, utiliza el siguiente comando:

client.fine_tuning.jobs.list_events(fine_tuning_job_id=fine_tuning_job.id)

Entre toda la información disponible de esta función, obtendrás algo como esto: message='Step 901/945: training loss=0.40, validation loss=0.22'. Esto significa que, en este caso, el proceso de ajuste ya ha realizado 901 de 945 pasos, y actualmente, las pérdidas de entrenamiento y validación son de 0,40 y 0,22, respectivamente. Esto proporciona información sobre el progreso del proceso de actualización del modelo.

Para cancelar el proceso de ajuste fino, utiliza este comando:

client.fine_tuning.jobs.cancel(fine_tuning_job.id)

Evaluación del modelo afinado

Una vez finalizado el proceso de actualización, conviene verificar que ha ido bien. Para ello, puedes extraer un archivo que contenga la evolución del error de entrenamiento y validación a lo largo de los pasos de actualización:

job = client.fine_tuning.jobs.retrieve(fine_tuning_job.id)
result_file = job.result_files[0]
Nota

La evolución del error de validación sólo está disponible si has proporcionado un conjunto de validación en fine_tuning.jobs.create().

El result_file es un identificador de archivo que aún tenemos que extraer:

content = client.files.content(result_files)

Como content es un archivo binario, todavía tenemos que transformarlo en una cadena, y luego lo cargamos en un marco de datos Pandas:

import pandas as pd
import io
content_str = content.read().decode('utf-8')
df = pd.read_csv(io.StringIO(content_str))

La función io.StringIO permite a pd.read_csv leer una cadena como si fuera un archivo. El siguiente código Python te muestra cómo visualizar la evolución de la pérdida de entrenamiento y la pérdida de validación en función de los pasos de actualización:

# For the NaN is 'valid_loss'
df['valid_loss_interpolated'] = df['valid_loss'].interpolate()

plt.figure(figsize=(10, 6))  # Set the figure size for better readability
plt.plot(df['step'], df['train_loss'], label='Train Loss', marker='o')
plt.plot(df['step'], df['valid_loss_interpolated'],
         label='Valid Loss', marker='o', markersize=2)

plt.xlabel('Step')
plt.ylabel('Loss')
plt.title('Training and Validation Loss per Step')

plt.legend()
plt.grid(True)
plt.show()

La Figura 4-8 muestra los resultados.

Figura 4-8. Evolución de la pérdida en relación con los pasos de actualización

Esta figura muestra la evolución de la pérdida en función de los pasos de la actualización del modelo. En azul se muestra la evolución del error en el conjunto de entrenamiento, y en naranja la evolución del error en el conjunto de validación. Durante el ajuste fino, las observaciones del conjunto de entrenamiento se utilizan para calcular los cambios que hay que hacer en los parámetros del modelo. Las observaciones de prueba no se utilizan para calcular las actualizaciones de los parámetros.

Para que sepamos que el ajuste fino ha ido bien, las dos curvas deben disminuir, y la curva de validación no debe alejarse demasiado de la otra curva de prueba. En nuestro caso, parece que ha funcionado bien. También podemos ver que nuestro modelo ya había convergido tras 200 iteraciones. Probablemente no era necesario pasar por 945 iteraciones.

Consejo

Los desarrolladores más avanzados en aprendizaje automático tienen acceso a algunos de los hiperparámetros utilizados para el trabajo de ajuste mediante la función hyperparameters de la función fine_tuning.jobs.create().

Utilizar el modelo afinado para completar textos

Una vez construido tu nuevo modelo, puedes acceder a él de distintas formas para hacer nuevas terminaciones. La forma más fácil de probarlo es probablemente a través del Patio de recreo. Para acceder a tus modelos en esta herramienta, puedes buscarlos en el menú desplegable de la parte derecha de la interfaz del Patio de recreo (ver Figura 4-9). Todos tus modelos ajustados se encuentran al final de esta lista. Una vez seleccionado tu modelo, puedes utilizarlo para hacer predicciones.

Figura 4-9. Utilizar el modelo afinado en el Playground

Utilizamos el LLM afinado en el siguiente ejemplo con la instrucción de entrada Veterinario, Rabat, pequeño. Sin más instrucciones, el modelo generó automáticamente un anuncio para vender un servicio de pago por comercio electrónico para una pequeña clínica veterinaria de Rabat.

Ya obtuvimos excelentes resultados con un pequeño conjunto de datos compuesto por sólo 315 ejemplos. Para una tarea de ajuste, se suele recomendar tener varios cientos de instancias, y lo ideal sería tener varios miles de instancias. Además, nuestro conjunto de entrenamiento se generó sintéticamente, cuando lo ideal sería que lo hubiera escrito un humano experto en marketing.

Para utilizarlo con la API OpenAI, procedemos como antes con openai.Completion. cre ate() , salvo que necesitamos utilizar el nombre de nuestro nuevo modelo como parámetro de entrada. No olvides terminar todas tus indicaciones con -> y establecer \n como palabras de parada:

client = OpenAI()
completion = client.chat.completions.create(
    model='ft:gpt-3.5-turbo-0613:mycompany::8t0Mv0jv',
    messages=[
        {'role': 'user', 'content': 'Veterinary, Rabat, small'}
    ]
)

Obtenemos la siguiente respuesta:

{
    "id": "chatcmpl-8tbVPjocp(...)",
    "choices": [
        {
            "finish_reason": "stop",
            "index": 0,
            "logprobs": null,
            "message": {
                "content": "Boost your veterinary business in Rabat with our new ecommerce payment service, designed exclusively for small stores like yours.",
                "role": "assistant",
                "function_call": null,
                "tool_calls": null
            }
        }
    ],
    "created": 1708262903,
    "model": "ft:gpt-3.5-turbo-0613:mycompany::8t0Mv0jv",
    "object": "chat.completion",
    "system_fingerprint": null,
    "usage": {
        "completion_tokens": 23,
        "prompt_tokens": 14,
        "total_tokens": 37
    }
}

Como hemos demostrado, el ajuste fino puede permitir a los desarrolladores de Python adaptar los LLM a sus necesidades empresariales únicas, especialmente en dominios dinámicos como nuestro ejemplo de marketing por correo electrónico. Es un enfoque potente para personalizar los modelos de lenguaje que necesitas para tus aplicaciones. En última instancia, esto puede ayudarte fácilmente a servir mejor a tus clientes e impulsar el crecimiento de tu negocio.

Coste de la puesta a punto

El uso en de modelos afinados es costoso. Primero tienes que pagar el entrenamiento, y una vez que el modelo esté listo, cada predicción te costará un poco más que si hubieras utilizado los modelos base proporcionados por OpenAI.

Los últimos precios están disponibles en la página de precios de OpenAI. Para tu información, la ejecución de los ejemplos anteriores nos costó 0,02 $ en total.

RAG

En los capítulos anteriores hemos introducido en el concepto de generación aumentada por recuperación (RAG) y hemos dado un sencillo ejemplo de aplicación. Esta sección pretende profundizar en los principios de la implementación de la RAG.

La primera limitación de los modelos GPT que me viene a la mente es la falta de conocimientos:

Datos actualizados
Un modelo GPT no puede tener información sobre hechos ocurridos después de su entrenamiento.
Datos protegidos
Un modelo GPT no puede responder a preguntas sobre datos privados a los que no tuvo acceso durante su entrenamiento.
Datos específicos de un dominio o "nicho
El modelo podría haber tenido acceso a estos datos, pero no lo suficiente para que los aprendiera realmente.

En la sección anterior has visto que el ajuste fino es una opción interesante para adaptar los LLM a tus necesidades. Sin embargo, hay estudios que han analizado cómo se comparan el ajuste fino y la RAG en lo que respecta a la inyección de conocimientos, y se ha descubierto que la RAG es la solución más fiable. Encontrarás más información sobre cómo elegir en "Elegir entre estrategias".

Veamos ahora diferentes estrategias de aplicación de la GAR.

RAG ingenuo

RAGingenuo es el punto de partida: ingenuo significa que es la implementación RAG más sencilla posible.

El principio es el siguiente, como viste en el capítulo 3 (véase también la Figura 4-10):

  1. Las incrustaciones se calculan a partir de la base de conocimientos, y esas incrustaciones se almacenan para su uso posterior (en una base de datos o en memoria, serializadas en disco, etc.).

  2. Para cada consulta de un usuario, se calcula una incrustación.

  3. Esta incrustación se utiliza para realizar una búsqueda vectorial en las incrustaciones calculadas previamente. El resultado son extractos de documentos.

  4. Por último, llamamos a un modelo GPT con una pregunta como Dados los HECHOS, responde a la PREGUNTA. HECHOS: . . . PREGUNTA: . . .

Figura 4-10. Los tres pasos principales del proceso GAR

RAG avanzado

El éxito de naive RAG depende en gran medida de dos condiciones: que la pregunta del usuario esté bien formulada y que los datos estén bien estructurados y sean de buena calidad. Eso suponiendo que los datos contengan la respuesta a la pregunta.

Estos dos aspectos suelen ser demasiado idealistas para un proyecto del mundo real, así que tenemos que mejorar nuestro diseño para compensar. Podemos mejorar la búsqueda en sí, y mejorar algunos pasos: el preprocesamiento de la consulta del usuario, y el preprocesamiento de la base de documentos antes de vectorizarla.

Consejo

Algunos marcos, como LlamaIndex y LangChain, ofrecen soluciones para facilitar la implementación de un diseño RAG avanzado. Encontrarás ejemplos en el Capítulo 5.

Preprocesa la consulta del usuario

Este es algo que ya hicimos en nuestro proyecto de ejemplo de la sección anterior: extrajimos las palabras clave. Dependiendo de tu proyecto, también podrías generar una reformulación de la consulta con un modelo GPT, para asegurarte de que los errores tipográficos o una mala formulación no repercuten en los resultados. La reformulación también puede tener en cuenta el historial de la conversación, como, por ejemplo, cuando el usuario hace referencia a un concepto anterior: ¿Cómo funciona ? debería reformularse como ¿Cómo funciona la espada de Link? Otras transformaciones se serializan, dependiendo de tu caso de uso: asegúrate de que no hay siglas que puedan hacer fallar la búsqueda semántica, utiliza términos específicos del dominio, etc.

Como se muestra en la Figura 4-11, también puedes utilizar el modelo OpenAI para generar una lista de consultas que amplíen la búsqueda y aumenten tus posibilidades de recuperar los datos correctos:

  • Generar consultas similares

  • Descomponer la entrada del usuario en varias consultas

  • Genera consultas más amplias para que los resultados de la búsqueda incluyan más contexto

Figura 4-11. Preprocesar la consulta del usuario generando una lista de preguntas

El objetivo aquí es mejorar la consulta que se utilizará para realizar la búsqueda semántica. Puedes probar varias técnicas y ver cuál se adapta mejor a tu solución y cómo se equilibra con tus costes.

Preprocesa la base de conocimientos

El primer aspecto que se puede estudiar es chunking. En el ejemplo del Proyecto 3 del Capítulo 3, dividimos el documento de entrada en trozos de longitud fija, que es la solución más sencilla. Para mejorar los resultados de la búsqueda, podríamos dividir el documento de forma que los trozos tuvieran significado por sí mismos, por ejemplo en párrafos o frases, en lugar de tener frases divididas en dos. También hay que llegar a un compromiso con el tamaño de los trozos. Cada trozo debe ser lo suficientemente pequeño como para que la búsqueda funcione bien (los vectores tienen una longitud fija independientemente del tamaño del trozo, por lo que un vector de unas pocas palabras o frases será más preciso que un vector creado sobre varias páginas de texto). Pero también debe ser lo suficientemente grande como para que el LLM tenga suficiente contexto para poder procesarlo. Para evitarlo, existe otro enfoque, como se muestra en la Figura 4-12: realizar la búsqueda en trozos pequeños, pero ampliar los resultados enviados al LLM en varias frases o trozos antes y después del acierto.

Figura 4-12. Trozos pequeños pero contexto grande enviados a LLM

A continuación, examinaremos la estructura del documento y los metadatos. Dividir los documentos en párrafos podría eliminar los títulos de las secciones o capítulos, y eso podría hacer que fallara la búsqueda. Añadir algunos metadatos a los trozos podría ser una solución en este caso, ya sea realizando otra búsqueda en los metadatos o vectorizando una combinación de los metadatos y el propio trozo. Tus documentos también pueden contener imágenes, diagramas o tablas; también podría ser necesario procesar estos elementos para obtener mejores resultados de búsqueda.

La siguiente idea para mejorar los resultados de la búsqueda es mejorar la similitud semántica con las preguntas del usuario convirtiéndolas en preguntas hipotéticas. Una vez más, una llamada a GPT-3.5 Turbo o GPT-4 ayudará en este caso. Se puede utilizar un prompt como Generar una pregunta que este párrafo pueda responder, y las preguntas generadas se pueden utilizar para realizar la búsqueda. También puede hacerse a la inversa: el enfoque hypothetical document embeddings (HyDE) consiste en generar documentos hipotéticos basados en la consulta del usuario y realizar la búsqueda vectorial a partir de esos documentos.

Advertencia

Las técnicas avanzadas de GAR tienen un coste: dependen en gran medida de hacer más llamadas a la API de OpenAI, lo que eleva el gasto de una implementación de este tipo. Podría ser útil dar un paso atrás y analizar la calidad y la estructura de los datos antes de considerar soluciones de GAR.

Postprocesado

A continuación, los resultados pueden ser postprocesados, con filtrado o reordenación utilizando los metadatos introducidos anteriormente, o transformados de un modo u otro para ayudar a tu LLM a responder a la pregunta original. Las posibilidades son infinitas.

Los resultados de todas estas mejoras se resumen en la Figura 4-14.

Figura 4-14. Flujo RAG avanzado
Consejo

Un diseño avanzado de GAR con éxito se construye empíricamente probando distintos enfoques, analizando los resultados y volviendo a intentarlo. La clave está en medir eficazmente el rendimiento del diseño GAR, para que este enfoque iterativo conduzca realmente a mejoras reales y no se base simplemente en intuiciones. Para ello, se pueden establecer distintas estrategias de evaluación, basadas tanto en el rendimiento de la búsqueda en sí como en la capacidad del LLM para procesar los trozos de documentos y dar una respuesta relevante. Puedes encontrar ejemplos con las herramientas LangChain y LlamaIndex presentadas en el Capítulo 5, o con otras como Ragas, o puedes implementar las tuyas propias.

Limitaciones RAG

En algunos casos, puede que tengas datos protegidos por derechos de propiedad, pero la GAR no sea la más adecuada; por ejemplo, cuando los documentos no contienen explícitamente las respuestas a las preguntas de los usuarios. Por ejemplo:

  • ¿Cuáles son los errores del documento MiDocumento.docx? Esta pregunta no puede responderse con una búsqueda semántica, sino que requiere un análisis del documento.

  • ¿En cuántos lenguajes de programación está disponible el SDK? Tu base de conocimientos podría contener una frase como "OpenAI proporciona un SDK para Python y Java", pero la pregunta requiere un análisis más profundo que el LLM podría no ser capaz de realizar, especialmente si hay muchos otros resultados de búsqueda. Además, puede que tu documentación no tenga esta frase, sino simplemente una sección para Python y otra para Java. En ese caso, no habrá ningún resultado con una búsqueda semántica.

También es posible que quieras realizar preguntas puntuales sobre tu documento, y actualizar y mantener las incrustaciones de documentos que están evolucionando podría ser demasiado complejo de mantener y no merecería la pena el coste. En ese caso, puede que tengas que utilizar estrategias diferentes, como aprovechar el contexto largo y simplemente pasar documentos enteros al modelo GPT.

La RAG es un tema complejo: es fácil configurar un diseño ingenuo, pero los diseños avanzados de RAG pueden llegar a ser extremadamente intrincados, con múltiples canalizaciones, procesamiento paralelo y muchas llamadas a la API de OpenAI. Podríamos ir incluso más allá de lo descrito en estos párrafos e introducir un comportamiento agéntico y capacidades de chat, haciendo que el sistema fuera extremadamente potente. Pero a veces la mejor solución es trabajar en la base de conocimientos para mejorar su calidad, en lugar de diseñar una solución muy compleja, difícil de mantener y costosa.

Elegir entre estrategias

Todas las técnicas presentadas en este capítulo podrían parecer tener el mismo propósito: mejorar la ejecución de la tarea sometida al modelo de OpenAI. Pero, ¿por dónde empezar?

Comparación de estrategias

La Tabla 4-2 ofrece un resumen rápido de todas las técnicas presentadas en este capítulo, cuándo utilizarlas y cómo se comparan.

Tabla 4-2. Comparación de diferentes técnicas
Aprendizaje sin disparos Aprendizaje con pocos disparos Trucos de ingeniería de prompts Ajuste fino RAG
Definición Predecir tareas no vistas sin ejemplos previos La pregunta incluye ejemplos de entradas y salida deseada Indicación detallada que puede incluir contexto, función y tareas, o trucos como "piensa paso a paso" El modelo se entrena posteriormente en un conjunto de datos más pequeño y específico; las indicaciones utilizadas son sencillas Combinación de búsqueda vectorial y generación de LLM
Caso práctico Tareas sencillas Tareas bien definidas pero complejas, normalmente con un formato de salida específico Tareas creativas y complejas Tareas muy complejas, o tareas que requieren un formato de salida, tono o estilo específicos Consultas sobre datos protegidos o sobre los que el LLM tiene poco o ningún conocimiento
Datos No requiere datos de ejemplo adicionales Requiere algunos ejemplos La cantidad de datos depende de la técnica de ingeniería de prompts Requiere un conjunto de datos de entrenamiento amplio y de buena calidad Requiere una documentación bien estructurada y de buena calidad; los datos deben contener la respuesta a la pregunta del usuario
Precios Uso: precio por ficha (entrada + salida) Uso: precio por token (entrada + salida); puede dar lugar a peticiones largas Uso: precio por token (entrada + salida); puede dar lugar a peticiones largas Formación: precio por token de los datos de formación; Uso: el precio por token (entrada + salida) es entre cuatro y seis veces más caro para GPT-3.5 Turbo con ajuste fino que para GPT-3.5 Turbo sin ajuste fino; esto significa que el ajuste fino es preferible desde el punto de vista económico si otras técnicas conducen a un plazo más de seis veces mayor Configuración: precio por token para crear incrustaciones para la base de conocimientos; Uso: precio por token para crear la consulta vectorizada con un modelo de incrustación + precio por token de la consulta + resultados de la búsqueda vectorial enviados a un LLM
Conclusión Utilizar por defecto Si el aprendizaje de cero disparos no funciona porque la salida debe ser particular, utiliza el aprendizaje de pocos disparos. Si el aprendizaje sin disparos no funciona porque la tarea es demasiado compleja, prueba con la ingeniería de prompts. Si tienes un conjunto de datos muy específico y las demás soluciones no dan resultados suficientemente buenos, ésta debe utilizarse después de probar la ingeniería de prompts. El GAR puede ser una solución fácil de configurar, que produce mejores resultados y menores costes que alimentar documentos enteros a un LLM. Los resultados dependen mucho de la calidad del documento, y las técnicas avanzadas de GAR pueden hacer que la solución sea costosa y excesivamente compleja.

Estas técnicas no son excluyentes entre sí, y de hecho los mejores resultados se obtienen cuando se combinan.

OpenAI ha ofrecido un flujo de optimización, como se muestra en la Figura 4-15.

Figura 4-15. El flujo de optimización, según OpenAI

La regla general es empezar siempre con un prompt sencillo y luego experimentar con técnicas de aprendizaje de pocos disparos e ingeniería de prompts. Esto se debe a que podrás obtener resultados rápidamente a bajo coste e iterar con eficacia. Si la ingeniería de prompts no funciona en absoluto, es probable que otras técnicas, como la RAG o el ajuste fino, tampoco funcionen.

A continuación, determina si los resultados deben mejorarse por su contenido o por su estilo y formato. Si el problema es el contenido, entonces el RAG es un buen paso siguiente. Si es la forma y el estilo, entonces considera la posibilidad de afinarlos.

Por último, la combinación de todos los enfoques suele ser la solución que da mejores resultados a los problemas complejos: RAG para el conocimiento, ingeniería de prompts para mejorar la generación por parte del LLM, y ajuste fino para asegurarse de que la respuesta generada tiene el estilo y el formato esperados.

Hemos intentado representar este proceso en la Figura 4-16.

Figura 4-16. El flujo de trabajo de optimización, según nosotros

Este diagrama pretende ser un recordatorio visual del flujo de trabajo, destacando cuándo utilizar la ingeniería de prompts, el GAR y el ajuste fino. Como puedes ver con las múltiples flechas hacia atrás, insistimos en la importancia de trabajar de forma iterativa. Este diagrama también tiene múltiples pasos de evaluación, que detallaremos en la siguiente sección.

Evaluaciones

La clave para mejorar tu sistema es trabajar de forma iterativa, en pequeños pasos, en lugar de lanzarse de cabeza a una solución compleja. Para ello, evaluar los resultados es crucial para poder comparar eficazmente las soluciones y la aplicación en función de distintos criterios, como la relevancia, las alucinaciones, la precisión en la respuesta a las preguntas, la toxicidad y las métricas específicas de la recuperación. También es una buena idea tener pruebas automatizadas que incluyan la evaluación LLM en tu proyecto para detectar posibles fallos, regresiones o cambios en el modelo.

Si quieres confiar en herramientas externas, algunos marcos ofrecen métricas de evaluación listas para usar, como verás en el Capítulo 5. OpenAI también proporciona un marco llamado evals para ayudarte en esta tarea.

Consejo

OpenAI ha publicado un libro de recetas para iniciarte en las evaluaciones.

Además, algunas herramientas muy conocidas, como MLflow, integran ahora funcionalidades de evaluación LLM. Otras herramientas, como promptfoo (ya mencionada en este capítulo), se dedican a la evaluación rápida.

De una aplicación estándar a una solución potenciada por LLM

Construir una aplicación impulsada por LLM conlleva ciertos retos que no se encuentran en otras aplicaciones.

Sensibilidad inmediata

Si has empezado con la ingeniería de prompts utilizando algunos prompts complejos, te habrás dado cuenta de que la sensibilidad de los prompts es un verdadero problema. Cambiar una palabra, el orden de las frases o incluso la puntuación de tus instrucciones puede afectar significativamente a los resultados.

Para gestionar esto, hay que combinar dos estrategias:

Sigue tus indicaciones
Tanto si utilizas un sistema de control de versiones como Git, una base de datos o incluso una hoja de Excel, te recomendamos encarecidamente que hagas un seguimiento de la evolución de tus avisos a lo largo de la fase de construcción de tu proyecto. Si descubres que el rendimiento de tu aplicación se degrada -y probablemente lo hará en algún momento- tendrás una forma de volver a un aviso que funcione. Promptfoo puede ser útil en este caso, y varias herramientas de seguimiento de experimentos, como W&B, han lanzado herramientas de versionado de avisos.
Mide los resultados
Si tu aplicación se complica, no podrás confiar simplemente en tu sensación de éxito. Tendrás que cuantificar el rendimiento de tus avisos y realizar pruebas comparativas con distintos escenarios que cubran el uso de la solución que estás diseñando. Será útil definir métricas basadas en la precisión, la relevancia, la coherencia, la creatividad, la exhaustividad, etc. Frameworks como Ragas proporcionan un buen punto de partida, dependiendo de tu escenario. Ragas no sólo cubre las indicaciones, sino que también proporciona diferentes métricas para la recuperación, la generación y la evaluación de principio a fin, centrándose en la GAR.

No determinismo

En, en el momento de escribir este libro, no hay forma de garantizar una salida determinista de las llamadas a la API de OpenAI. Como se vio en el Capítulo 2, si se ponen a cero los parámetros top_p y temperature se obtendrá una respuesta muy consistente. El parámetro seed también puede ayudar. Como indica la documentación de OpenAI "Si se especifica [el parámetro seed ], nuestro sistema se esforzará al máximo por muestrear de forma determinista, de modo que las solicitudes repetidas con la misma semilla y los mismos parámetros deberían devolver el mismo resultado". Sin embargo, por ahora este parámetro está en fase beta y sigue sin garantizar el determinismo, aunque sin duda reduce el riesgo de respuestas incoherentes. El hecho de que el parámetro seed se haya introducido recientemente corrobora que OpenAI se está tomando en serio la cuestión del no determinismo, por lo que podría cambiar en el futuro. Mientras tanto, podemos seguir considerando el no determinismo como un hecho.

Para los casos de uso creativo, el no determinismo es una característica deseable, y generalmente tener un parámetro temperature y top_p superior a cero proporciona un mejor rendimiento. En las aplicaciones estándar, normalmente puedes garantizar las funcionalidades de tus aplicaciones con una buena estrategia de pruebas.

¿Qué ocurre cuando tu aplicación incluye un componente no determinista? Esto induce un cambio en la mentalidad necesaria para desarrollar tus aplicaciones. La combinación de dos estrategias debería dar los mejores resultados:

  1. Demuestra que tu aplicación debería funcionar la mayor parte del tiempo. Esto se apoya en el apartado anterior: realiza campañas de pruebas con varias repeticiones de los mismos escenarios, con las métricas pertinentes. También deberían ser útiles las estrategias desarrolladas en la sección siguiente para reducir las alucinaciones.

  2. Ten estrategias alternativas en caso de que tu aplicación no funcione. Maneja los errores con elegancia, o ten una opción de "reintento".

Con este último punto, podría surgir la pregunta: ¿puede el no determinismo ser realmente una ventaja? Para tareas sencillas, probablemente no. Pero para sistemas complejos, como el Proyecto 4 del Capítulo 3, o para agentes como los presentados en el Capítulo 5, podría ser una forma de asegurarte de que tu sistema no se queda atascado en un estado de fallo. De todos modos, no es posible dominar el resultado de esos sistemas complejos, así que más vale que aceptes el no determinismo y lo aproveches al máximo.

Alucinaciones

En el Capítulo 1 explicamos en que los modelos GPT están sujetos a alucinaciones, lo que hace que sus resultados no sean fiables. Existen varias estrategias para mitigar este riesgo:

Aplica técnicas de ingeniería de prompts
Unas indicaciones cuidadosamente diseñadas ayudan a reducir el alcance de la tarea encomendada y, por tanto, el riesgo de alucinación. Técnicas como la Cadena de Pensamiento, tratada anteriormente en este capítulo, han demostrado empíricamente que mejoran la capacidad de razonamiento y reducen las alucinaciones de los LLM.
Utiliza el ajuste fino
Del mismo modo, los modelos afinados están diseñados para realizar una tarea específica, por lo que pueden reducir las alucinaciones.
Implementar una solución basada en RAG
Aunque la GAR no garantiza que el modelo no alucine respuestas, las soluciones de GAR no dependen de la capacidad del modelo para generar información objetiva basada en su conjunto de datos de entrenamiento por diseño. Las técnicas avanzadas de GAR pueden reducir las alucinaciones mejorando la búsqueda y limitando el alcance de los datos generados.
Utiliza el parámetro logprobs :
Como se describe en el Capítulo 2, las probabilidades logarítmicas ayudan a evaluar la confianza del modelo en sus resultados. Unas probabilidades logarítmicas más altas indican que la probabilidad del token es mayor, y una probabilidad logarítmica baja podría ser una señal de advertencia de alucinaciones, pero esto no es infalible. OpenAI ha proporcionado un interesante libro de recetas sobre el tema.
Implantar barandillas
Implementar la sintaxis y la validación semántica puede ser el primer paso de bajo nivel para detectar alucinaciones. Herramientas como Guardrails o NeMo Guardrails pueden acelerar su implementación. Las estrategias de retroceso o reintentos pueden ser útiles del mismo modo que el manejo del problema de no determinación anterior.
Llamadas al modelo en cadena
La idea aquí es ayudar al rendimiento del sistema multiplicando las llamadas a los modelos. Por ejemplo, el patrón Cadena de Verificación recomienda tener dos llamadas con dos funciones diferentes: la primera llamada a la API contendrá el aviso con la tarea real, y la segunda llamada a la API contendrá un aviso pidiendo que se compruebe el resultado de la primera, como se muestra en la Figura 4-17.
Figura 4-17. Flujo entre un modelo utilizado como asistente y un modelo utilizado como inspector
Consejo

Construir aplicaciones impulsadas por LLM conlleva retos técnicos, pero también consideraciones de diseño específicas de. Este documento de Microsoft se publicó en 2019, pero su contenido sigue mereciendo la pena. El documento proporciona 18 directrices para la interacción entre humanos e IA, como "Dejar claro lo que el sistema puede hacer" o "Aprender del comportamiento del usuario", que pueden ayudarte a construir una aplicación que los usuarios disfruten.

Como conclusión de esta sección, nos gustaría destacar cómo la API de OpenAI hace que sea increíblemente fácil construir maravillosas pruebas de concepto y dar un toque mágico a nuestros proyectos. Sin embargo, construir soluciones de nivel empresarial y de producción requiere un trabajo duro, y no hay una forma fácil de evitar la sensibilidad rápida, el no determinismo y las alucinaciones. Te recomendamos que no subestimes los retos que hay que superar al construir aplicaciones impulsadas por LLM.

Resumen

En este capítulo se han analizado técnicas avanzadas para liberar todo el potencial de la API de OpenAI y se han proporcionado consejos prácticos clave para mejorar el desarrollo de aplicaciones que utilicen LLMs.

Los desarrolladores pueden beneficiarse de la comprensión de la ingeniería de prompts, el aprendizaje sin disparos, el aprendizaje con pocos disparos y el ajuste fino para crear aplicaciones más eficaces y específicas. Exploramos cómo crear avisos eficaces teniendo en cuenta el contexto, la tarea y la función, lo que permite interacciones más precisas con los modelos. Con el razonamiento paso a paso, los desarrolladores pueden animar al modelo a razonar de forma más eficaz y manejar tareas complejas. Además, discutimos la flexibilidad y adaptabilidad que ofrece el aprendizaje de pocos disparos, destacando su naturaleza de eficiencia de datos y su capacidad para adaptarse rápidamente a distintas tareas.

Las técnicas RAG te permiten conectar los datos a los LLM con fines de consulta y aprovechar lo mejor de ambos mundos: la búsqueda semántica para encontrar datos relevantes y los LLM para procesar y generar respuestas. Si hay algo que recordar de este capítulo, sería el flujo de optimización: empezar con técnicas de ingeniería de prompts para experimentar e iterar rápidamente, buscar en RAG para optimizar el contenido, y considerar el ajuste fino para optimizar el formato de las respuestas. Recuerda que la mejor solución para tareas complejas es probablemente una combinación de todas ellas.

Integrar estas técnicas avanzadas y trabajar con un enfoque de perfeccionamiento iterativo con una sólida estrategia de evaluación es la mejor manera de crear aplicaciones potentes e innovadoras que liberen el verdadero potencial de los modelos de OpenAI.

En el próximo capítulo, descubrirás formas adicionales de integrar las capacidades de los LLM en tus aplicaciones: marcos y herramientas adicionales proporcionadas por OpenAI. Estas herramientas permiten a los desarrolladores crear aplicaciones innovadoras, acceder a información actualizada y simplificar el desarrollo de aplicaciones que integren LLMs. También ofreceremos información sobre el futuro de los LLM y su impacto en el desarrollo de aplicaciones.

Get Desarrollo de aplicaciones con GPT-4 y ChatGPT, 2ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.