Analisi dell’output LLM: chiamata di funzione e LangChain |  di Gabriele Cassimiro |  Settembre 2023

 | Intelligenza-Artificiale

La creazione di strumenti con LLM richiede più componenti, come database vettoriali, catene, agenti, divisori di documenti e molti altri nuovi strumenti.

Tuttavia, uno dei componenti più cruciali è l’analisi dell’output LLM. Se non riesci a ricevere risposte strutturate dal tuo LLM, avrai difficoltà a lavorare con le generazioni. Ciò diventa ancora più evidente quando vogliamo che una singola chiamata al LLM produca più di un’informazione.

Illustriamo il problema con uno scenario ipotetico:

Vogliamo che il LLM esca da una singola chiamata the ingredienti e il passi per realizzare una determinata ricetta. Ma vogliamo avere entrambi questi elementi separatamente da utilizzare in due parti diverse del nostro sistema.

import openai

recipe = 'Fish and chips'
query = f"""What is the recipe for {recipe}?
Return the ingredients list and steps separately."""

response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=({"role": "user", "content": query}))

response_message = response("choices")(0)("message")
print(response_message('content'))

Ciò restituisce quanto segue:

Ingredients for fish and chips:
- 1 pound white fish fillets (such as cod or haddock)
- 1 cup all-purpose flour
- 1 teaspoon baking powder
- 1 teaspoon salt
- 1/2 teaspoon black pepper
- 1 cup cold beer
- Vegetable oil, for frying
- 4 large russet potatoes
- Salt, to taste

Steps to make fish and chips:

1. Preheat the oven to 200°C (400°F).
2. Peel the potatoes and cut them into thick, uniform strips. Rinse the potato strips in cold water to remove excess starch. Pat them dry using a clean kitchen towel.
3. In a large pot or deep fryer, heat vegetable oil to 175°C (350°F). Ensure there is enough oil to completely submerge the potatoes and fish.
4. In a mixing bowl, combine the flour, baking powder, salt, and black pepper. Whisk in the cold beer gradually until a smooth batter forms. Set the batter aside.
5. Take the dried potato strips and fry them in batches for about 5-6 minutes or until golden brown. Remove the fries using a slotted spoon and place them on a paper towel-lined dish to drain excess oil. Keep them warm in the preheated oven.
6. Dip each fish fillet into the prepared batter, ensuring it is well coated. Let any excess batter drip off before carefully placing the fillet into the hot oil.
7. Fry the fish fillets for 4-5 minutes on each side or until they turn golden brown and become crispy. Remove them from the oil using a slotted spoon and place them on a paper towel-lined dish to drain excess oil.
8. Season the fish and chips with salt while they are still hot.
9. Serve the fish and chips hot with tartar sauce, malt vinegar, or ketchup as desired.

Enjoy your homemade fish and chips!

Questa è una stringa enorme e analizzarla sarebbe difficile perché LLM può restituire strutture leggermente diverse interrompendo qualunque codice tu scriva. Potresti sostenere che chiedere nel prompt di restituire sempre “Ingredienti:” e “Passaggi:” potrebbe risolvere e non hai torto. Potrebbe funzionare, ma dovresti comunque elaborare la stringa manualmente ed essere aperto a eventuali variazioni e allucinazioni.

Ci sono un paio di modi in cui potremmo risolvere questo problema. Uno è stato menzionato sopra, ma ci sono un paio di modi testati che potrebbero essere migliori. In questo articolo, mostrerò due opzioni:

  1. Chiamata aperta della funzione AI;
  2. Parser di output LangChain.

Aprire la chiamata alla funzione AI

Questo è un metodo che ho provato e sta dando i risultati più coerenti. Utilizziamo la funzionalità Function Calling dell’API Open AI in modo che il modello restituisca la risposta come JSON strutturato.

Questa funzionalità ha l’obiettivo di fornire all’LLM la possibilità di chiamare una funzione esterna fornendo gli input come JSON. I modelli sono stati messi a punto per capire quando è necessario utilizzare una determinata funzione. Un esempio di ciò è una funzione per il tempo attuale. Se chiedi a GPT il tempo attuale, non sarà in grado di dirtelo, ma puoi fornire una funzione che lo faccia e passarla a GPT in modo che sappia che è possibile accedervi dando qualche input.

Se vuoi approfondire questa funzionalità ecco il annuncio da Open AI ed ecco un ottimo articolo.

