Startseite / Artikel / Erlauben Sie Gemini, die Quelle auszuwählen: FAISS, Tavily und direkte Antworten in LangGraph

Erlauben Sie Gemini, die Quelle auszuwählen: FAISS, Tavily und direkte Antworten in LangGraph

Erstellen Sie einen kleinen LangGraph-Ablauf, bei dem Gemini jede Frage an eine FAISS-Wissensdatenbank, eine Tavily-Websuche oder eine direkte Antwort leitet, wobei die Ausgabe strukturiert wird.

3771 Wörter

Die meisten Prototypen zur Beantwortung von Fragen leiten jede Anfrage durch einen festgelegten Ablauf, obwohl die Fragen unterschiedliche Anforderungen haben: Einige benötigen interne Unternehmensdokumente, andere täglich wechselnde Fakten und wieder andere lediglich allgemeines Wissen. In dieser Anleitung wird ein kompakter Python-Ablauf erstellt, bei dem ein Sprachmodell zunächst jede Frage prüft und sie an den richtigen Ort weiterleitet: an einen FAISS-Vektorlagern für internes Wissen, an Tavily für aktuelle Web-Ergebnisse oder direkt an Gemini. Am Ende werden Sie verstehen, wie Zustände, Knoten und bedingte Kanten in LangGraph zusammenwirken, warum eine strukturierte Ausgabe einen LLM-Router zuverlässig macht und wo dieses lehrreiche Design vor der Nutzung durch echte Nutzer noch verbessert werden muss. Wenn Sie ein umfassenderes Verzeichnis von Graphformen wünschen, finden Sie den Artikel zu Routing, Fan-Out hier.

„Kritik- und Genehmigungsmustere in LangGraph“ ergänzt diesen praktischen Aufbau.

Drei Fragen, drei verschiedene Quellen

Stellen Sie sich drei Benutzer vor. Der erste fragt nach der Urlaubsrichtlinie des Unternehmens; nur interne Dokumente können darauf antworten. Der zweite möchte das Wetter in Uttarakhand für heute wissen; kein internes Dokument enthält diese Information, daher muss das System im Internet suchen. Der dritte fragt, was RAG ist; das Modell weiß es bereits, und das Abrufen weiterer Informationen würde nur zu Verzögerungen führen.

Daher sieht die Zielarchitektur vor, einen mit Gemini ausgestatteten Router vor drei Zweige zu platzieren, die alle auf einem einzigen Schritt zur Erstellung der Antwort zusammenlaufen:

User Question
                               |
                               v
                       +----------------+
                       |   AI Router    |
                       |    (Gemini)    |
                       +----------------+
                         /      |      \
                        /       |       \
                       v        v        v
                    FAISS    Tavily    Gemini
                 Internal DB  Web      Direct
                       \        |        /
                        \       |       /
                         v      v       v
                       +----------------+
                       | Generate Answer|
                       |     Gemini     |
                       +----------------+
                               |
                               v
                            Answer

Der Router ist das Herzstück des Designs. Der verlockende Kurzweg besteht darin, Schlüsselwörter abzugleichen, wie im folgenden Pseudocode gezeigt, bei dem bestimmte Wörter in der Frage einen Zweig auswählen:

if "weather" in question:
    use_tavily()
elif "leave" in question:
    use_faiss()
else:
    use_gemini()

Hier wird diese Entscheidung stattdessen an Gemini delegiert. Da das Modell die Frage interpretiert anstatt nach Auslösewörtern darin zu suchen, erhält der Workflow die Bezeichnung „aggressiv“.

Generative KI, Agenten und aggressive Workflows

Diese Begriffe werden oft austauschbar verwendet, beschreiben aber unterschiedliche Ebenen der Modellbeteiligung.

Einfache generative KI

In der einfachsten Anwendung wird eine Anfrage an ein Modell übergeben und das Ergebnis, das es erzeugt, zurückgegeben:

User Question
      |
      v
     LLM
      |
      v
    Answer

Es kann RAG anhand von Schulungsdaten erklären, kennt aber nichts über Ihre Urlaubsrichtlinien.

Agenten, die Tools aufrufen

Ein Agent erweitert das Modell um Tools wie eine Datenbank, einen Suchmaschinen oder eine externe API und lässt es entscheiden, ob es eines davon vor der Antwort aufrufen soll:

User Question
      |
      v
     LLM
      |
      +------> Database
      |
      +------> Web Search
      |
      +------> API
      |
      v
    Answer

Eine Wetterfrage kann beispielsweise durch Abfragen eines Wetterdienstes beantwortet werden, anstatt dass das Modell eine plausible Prognose erfinden muss.

Agentenbasierte Workflows

Ein agentenbasierter Workflow gibt dem Modell Einfluss auf den Ablauf der Ausführung zur Laufzeit. In diesem Projekt sieht der Ablauf wie folgt aus:

Question
   |
   v
Router
   |
   +----> FAISS
   |
   +----> Tavily
   |
   +----> Gemini

Der Entwickler definiert die drei möglichen Routen; das Modell wählt lediglich für jede eingehende Frage daraus aus. Es erhält keine freie Kontrolle über die Anwendung, sondern nur eine Liste der zulässigen Aktionen. „Agentenbasiertes Routing-Workflow“ ist der präziseste Name für diese Struktur.

Projekt einrichten

Es werden Python 3.10 oder neuer, eine API-Schlüssel für Google Gemini sowie eine API-Schlüssel für Tavily benötigt. Installieren Sie die Bibliotheken auf einmal:

pip install -U langchain langchain-google-genai langchain-community langgraph faiss-cpu tavily-python python-dotenv pydantic

Legen Sie beide Schlüssel in eine .env-Datei statt im Quellcode ab:

GOOGLE_API_KEY=your_google_api_key
TAVILY_API_KEY=your_tavily_api_key

Die Imports umfassen Typhilfen, Pydantic für das Routing-Schema, die Dokumenten- und FAISS-Wrapper von LangChain, die Gemini-Chat- und Embedding-Klassen, die Graph-Primitiven von LangGraph sowie den Tavily-Client:

import os
from typing import List, Literal
from typing_extensions import TypedDict
from dotenv import load_dotenv
from pydantic import BaseModel
from langchain_core.documents import Document
from langchain_community.vectorstores import FAISS
from langchain_google_genai import (
    ChatGoogleGenerativeAI,
    GoogleGenerativeAIEmbeddings,
)
from langgraph.graph import StateGraph, START, END
from tavily import TavilyClient

Der nächste Schritt, der als einzeiliger Codeausschnitt dargestellt wird, besteht einfach darin, die Umgebungsvariablen zu laden:

Load the environment variables:

Mit override=True haben die Werte aus .env Vorrang vor Variablen, die bereits in der Shell gesetzt wurden:

load_dotenv(override=True)

Konfigurieren von Gemini für Routing, Antworten und Embeddings

Das Chat-Modell erfüllt zwei Funktionen. Zunächst wählt es aus, welche Quelle eine Frage bearbeiten soll, und erstellt anschließend die endgültige Antwort aus dem gesammelten Kontext. Die untenstehende Konfiguration ermöglicht zwei automatische Wiederholungsversuche und lässt die Token- sowie Zeitlimits ungesetzt:

model = ChatGoogleGenerativeAI(
    model="gemini-3.6-flash",
    max_tokens=None,
    timeout=None,
    max_retries=2,
    api_key=os.getenv("GOOGLE_API_KEY"),
)

Modellidentifikatoren ändern sich häufig, daher sollten Sie den Namen im Auszug mit der aktuellen Liste der Gemini-Modelle abgleichen, bevor Sie es ausführen.

Embeddings sind ein eigenständiges Konzept, das von einem separaten Modell behandelt wird. Gemini Embedding wandelt jedes Dokument in einen numerischen Vektor um:

embeddings = GoogleGenerativeAIEmbeddings(
    model="models/gemini-embedding-001",
    google_api_key=os.getenv("GOOGLE_API_KEY"),
)

Vektoren machen die semantische Suche möglich: Texte mit ähnlicher Bedeutung liegen im Vektorraum nah beieinander, sodass eine Frage relevante Abschnitte finden kann, selbst wenn sie nur wenige exakte Wörter miteinander teilen.

Eine kleine interne Wissensdatenbank in FAISS

Um die Aufmerksamkeit auf den Arbeitsablauf zu richten, enthält die Wissensdatenbank nur drei kurze Dokumente: eine Rückerstattungspolitik für 30 Tage, einen Anspruch auf 20 bezahlte Urlaubstage unter der Voraussetzung einer siebentägigen Ankündigungsfrist sowie die Support-Stunden an Wochentagen. In einem echten System stammen diese Texte aus Handbüchern, PDFs, Notion-Seiten, Support-Tickets, internen Dokumenten oder Datenbankzeilen.

documents = [
    Document(
        page_content="""
        Our company provides a 30-day refund policy.
        Customers can request a refund within 30 days of purchase.
        """
    ),
    Document(
        page_content="""
        Employees receive 20 paid vacation days per year.
        Vacation requests must be submitted at least 7 days in advance.
        """
    ),
    Document(
        page_content="""
        The company provides technical support from Monday to Friday,
        9 AM to 6 PM IST.
        """
    ),
]

Um den Store zu erstellen und ihn als Retriever zu verpacken, sind zwei Aufrufe erforderlich. Wenn k auf 3 gesetzt wird, werden die drei nächsten Dokumente angefordert, was bei diesem vereinfachten Datensatz bedeutet, dass jedes Dokument nach Ähnlichkeit gerankt zurückgegeben wird:

vector_store = FAISS.from_documents(
    documents,
    embeddings,
)

retriever = vector_store.as_retriever(
    search_kwargs={"k": 3}
)

FAISS versteht keine Sprache; es handelt sich um einen Index für die Suche nach dem nächsten Nachbarn. Das Embedding-Modell wandelt die eingegebene Frage in einen Vektor um, und FAISS gibt die gespeicherten Dokumente zurück, deren Vektoren am nächsten daran liegen.

Hinzufügen eines Tavily-Clients für Echtzeitinformationen

Die Websuche erfordert lediglich eine Client-Instanz, die mit dem zweiten API-Schlüssel erstellt wird:

tavily_client = TavilyClient(
    api_key=os.getenv("TAVILY_API_KEY")
)

Entwurf des gemeinsamen Zustands

Zustand ist die zentrale Idee in LangGraph: ein typisiertes Dictionary, das durch den Graphen wandert und von jedem Knoten gelesen sowie ergänzt wird. Dieser Ablauf benötigt die Frage, alle abgerufenen Dokumente, die Tavily-Ergebnisse, die ausgewählte Quelle sowie die endgültige Antwort:

class AgentState(TypedDict):
    question: str
    documents: List[Document]
    tavily_response: str
    source: str
    answer: str

Konzeptionell verhält es sich wie ein gemeinsames Datensatz, den jeder Schritt einsehen kann:

AgentState
                    |
        +-----------+-----------+
        |           |           |
    question    documents    source
                    |
             tavily_response
                    |
                  answer

Ein Knoten erhält den aktuellen Zustand, verwendet die für ihn relevanten Felder und gibt Aktualisierungen zurück, die LangGraph vor dem Weitergeben an den nächsten Knoten zusammenführt.

Der FAISS-Auswertungsknoten

Dieser Knoten nimmt die Frage aus dem Zustand entgegen, führt sie über den Suchmechanismus durch und speichert die passenden Dokumente:

def retrieve_from_faiss(state : AgentState) -> AgentState:
    question = state['question']

    """ Fetch the details from the FAISS vector database
    """

    result = retriever.invoke(question)

    return {**state, "documents": result}

Es wird nur ausgeführt, wenn der Router die interne Wissensdatenbank ausgewählt hat. Eine Frage wie die unten stehende ist ein typischer Auslöser:

"What is our company leave policy?"

Für diese Eingabe sollte der Abrufmechanismus das Urlaubsdokument anzeigen:

Employees receive 20 paid vacation days per year.
Vacation requests must be submitted at least 7 days in advance.

Der Tavily-Suchknoten

Der Web-Suchknoten sendet die Frage an Tavily mit einem search_depth, der auf advanced gesetzt ist, und extrahiert anschließend das content-Feld aus jedem Ergebnis:

def search_with_tavily(state: AgentState) -> AgentState:
    question = state['question']
    """ Using the Tavily to search the web and
      fetch the latest information about user query
    """

    response = tavilyClient.search(
        query=question,
        search_depth='advanced'
    )

    contents = [result["content"] for result in response["results"]]
    return {**state, "tavilyResponse":contents}

Diese Auszüge bilden den Kontext, den Gemini beim Erstellen der Antwort liest. Beachten Sie, dass der Knoten eine Liste von Zeichenketten zurückgibt; falls Sie einen einzigen Textblock bevorzugen, fügen Sie die Einträge vor der Speicherung zusammen, damit das Feld zum in dem Zustand deklarierten str-Typ passt.

Den Router durch strukturierte Ausgaben zuverlässig machen

Ein naiver Router bittet das Modell, mit einem von drei einfachen Wörtern zu antworten:

Return only:
faiss
tavily
gemini

Sprachmodelle befolgen nicht immer die Formatierungsanweisungen. Es kann vorkommen, dass eine vollständige Satzzeile zurückgegeben wird:

I would choose tavily.

Oder eine leicht abweichende Formulierung:

The best option is: tavily

Beide Antworten stören das Diagramm, das eine exakte Zeichenkette zur Auswahl einer Kante benötigt. Strukturierte Ausgaben schließen diese Lücke. Zunächst sollten die zulässigen Entscheidungen als Pydantic-Modell beschrieben werden, dessen einziges Feld auf drei festgelegte Werte beschränkt ist:

class RouteDecision(BaseModel):
    source: Literal["faiss", "tavily", "gemini"]

Dann sollte ein Router-Modell abgeleitet werden, das eine Instanz dieses Schemas zurückgeben muss. Die json_schema-Methode verlangt vom Anbieter, die Erstellung auf das Schema zu beschränken, anstatt sich allein auf die Formulierung der Anfrage zu verlassen:

router_model = model.with_structured_output(
    RouteDecision,
    method="json_schema",
)

Die Ausgabe wird anhand des Literal-Typs überprüft, sodass ungültige Werte als Fehler angezeigt werden, anstatt das Diagramm in eine undefinierte Richtung zu lenken.

Erstellung des Entscheidungsnodes

Die Routing-Anweisung beschreibt jede Option: faiss für Fragen zur Unternehmenspolitik und anderen internen Kenntnissen, tavily für alles, was aktuelle oder webbasierte Informationen erfordert, sowie gemini für allgemeines Wissen, das weder das eine noch das andere benötigt:

def decide_source(state:AgentState)-> AgentState:
    question = state["question"]
    prompt = f"""
    Decide the best source for answering this question.

    Choose exactly one:

    faiss:
    Use when the question can be answered using our internal
    knowledge base.related to company policy and all

    tavily:
    Use when the question requires current, recent, or web-based
    information.

    gemini:
    Use when the question is general knowledge and does not
    require our internal documents or current web information.

    Question:
    {question}

    Return only one word:
    faiss, tavily, or gemini
    """

    response = model.invoke(prompt)
    # return response

    return {**state,"source":response.text}

Betrachten Sie die letzten Zeilen genau. Wie sie geschrieben sind, ruft der Node weiterhin den einfachen model auf und speichert response.text, was genau der oben beschriebene anfällige Ansatz mit freiem Text ist. Um die Vorteile des Schemas zu nutzen, rufen Sie stattdessen router_model.invoke(prompt) auf und speichern Sie das source-Attribut des Ergebnisses. Durch diese Änderung gibt der Router ein typisiertes Objekt wie dieses zurück anstelle von willkürlicher Prosa:

RouteDecision(source="faiss")

Angabe an LangGraph, wohin es als Nächstes gehen soll

Konditionale Kanten benötigen eine Funktion, die angibt, welcher Zweig verfolgt werden soll. Diese Funktion trifft keine eigenen Entscheidungen; sie liest die bereits vom Router im Zustand gespeicherte Wahl aus und gibt sie an LangGraph zurück:

def route_source(
    state: AgentState,
) -> Literal["faiss", "tavily", "gemini"]:
    return state["source"]

Ein Antwortknoten pro Route

Sie alle drei Zweige enden im selben Generierungsschritt. Dabei wird source überprüft und entsprechend ein Prompt erstellt: abgerufene Dokumente werden zu einem Kontext für FAISS zusammengeführt, Suchauszüge dienen als Referenzmaterial für Tavily oder die ursprüngliche Frage wird direkt beantwortet:

# Generate the answer for the user
def generateAnswer(state:AgentState) -> AgentState:
    source = state["source"]
    question = state['question']
    documents = state['documents']
    tavilyResponse = state['tavilyResponse']

    if source == "faiss":
        context = "\n\n".join([doc.page_content for doc in documents])
        prompt = f"""Based on the following context answer the question below
           Context:
           {context}

        Question:
        {question}
           """
    elif source == "tavily":
        prompt = f""" Based on the following search result , use this as an reference and provdie the
         answer to the below question

         Context:
         {tavilyResponse}

         Question:
         {question}
        """
    else:
        prompt = f" Answer the following question : {question}"
    response = model.invoke(prompt)
    answer = response.content

    return {**state, "answer":answer}

Das gleiche Gemini-Modell schreibt jede Antwort. Das Einzige, was sich unterscheidet, ist der vorangestellte Kontext – das ist die wesentliche Erkenntnis von RAG: bessere Eingaben, nicht ein anderes Modell, erzeugen fundierte Antworten.

Namekonsistenz bei der Zusammenstellung der Auszüge

Die Auszüge kombinieren zwei Namenskonventionen, und die Unstimmigkeiten führen zu Fehlern, wenn man sie unverändert zusammenklebt. Der State deklariert tavily_response, während die Nodes tavilyResponse lesen und schreiben; der Client wird als tavily_client erstellt, aber als tavilyClient aufgerufen; und die Answer-Funktion ist als generateAnswer definiert, wird aber als generate_answer registriert. Wählen Sie eine Konvention aus und wenden Sie sie überall an, bevor Sie das Graph ausführen.

Aufbau des Graphs

Zu diesem Zeitpunkt bestehen die Bestandteile aus einem Entscheidungsnode sowie drei möglichen Fortsetzungen:

decide_source
      |
      +----> faiss
      |
      +----> tavily
      |
      +----> gemini

Es gibt hier eine Feinheit. Der gemini-Pfad ist überhaupt kein Abrufschritt; er bedeutet „Abruf überspringen und direkt antworten“. Daher kann dieser Zweig anstelle eines leeren Nodes direkt auf den gemeinsamen Generierungsnode verweisen.

Fangen Sie damit an, ein Diagramm für den Zustandstyp zu erstellen:

workflow = StateGraph(AgentState)

Registrieren Sie die vier Knoten:

workflow.add_node("decide", decide_source)
workflow.add_node("faiss", retrieve_from_faiss)
workflow.add_node("tavily", search_with_tavily)
workflow.add_node("generate", generate_answer)

Machen Sie den Entscheidungsknoten zum Eingangspunkt:

workflow.add_edge(START, "decide")

Verschließen Sie die bedingten Kanten. Die Zuordnung wandelt jeden Wert um, den die Routenfunktion zurückgeben kann, in einen Knotennamen um – dorthin wird gemini direkt an generate gesendet:

workflow.add_conditional_edges(
    "decide",
    route_source,
    {
        "faiss": "faiss",
        "tavily": "tavily",
        "gemini": "generate",
    },
)

Die Abfrageschleifen müssen anschließend in die Erstellung der Antwort fließen:

workflow.add_edge("faiss", "generate")
workflow.add_edge("tavily", "generate")

Die Erstellung ist der letzte Schritt, bevor das Diagramm endet:

workflow.add_edge("generate", END)

Durch Kompilieren wird die Definition in eine ausführbare Anwendung umgewandelt:

app = workflow.compile()

Das fertige Diagramm

Der gesamte Ablauf, von Anfang bis Ende:

START
                           |
                           v
                    +--------------+
                    |    decide    |
                    |    source    |
                    +--------------+
                     /      |      \
                    /       |       \
                   v        v        v
               +------+ +--------+ +---------+
               |FAISS | | Tavily | | Generate|
               |      | |        | | directly|
               +------+ +--------+ +---------+
                   \        |         /
                    \       |        /
                     v      v       v
                    +----------------+
                    |    generate    |
                    |     answer     |
                    +----------------+
                            |
                            v
                           END

Bleiben Sie beim grundlegenden Prinzip: Das Graph definiert die Menge möglicher Pfade, und das Modell wählt laufend einen davon aus. Diese Kombination sorgt dafür, dass der Workflow proaktiv ist, ohne unvorhersehbar zu werden.

Probieren der drei Ansätze

Ein kleiner Hilfsprogramm erstellt den Anfangszustand und ruft die kompilierte Anwendung auf:

def ask_question(question: str):
    initial_state = {
        "question":question,
        "documents":[],
        "tavilyResponse":"",
        "source":""
    }

    result = app.invoke(initial_state)
    return result

Eine Frage, die das Internet erfordert

Die Wetterfrage sollte an Tavily gesendet werden:

result = ask_question(
    "What is the current weather in Uttarakhand?"
)

Drucken Sie sowohl die ausgewählte Quelle als auch die erzeugte Antwort:

print("Source:", result["source"])
print("Answer:", result["answer"])

Der erwartete Ansatz:

Source: tavily

Die aktuellen Bedingungen existieren nur im Internet.

Eine Allgemeinwissen-Frage

Folgend sollte nach Retrieval Augmented Generation gefragt werden:

result = ask_question(
    "What is Retrieval Augmented Generation?"
)

Drucken Sie das Ergebnis auf dieselbe Weise:

print("Source:", result["source"])
print("Answer:", result["answer"])

Der Router sollte die Datenerfassung gänzlich überspringen:

Source: gemini

Das Modell kann das Konzept von selbst erklären.

Eine Frage zur internen Richtlinie

Zum Schluss die Frage zur Urlaubsrichtlinie:

result = ask_question(
    "What is our company leave policy?"
)

Und die gleichen Ausgabeanweisungen:

print("Source:", result["source"])
print("Answer:", result["answer"])

Dieses Mal sollte die interne Wissensdatenbank gewinnen:

Source: faiss

Die Routierung von LLMs ist probabilistisch, daher sollten diese Ergebnisse als erwartete Ausfälle und nicht als Garantien betrachtet werden.

Warum semantische Routierung bessere Ergebnisse liefert als Schlüsselwortregeln

Hier ist wieder die fest programmierte Alternative:

if "weather" in question:
    use_tavily()
elif "leave" in question:
    use_faiss()
else:
    use_gemini()

Solche Regeln verfallen schnell. Stellen Sie sich vor, ein Benutzer fragt, ob das Büro am Samstag geöffnet ist. In diesem Satz wird keine Richtlinie erwähnt, doch die Antwort könnte sehr wohl in der internen Dokumentation zu finden sein. Ein modellbasiertes Router kann die Absicht ableiten; eine Schlüsselwortliste kann das nicht, es sei denn, jemand antizipiert jede mögliche Formulierung.

Der Ansatz skaliert außerdem besser. Wenn neue Backend-Systeme hinzukommen, wie die unten aufgeführten, erweitert man das Schema sowie den Prompt anstelle davon, ein Gewirr aus bedingten Anweisungen zu entwickeln:

SQL Database
Internal API
CRM
Customer Support System
Documentation
Web Search

Der Kompromiss besteht in einem zusätzlichen Modellaufruf mit seinen Kosten und Latenzzeiten, bevor die eigentliche Arbeit beginnt.

Ist das wirklich ein KI-Agent?

Es ist genauer, ihn als kleinen agierenden Workflow statt als autonomen Agenten zu bezeichnen. Die verfügbaren Aktionen sind im Voraus festgelegt:

FAISS
Tavily
Direct Gemini

Das Modell hat keine Möglichkeit, selbstständig etwas wie Folgendes zu entscheiden, weil ihm diese Fähigkeiten nie gegeben wurden:

delete a database
send an email
call an arbitrary API

Die Architektur lässt sich als Kette der Verantwortlichkeiten verstehen:

Developer defines possible actions
              |
              v
        LLM chooses action
              |
              v
        LangGraph executes
              |
              v
           Result

Diese Einschränkung ist eigentlich ein Vorteil: begrenzte Auswahlmöglichkeiten sorgen dafür, dass das Verhalten in der Produktion vorhersehbar und überprüfbar bleibt.

Die Rolle jeder Komponente

LangGraph: Der Workflow-Engine

LangGraph besitzt die Struktur der Anwendung: Zustand, Knoten, Kanten, bedingte Routenplanung sowie die Ausführungsreihenfolge. Im Großen und Ganzen folgt jeder Schritt demselben Muster:

State
  |
  v
Node
  |
  v
Updated State
  |
  v
Conditional Edge
  |
  +----> Node A
  |
  +----> Node B
  |
  +----> Node C

Ein Graph aus kleinen Schritten lässt sich leichter testen und beobachten als eine umfangreiche Funktion.

FAISS: die Abrufschicht

FAISS stellt den RAG-Bereich des Systems bereit. In vereinfachter Form:

Company Documents
       |
       v
   Embeddings
       |
       v
     FAISS
       |
       v
Similar Documents
       |
       v
     Gemini
       |
       v
     Answer

Bei einer echten Implementierung wird davor ein Einpflegepipeline hinzugefügt:

Documents
   |
   v
Load
   |
   v
Split into chunks
   |
   v
Generate embeddings
   |
   v
Store vectors
   |
   v
Retrieve relevant chunks
   |
   v
Generate answer

Das Beispiel überspringt das Laden und Aufteilen in Blöcke, um sich auf den Workflow zu konzentrieren; echte Handbücher benötigen beides.

Tavily: Live-Websuche

Tavily kümmert sich um Informationen, die sich im Laufe der Zeit ändern:

User Question
      |
      v
   Router
      |
      v
    Tavily
      |
      v
 Search Results
      |
      v
    Gemini
      |
      v
    Answer

Typische Beispiele sind das aktuelle Wetter, aktuelle Nachrichten, neu veröffentlichte Produkte, aktuelle Dokumentationen, kürzliche Ereignisse sowie Echtzeit-Marktdaten. In der Produktion muss außerdem entschieden werden, wie Suchergebnisse zitiert, gefiltert, überprüft und dargestellt werden sollen, da Webinhalte weder garantiert genau noch sicher sind, um unüberprüft an ein Modell weitergeleitet zu werden.

Das Muster, das man sich merken sollte

Die Komponenten sind austauschbar. Die zentrale Idee ist das Routing: Jede Anfrage wird der am besten geeigneten Fähigkeit zugeordnet, um sie zu beantworten.

Internal Knowledge
        |
        +------ FAISS
Current Information
        |
        +------ Tavily
General Knowledge
        |
        +------ Gemini

Diese Auswahl der Fähigkeit pro Anfrage kommt in nahezu jeder ernsthaften agierenden Anwendung vor.

Welchen nächsten Schritt gehen?

Mehr Tools hinzufügen

Der Router könnte aus vielen weiteren Backend-Systemen wählen:

SQL Database
REST APIs
CRM
Email
Calendar
Internal Documentation

Die Abfragefunktion stärken

Drei Dokumente dienen nur zur Demonstration und stellen keine Wissensdatenbank dar. Ein echtes RAG benötigt Dokumentlader, Aufteilung in Blöcke, Metadaten, bessere Suchstrategien, Neubewertung der Ergebnisse, Quellenangaben sowie Zugriffskontrolle, damit Benutzer nur das ansehen können, was ihnen gestattet ist.

Überprüfung der Routing-Entscheidungen

Eine Überprüfungsstufe zwischen dem Router und den Tools kann unplausible Auswahlmöglichkeiten ablehnen und sicher auf eine Alternative zurückgreifen:

Router
   |
   v
Validator
   |
   +---- valid ----> Tool
   |
   +---- invalid --> Fallback

Das wird umso wichtiger, je mehr Tools vorhanden sind.

Fehlerbehandlung

Eine strukturierte Ausgabe garantiert zwar einen gültigen Weg, aber nicht unbedingt einen erfolgreichen Aufruf eines Tools. Eine Such-API kann ablaufen oder nichts zurückgeben:

Router
   |
   v
Tavily
   |
   X
Search failed
   |
   v
Fallback

Produktionsysteme benötigen Wiederholungsversuche sowie einen Ersatzmechanismus, beispielsweise eine direkte Antwort mit klarer Einschränkung. Der Artikel zu der Erstellung widerstandsfähiger Agentengraphen mit Wiederholungsversuchen und Ersatzmechanismen geht dieser Thematik näher auf den Grund.

Mach es beobachtbar

Wenn eine Antwort falsch ist, muss man wissen, in welcher Phase ein Fehler aufgetreten ist:

Wrong route?
      |
      v
Bad retrieval?
      |
      v
Bad search results?
      |
      v
Bad generation?

Durch das Protokollieren der ausgewählten Quellen- und Zwischenergebnisse lässt sich diese Frage beantworten.

Führe Schleifen ein

Der aktuelle Graph trifft genau eine Entscheidung:

Question
   |
   v
Router
   |
   v
Tool
   |
   v
Answer

Ein leistungsfähigerer Agent bewertet das Findene und entscheidet, ob er erneut handeln soll:

Question
   |
   v
Reason
   |
   v
Tool
   |
   v
Evaluate Result
   |
   +---- Need more information?
   |            |
   |            v
   |          Tool
   |            |
   +------------+
   |
   v
Final Answer

Hier gewinnen agierende Systeme ihre wahre Kraft: Das Modell beurteilt, ob die vorliegenden Informationen ausreichen oder ob eine weitere Aktion erforderlich ist. Auch hier sind Iterationslimits notwendig, damit ein Schleifenlauf nicht endlos weiterläuft.

Kernpunkte

Das gesamte System beantwortet eine Frage: Wie sollte eine KI-Anwendung entscheiden, woher die Antwort stammt? Der fertige Workflow sieht wie folgt aus:

User Question
                   |
                   v
                Gemini
                Router
                   |
        +----------+----------+
        |          |          |
        v          v          v
      FAISS      Tavily    Gemini
   Internal DB    Web      Direct
        |          |          |
        +----------+----------+
                   |
                   v
                Gemini
              Final Answer
  • Definieren Sie Fähigkeiten und Grenzen im Graphen; lassen Sie das Modell laufzeitbasiert daraus wählen.
  • Verwenden Sie strukturierte Ausgaben für Routing-Entscheidungen und stellen Sie sicher, dass der Entscheidungsnode tatsächlich das strukturierte Modell aufruft.
  • Behalten Sie einen einzigen Antwortgenerierungsnode bei und variieren nur den Kontext, den Sie ihm zuführen.
  • Betrachten Sie das Routing als testbaren Komponenten: Protokollieren Sie Entscheidungen und überprüfen Sie sie anhand repräsentativer Fragen.
  • Fügen Sie vor dem Hinzufügen weiterer Tools Validierungen, Fallback-Mechanismen sowie Überwachungsfunktionen hinzu, denn jedes neue Tool erhöht die Anzahl der Möglichkeiten, bei denen eine Anfrage fehlschlagen kann.
  • Von hier aus erstreckt sich diese Grundlage selbstverständlich auf SQL-Datenbanken, APIs, Arbeitsspeicher, menschliche Freigabe, Bewertungsknoten, Wiederholungsversuche sowie mehragentige Setups. Das Netzwerk wächst, doch das Prinzip bleibt unverändert: Geben Sie dem Modell nützliche Fähigkeiten, definieren Sie, wie sie verwendet werden dürfen, und lassen Sie es entscheiden, welche für die Aufgabe am besten geeignet ist.

    Verwandte Literatur