Cómo obtener la salida JSON de los LLM: una guía práctica | por Youness Mansar | agosto de 2024

Tutorial sobre cómo aplicar la salida JSON con Llama.cpp o la API de Gemini

Foto de Étienne Girardet en Dejar de salpicar

Los modelos de lenguaje grandes (LLM) son excelentes para generar texto, pero obtener un resultado estructurado como JSON generalmente requiere indicaciones inteligentes y esperar que el LLM comprenda. Afortunadamente, Modo JSON Se está volviendo cada vez más común en los marcos y servicios LLM. Esto le permite definir el esquema de salida exacto que desea.

Esta publicación analiza la generación restringida mediante el modo JSON. Usaremos un ejemplo de esquema JSON complejo, anidado y realista para guiar a los marcos/API de LLM como Llama.cpp o Gemini API para generar datos estructurados, específicamente información de ubicación turística. Esto se basa en una publicación anterior sobre la generación restringida mediante Guíapero se centra en el modo JSON, más ampliamente adoptado.

Aunque más limitado que GuíaEl soporte más amplio del modo JSON lo hace más accesible, especialmente con proveedores de LLM basados ​​en la nube.

Durante un proyecto personal, descubrí que, si bien el modo JSON era sencillo con Llama.cpp, lograr que funcionara con la API de Gemini requería algunos pasos adicionales. En esta publicación, comparto esas soluciones para ayudarlo a utilizar el modo JSON de manera eficaz.

Nuestro esquema de ejemplo representa una TouristLocationEs una estructura no trivial con objetos anidados, listas, enumeraciones y varios tipos de datos como cadenas y números.

Aquí hay una versión simplificada:

{
"name": "string",
"location_long_lat": ["number", "number"],
"climate_type": {"type": "string", "enum": ["tropical", "desert", "temperate", "continental", "polar"]},
"activity_types": ["string"],
"attraction_list": [
{
"name": "string",
"description": "string"
}
],
"tags": ["string"],
"description": "string",
"most_notably_known_for": "string",
"location_type": {"type": "string", "enum": ["city", "country", "establishment", "landmark", "national park", "island", "region", "continent"]},
"parents": ["string"]
}

Puedes escribir este tipo de esquema a mano o puedes generarlo usando la biblioteca Pydantic. Aquí te mostramos cómo hacerlo con un ejemplo simplificado:

from typing import List
from pydantic import BaseModel, Field

class TouristLocation(BaseModel):
"""Model for a tourist location"""

high_season_months: List[int] = Field(
[], description="List of months (1-12) when the location is most visited"
)

tags: List[str] = Field(
...,
description="List of tags describing the location (e.g. accessible, sustainable, sunny, cheap, pricey)",
min_length=1,
)
description: str = Field(..., description="Text description of the location")

# Example usage and schema output
location = TouristLocation(
high_season_months=[6, 7, 8],
tags=["beach", "sunny", "family-friendly"],
description="A beautiful beach with white sand and clear blue water.",
)

schema = location.model_json_schema()
print(schema)

Este código define una versión simplificada de TouristLocation Clase de datos que utiliza Pydantic. Tiene tres campos:

  • high_season_months: Una lista de números enteros que representan los meses del año (1 a 12) en los que la ubicación es más visitada. El valor predeterminado es una lista vacía.
  • tags:Una lista de cadenas que describen la ubicación con etiquetas como “accesible”, “sostenible”, etc. Este campo es obligatorio (...) y debe tener al menos un elemento (min_length=1).
  • description: Campo de cadena que contiene una descripción textual de la ubicación. Este campo también es obligatorio.

A continuación, el código crea una instancia del TouristLocation Clase y usos model_json_schema() para obtener la representación del modelo en esquema JSON. Este esquema define la estructura y los tipos de datos esperados para esta clase.

model_json_schema() devoluciones:

{'description': 'Model for a tourist location',
'properties': {'description': {'description': 'Text description of the '
'location',
'title': 'Description',
'type': 'string'},
'high_season_months': {'default': [],
'description': 'List of months (1-12) '
'when the location is '
'most visited',
'items': {'type': 'integer'},
'title': 'High Season Months',
'type': 'array'},
'tags': {'description': 'List of tags describing the location '
'(e.g. accessible, sustainable, sunny, '
'cheap, pricey)',
'items': {'type': 'string'},
'minItems': 1,
'title': 'Tags',
'type': 'array'}},
'required': ['tags', 'description'],
'title': 'TouristLocation',
'type': 'object'}

Ahora que tenemos nuestro esquema, veamos cómo podemos implementarlo. Primero en Llama.cpp con su contenedor Python y segundo usando la API de Gemini.

Llama.cpp, una biblioteca de C++ para ejecutar modelos de Llama localmente. Es ideal para principiantes y tiene una comunidad activa. La usaremos a través de su contenedor Python.

Aquí se explica cómo generar TouristLocation datos con él:

# Imports and stuff

# Model init:
checkpoint = "lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF"

model = Llama.from_pretrained(
repo_id=checkpoint,
n_gpu_layers=-1,
filename="*Q4_K_M.gguf",
verbose=False,
n_ctx=12_000,
)

messages = [
{
"role": "system",
"content": "You are a helpful assistant that outputs in JSON."
f"Follow this schema {TouristLocation.model_json_schema()}",
},
{"role": "user", "content": "Generate information about Hawaii, US."},
{"role": "assistant", "content": f"{location.model_dump_json()}"},
{"role": "user", "content": "Generate information about Casablanca"},
]
response_format = {
"type": "json_object",
"schema": TouristLocation.model_json_schema(),
}

start = time.time()

outputs = model.create_chat_completion(
messages=messages, max_tokens=1200, response_format=response_format
)

print(outputs["choices"][0]["message"]["content"])

print(f"Time: {time.time() - start}")

El código primero importa las bibliotecas necesarias e inicializa el modelo LLM. Luego, define una lista de mensajes para una conversación con el modelo, incluido un mensaje del sistema que indica al modelo que debe generar la salida en formato JSON según un esquema específico, solicitudes de información del usuario sobre Hawái y Casablanca y una respuesta del asistente que utiliza el esquema especificado.

Llama.cpp utiliza gramáticas libres de contexto para restringir la estructura y generar una salida JSON válida para una nueva ciudad.

En la salida obtenemos la siguiente cadena generada:

{'activity_types': ['shopping', 'food and wine', 'cultural'],
'attraction_list': [{'description': 'One of the largest mosques in the world '
'and a symbol of Moroccan architecture',
'name': 'Hassan II Mosque'},
{'description': 'A historic walled city with narrow '
'streets and traditional shops',
'name': 'Old Medina'},
{'description': 'A historic square with a beautiful '
'fountain and surrounding buildings',
'name': 'Mohammed V Square'},
{'description': 'A beautiful Catholic cathedral built in '
'the early 20th century',
'name': 'Casablanca Cathedral'},
{'description': 'A scenic waterfront promenade with '
'beautiful views of the city and the sea',
'name': 'Corniche'}],
'climate_type': 'temperate',
'description': 'A large and bustling city with a rich history and culture',
'location_type': 'city',
'most_notably_known_for': 'Its historic architecture and cultural '
'significance',
'name': 'Casablanca',
'parents': ['Morocco', 'Africa'],
'tags': ['city', 'cultural', 'historical', 'expensive']}

Que luego puede analizarse en una instancia de nuestra clase Pydantic.

La API de Gemini, el servicio LLM administrado por Google, afirma en su documentación que ofrece compatibilidad limitada con el modo JSON para Gemini Flash 1.5. Sin embargo, se puede hacer que funcione con algunos ajustes.

Aquí están las instrucciones generales para que funcione:

schema = TouristLocation.model_json_schema()
schema = replace_value_in_dict(schema.copy(), schema.copy())
del schema["$defs"]
delete_keys_recursive(schema, key_to_delete="title")
delete_keys_recursive(schema, key_to_delete="location_long_lat")
delete_keys_recursive(schema, key_to_delete="default")
delete_keys_recursive(schema, key_to_delete="default")
delete_keys_recursive(schema, key_to_delete="minItems")

print(schema)

messages = [
ContentDict(
role="user",
parts=[
"You are a helpful assistant that outputs in JSON."
f"Follow this schema {TouristLocation.model_json_schema()}"
],
),
ContentDict(role="user", parts=["Generate information about Hawaii, US."]),
ContentDict(role="model", parts=[f"{location.model_dump_json()}"]),
ContentDict(role="user", parts=["Generate information about Casablanca"]),
]

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

# Using `response_mime_type` with `response_schema` requires a Gemini 1.5 Pro model
model = genai.GenerativeModel(
"gemini-1.5-flash",
# Set the `response_mime_type` to output JSON
# Pass the schema object to the `response_schema` field
generation_config={
"response_mime_type": "application/json",
"response_schema": schema,
},
)

response = model.generate_content(messages)
print(response.text)

A continuación te explicamos cómo superar las limitaciones de Géminis:

  1. Reemplazar $ref con definiciones completas: Géminis tropieza con referencias de esquema ($ref). Se utilizan cuando se tiene una definición de objeto anidada. Reemplácelas con la definición completa de su esquema.
def replace_value_in_dict(item, original_schema):
# Source: https://github.com/pydantic/pydantic/issues/889
if isinstance(item, list):
return [replace_value_in_dict(i, original_schema) for i in item]
elif isinstance(item, dict):
if list(item.keys()) == ["$ref"]:
definitions = item["$ref"][2:].split("/")
res = original_schema.copy()
for definition in definitions:
res = res[definition]
return res
else:
return {
key: replace_value_in_dict(i, original_schema)
for key, i in item.items()
}
else:
return item
  1. Eliminar claves no compatibles: Gemini aún no maneja claves como “title”, “AnyOf” o “minItems”. Elimínelas de su esquema. Esto tiene como consecuencia un esquema menos legible y menos restrictivo, pero no tenemos otra opción si insistimos en usar Gemini.
def delete_keys_recursive(d, key_to_delete):
if isinstance(d, dict):
# Delete the key if it exists
if key_to_delete in d:
del d[key_to_delete]
# Recursively process all items in the dictionary
for k, v in d.items():
delete_keys_recursive(v, key_to_delete)
elif isinstance(d, list):
# Recursively process all items in the list
for item in d:
delete_keys_recursive(item, key_to_delete)
  1. Solicitud de una o varias veces para enumeraciones: A veces, Gemini tiene problemas con las enumeraciones, ya que muestra todos los valores posibles en lugar de una única selección. Los valores también están separados por “|” en una sola cadena, haciéndolos inválido De acuerdo con nuestro esquema, utilice indicaciones puntuales, proporcionando un ejemplo con el formato correcto, para guiarlo hacia el comportamiento deseado.

Al aplicar estas transformaciones y proporcionar ejemplos claros, puede generar con éxito una salida JSON estructurada con la API de Gemini.

El modo JSON le permite obtener datos estructurados directamente de sus LLM, lo que los hace más útiles para aplicaciones prácticas. Si bien los marcos como Llama.cpp ofrecen implementaciones sencillas, es posible que tenga problemas con servicios en la nube como Gemini API.

Con suerte, este blog le permitió obtener una mejor comprensión práctica de cómo funciona el modo JSON y cómo puede usarlo incluso cuando usa la API de Gemini, que hasta ahora solo tiene soporte parcial.

Ahora que logré que Gemini funcione de alguna manera con el modo JSON, puedo completar la implementación de mi flujo de trabajo LLM donde es necesario tener datos estructurados de una manera específica.

Puedes encontrar el código principal de esta publicación aquí: https://gist.github.com/CVxTz/8eace07d9bd2c5123a89bf790b5cc39e