Quindi diamo un’occhiata al codice per vedere come apparirebbe dato il nostro problema in questione. Analizziamo il codice:

functions = (
{
"name": "return_recipe",
"description": "Return the recipe asked",
"parameters": {
"type": "object",
"properties": {
"ingredients": {
"type": "string",
"description": "The ingredients list."
},
"steps": {
"type": "string",
"description": "The recipe steps."
},
},
},
"required": ("ingredients","steps"),
}
)

La prima cosa che dobbiamo fare è dichiarare le funzioni che saranno disponibili per LLM. Dobbiamo dargli un nome e una descrizione in modo che il modello capisca quando deve utilizzare la funzione. Qui diciamo che questa funzione viene utilizzata per restituire la ricetta richiesta.

Poi entriamo nei parametri. Innanzitutto diciamo che è di tipo oggetto e le proprietà che può utilizzare sono ingredienti e passaggi. Entrambi hanno anche una descrizione e una tipologia per guidare il LLM sull’output. Infine, specifichiamo quali di queste proprietà sono richieste per chiamare la funzione (questo significa che potremmo avere campi opzionali che LLM giudicherebbe se volesse utilizzarli).

Usiamolo ora in una chiamata al LLM:

import openai

recipe = 'Fish and chips'
query = f"What is the recipe for {recipe}? Return the ingredients list and steps separately."

response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=({"role": "user", "content": query}),
functions=functions,
function_call={'name':'return_recipe'}
)
response_message = response("choices")(0)("message")

print(response_message)
print(response_message('function_call')('arguments'))

Qui iniziamo creando la nostra query sull’API formattando un prompt di base con quello che potrebbe essere un input variabile (ricetta). Quindi, dichiariamo la nostra chiamata API utilizzando “gpt-3.5-turbo-0613”, passiamo la nostra query nell’argomento message e ora passiamo le nostre funzioni.

Ci sono due argomenti riguardanti le nostre funzioni. Per prima cosa passiamo l’elenco degli oggetti nel formato mostrato sopra con le funzioni a cui ha accesso il modello. E il secondo argomento “function_call” specifichiamo come il modello dovrebbe utilizzare quelle funzioni. Ci sono tre opzioni:

  1. “Auto” -> il modello decide tra la risposta dell’utente o la chiamata della funzione;
  2. “none” -> il modello non chiama la funzione e restituisce la risposta dell’utente;
  3. {“name”: “my_function_name”} -> specificando un nome di funzione si forza il modello a usarlo.

Puoi trovare la documentazione ufficiale Qui.

Nel nostro caso e per l’utilizzo come parsing di output abbiamo utilizzato quest’ultimo:

function_call={'name':'return_recipe'}

Quindi ora possiamo guardare le nostre risposte. La risposta che otteniamo (dopo questo filtro (“scelte”)(0)(“messaggio”)) è:

{
"role": "assistant",
"content": null,
"function_call": {
"name": "return_recipe",
"arguments": "{\n \"ingredients\": \"For the fish:\\n- 1 lb white fish fillets\\n- 1 cup all-purpose flour\\n- 1 tsp baking powder\\n- 1 tsp salt\\n- 1/2 tsp black pepper\\n- 1 cup cold water\\n- Vegetable oil, for frying\\nFor the chips:\\n- 4 large potatoes\\n- Vegetable oil, for frying\\n- Salt, to taste\",\n \"steps\": \"1. Start by preparing the fish. In a shallow dish, combine the flour, baking powder, salt, and black pepper.\\n2. Gradually whisk in the cold water until the batter is smooth.\\n3. Heat vegetable oil in a large frying pan or deep fryer.\\n4. Dip the fish fillets into the batter, coating them evenly.\\n5. Gently place the coated fillets into the hot oil and fry for 4-5 minutes on each side, or until golden brown and crispy.\\n6. Remove the fried fish from the oil and place them on a paper towel-lined plate to drain any excess oil.\\n7. For the chips, peel the potatoes and cut them into thick chips.\\n8. Heat vegetable oil in a deep fryer or large pan.\\n9. Fry the chips in batches until golden and crisp.\\n10. Remove the chips from the oil and place them on a paper towel-lined plate to drain any excess oil.\\n11. Season the chips with salt.\\n12. Serve the fish and chips together, and enjoy!\"\n}"
}
}

Se lo analizziamo ulteriormente nella “function_call” possiamo vedere la risposta strutturata prevista:

{
"ingredients": "For the fish:\n- 1 lb white fish fillets\n- 1 cup all-purpose flour\n- 1 tsp baking powder\n- 1 tsp salt\n- 1/2 tsp black pepper\n- 1 cup cold water\n- Vegetable oil, for frying\nFor the chips:\n- 4 large potatoes\n- Vegetable oil, for frying\n- Salt, to taste",
"steps": "1. Start by preparing the fish. In a shallow dish, combine the flour, baking powder, salt, and black pepper.\n2. Gradually whisk in the cold water until the batter is smooth.\n3. Heat vegetable oil in a large frying pan or deep fryer.\n4. Dip the fish fillets into the batter, coating them evenly.\n5. Gently place the coated fillets into the hot oil and fry for 4-5 minutes on each side, or until golden brown and crispy.\n6. Remove the fried fish from the oil and place them on a paper towel-lined plate to drain any excess oil.\n7. For the chips, peel the potatoes and cut them into thick chips.\n8. Heat vegetable oil in a deep fryer or large pan.\n9. Fry the chips in batches until golden and crisp.\n10. Remove the chips from the oil and place them on a paper towel-lined plate to drain any excess oil.\n11. Season the chips with salt.\n12. Serve the fish and chips together, and enjoy!"
}

Conclusione per la chiamata di funzione

È possibile utilizzare la funzionalità di chiamata di funzioni direttamente dall’API Open AI. Ciò ci consente di avere una risposta in formato dizionario con le stesse chiavi ogni volta che viene chiamato LLM.

Usarlo è piuttosto semplice, devi solo dichiarare l’oggetto funzioni specificando il nome, la descrizione e le proprietà focalizzate sul tuo compito ma specificando (nella descrizione) che questa dovrebbe essere la risposta del modello. Inoltre, quando chiamiamo l’API possiamo forzare il modello a utilizzare la nostra funzione, rendendolo ancora più coerente.

Lo svantaggio principale di questo metodo è che non è supportato da tutti i modelli e le API LLM. Quindi, se volessimo utilizzare l’API Google PaLM, dovremmo utilizzare un altro metodo.

Un’alternativa che abbiamo che è indipendente dal modello è l’utilizzo di LangChain.

Innanzitutto, cos’è LangChain?

LangChain è un framework per lo sviluppo di applicazioni basate su modelli linguistici.

Questa è la definizione ufficiale di LangChain. Questo framework è stato creato di recente ed è già utilizzato come standard di settore per la creazione di strumenti basati su LLM.

Ha una funzionalità ottima per il nostro caso d’uso chiamata “Parser di output”. In questo modulo è possibile creare più oggetti per restituire e analizzare diversi tipi di formati dalle chiamate LLM. Raggiunge questo obiettivo dichiarando innanzitutto qual è il formato e passandolo nel prompt a LLM. Quindi utilizza l’oggetto creato in precedenza per analizzare la risposta.

Analizziamo il codice:

from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import ResponseSchema, StructuredOutputParser
from langchain.llms import GooglePalm, OpenAI

ingredients = ResponseSchema(
name="ingredients",
description="The ingredients from recipe, as a unique string.",
)
steps = ResponseSchema(
name="steps",
description="The steps to prepare the recipe, as a unique string.",
)

output_parser = StructuredOutputParser.from_response_schemas(
(ingredients, steps)
)

response_format = output_parser.get_format_instructions()
print(response_format)

prompt = ChatPromptTemplate.from_template("What is the recipe for {recipe}? Return the ingredients list and steps separately. \n {format_instructions}")

La prima cosa che facciamo qui è creare il nostro schema di risposta che sarà l’input per il nostro parser. Ne creiamo uno per gli ingredienti e uno per i passaggi, ciascuno contenente un nome che sarà la chiave del dizionario e una descrizione che guiderà il LLM sulla risposta.

Quindi creiamo il nostro StructuredOutputParser da quegli schemi di risposta. Esistono diversi modi per farlo, con diversi stili di parser. Aspetto Qui per saperne di più su di loro.

Infine, otteniamo le nostre istruzioni di formato e definiamo il nostro prompt che avrà come input il nome della ricetta e le istruzioni di formato. Le istruzioni per il formato sono queste:

"""
The output should be a markdown code snippet formatted in the following schema, including the leading and trailing "```json" and "```":

```json
{
"ingredients": string // The ingredients from recipe, as a unique string.
"steps": string // The steps to prepare the recipe, as a unique string.
}
"""

