1fqicjfctcaxbphltc10tdw.png

Ayudamos a mejorar su comprensión y uso óptimo de los resultados estructurados y los LLM

Figura 1: pasos que se ejecutan tanto explícitamente como implícitamente, desde la perspectiva del usuario, al aplicar salidas estructuradas; imagen del autor

En el Artículo anteriornos presentaron a salidas estructuradas utilizando OpenAI. Desde la versión de disponibilidad general de la API ChatCompletions (versión 1.40.0), las salidas estructuradas se han aplicado en docenas de casos de uso y han generado numerosos hilos en Foros de OpenAI.

En este artículo, nuestro objetivo es brindarle una comprensión aún más profunda, disipar algunos conceptos erróneos y brindarle algunos consejos sobre cómo aplicarlos de la manera más óptima posible en diferentes escenarios.

Las salidas estructuradas son una forma de hacer que la salida de un LLM siga un esquema predefinido, generalmente un esquema JSON. Esto funciona transformando el esquema en un gramática libre de contexto (CFG)que durante el paso de muestreo de tokens se utiliza junto con los tokens generados previamente para informar qué tokens subsiguientes son válidos. Es útil pensar en ello como la creación de un expresión regular para la generación de tokens.

La implementación de la API de OpenAI en realidad rastrea un subconjunto limitado de características del esquema JSON. Con soluciones de salida estructurada más generales, como EsquemasEs posible utilizar un subconjunto algo más grande del esquema JSON e incluso definir esquemas no JSON completamente personalizados, siempre que se tenga acceso a un modelo de ponderación abierto. Para los fines de este artículo, asumiremos la implementación de la API de OpenAI.

De acuerdo a Especificación básica del esquema JSON, “El esquema JSON establece cómo debe verse un documento JSON, las formas de extraer información de él y cómo interactuar con él”El esquema JSON define seis tipos primitivos: nulo, booleano, objeto, matriz, número y cadena. También define ciertas palabras clave, anotaciones y comportamientos específicos. Por ejemplo, podemos especificar en nuestro esquema que esperamos un array y añadir una anotación que minItems Será 5 .

Pydantic es una biblioteca de Python que implementa la especificación del esquema JSON. Usamos Pydantic para crear software robusto y fácil de mantener en Python. Dado que Python es un lenguaje de tipado dinámico, los científicos de datos no necesariamente piensan en términos de tipos de variables — estos son a menudo implícito En su código, por ejemplo, una fruta se especificaría como:

fruit = dict(
name="apple",
color="red",
weight=4.2
)

…mientras que una declaración de función que devuelve “fruta” a partir de algún dato a menudo se especificaría como:

def extract_fruit(s):
...
return fruit

Pydantic, por otro lado, nos permite generar una clase compatible con el esquema JSON, con variables anotadas correctamente y sugerencias de tipohaciendo que nuestro código sea más legible/mantenible y en general más robusto, es decir

class Fruit(BaseModel):
name: str
color: Literal['red', 'green']
weight: Annotated[float, Gt(0)]

def extract_fruit(s: str) -> Fruit:
...
return fruit

OpenAI en realidad Recomiendo encarecidamente el uso de Pydantic para especificar esquemas, en lugar de especificar el esquema JSON «sin procesar» directamente. Hay varias razones para esto. En primer lugar, se garantiza que Pydantic se adhiere a la especificación del esquema JSON, por lo que le ahorra pasos de prevalidación adicionales. En segundo lugar, para esquemas más grandes, es menos detallado, lo que le permite escribir código más limpio, más rápido. openai El paquete Python en realidad hace algunas tareas de mantenimiento, como configurar additionalProperties a False para usted, mientras que al definir su esquema «a mano» usando JSON, necesitaría configúrelos manualmentepara cada objeto en su esquema (si no lo hace, se producirá un error de API bastante molesto).

Como mencionamos anteriormente, la API ChatCompletions proporciona un subconjunto limitado de la especificación completa del esquema JSON. Existen numerosos Palabras clave que aún no son compatiblescomo minimum y maximum para números, y minItems y maxItems para matrices: anotaciones que de otro modo serían muy útiles para reducir las alucinaciones o restringir el tamaño de salida.

Algunas funciones de formato tampoco están disponibles. Por ejemplo, el siguiente esquema de Pydantic generaría un error de API al pasarlo a response_format en ChatCompleciones:

class NewsArticle(BaseModel):
headline: str
subheading: str
authors: List[str]
date_published: datetime = Field(None, description="Date when article was published. Use ISO 8601 date format.")

Fracasaría porque openai El paquete no tiene manejo de formato para datetime por lo que en su lugar necesitarías configurar date_published como un str y realizar la validación del formato (por ejemplo, conformidad con ISO 8601) post-hoc.

Otras limitaciones clave incluyen las siguientes:

  • Las alucinaciones todavía son posibles — por ejemplo, al extraer los identificadores de productos, deberá definir en su esquema de respuesta lo siguiente: product_ids: List[str] ; si bien se garantiza que la salida producirá una lista de cadenas (ID de productos), las cadenas en sí mismas pueden ser alucinantes, por lo que en este caso de uso, es posible que desee validar la salida contra un conjunto predefinido de ID de productos.
  • La salida está limitada en 4096 tokens, o el número menor que establezca dentro del max_tokens parámetro — por lo que, aunque el esquema se seguirá con precisión, si la salida es demasiado grande, se truncará y producirá un JSON no válido, especialmente molesto en archivos muy grandes. API por lotes ¡trabajos!
  • Esquemas profundamente anidados con muchas propiedades de objeto puede generar errores de API: hay una limitación de la profundidad y amplitud de su esquema, pero en general es mejor ceñirse a estructuras planas y simples, no solo para evitar errores de API sino también para exprimir el máximo rendimiento posible de los LLM (los LLM en general tienen problemas para atender estructuras profundamente anidadas).
  • No son posibles esquemas altamente dinámicos o arbitrarios – a pesar de Se admite la recursiónNo es posible crear un esquema altamente dinámico de, digamos, una lista de objetos clave-valor arbitrarios, es decir [{"key1": "val1"}, {"key2": "val2"}, ..., {"keyN": "valN"}] ya que las “claves” en este caso debe estar predefinido; en tal escenario, la mejor opción es no utilizar salidas estructuradas en absoluto, sino optar por el modo JSON estándar y proporcionar las instrucciones sobre la estructura de salida dentro del mensaje del sistema.

Con todo esto en mente, ahora podemos revisar un par de casos de uso con consejos y trucos sobre cómo mejorar el rendimiento al utilizar salidas estructuradas.

Creando flexibilidad usando parámetros opcionales

Supongamos que estamos creando una aplicación de extracción de datos web en la que nuestro objetivo es recopilar componentes específicos de las páginas web. Para cada página web, proporcionamos el HTML sin procesar en el mensaje de solicitud del usuario, damos instrucciones de extracción específicas en el mensaje de solicitud del sistema y definimos el siguiente modelo de Pydantic:

class Webpage(BaseModel):
title: str
paragraphs: Optional[List[str]] = Field(None, description="Text contents enclosed within <p></p> tags.")
links: Optional[List[str]] = Field(None, description="URLs specified by `href` field within <a></a> tags.")
images: Optional[List[str]] = Field(None, description="URLs specified by the `src` field within the <img></img> tags.")

Entonces llamaríamos a la API de la siguiente manera…

response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "You are to parse HTML and return the parsed page components."
},
{
"role": "user",
"content": """
<html>
<title>Structured Outputs Demo</title>
<body>
<img src="test.gif"></image>
<p>Hello world!</p>
</body>
</html>
"""
}
],
response_format=Webpage
)

…con la siguiente respuesta:

{
'images': ['test.gif'],
'links': None,
'paragraphs': ['Hello world!'],
'title': 'Structured Outputs Demo'
}

Esquema de respuesta suministrado a la API mediante salidas estructuradas debe Devuelve todos los campos especificados. Sin embargo, podemos “emular” los campos opcionales y agregar más flexibilidad utilizando el Optional Anotación de tipo. En realidad, también podríamos utilizar Union[List[str], None] — son sintácticamente exactamente iguales. En ambos casos, obtenemos una conversión a anyOf palabra clave según la especificación del esquema JSON. En el ejemplo anterior, dado que no hay <a></a> etiquetas presentes en la página web, la API todavía devuelve el links campo, pero está configurado para None .

