L’analisi dell’output di LLM chiamata di funzioni vs LangChain

LLM output analysis function call vs LangChain

Come analizzare in modo coerente le uscite dagli LLM utilizzando l’API di Open AI e la chiamata della funzione LangChain: valutazione dei vantaggi e degli svantaggi dei metodi

Immagine di Victor Barrios su Unsplash

La creazione di strumenti con gli LLM richiede diversi componenti, come database vettoriali, catene, agenti, suddivisori di documenti e molti altri nuovi strumenti.

Tuttavia, uno dei componenti più cruciali è l’analisi delle uscite degli LLM. Se non puoi ricevere risposte strutturate dal tuo LLM, avrai difficoltà a lavorare con le generazioni. Questo diventa ancora più evidente quando vogliamo che una singola chiamata all’LLM restituisca più di una informazione.

Illustreremo il problema con uno scenario ipotetico:

Vogliamo che l’LLM restituisca da una singola chiamata gli ingredienti e i passaggi per preparare una determinata ricetta. Ma vogliamo avere entrambi questi elementi separatamente per usarli in due diverse parti del nostro sistema.

import openairecipe = 'Fish and chips'query = f"""Qual è la ricetta per {recipe}? Restituisci separatamente l'elenco degli ingredienti e i passaggi."""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'])

Questo restituisce quanto segue:

Ingredienti per fish and chips:- 1 libbra di filetti di pesce bianco (come merluzzo o nasello)- 1 tazza di farina per tutti gli usi- 1 cucchiaino di lievito in polvere- 1 cucchiaino di sale- 1/2 cucchiaino di pepe nero- 1 tazza di birra fredda- Olio vegetale, per friggere- 4 grosse patate russet- Sale, q.b.Passaggi per preparare fish and chips:1. Preriscalda il forno a 200°C (400°F).2. Sbuccia le patate e tagliale a strisce spesse e uniformi. Risciacqua le strisce di patate in acqua fredda per rimuovere l'eccesso di amido. Asciugale con un panno da cucina pulito.3. In una pentola grande o in una friggitrice, scalda l'olio vegetale a 175°C (350°F). Assicurati di avere abbastanza olio per immergere completamente le patate e il pesce.4. In una ciotola, mescola la farina, il lievito in polvere, il sale e il pepe nero. Aggiungi gradualmente la birra fredda e mescola fino a ottenere una pastella liscia. Metti da parte la pastella.5. Prendi le strisce di patate asciutte e friggile in piccoli lotti per circa 5-6 minuti o finché non diventano dorati. Rimuovi le patatine fritte con un mestolo forato e mettile su un piatto foderato con carta assorbente per eliminare l'olio in eccesso. Mantienile calde nel forno preriscaldato.6. Immergi ogni filetto di pesce nella pastella preparata, assicurandoti che sia ben rivestito. Fai gocciolare eventuali eccessi di pastella prima di posizionare il filetto nell'olio caldo.7. Friggi i filetti di pesce per 4-5 minuti per lato o finché diventano dorati e croccanti. Rimuovili dall'olio con un mestolo forato e mettili su un piatto foderato con carta assorbente per eliminare l'olio in eccesso.8. Condisci il pesce e le patatine con sale mentre sono ancora caldi.9. Servi il pesce e le patatine caldi con salsa tartara, aceto di malto o ketchup a piacere.Goditi il tuo fish and chips fatto in casa!

Questo è una stringa molto lunga e analizzarla sarebbe difficile perché l’LLM può restituire strutture leggermente diverse che potrebbero rompere il codice che scrivi. Potresti sostenere che chiedere esplicitamente nella richiesta di restituire sempre “Ingredienti:” e “Passaggi:” potrebbe risolvere il problema e non avresti torto. Questo potrebbe funzionare, tuttavia dovresti comunque elaborare manualmente la stringa e tenere conto di eventuali variazioni e allucinazioni.

Soluzione

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

  1. Chiamata di funzione di Open AI;
  2. Analizzatore di output di LangChain.

Chiamata di funzione di Open AI

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

Questa funzionalità ha l’obiettivo di fornire a LLM la capacità di chiamare una funzione esterna fornendo gli input come JSON. I modelli sono stati ottimizzati per capire quando devono utilizzare una determinata funzione. Un esempio di ciò è una funzione per il meteo attuale. Se chiedi a GPT il meteo attuale, non sarà in grado di dirtelo, ma puoi fornire una funzione che lo faccia e passarla a GPT in modo che sappia che può essere accessibile dato un determinato input.

Se vuoi approfondire questa funzionalità, ecco l’annuncio di Open AI e qui c’è un ottimo articolo.

Quindi diamo un’occhiata al codice per capire come apparirebbe dato il nostro problema. Scomponiamo il codice:

functions = [    {        "name": "return_recipe",        "description": "Restituisce la ricetta richiesta",        "parameters": {            "type": "object",            "properties": {                "ingredients": {                    "type": "string",                    "description": "Elenco degli ingredienti."                },                "steps": {                    "type": "string",                    "description": "I passaggi della ricetta."                },            },            },            "required": ["ingredients","steps"],        }]

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

Poi passiamo ai parametri. Prima, diciamo che è di tipo oggetto e le proprietà che può utilizzare sono ingredienti e passaggi. Entrambi hanno anche una descrizione e un tipo per guidare LLM sull’output. Infine, specifichiamo quali di queste proprietà sono richieste per chiamare la funzione (ciò significa che potremmo avere campi opzionali che LLM dovrebbe valutare se vuole utilizzare).

Usiamolo ora in una chiamata a LLM:

import openairecipe = 'Fish and chips'query = f"Qual è la ricetta per {ricetta}? Restituisci separatamente l'elenco degli ingredienti e i passaggi."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 all’API formattando un prompt di base con ciò che potrebbe essere un input variabile (ricetta). Quindi dichiariamo la nostra chiamata all’API utilizzando “gpt-3.5-turbo-0613”, passiamo la nostra query nell’argomento messages e ora passiamo le nostre funzioni.

Ci sono due argomenti relativi alle nostre funzioni. Nel primo passiamo la lista di oggetti nel formato mostrato sopra con le funzioni a cui il modello ha accesso. E nel 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 di funzione;
  2. “none” -> il modello non chiama la funzione e restituisce la risposta dell’utente;
  3. { “name”: “nome_mia_funzione” } -> specificando un nome di funzione si forza il modello a utilizzarla.

Puoi trovare la documentazione ufficiale qui.

Nel nostro caso e per l’utilizzo come output parsing abbiamo usato quest’ultima opzione:

function_call={'name':'return_recipe'}

Ora possiamo guardare le nostre risposte. La risposta che otteniamo (dopo questo filtro [ “choices” ] [ 0 ] [ “message” ]) è:

{  "role": "assistant",  "content": null,  "function_call": {    "name": "return_recipe",    "arguments": "{\n  \"ingredients\": \"Per il pesce:\\n- 1 libbra di filetti di pesce bianco\\n- 1 tazza di farina per tutti gli usi\\n- 1 cucchiaino di lievito in polvere\\n- 1 cucchiaino di sale\\n- 1/2 cucchiaino di pepe nero\\n- 1 tazza di acqua fredda\\n- Olio vegetale, per friggere\\nPer le patatine fritte:\\n- 4 patate grandi\\n- Olio vegetale, per friggere\\n- Sale, q.b.\",\n  \"steps\": \"1. Inizia preparando il pesce. In una teglia bassa, mescola la farina, il lievito in polvere, il sale e il pepe nero.\\n2. Mescola gradualmente l'acqua fredda fino a che l'impasto risulti liscio.\\n3. Scalda l'olio vegetale in una grande padella o friggitrice.\\n4. Immergi i filetti di pesce nell'impasto, ricoprendoli uniformemente.\\n5. Posiziona delicatamente i filetti ricoperti nell'olio caldo e friggi per 4-5 minuti per lato, o fino a che siano dorati e croccanti.\\n6. Rimuovi il pesce fritto dall'olio e mettilo su un piatto foderato di carta assorbente per eliminare l'olio in eccesso.\\n7. Per le patatine fritte, sbuccia le patate e tagliale a fette spesse.\\n8. Scalda l'olio vegetale in una friggitrice o padella grande.\\n9. Friggi le patatine in piccoli lotti fino a che siano dorate e croccanti.\\n10. Rimuovi le patatine dall'olio e mettile su un piatto foderato di carta assorbente per eliminare l'olio in eccesso.\\n11. Condisci le patatine con sale.\\n12. Servi il pesce e le patatine insieme e buon appetito!\"\n}"  }}

Se lo analizziamo ulteriormente in “function_call”, possiamo vedere la nostra risposta strutturata desiderata:

{  "ingredienti": "Per il pesce:\n- 1 lb di filetti di pesce bianco\n- 1 tazza di farina 00\n- 1 cucchiaino di lievito in polvere\n- 1 cucchiaino di sale\n- 1/2 cucchiaino di pepe nero\n- 1 tazza di acqua fredda\n- Olio vegetale, per friggere\nPer le patatine:\n- 4 patate grandi\n- Olio vegetale, per friggere\n- Sale, a piacere",  "passaggi": "1. Inizia preparando il pesce. In una teglia poco profonda, mescola insieme la farina, il lievito in polvere, il sale e il pepe nero.\n2. Gradualmente incorpora l'acqua fredda nella pastella finché non risulta liscia.\n3. Scalda l'olio vegetale in una grande padella o in una friggitrice.\n4. Intingi i filetti di pesce nella pastella, ricoprendoli uniformemente.\n5. Adagia delicatamente i filetti ricoperti nell'olio caldo e friggili per 4-5 minuti su ogni lato, o fino a quando non diventano dorati e croccanti.\n6. Togli il pesce fritto dall'olio e mettilo su un piatto foderato con carta assorbente per eliminare l'olio in eccesso.\n7. Per le patatine, sbuccia le patate e tagliale a fette spesse.\n8. Scalda l'olio vegetale in una friggitrice o in una grande padella.\n9. Friggi le patatine a lotti fino a quando diventano dorate e croccanti.\n10. Togli le patatine dall'olio e mettile su un piatto foderato con carta assorbente per eliminare l'olio in eccesso.\n11. Condisci le patatine con il sale.\n12. Servi insieme il pesce e le patatine, e buon appetito!"}

Conclusione per la chiamata di funzione

È possibile utilizzare la funzionalità di chiamata di funzione direttamente dall’API di Open AI. Ciò ci consente di ottenere una risposta strutturata nel formato di un dizionario ogni volta che viene chiamato il LLM.

Utilizzarlo è piuttosto semplice, è sufficiente dichiarare l’oggetto delle 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.

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

Parser di output di LangChain

Un’alternativa che abbiamo, indipendente dal modello, è utilizzare LangChain.

Prima di tutto, cos’è LangChain?

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

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à molto utile per il nostro caso chiamata “Output Parsers”. In questo modulo, sono presenti diversi oggetti che possono essere creati per restituire e analizzare diversi tipi di formati dalle chiamate di LLM. Per fare ciò, viene prima dichiarato il formato e viene passato come prompt al LLM. Quindi, viene utilizzato l’oggetto creato in precedenza per analizzare la risposta.

Ecco come funziona il codice:

from langchain.prompts import ChatPromptTemplatefrom langchain.output_parsers import ResponseSchema, StructuredOutputParserfrom langchain.llms import GooglePalm, OpenAIingredienti = ResponseSchema(        name="ingredienti",        description="Gli ingredienti della ricetta, come stringa unica.",    )passaggi = ResponseSchema(        name="passaggi",        description="I passaggi per preparare la ricetta, come stringa unica.",    )output_parser = StructuredOutputParser.from_response_schemas(    [ingredienti, passaggi])formato_risposta = output_parser.get_format_instructions()print(formato_risposta)prompt = ChatPromptTemplate.from_template("Qual è la ricetta per {ricetta}? Restituisci l'elenco degli ingredienti e dei passaggi separatamente. \n {istruzioni_formato}")

La prima cosa che facciamo qui è creare il nostro Response Schema che sarà l’input per il nostro parser. 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. Ci sono diversi modi per farlo, con diversi stili di parser. Guarda qui per saperne di più su di loro.

Infine, otteniamo le istruzioni sul formato e definiamo il nostro prompt che avrà il nome della ricetta e le istruzioni sul formato come input. Le istruzioni sul formato sono le seguenti:

"""L'output dovrebbe essere un frammento di codice markdown formattato nello schema seguente, compresi "```json" iniziale e finale":```json{ "ingredients": string  // Gli ingredienti della ricetta, come stringa unica. "steps": string  // I passaggi per preparare la ricetta, come stringa unica.}  """

Ora ci rimane solo chiamare l’API. Qui mostrerò sia l’API Open AI che quella di Google PaLM.

llm_openai = OpenAI()llm_palm = GooglePalm()ricetta = 'Fish and chips'prompt_formattato = prompt.format(**{"ricetta":ricetta, "format_instructions":output_parser.get_format_instructions()})risposta_palm = llm_palm(prompt_formattato)risposta_openai = llm_openai(prompt_formattato)print("PaLM:")print(risposta_palm)print(output_parser.parse(risposta_palm))print("Open AI:")print(risposta_openai)print(output_parser.parse(risposta_openai))

Come puoi vedere, è molto facile passare da un modello all’altro. Tutto la struttura definita in precedenza può essere utilizzata nello stesso modo per qualsiasi modello supportato da LangChain. Abbiamo anche utilizzato lo stesso parser per entrambi i modelli.