Ora ciò che ci resta è solo chiamare l’API. Qui dimostrerò sia l’API Open AI che l’API Google PaLM.

llm_openai = OpenAI()
llm_palm = GooglePalm()

recipe = 'Fish and chips'

formated_prompt = prompt.format(**{"recipe":recipe, "format_instructions":output_parser.get_format_instructions()})

response_palm = llm_palm(formated_prompt)
response_openai = llm_openai(formated_prompt)

print("PaLM:")
print(response_palm)
print(output_parser.parse(response_palm))

print("Open AI:")
print(response_openai)
print(output_parser.parse(response_openai))

Come puoi vedere, è davvero facile passare da un modello all’altro. L’intera struttura definita prima può essere utilizzata esattamente allo stesso modo per qualsiasi modello supportato da LangChain. Abbiamo utilizzato anche lo stesso parser per entrambi i modelli.

Ciò ha generato il seguente output:

# PaLM:
{
'ingredients': '''- 1 cup all-purpose flour\n
- 1 teaspoon baking powder\n
- 1/2 teaspoon salt\n
- 1/2 cup cold water\n
- 1 egg\n
- 1 pound white fish fillets, such as cod or haddock\n
- Vegetable oil for frying\n- 1 cup tartar sauce\n
- 1/2 cup malt vinegar\n- Lemon wedges''',
'steps': '''1. In a large bowl, whisk together the flour, baking powder, and salt.\n
2. In a separate bowl, whisk together the egg and water.\n
3. Dip the fish fillets into the egg mixture, then coat them in the flour mixture.\n
4. Heat the oil in a deep fryer or large skillet to 375 degrees F (190 degrees C).\n
5. Fry the fish fillets for 3-5 minutes per side, or until golden brown and cooked through.\n
6. Drain the fish fillets on paper towels.\n
7. Serve the fish fillets immediately with tartar sauce, malt vinegar, and lemon wedges.
'''
}

# Open AI
{
'ingredients': '1 ½ pounds cod fillet, cut into 4 pieces,
2 cups all-purpose flour,
2 teaspoons baking powder,
1 teaspoon salt,
1 teaspoon freshly ground black pepper,
½ teaspoon garlic powder,
1 cup beer (or water),
vegetable oil, for frying,
Tartar sauce, for serving',
'steps': '1. Preheat the oven to 400°F (200°C) and line a baking sheet with parchment paper.
2. In a medium bowl, mix together the flour, baking powder, salt, pepper and garlic powder.
3. Pour in the beer and whisk until a thick batter forms.
4. Dip the cod in the batter, coating it on all sides.
5. Heat about 2 inches (5 cm) of oil in a large pot or skillet over medium-high heat.
6. Fry the cod for 3 to 4 minutes per side, or until golden brown.
7. Transfer the cod to the prepared baking sheet and bake for 5 to 7 minutes.
8. Serve warm with tartar sauce.'
}

Conclusione: analisi dell’output LangChain

Anche questo metodo è davvero valido e ha come caratteristica principale la flessibilità. Creiamo un paio di strutture come schema di risposta, parser di output e modelli di prompt che possono essere assemblati facilmente e utilizzati con modelli diversi. Un altro buon vantaggio è il supporto per più formati di output.

Lo svantaggio principale deriva dal passaggio delle istruzioni di formato tramite il prompt. Ciò consente errori casuali e allucinazioni. Un esempio reale è stato tratto da questo caso specifico in cui ho dovuto specificare “come stringa univoca” nella descrizione dello schema di risposta. Se non lo specificavo, il modello restituiva un elenco di stringhe con i passaggi e le istruzioni e ciò causava un errore di analisi nell’output Parser.

Esistono diversi modi per utilizzare un parser di output per l’applicazione basata su LLM. Tuttavia, la scelta potrebbe cambiare a seconda del problema in questione. Per quanto mi riguarda, mi piace seguire questa idea:

Utilizzo sempre un parser di output, anche se ho un solo output da LLM. Ciò mi consente di controllare e specificare i miei output. Se lavoro con Open AI, Function Calling è la mia scelta perché ha il massimo controllo ed eviterà errori casuali in un’applicazione di produzione. Tuttavia, se utilizzo un LLM diverso o ho bisogno di un formato di output diverso, la mia scelta è LangChain, ma con molti test sugli output, in modo da creare il prompt con il minor numero di errori.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *