Migliorare l’interazione tra modelli linguistici e database grafici tramite un livello semantico |  di Tomaz Bratanic |  Gennaio 2024

 | Intelligenza-Artificiale

Fornire a un agente LLM una suite di strumenti robusti che può utilizzare per interagire con un database a grafo

11 minuti di lettura

14 ore fa

I grafici della conoscenza forniscono un’ottima rappresentazione dei dati con uno schema di dati flessibile che può essere archiviato informazioni strutturate e non strutturate. È possibile utilizzare le istruzioni Cypher per recuperare informazioni da un database a grafo come Neo4j. Un’opzione è utilizzare LLM per generare istruzioni Cypher. Sebbene tale opzione offra un’eccellente flessibilità, la verità è che i LLM di base sono ancora fragili nel generare costantemente precise istruzioni Cypher. Pertanto, dobbiamo cercare un’alternativa per garantire coerenza e robustezza. Cosa succederebbe se, invece di sviluppare istruzioni Cypher, LLM estraesse parametri dall’input dell’utente e utilizzasse funzioni predefinite o modelli Cypher in base all’intento dell’utente? In breve, potresti fornire al LLM una serie di strumenti predefiniti e istruzioni su quando e come utilizzarli in base all’input dell’utente, noto anche come livello semantico.

Il livello semantico è un passaggio intermedio che fornisce ulteriore precisione e modalità solida per l’interazione degli LLM con un grafico della conoscenza. Immagine dell’autore. Ispirato da questa immagine.

Uno strato semantico è costituito da vari strumenti esposti a un LLM che può utilizzare per interagire con un grafico della conoscenza. Possono essere di varia complessità. Puoi pensare a ciascuno strumento in un livello semantico come a una funzione. Ad esempio, dai un’occhiata alla seguente funzione.

def get_information(entity: str, type: str) -> str:
candidates = get_candidates(entity, type)
if not candidates:
return "No information was found about the movie or person in the database"
elif len(candidates) > 1:
newline = "\n"
return (
"Need additional information, which of these "
f"did you mean: {newline + newline.join(str(d) for d in candidates)}"
)
data = graph.query(
description_query, params={"candidate": candidates(0)("candidate")}
)
return data(0)("context")

Gli strumenti possono avere più parametri di input, come nell’esempio sopra, che consente di implementare strumenti complessi. Inoltre, il flusso di lavoro può consistere in qualcosa di più di una semplice query sul database, consentendoti di gestire eventuali casi limite o eccezioni come ritieni opportuno. Il vantaggio è che si trasformano problemi di ingegneria tempestiva, che potrebbero funzionare per la maggior parte del tempo, in problemi di ingegneria del codice, che funzionano ogni volta esattamente come previsto dallo script.

Agente cinematografico

In questo post del blog dimostreremo come implementare un livello semantico che consenta a un agente LLM di interagire con un grafico della conoscenza che contiene informazioni su attori, film e relative valutazioni.

Architettura dell’agente cinematografico. Immagine dell’autore.

Tratto dalla documentazione (scritta anche da me):

L’agente utilizza diversi strumenti per interagire in modo efficace con il database grafico Neo4j.

* Strumento di informazione: recupera dati su film o individui, garantendo che l’agente abbia accesso alle informazioni più recenti e pertinenti.

* Strumento di raccomandazione: fornisce consigli sui film in base alle preferenze e all’input dell’utente.

* Strumento di memoria: memorizza informazioni sulle preferenze dell’utente nel grafico della conoscenza, consentendo un’esperienza personalizzata su più interazioni.

Un agente può utilizzare strumenti di informazioni o suggerimenti per recuperare informazioni dal database o utilizzare lo strumento di memoria per memorizzare le preferenze dell’utente nel database.
Funzioni e strumenti predefiniti consentono all’agente di orchestrare esperienze utente complesse, guidando gli individui verso obiettivi specifici o fornendo informazioni su misura in linea con la loro posizione attuale all’interno del percorso dell’utente.
Questo approccio predefinito migliora la robustezza del sistema riducendo la libertà artistica di un LLM, garantendo che le risposte siano più strutturate e allineate con i flussi di utenti predeterminati, migliorando così l’esperienza complessiva dell’utente.

Il backend del livello semantico di un agente cinematografico è implementato e disponibile come file Modello LangChain. Ho utilizzato questo modello per creare una semplice applicazione di chat ottimizzata.

Interfaccia di chat semplificata. Immagine dell’autore.

Il codice è disponibile su GitHub. È possibile avviare il progetto definendo le variabili di ambiente ed eseguendo il seguente comando:

docker-compose up

Modello grafico

Il grafico è basato su MovieLens set di dati. Contiene informazioni su attori, film e 100.000 valutazioni degli utenti sui film.