Questo ha generato il seguente output:

# PaLM:{'ingredients': '''- 1 tazza di farina 00\n- 1 cucchiaino di lievito in polvere\n- 1/2 cucchiaino di sale\n- 1/2 tazza di acqua fredda\n- 1 uovo\n- 450 g di filetti di pesce bianco, come merluzzo o nasello\n- Olio vegetale per friggere\n- 1 tazza di salsa tartara\n- 1/2 tazza di aceto di malto\n- Spicchi di limone''', 'steps': '''1. In una ciotola grande, mescolare insieme la farina, il lievito in polvere e il sale.\n2. In una ciotola separata, mescolare insieme l'uovo e l'acqua.\n3. Immergere i filetti di pesce nella miscela di uova, quindi ricoprirli nella miscela di farina.\n4. Scaldare l'olio in una friggitrice o in una padella grande a 190 gradi Celsius.\n5. Friggere i filetti di pesce per 3-5 minuti per lato, o fino a quando sono dorati e cotti.\n6. Scolare i filetti di pesce su carta assorbente.\n7. Servire immediatamente i filetti di pesce con salsa tartara, aceto di malto e spicchi di limone.'''}# Open AI{'ingredients': '700 g di filetto di merluzzo, tagliato in 4 pezzi, 250 g di farina 00, 2 cucchiaini di lievito in polvere, 1 cucchiaino di sale, 1 cucchiaino di pepe nero appena macinato, 1/2 cucchiaino di aglio in polvere, 240 ml di birra (o acqua), olio vegetale per friggere, salsa tartara, per servire', 'steps': '1. Preriscaldare il forno a 200°C e foderare una teglia con carta da forno. 2. In una ciotola, mescolare insieme la farina, il lievito in polvere, il sale, il pepe e l'aglio in polvere. 3. Versare la birra e mescolare fino ad ottenere un impasto denso. 4. Immergere il merluzzo nell'impasto, rivestendolo su tutti i lati. 5. Scaldare circa 5 cm di olio in una pentola o padella grande a fuoco alto. 6. Friggere il merluzzo per 3-4 minuti per lato, o fino a quando è dorato. 7. Trasferire il merluzzo sulla teglia preparata e cuocere in forno per 5-7 minuti. 8. Servire caldo con salsa tartara.'}

Conclusione: Parsing dell’output di LangChain

Questo metodo è davvero buono e ha come principale caratteristica la flessibilità. Creiamo un paio di strutture come Response Schema, Output Parser e Prompt Templates che possono essere facilmente assemblate e utilizzate con modelli diversi. Un altro buon vantaggio è il supporto per diversi formati di output.

Il principale svantaggio deriva dal passaggio delle istruzioni di formattazione tramite il prompt. Questo permette errori casuali e allucinazioni. Un esempio reale è stato in questo caso specifico in cui ho dovuto specificare “come stringa unica” nella descrizione dello schema di risposta. Se non avessi specificato questo, il modello avrebbe restituito una lista di stringhe con i passaggi e le istruzioni e ciò avrebbe causato un errore di parsing nell’Output Parser.

Conclusione

Ci sono più modi di utilizzare un parser di output per la tua applicazione basata su LLM. Tuttavia, la scelta può cambiare a seconda del problema da affrontare. Per me, mi piace seguire questa idea:

Utilizzo sempre un parser di output, anche se ho solo un output dal LLM. Questo mi permette di controllare e specificare le mie uscite. Se sto lavorando con Open AI, la chiamata di funzione è la mia scelta perché offre il maggiore controllo e evita errori casuali in un’applicazione di produzione. Tuttavia, se sto usando un diverso LLM o ho bisogno di un formato di output diverso, la mia scelta è LangChain, ma con molti test sulle uscite, al fine di creare il prompt con il minor numero di errori.

Grazie per aver letto.

Il codice completo può essere trovato qui.

Se ti piace il contenuto e vuoi supportarmi, puoi offrirmi un caffè:

Gabriel Cassimiro è un Data Scientist che condivide gratuitamente contenuti alla comunità

Amo supportare i creatori!

www.buymeacoffee.com

Ecco alcuni altri articoli che potrebbero interessarti:

Chiamate asincrone per catene con Langchain

Come rendere le catene Langchain funzionanti con chiamate asincrone a LLM, velocizzando il tempo necessario per eseguire un lungo sequenziale…

towardsdatascience.com

Risoluzione dell’ambiente Unity con Deep Reinforcement Learning

Progetto end-to-end con codice di un’implementazione di un agente di Deep Reinforcement Learning in PyTorch.

towardsdatascience.com