Reducción de alucinaciones mediante enumeraciones

Mencionamos anteriormente que incluso si se garantiza que el LLM sigue el esquema de respuesta proporcionado, aún puede alucinar los valores reales. Además de esto, un artículo reciente Descubrieron que imponer un esquema fijo en las salidas, en realidad provoca que el LLM alucine o se degrade en términos de sus capacidades de razonamiento.

Una forma de superar estas limitaciones es intentar utilizar enumeraciones tanto como sea posible. Las enumeraciones restringen la salida a un conjunto muy específico de tokens, lo que asigna una probabilidad de cero a todo lo demás. Por ejemplo, supongamos que está intentando volver a clasificar los resultados de similitud de productos entre un producto objetivo que contiene un description y un único product_id y Los 5 mejores productos que se obtuvieron mediante alguna búsqueda de similitud vectorial (por ejemplo, utilizando una métrica de distancia de coseno). Cada uno de esos 5 productos principales también contiene la descripción textual correspondiente y una identificación única. En su respuesta, simplemente desea obtener la reclasificación del 1 al 5 como una lista (por ejemplo, [1, 4, 3, 5, 2] ), en lugar de obtener una lista de cadenas de ID de productos reclasificadas, que pueden ser erróneas o no válidas. Configuramos nuestro modelo Pydantic de la siguiente manera…

class Rank(IntEnum):
RANK_1 = 1
RANK_2 = 2
RANK_3 = 3
RANK_4 = 4
RANK_5 = 5

class RerankingResult(BaseModel):
ordered_ranking: List[Rank] = Field(description="Provides ordered ranking 1-5.")

…y ejecuta la API de la siguiente manera:

response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": """
You are to rank the similarity of the candidate products against the target product.
Ranking should be orderly, from the most similar, to the least similar.
"""
},
{
"role": "user",
"content": """
## Target Product
Product ID: X56HHGHH
Product Description: 80" Samsung LED TV

## Candidate Products
Product ID: 125GHJJJGH
Product Description: NVIDIA RTX 4060 GPU

Product ID: 76876876GHJ
Product Description: Sony Walkman

Product ID: 433FGHHGG
Product Description: Sony LED TV 56"

Product ID: 777888887888
Product Description: Blueray Sony Player

Product ID: JGHHJGJ56
Product Description: BenQ PC Monitor 37" 4K UHD
"""
}
],
response_format=RerankingResult
)

El resultado final es simplemente:

{'ordered_ranking': [3, 5, 1, 4, 2]}

Por lo tanto, el LLM clasificó al televisor LED de Sony (es decir, el artículo número «3» en la lista) y al monitor de PC de BenQ (es decir, el artículo número «5») como los dos candidatos a productos más similares, es decir, los dos primeros elementos de la ordered_ranking ¡lista!

En este artículo, analizamos en profundidad los resultados estructurados. Presentamos el esquema JSON y los modelos Pydantic, y los conectamos a la API ChatCompletions de OpenAI. Analizamos varios ejemplos y mostramos algunas formas óptimas de resolverlos mediante resultados estructurados. A continuación, se resumen algunas conclusiones clave:

  • Las salidas estructuradas compatibles con la API de OpenAI y otros marcos de terceros implementan solo una subconjunto de la especificación del esquema JSON —Informarse mejor sobre sus características y limitaciones le ayudará a tomar las decisiones de diseño correctas.
  • Usando Pidántico o marcos similares que rastrean fielmente la especificación del esquema JSON, es muy recomendable, ya que le permite crear un código válido y más limpio.
  • Si bien aún se esperan alucinaciones, hay diferentes formas de mitigarlas, simplemente mediante una elección del diseño del esquema de respuesta; por ejemplo, utilizando enumeraciones cuando sea apropiado.

Acerca del autor

Armin Catović es Secretario de la Junta Directiva de Inteligencia artificial de Estocolmoy vicepresidente e ingeniero sénior de ML/IA en la Grupo EQTcon 18 años de experiencia en ingeniería en Australia, el sudeste asiático, Europa y los EE. UU., y una serie de patentes y publicaciones de IA de primer nivel revisadas por pares.