Schema grafico. Immagine dell’autore.

La visualizzazione raffigura un grafico della conoscenza delle persone che hanno recitato o diretto un film, ulteriormente classificato per genere. Ogni nodo del film contiene informazioni sulla data di uscita, sul titolo e sulla valutazione IMDb. Il grafico contiene anche le valutazioni degli utenti, che possiamo utilizzare per fornire consigli.

È possibile popolare il grafico eseguendo il comando ingest.py script, che si trova nella directory principale della cartella.

Definizione degli strumenti

Ora definiremo gli strumenti che un agente può utilizzare per interagire con il grafo della conoscenza. Inizieremo con il strumento di informazione. Lo strumento di informazione è progettato per recuperare informazioni rilevanti su attori, registi e film. Il codice Python è il seguente:

def get_information(entity: str, type: str) -> str:
# Use full text index to find relevant movies or people
candidates = get_candidates(entity, type)
if not candidates:
return "No information was found about the movie or person in the database"
elif len(candidates) > 1:
newline = "\n"
return (
"Need additional information, which of these "
f"did you mean: {newline + newline.join(str(d) for d in candidates)}"
)
data = graph.query(
description_query, params={"candidate": candidates(0)("candidate")}
)
return data(0)("context")

La funzione inizia trovando persone o film rilevanti menzionati utilizzando un indice di testo completo. IL indice del testo completo in Neo4j usa Lucene sotto il cofano. Consente un’implementazione fluida delle ricerche di testo basate sulla distanza, che consentono all’utente di scrivere in modo errato alcune parole e ottenere comunque risultati. Se non vengono trovate entità rilevanti, possiamo restituire direttamente una risposta. D’altra parte, se vengono identificati più candidati, possiamo guidare l’agente a porre all’utente una domanda di follow-up ed essere più specifici riguardo al film o alla persona a cui è interessato. Immagina che un utente chieda: “Chi è John? ”.

print(get_information("John", "person"))
# Need additional information, which of these did you mean:
# {'candidate': 'John Lodge', 'label': 'Person'}
# {'candidate': 'John Warren', 'label': 'Person'}
# {'candidate': 'John Gray', 'label': 'Person'}

In questo caso, lo strumento informa l’agente che ha bisogno di ulteriori informazioni. Con un semplice prompt engineering, possiamo guidare l’agente a porre all’utente una domanda di follow-up. Supponiamo che l’utente sia sufficientemente specifico da consentire allo strumento di identificare un particolare film o una persona. In tal caso, utilizziamo un’istruzione Cypher parametrizzata per recuperare le informazioni rilevanti.

print(get_information("Keanu Reeves", "person"))
# type:Actor
# title: Keanu Reeves
# year:
# ACTED_IN: Matrix Reloaded, The, Side by Side, Matrix Revolutions, The, Sweet November, Replacements, The, Hardball, Matrix, The, Constantine, Bill & Ted's Bogus Journey, Street Kings, Lake House, The, Chain Reaction, Walk in the Clouds, A, Little Buddha, Bill & Ted's Excellent Adventure, The Devil's Advocate, Johnny Mnemonic, Speed, Feeling Minnesota, The Neon Demon, 47 Ronin, Henry's Crime, Day the Earth Stood Still, The, John Wick, River's Edge, Man of Tai Chi, Dracula (Bram Stoker's Dracula), Point Break, My Own Private Idaho, Scanner Darkly, A, Something's Gotta Give, Watcher, The, Gift, The
# DIRECTED: Man of Tai Chi

Con queste informazioni, l’agente può rispondere alla maggior parte delle domande che riguardano Keanu Reeves.

Ora guidiamo l’agente nell’utilizzo efficace di questo strumento. Fortunatamente, con LangChain, il processo è semplice ed efficiente. Innanzitutto, definiamo i parametri di input della funzione utilizzando un oggetto Pydantic.

class InformationInput(BaseModel):
entity: str = Field(description="movie or a person mentioned in the question")
entity_type: str = Field(
description="type of the entity. Available options are 'movie' or 'person'"
)

Qui descriviamo che sia i parametri entità che quelli tipo_entità sono stringhe. L’input del parametro di entità è definito come il film o una persona menzionata nella domanda. D’altra parte, con entità_tipo forniamo anche le opzioni disponibili. Quando si ha a che fare con cardinalità basse, ovvero quando è presente un numero limitato di valori distinti, possiamo fornire le opzioni disponibili direttamente a un LLM in modo che possa utilizzare input validi. Come abbiamo visto prima, utilizziamo un indice di testo completo per chiarire le ambiguità di film o persone poiché ci sono troppi valori da fornire direttamente nel prompt.

Ora mettiamo tutto insieme in una definizione di strumento di informazione.

