paint-brush
Über Texteinbettungen hinaus: Behebung der Lücken in RAG-Anwendungen für strukturierte Datenabfragenvon@neo4j
Neue Geschichte

Über Texteinbettungen hinaus: Behebung der Lücken in RAG-Anwendungen für strukturierte Datenabfragen

von Neo4j17m2024/10/29
Read on Terminal Reader

Zu lang; Lesen

Während Texteinbettungen für unstrukturierten Text in RAG-Anwendungen leistungsstark sind, reichen sie für strukturierte Abfragen nicht aus. Wissensgraphen bieten eine Lösung.
featured image - Über Texteinbettungen hinaus: Behebung der Lücken in RAG-Anwendungen für strukturierte Datenabfragen
Neo4j HackerNoon profile picture
0-item
1-item


Jeder liebt Texteinbettungsmodelle und das aus gutem Grund: Sie eignen sich hervorragend zum Kodieren unstrukturierten Textes und erleichtern so das Entdecken semantisch ähnlicher Inhalte. Es ist keine Überraschung, dass sie das Rückgrat der meisten RAG-Anwendungen bilden, insbesondere angesichts der aktuellen Betonung auf dem Kodieren und Abrufen relevanter Informationen aus Dokumenten und anderen Textressourcen. Es gibt jedoch klare Beispiele für Fragen, bei denen der Texteinbettungsansatz für RAG-Anwendungen zu kurz greift und falsche Informationen liefert.


Wie bereits erwähnt, eignen sich Text-Embeddings hervorragend zum Kodieren von unstrukturiertem Text. Auf der anderen Seite sind sie nicht so gut im Umgang mit strukturierten Informationen und Operationen wie Filtern , Sortieren oder Aggregationen . Stellen Sie sich eine einfache Frage wie diese vor:


Welcher Film aus dem Jahr 2024 hat die höchsten Einschaltquoten?


Um diese Frage zu beantworten, müssen wir zunächst nach Erscheinungsjahr filtern und dann nach Bewertung sortieren. Wir untersuchen, wie ein naiver Ansatz mit Texteinbettungen funktioniert, und zeigen dann, wie man mit solchen Fragen umgeht. Dieser Blogbeitrag zeigt, dass Sie beim Umgang mit strukturierten Datenoperationen wie Filtern, Sortieren oder Aggregieren andere Tools verwenden müssen, die Struktur bieten, wie z. B. Wissensgraphen.


Der Code ist auf GitHub verfügbar.

Umgebungs-Setup

Für diesen Blogbeitrag verwenden wir das Empfehlungsprojekt in Neo4j Sandbox . Das Empfehlungsprojekt verwendet den MovieLens-Datensatz , der Filme, Schauspieler, Bewertungen und weitere Informationen enthält.


Graphenschema der Empfehlungsdatenbank.


Der folgende Code instanziiert einen LangChain-Wrapper zur Verbindung mit der Neo4j-Datenbank:


 os.environ["NEO4J_URI"] = "bolt://44.204.178.84:7687" os.environ["NEO4J_USERNAME"] = "neo4j" os.environ["NEO4J_PASSWORD"] = "minimums-triangle-saving" graph = Neo4jGraph(refresh_schema=False)


Zusätzlich benötigen Sie einen OpenAI API-Schlüssel, den Sie im folgenden Code übergeben:


 os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")


Die Datenbank enthält 10.000 Filme, aber die Texteinbettungen sind noch nicht gespeichert. Um die Einbettungen nicht für alle Filme berechnen zu müssen, versehen wir die 1.000 bestbewerteten Filme mit einem sekundären Label namens Target :


 graph.query(""" MATCH (m:Movie) WHERE m.imdbRating IS NOT NULL WITH m ORDER BY m.imdbRating DESC LIMIT 1000 SET m:Target """)

Berechnen und Speichern von Texteinbettungen