class InformationTool(BaseTool):
name = "Information"
description = (
"useful for when you need to answer questions about various actors or movies"
)
args_schema: Type(BaseModel) = InformationInput

def _run(
self,
entity: str,
entity_type: str,
run_manager: Optional(CallbackManagerForToolRun) = None,
) -> str:
"""Use the tool."""
return get_information(entity, entity_type)

Le definizioni degli strumenti accurate e concise sono una parte importante di uno strato semantico, in modo che un agente possa scegliere correttamente gli strumenti pertinenti quando necessario.

Lo strumento di raccomandazione è leggermente più complesso.

def recommend_movie(movie: Optional(str) = None, genre: Optional(str) = None) -> str:
"""
Recommends movies based on user's history and preference
for a specific movie and/or genre.
Returns:
str: A string containing a list of recommended movies, or an error message.
"""
user_id = get_user_id()
params = {"user_id": user_id, "genre": genre}
if not movie and not genre:
# Try to recommend a movie based on the information in the db
response = graph.query(recommendation_query_db_history, params)
try:
return ", ".join((el("movie") for el in response))
except Exception:
return "Can you tell us about some of the movies you liked?"
if not movie and genre:
# Recommend top voted movies in the genre the user haven't seen before
response = graph.query(recommendation_query_genre, params)
try:
return ", ".join((el("movie") for el in response))
except Exception:
return "Something went wrong"

candidates = get_candidates(movie, "movie")
if not candidates:
return "The movie you mentioned wasn't found in the database"
params("movieTitles") = (el("candidate") for el in candidates)
query = recommendation_query_movie(bool(genre))
response = graph.query(query, params)
try:
return ", ".join((el("movie") for el in response))
except Exception:
return "Something went wrong"

La prima cosa da notare è che entrambi i parametri di input sono facoltativi. Pertanto, dobbiamo introdurre flussi di lavoro che gestiscano tutte le possibili combinazioni di parametri di input e la loro mancanza. Per produrre consigli personalizzati, per prima cosa otteniamo a user_id che viene poi passato alle dichiarazioni di raccomandazione Cypher downstream.

Allo stesso modo di prima, dobbiamo presentare l’input della funzione all’agente.

class RecommenderInput(BaseModel):
movie: Optional(str) = Field(description="movie used for recommendation")
genre: Optional(str) = Field(
description=(
"genre used for recommendation. Available options are:" f"{all_genres}"
)
)

Poiché esistono solo 20 generi disponibili, forniamo i relativi valori come parte della richiesta. Per chiarire le ambiguità dei film, utilizziamo nuovamente un indice di testo completo all’interno della funzione. Come prima, terminiamo con la definizione dello strumento per informare il LLM quando utilizzarlo.

class RecommenderTool(BaseTool):
name = "Recommender"
description = "useful for when you need to recommend a movie"
args_schema: Type(BaseModel) = RecommenderInput

def _run(
self,
movie: Optional(str) = None,
genre: Optional(str) = None,
run_manager: Optional(CallbackManagerForToolRun) = None,
) -> str:
"""Use the tool."""
return recommend_movie(movie, genre)

Finora abbiamo definito due strumenti per recuperare i dati dal database. Tuttavia, il flusso di informazioni non deve essere unidirezionale. Ad esempio, quando un utente informa l’agente che ha già visto un film e forse gli è piaciuto, possiamo memorizzare tali informazioni nel database e utilizzarle per ulteriori consigli. È qui che lo strumento di memoria torna utile.

def store_movie_rating(movie: str, rating: int):
user_id = get_user_id()
candidates = get_candidates(movie, "movie")
if not candidates:
return "This movie is not in our database"
response = graph.query(
store_rating_query,
params={"user_id": user_id, "candidates": candidates, "rating": rating},
)
try:
return response(0)("response")
except Exception as e:
print(e)
return "Something went wrong"

class MemoryInput(BaseModel):
movie: str = Field(description="movie the user liked")
rating: int = Field(
description=(
"Rating from 1 to 5, where one represents heavy dislike "
"and 5 represent the user loved the movie"
)
)

Lo strumento di memoria ha due parametri di input obbligatori che definiscono il film e la sua valutazione. È uno strumento semplice. Una cosa che dovrei menzionare è che ho notato durante i miei test che probabilmente ha senso fornire esempi di quando dare una valutazione specifica, poiché il LLM non è il migliore fuori dagli schemi.

Agente

Mettiamolo ora tutto insieme usando Linguaggio di espressione LangChain (LCEL) per definire un agente.

llm = ChatOpenAI(temperature=0, model="gpt-4", streaming=True)
tools = (InformationTool(), RecommenderTool(), MemoryTool())

llm_with_tools = llm.bind(functions=(format_tool_to_openai_function

Fonte: towardsdatascience.com

Lascia un commento

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