Die Entscheidung, was eingebettet werden soll, ist eine wichtige Überlegung. Da wir das Filtern nach Jahr und das Sortieren nach Bewertung demonstrieren, wäre es nicht fair, diese Details aus dem eingebetteten Text auszuschließen. Aus diesem Grund habe ich mich dafür entschieden, das Erscheinungsjahr, die Bewertung, den Titel und die Beschreibung jedes Films zu erfassen.


Hier ist ein Beispiel für einen Text, den wir für den Film „The Wolf of Wall Street“ einbetten werden:


 plot: Based on the true story of Jordan Belfort, from his rise to a wealthy stock-broker living the high life to his fall involving crime, corruption and the federal government. title: Wolf of Wall Street, The year: 2013 imdbRating: 8.2


Sie könnten sagen, dass dies kein guter Ansatz zum Einbetten strukturierter Daten ist, und ich würde nicht widersprechen, da ich den besten Ansatz nicht kenne. Vielleicht sollten wir sie statt in Schlüsselwertelemente in Text oder etwas Ähnliches umwandeln. Lassen Sie es mich wissen, wenn Sie Ideen haben, was besser funktionieren könnte.


Das Neo4j-Vektorobjekt in LangChain verfügt über eine praktische Methode from_existing_graph, mit der Sie auswählen können, welche Texteigenschaften codiert werden sollen:


 embedding = OpenAIEmbeddings(model="text-embedding-3-small") neo4j_vector = Neo4jVector.from_existing_graph( embedding=embedding, index_name="movies", node_label="Target", text_node_properties=["plot", "title", "year", "imdbRating"], embedding_node_property="embedding", )


In diesem Beispiel verwenden wir OpenAIs Modell text-embedding-3-small zur Generierung von Einbettungen. Wir initialisieren das Neo4jVector-Objekt mit der Methode from_existing_graph. Der Parameter node_label filtert die zu kodierenden Knoten, insbesondere die mit Target gekennzeichneten. Der Parameter text_node_properties definiert die einzubettenden Knoteneigenschaften, darunter plot , title , year und imdbRating . Schließlich definiert embedding_node_property die Eigenschaft, in der die generierten Einbettungen gespeichert werden, bezeichnet als embedding .

Der naive Ansatz

Versuchen wir zunächst, einen Film anhand seiner Handlung oder Beschreibung zu finden:


 pretty_print( neo4j_vector.similarity_search( "What is a movie where a little boy meets his hero?" ) )


Ergebnisse:


 plot: A young boy befriends a giant robot from outer space that a paranoid government agent wants to destroy. title: Iron Giant, The year: 1999 imdbRating: 8.0 plot: After the death of a friend, a writer recounts a boyhood journey to find the body of a missing boy. title: Stand by Me year: 1986 imdbRating: 8.1 plot: A young, naive boy sets out alone on the road to find his wayward mother. Soon he finds an unlikely protector in a crotchety man and the two have a series of unexpected adventures along the way. title: Kikujiro (Kikujirô no natsu) year: 1999 imdbRating: 7.9 plot: While home sick in bed, a young boy's grandfather reads him a story called The Princess Bride. title: Princess Bride, The year: 1987 imdbRating: 8.1


Die Ergebnisse scheinen insgesamt ziemlich solide zu sein. Es ist immer ein kleiner Junge dabei, obwohl ich nicht sicher bin, ob er immer seinen Helden trifft. Andererseits umfasst der Datensatz nur 1.000 Filme, sodass die Auswahl etwas begrenzt ist.


Versuchen wir es nun mit einer Abfrage, die eine grundlegende Filterung erfordert:


 pretty_print( neo4j_vector.similarity_search( "Which movies are from year 2016?" ) )


Ergebnisse:


 plot: Six short stories that explore the extremities of human behavior involving people in distress. title: Wild Tales year: 2014 imdbRating: 8.1 plot: A young man who survives a disaster at sea is hurtled into an epic journey of adventure and discovery. While cast away, he forms an unexpected connection with another survivor: a fearsome Bengal tiger. title: Life of Pi year: 2012 imdbRating: 8.0 plot: Based on the true story of Jordan Belfort, from his rise to a wealthy stock-broker living the high life to his fall involving crime, corruption and the federal government. title: Wolf of Wall Street, The year: 2013 imdbRating: 8.2 plot: After young Riley is uprooted from her Midwest life and moved to San Francisco, her emotions - Joy, Fear, Anger, Disgust and Sadness - conflict on how best to navigate a new city, house, and school. title: Inside Out year: 2015 imdbRating: 8.3


Es ist lustig, aber kein einziger Film aus dem Jahr 2016 wurde ausgewählt. Vielleicht könnten wir mit einer anderen Textvorbereitung für die Kodierung bessere Ergebnisse erzielen. Texteinbettungen sind hier jedoch nicht anwendbar, da es sich um eine einfache strukturierte Datenoperation handelt, bei der wir Dokumente oder in diesem Beispiel Filme basierend auf einer Metadateneigenschaft filtern müssen. Die Metadatenfilterung ist eine bewährte Technik, die häufig eingesetzt wird, um die Genauigkeit von RAG-Systemen zu verbessern.


Die nächste Abfrage, die wir versuchen, erfordert ein wenig Sortierung:


 pretty_print( neo4j_vector.similarity_search("Which movie has the highest imdb score?") )


Ergebnisse:


 plot: A silent film production company and cast make a difficult transition to sound. title: Singin' in the Rain year: 1952 imdbRating: 8.3 plot: A film about the greatest pre-Woodstock rock music festival. title: Monterey Pop year: 1968 imdbRating: 8.1 plot: This movie documents the Apollo missions perhaps the most definitively of any movie under two hours. Al Reinert watched all the footage shot during the missions--over 6,000,000 feet of it, ... title: For All Mankind year: 1989 imdbRating: 8.2 plot: An unscrupulous movie producer uses an actress, a director and a writer to achieve success. title: Bad and the Beautiful, The year: 1952 imdbRating: 7.9


Wenn Sie mit den IMDb-Bewertungen vertraut sind, wissen Sie, dass es viele Filme mit einer Bewertung über 8,3 gibt. Der Titel mit der höchsten Bewertung in unserer Datenbank ist tatsächlich eine Serie – Band of Brothers – mit einer beeindruckenden Bewertung von 9,6. Auch hier schneiden Texteinbettungen beim Sortieren der Ergebnisse schlecht ab.


Lassen Sie uns auch eine Frage bewerten, die eine Art Aggregation erfordert:


 pretty_print(neo4j_vector.similarity_search("How many movies are there?"))


Ergebnisse:


 plot: Ten television drama films, each one based on one of the Ten Commandments. title: Decalogue, The (Dekalog) year: 1989 imdbRating: 9.2 plot: A documentary which challenges former Indonesian death-squad leaders to reenact their mass-killings in whichever cinematic genres they wish, including classic Hollywood crime scenarios and lavish musical numbers. title: Act of Killing, The year: 2012 imdbRating: 8.2 plot: A meek Hobbit and eight companions set out on a journey to destroy the One Ring and the Dark Lord Sauron. title: Lord of the Rings: The Fellowship of the Ring, The year: 2001 imdbRating: 8.8 plot: While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard. title: Lord of the Rings: The Two Towers, The year: 2002 imdbRating: 8.7


Die Ergebnisse sind hier definitiv nicht hilfreich, da wir vier zufällig ausgewählte Filme zurückbekommen. Es ist praktisch unmöglich, aus diesen vier zufällig ausgewählten Filmen den Schluss zu ziehen, dass wir für dieses Beispiel insgesamt 1.000 Filme getaggt und eingebettet haben.


Was ist also die Lösung? Sie ist ganz einfach: Für Fragen, die strukturierte Vorgänge wie Filtern, Sortieren und Aggregieren erfordern, sind Tools erforderlich, die für die Arbeit mit strukturierten Daten entwickelt wurden.

Tools für strukturierte Daten

Im Moment denken die meisten Leute anscheinend an den Text2Query-Ansatz, bei dem ein LLM eine Datenbankabfrage generiert, um basierend auf der bereitgestellten Frage und dem bereitgestellten Schema mit einer Datenbank zu interagieren. Für Neo4j ist dies Text2Cypher, aber es gibt auch Text2SQL für SQL-Datenbanken. In der Praxis stellt sich jedoch heraus, dass es nicht zuverlässig und nicht robust genug für den Produktionseinsatz ist.


Auswertung der Generierung von Cypher-Anweisungen. Aus meinem Blogbeitrag zur Auswertung von Cypher .


Sie können Techniken wie Gedankenketten, Beispiele mit wenigen Versuchen oder Feinabstimmungen verwenden, aber in dieser Phase ist es nahezu unmöglich, eine hohe Genauigkeit zu erreichen. Der Text2Query-Ansatz funktioniert gut für einfache Fragen zu unkomplizierten Datenbankschemata, aber das entspricht nicht der Realität in Produktionsumgebungen. Um dieses Problem zu lösen, verlagern wir die Komplexität der Generierung von Datenbankabfragen von einem LLM weg und behandeln es als Codeproblem, bei dem wir Datenbankabfragen deterministisch basierend auf Funktionseingaben generieren. Der Vorteil ist eine deutlich verbesserte Robustheit, allerdings auf Kosten einer geringeren Flexibilität. Es ist besser, den Umfang der RAG-Anwendung einzugrenzen und diese Fragen genau zu beantworten, als zu versuchen, alles zu beantworten, dies aber ungenau zu tun.


Da wir Datenbankabfragen – in diesem Fall Cypher-Anweisungen – auf der Grundlage von Funktionseingaben generieren, können wir die Toolfunktionen von LLMs nutzen. Dabei füllt das LLM die relevanten Parameter auf der Grundlage der Benutzereingaben auf, während die Funktion das Abrufen der erforderlichen Informationen übernimmt. Für diese Demonstration implementieren wir zunächst zwei Tools: eines zum Zählen und eines zum Auflisten von Filmen. Anschließend erstellen wir mit LangGraph einen LLM-Agenten.

Tool zum Zählen von Filmen

Wir beginnen mit der Implementierung eines Tools zum Zählen von Filmen auf der Grundlage vordefinierter Filter. Zunächst müssen wir definieren, was diese Filter sind, und einem LLM beschreiben, wann und wie sie zu verwenden sind:


 class MovieCountInput(BaseModel): min_year: Optional[int] = Field( description="Minimum release year of the movies" ) max_year: Optional[int] = Field( description="Maximum release year of the movies" ) min_rating: Optional[float] = Field(description="Minimum imdb rating") grouping_key: Optional[str] = Field( description="The key to group by the aggregation", enum=["year"] )


LangChain bietet mehrere Möglichkeiten, Funktionseingaben zu definieren, aber ich bevorzuge den Pydantic-Ansatz. In diesem Beispiel stehen uns drei Filter zur Verfügung, um die Filmergebnisse zu verfeinern: min_year, max_year und min_rating. Diese Filter basieren auf strukturierten Daten und sind optional, da der Benutzer wählen kann, ob er einige, alle oder keine davon einschließen möchte. Zusätzlich haben wir eine grouping_key-Eingabe eingeführt, die der Funktion mitteilt, ob die Anzahl nach einer bestimmten Eigenschaft gruppiert werden soll. In diesem Fall ist die einzige unterstützte Gruppierung die nach Jahr, wie im Enumerationsabschnitt definiert.


Definieren wir nun die eigentliche Funktion:


 @tool("movie-count", args_schema=MovieCountInput) def movie_count( min_year: Optional[int], max_year: Optional[int], min_rating: Optional[float], grouping_key: Optional[str], ) -> List[Dict]: """Calculate the count of movies based on particular filters""" filters = [ ("t.year >= $min_year", min_year), ("t.year <= $max_year", max_year), ("t.imdbRating >= $min_rating", min_rating), ] # Create the parameters dynamically from function inputs params = { extract_param_name(condition): value for condition, value in filters if value is not None } where_clause = " AND ".join( [condition for condition, value in filters if value is not None] ) cypher_statement = "MATCH (t:Target) " if where_clause: cypher_statement += f"WHERE {where_clause} " return_clause = ( f"t.`{grouping_key}`, count(t) AS movie_count" if grouping_key else "count(t) AS movie_count" ) cypher_statement += f"RETURN {return_clause}" print(cypher_statement) # Debugging output return graph.query(cypher_statement, params=params)


Die Funktion movie_count generiert eine Cypher-Abfrage zum Zählen von Filmen basierend auf optionalen Filtern und Gruppierungsschlüsseln. Sie beginnt mit der Definition einer Liste von Filtern mit entsprechenden Werten, die als Argumente bereitgestellt werden. Die Filter werden verwendet, um die WHERE-Klausel dynamisch zu erstellen, die für die Anwendung der angegebenen Filterbedingungen in der Cypher-Anweisung verantwortlich ist und nur die Bedingungen einschließt, deren Werte nicht „None“ sind.


Anschließend wird die RETURN-Klausel der Cypher-Abfrage erstellt, entweder durch Gruppierung nach dem angegebenen Gruppierungsschlüssel oder durch einfaches Zählen der Gesamtzahl der Filme. Schließlich führt die Funktion die Abfrage aus und gibt die Ergebnisse zurück.


Die Funktion kann bei Bedarf mit weiteren Argumenten und einer komplexeren Logik erweitert werden. Es muss jedoch sichergestellt werden, dass sie klar bleibt, damit ein LLM sie korrekt und präzise aufrufen kann.

Tool zum Auflisten von Filmen

Auch hier müssen wir damit beginnen, die Argumente der Funktion zu definieren:


 class MovieListInput(BaseModel): sort_by: str = Field( description="How to sort movies, can be one of either latest, rating", enum=["latest", "rating"], ) k: Optional[int] = Field(description="Number of movies to return") description: Optional[str] = Field(description="Description of the movies") min_year: Optional[int] = Field( description="Minimum release year of the movies" ) max_year: Optional[int] = Field( description="Maximum release year of the movies" ) min_rating: Optional[float] = Field(description="Minimum imdb rating")


Wir behalten dieselben drei Filter wie in der Filmzählfunktion bei, fügen aber das Beschreibungsargument hinzu. Mit diesem Argument können wir Filme anhand ihrer Handlung suchen und auflisten, indem wir eine Vektorähnlichkeitssuche verwenden. Nur weil wir strukturierte Tools und Filter verwenden, heißt das nicht, dass wir keine Text-Embedding- und Vektorsuchmethoden einbinden können. Da wir in den meisten Fällen nicht alle Filme zurückgeben möchten, fügen wir einen optionalen k-Input mit einem Standardwert hinzu. Außerdem möchten wir für die Auflistung die Filme so sortieren, dass nur die relevantesten zurückgegeben werden. In diesem Fall können wir sie nach Bewertung oder Erscheinungsjahr sortieren.


Lassen Sie uns die Funktion implementieren:


 @tool("movie-list", args_schema=MovieListInput) def movie_list( sort_by: str = "rating", k : int = 4, description: Optional[str] = None, min_year: Optional[int] = None, max_year: Optional[int] = None, min_rating: Optional[float] = None, ) -> List[Dict]: """List movies based on particular filters""" # Handle vector-only search when no prefiltering is applied if description and not min_year and not max_year and not min_rating: return neo4j_vector.similarity_search(description, k=k) filters = [ ("t.year >= $min_year", min_year), ("t.year <= $max_year", max_year), ("t.imdbRating >= $min_rating", min_rating), ] # Create parameters dynamically from function arguments params = { key.split("$")[1]: value for key, value in filters if value is not None } where_clause = " AND ".join( [condition for condition, value in filters if value is not None] ) cypher_statement = "MATCH (t:Target) " if where_clause: cypher_statement += f"WHERE {where_clause} " # Add the return clause with sorting cypher_statement += " RETURN t.title AS title, t.year AS year, t.imdbRating AS rating ORDER BY " # Handle sorting logic based on description or other criteria if description: cypher_statement += ( "vector.similarity.cosine(t.embedding, $embedding) DESC " ) params["embedding"] = embedding.embed_query(description) elif sort_by == "rating": cypher_statement += "t.imdbRating DESC " else: # sort by latest year cypher_statement += "t.year DESC " cypher_statement += " LIMIT toInteger($limit)" params["limit"] = k or 4 print(cypher_statement) # Debugging output data = graph.query(cypher_statement, params=params) return data


Diese Funktion ruft eine Liste von Filmen basierend auf mehreren optionalen Filtern ab: Beschreibung, Jahresbereich, Mindestbewertung und Sortiereinstellungen. Wenn nur eine Beschreibung ohne weitere Filter angegeben ist, führt sie eine Vektorindex-Ähnlichkeitssuche durch, um relevante Filme zu finden. Wenn zusätzliche Filter angewendet werden, erstellt die Funktion eine Cypher-Abfrage, um Filme basierend auf den angegebenen Kriterien wie Erscheinungsjahr und IMDb-Bewertung abzugleichen, und kombiniert sie mit einer optionalen beschreibungsbasierten Ähnlichkeit. Die Ergebnisse werden dann entweder nach dem Ähnlichkeitswert, der IMDb-Bewertung oder dem Jahr sortiert und auf k Filme beschränkt.

Alles als LangGraph-Agent zusammenfügen

Wir werden einen einfachen ReAct- Agenten mit LangGraph implementieren.


Implementierung des LangGraph-Agenten.


Der Agent besteht aus einem LLM- und einem Tools-Schritt. Wenn wir mit dem Agenten interagieren, rufen wir zuerst das LLM auf, um zu entscheiden, ob wir Tools verwenden sollen. Dann führen wir eine Schleife aus:


  1. Wenn der Agent eine Aktion anordnet (z. B. „Tool aufrufen“), führen wir die Tools aus und geben die Ergebnisse an den Agenten zurück.
  2. Wenn der Agent nicht die Ausführung von Tools angefordert hat, werden wir den Vorgang beenden (dem Benutzer antworten).


Die Code-Implementierung ist denkbar einfach. Zuerst binden wir die Tools an das LLM und definieren den Assistentenschritt:


 llm = ChatOpenAI(model='gpt-4-turbo') tools = [movie_count, movie_list] llm_with_tools = llm.bind_tools(tools) # System message sys_msg = SystemMessage(content="You are a helpful assistant tasked with finding and explaining relevant information about movies.") # Node def assistant(state: MessagesState): return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}


Als nächstes definieren wir den LangGraph-Flow:


 # Graph builder = StateGraph(MessagesState) # Define nodes: these do the work builder.add_node("assistant", assistant) builder.add_node("tools", ToolNode(tools)) # Define edges: these determine how the control flow moves builder.add_edge(START, "assistant") builder.add_conditional_edges( "assistant", # If the latest message (result) from assistant is a tool call -> tools_condition routes to tools # If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END tools_condition, ) builder.add_edge("tools", "assistant") react_graph = builder.compile()


Wir definieren zwei Knoten im LangGraph und verknüpfen sie mit einer bedingten Kante. Wenn ein Tool aufgerufen wird, wird der Fluss an die Tools weitergeleitet, andernfalls werden die Ergebnisse an den Benutzer zurückgesendet.


Lassen Sie uns jetzt unseren Agenten testen:


 messages = [ HumanMessage( content="What are the some movies about a girl meeting her hero?" ) ] messages = react_graph.invoke({"messages": messages}) for m in messages["messages"]: m.pretty_print()


Ergebnisse:


Ergebnisse


Im ersten Schritt wählt der Agent das Filmlistentool mit dem entsprechenden Beschreibungsparameter. Es ist unklar, warum er einen K-Wert von 5 auswählt, aber er scheint diese Zahl zu bevorzugen. Das Tool gibt die fünf relevantesten Filme basierend auf der Handlung zurück und das LLM fasst sie am Ende einfach für den Benutzer zusammen.


Wenn wir ChatGPT fragen, warum ihm ein K-Wert von 5 gefällt, erhalten wir die folgende Antwort.



Stellen wir als Nächstes eine etwas komplexere Frage, die eine Metadatenfilterung erfordert:


 messages = [ HumanMessage( content="What are the movies from the 90s about a girl meeting her hero?" ) ] messages = react_graph.invoke({"messages": messages}) for m in messages["messages"]: m.pretty_print()


Ergebnisse:


Ergebnisse


Dieses Mal wurden zusätzliche Argumente verwendet, um nur Filme aus den 1990er Jahren herauszufiltern. Dieses Beispiel wäre ein typisches Beispiel für die Filterung von Metadaten mit dem Vorfilteransatz. Die generierte Cypher-Anweisung grenzt die Filme zunächst ein, indem sie nach ihrem Erscheinungsjahr filtert. Im nächsten Teil verwendet die Cypher-Anweisung Texteinbettungen und eine Vektorähnlichkeitssuche, um Filme zu finden, in denen ein kleines Mädchen seinen Helden trifft.


Versuchen wir, Filme anhand verschiedener Bedingungen zu zählen:


 messages = [ HumanMessage( content="How many movies are from the 90s have the rating higher than 9.1?" ) ] messages = react_graph.invoke({"messages": messages}) for m in messages["messages"]: m.pretty_print()


Ergebnisse:


Ergebnisse


Mit einem dedizierten Tool zum Zählen verlagert sich die Komplexität vom LLM auf das Tool, sodass das LLM nur noch für das Auffüllen der relevanten Funktionsparameter verantwortlich ist. Diese Aufgabentrennung macht das System effizienter und robuster und reduziert die Komplexität der LLM-Eingabe.


Da der Agent mehrere Tools nacheinander oder parallel aufrufen kann, testen wir ihn mit etwas noch Komplexerem:


 messages = [ HumanMessage( content="How many were movies released per year made after the highest rated movie?" ) ] messages = react_graph.invoke({"messages": messages}) for m in messages["messages"]: m.pretty_print()


Ergebnisse


Ergebnisse


Wie erwähnt kann der Agent mehrere Tools aufrufen, um alle zur Beantwortung der Frage erforderlichen Informationen zu sammeln. In diesem Beispiel beginnt er mit der Auflistung der Filme mit der höchsten Bewertung, um zu ermitteln, wann der bestbewertete Film veröffentlicht wurde. Sobald diese Daten vorliegen, ruft er das Tool zur Filmzählung auf, um die Anzahl der Filme zu ermitteln, die nach dem angegebenen Jahr veröffentlicht wurden. Dabei wird ein in der Frage definierter Gruppierungsschlüssel verwendet.

Zusammenfassung

Während sich Texteinbettungen hervorragend zum Durchsuchen unstrukturierter Daten eignen, sind sie bei strukturierten Vorgängen wie Filtern , Sortieren und Aggregieren unzureichend. Für diese Aufgaben sind Tools erforderlich, die für strukturierte Daten entwickelt wurden und die nötige Präzision und Flexibilität für diese Vorgänge bieten. Die wichtigste Erkenntnis ist, dass Sie durch die Erweiterung des Tool-Sets in Ihrem System ein breiteres Spektrum von Benutzeranfragen bearbeiten können, wodurch Ihre Anwendungen robuster und vielseitiger werden. Die Kombination strukturierter Datenansätze und unstrukturierter Textsuchtechniken kann genauere und relevantere Antworten liefern und so letztendlich das Benutzererlebnis in RAG-Anwendungen verbessern.


Der Code ist wie immer auf GitHub verfügbar.


Um mehr über dieses Thema zu erfahren, besuchen Sie uns am 7. November bei NODES 2024, unserer kostenlosen virtuellen Entwicklerkonferenz zu intelligenten Apps, Wissensgraphen und KI. Jetzt registrieren!