Startseite / Artikel

Fragebeantwortung über SQL-Tools und Websuche mit einem Gemini-Agenten

Wie ein LangChain-Toolaufruf-Agent auf Vertex AI zwischen drei SQLite-Text-zu-SQL-Tools und einer Live-Websuche wählt, sowie die zu erwartenden Fehler bei den Daten, Abhängigkeiten und der Authentifizierung.

Verlag: REACTAPP.TOP13 Min. Lesezeit2891 Wörter

Quellen: Building a Multi-Tool AI Agent for Bangladesh: Teaching Gemini to Pick the Right Tool for the Job

Ein Chatbot, der auf einer einzigen Datenquelle basiert, kann eine bestimmte Art von Frage gut bewältigen, bricht jedoch sofort zusammen, sobald der Benutzer nach etwas Ähnlichem fragt – entweder erfindet er eine Antwort oder gibt auf. Ein nützlicheres ingenieurtechnisches Ziel ist ein System, das die Grenzen seines eigenen Wissens erkennt und jede Frage an den Komponenten weiterleitet, der sie tatsächlich beantworten kann. Diese Anleitung erstellt genau so ein System: einen einzigen Agenten, der Fragen zu Bangladesch (Schulen, Krankenhäuser, Restaurants und allgemeines Wissen) beantwortet, indem er für jede Frage eines von vier spezialisierten Werkzeugen auswählt. Dabei erfahren Sie, wie SQLite-Datenbanken als sichere Text-zu-SQL-Werkzeuge verwendet werden können, warum Werkzeugbeschreibungen wichtiger sind als Routing-Code sowie welche nicht-modellbezogenen Fehler am ehesten ein solches Projekt blockieren können.

Warum ein Orchesterator besser ist als ein Modell, das alles weiß

Die meisten ernsthaften KI-Systeme von heute weisen ein gemeinsames Muster auf: Ein Sprachmodell, das gut darin ist, Absichten zu verstehen, steht in Verbindung mit mehreren zuverlässigen, eng gefassten Werkzeugen, die gut darin sind, Fakten abzurufen. Support-Assistenten wechseln zwischen einem Abrechnungsbackend und einer Wissensdatenbank; Forschungsassistenten wechseln zwischen Websuche und internen Dokumenten. Das Modell muss die Antworten nicht selbst speichern – es muss lediglich entscheiden, auf welche Fähigkeit zurückgegriffen werden soll.

Das hier vorgestellte Projekt wendet dieses Muster mit vier Spezialisten und einem Orchestrierer an:

┌──────────────────────────┐
   question  ──────────▶ │      AgentExecutor        │
                         │  (Gemini + tool-calling)  │
                         └────────────┬──────────────┘
                                      │ picks 1+ tools based on
                                      │ their description
              ┌──────────────┬───────┴───────┬───────────────┐
              ▼              ▼               ▼               ▼
     InstitutionsDBTool HospitalsDBTool RestaurantsDBTool WebSearchTool
              │              │               │                │
      institutions.db   hospitals.db   restaurants.db     Tavily API
        (SQLite)          (SQLite)       (SQLite)         (live web)
  • InstitutionsDBTool umfasst Schulen, Hochschulen und Madrasas aus dem offiziellen EIIN-Register.
  • HospitalsDBTool umfasst staatliche Gesundheitseinrichtungen, die bei der DGHS registriert sind.
  • RestaurantsDBTool umfasst Restaurants, deren Daten aus Google Places gesammelt wurden.
  • WebSearchTool führt eine Echtzeit-Suche (über die Tavily API) durch, wenn die Datenbanken keine Antwort geben können.
  • Alles läuft über LangChain’s AgentExecutor, wobei Gemini auf Google Vertex AI als Reasoning-Engine dient. Jedes Datenbankwerkzeug wandelt eine Frage in natürlicher Sprache in SQL um, führt diese aus und konvertiert die Ergebnisse wieder in lesbare Sätze.

    Einige Beispiele für Routing-Entscheidungen machen den Zweck konkret:

    • "Wie viele Colleges gibt es in der Rajshahi-Region?" wird an InstitutionsDBTool weitergeleitet, da es sich um eine strukturierte Zählung der EIIN-Daten handelt.
    • "Liste die Regierungskrankenhäuser im Bezirk Chattogram auf" wird an HospitalsDBTool weitergeleitet, da es sich um eine strukturierte Liste der DGHS-Einrichtungen handelt.
  • "Find restaurants with biryani in the name near Pirojpur" führt zu RestaurantsDBTool, da es sich um eine Suche in der Restaurant-Tabelle handelt.
  • "What is the role of DGHS in Bangladesh?" führt zu WebSearchTool, weil keine Datenbank solche allgemeinen Informationen speichert.
  • Der Agent überprüft niemals die Datenbanken selbst. Er sieht nur vier Beschreibungen der Tools in einfachem Englisch, und die Qualität der Zuordnung hängt davon ab, wie gut diese Beschreibungen formuliert sind, und nicht von irgendwelchen fest programmierten Entscheidungswegen.

    Überprüfen Sie die Datensätze, bevor Sie etwas entwerfen

    Bevor es überhaupt Agentencodes gab, wurden die drei Datensätze von Hugging Face heruntergeladen und Zeile für Zeile untersucht. Diese Stunde zahlte sich mehr aus als jeder andere Schritt, denn die Projektbeschreibung entpuppte sich als falsch bezüglich des Inhalts der Daten. Sie ging davon aus, dass die Krankenhausdaten die Bettenkapazitäten und die Institutsdaten die Universitätsstudiengänge enthielten. In Wirklichkeit sah es anders aus:

    • all-bangladeshi-hospitals ist ein Verzeichnis der bei DGHS/MOPA registrierten Einrichtungen und Büros: Name, Einrichtungstyp, Behörde, Abteilung, Bezirk sowie ein Flag für privat oder staatlich. Es enthält keine Angaben zu Bettenkapazitäten oder Personalzahlen.
    • Institutional-Information-of-Bangladesh umfasst Schulen, Kollegien, Madrassas und technische Institute, die eine EIIN-Registrierung haben. Es beinhaltet keine Universitäten oder Informationen zu Studiengängen.
  • Bangladeshi-Restaurant-Data stammt aus dem Scraping von Google Places: Name, Adresse, Koordinaten, Bewertung und Anzahl der Rezensionen. Es gibt keine Spalte für die Küche.
  • Dieser Unterschied zwischen Beschreibung und Inhalt ist normal. Die richtige Vorgehensweise besteht darin, die Daten nicht dem Kurzbeschreibung anzupassen, sondern sich auf das wirklich Vorhandene zu konzentrieren und die Diskrepanz festzuhalten, damit niemand sie später als Fehler betrachtet. In diesem Fall führte eine Suche nach „Biryani“ zu einer Textübereinstimmung im Restaurantnamen, und ein Standortfilter zu einer Textübereinstimmung in der Adresse, da weder ein Feld für die Küche noch eines für den Stadtteil vorhanden ist. Der Kompromiss ist gering und klar dokumentiert.

    Die allgemeinere Gewohnheit, die man frühzeitig annehmen sollte, ist folgende: Betrachten Sie das Datensatz als Quelle der Wahrheit und jedes Schema-Diagramm oder Kurzbeschreibung als bloße Schätzung. Was .head() ausgibt, ist die eigentliche Spezifikation.

    Laden von unstrukturierten CSV-Dateien in typisierte SQLite-Tabellen

    Jedes Datensatz wird gereinigt, in die richtigen Typen umgewandelt und in eine eigene SQLite-Datei geschrieben. Das Schema wird explizit deklariert, damit SQLite keine Spaltenarten ableiten kann und die anschließende Umwandlung von Text in SQL sinnvolle Spaltennamen erhält. Beachten Sie die NOT NULL-Beschränkung für name sowie das Ganzzahl-Flag is_private mit einem Standardwert von null:

    HOSPITALS_SCHEMA = """
    CREATE TABLE hospitals (
        facility_id       INTEGER PRIMARY KEY,
        name              TEXT NOT NULL,
        name_bangla       TEXT,
        code              INTEGER,
        agency            TEXT,
        facility_type     TEXT,
        division          TEXT,
        district          TEXT,
        city_corporation  TEXT,
        upazila           TEXT,
        is_private        INTEGER NOT NULL DEFAULT 0
    );
    """
    

    Die Reinigungsfunktion ordnet die rohen CSV-Überschriften diesen Schema-Names zu, wandelt den Identifikator sowie das Privatflag in Zahlen um (ungültige Werte werden stattdessen zu null, anstatt dass es zu Fehlern kommt), entfernt Zeilen ohne ID oder Name und löscht doppelte Einträge:

    def clean_hospitals(df: pd.DataFrame) -> pd.DataFrame:
        rename_map = {
            "Id": "facility_id", "Name": "name", "Name (Bangla)": "name_bangla",
            "Code": "code", "Agency": "agency", "Type": "facility_type",
            "Division": "division", "District": "district",
            "City Corporation": "city_corporation", "Upazila": "upazila",
            "Private": "is_private",
        }
        out = _select_and_rename(df, rename_map, required=["facility_id", "name"]).copy()    out["facility_id"] = pd.to_numeric(out["facility_id"], errors="coerce").astype("Int64")
        out["is_private"] = pd.to_numeric(out["is_private"], errors="coerce").fillna(0).astype(int)    out = out.dropna(subset=["facility_id", "name"]).drop_duplicates(subset=["facility_id"])
        return out.reset_index(drop=True)
    

    Achten Sie auf den Aufruf von _select_and_rename – man könnte annehmen, es handele sich um einen einfachen df.rename()-Aufruf. Dieser Hilfsfunktion wurde erst nach einem tatsächlichen Fehler hinzugefügt, der im untenstehenden Abschnitt zu den Fehlern beschrieben wird. Es handelt sich dabei um ein gutes Beispiel für defensiven Code, dessen Zweck erst sichtbar wird, wenn man weiß, was schiefgelaufen ist.

    Eine wiederverwendbare Tool-Fabrik für Text zu SQL

    Sämtliche drei Datenbank-Tools folgen demselben Ablauf, weshalb dieser einmal als Fabrik implementiert und anschließend dreimal aufgerufen wird. Die Fabrik öffnet die Datenbank mithilfe von SQLDatabase.from_uri, erstellt eine Kette zur Erstellung von SQL-Abfragen mit create_sql_query_chain, bereitet ein Tool vor, das die Abfragen mit QuerySQLDatabaseTool ausführt, und erstellt schließlich eine Kette zur Formatierung der Antworten aus einer Anweisung, dem Modell sowie einem Zeichenkettenparser:

    def build_db_qa_tool(*, tool_name: str, description: str, db_path: Path, llm) -> Tool:
        db = SQLDatabase.from_uri(f"sqlite:///{db_path}")
        write_query = create_sql_query_chain(llm, db)
        execute_query = QuerySQLDatabaseTool(db=db)
        format_answer = _ANSWER_PROMPT | llm | StrOutputParser()
    

    Die innere Funktion erledigt die eigentliche Arbeit für jede Frage. Sie bittet das Modell um SQL, extrahiert die erste SELECT-Anweisung, stellt einen nur-Lese-Zugriff sowie eine Zeilenbegrenzung sicher, führt diese aus und übermittelt die Frage, die Abfrage sowie das rohe Ergebnis an die Formatierungslogik. Jede Ausnahme wird in eine verständliche Meldung umgewandelt anstelle eines Stack-Traces, was dem Agenten außerdem etwas Gebrauchbares zum Nachdenken liefert:

        def _answer(question: str) -> str:
            try:
                raw_sql = write_query.invoke({"question": question})
                sql = _enforce_read_only_and_limit(_first_select_statement(raw_sql), MAX_SQL_RESULT_ROWS)
                result = execute_query.invoke(sql)
            except Exception as exc:
                return f"I could not query the {tool_name} database for that question (reason: {exc})."        return format_answer.invoke({"question": question, "query": sql, "result": result})    return Tool(name=tool_name, description=description, func=_answer)
    

    Drei Gestaltungsentscheidungen in diesem Auszug können in einer ersten Version leicht übersprungen werden:

    1. Das Modell generiert den SQL über create_sql_query_chain. Es gibt keine manuell gepflegten Abfragenvorlagen, die mit dem Schema synchron gehalten werden müssen.
  • Die generierte SQL wird als unzuverlässiger Eingang behandelt. _enforce_read_only_and_limit lehnt alles ab, was kein SELECT-Statement ist, und begrenzt die Anzahl der Zeilen, sodass ein verwirrtes oder manipuliertes Modell keine Daten ändern oder eine ganze Tabelle in das Kontextfenster ausgeben kann.
  • Die Benutzer sehen niemals die rohen Ergebnistupel. Der Formatierungsprozess wandelt etwas wie „3 Zeilen, district='Dhaka'“ in einen korrekten Satz um.
  • Eine nützliche zusätzliche Schutzmaßnahme, falls Sie dieses Muster anwenden, ist es, die SQLite-Datei ebenfalls im Lesmodus zu öffnen. String-Prüfungen in SQL können auf subtile Weise umgangen werden, und eine Datenbankverbindung, die physisch nicht schreiben kann, stellt eine zweite Verteidigungslinie dar.

    Erlauben, dass Toolbeschreibungen die Routenbestimmung übernehmen

    Hier wird ein Skript zu einem Agenten. Die Systemanweisung beginnt damit, die Rolle des Assistenten zu definieren:

    SYSTEM_PROMPT = """You are a multi-tool research assistant for Bangladesh.
    

    Darüber hinaus werden die vier Tools aufgelistet und drei Routing-Regeln festgelegt: Verwenden Sie das passende Datenbanktool für Zählungen, Listen oder spezifische Datensätze – schätzen Sie niemals Zahlen; verwenden Sie die Websuche für allgemeines Wissen; und wenn eine Frage mehrere Datensätze umfasst, rufen Sie jedes relevante Tool auf und fügen Sie die Ergebnisse zusammen. Unterhalb der Anfrage bindet create_tool_calling_agent das Modell, die Tools sowie die Anfrage miteinander zusammen, und AgentExecutor führt den Loop mit einer Obergrenze von sechs Iterationen sowie einer toleranten Handhabung fehlerhafter Toolaufrufe aus:

    You have four tools:
      - InstitutionsDBTool : Bangladeshi schools, colleges, madrashas (EIIN registry)
      - HospitalsDBTool    : Bangladeshi government health facilities and offices
      - RestaurantsDBTool  : Bangladeshi restaurants and eateries (Google Places data)
      - WebSearchTool      : live web search, for anything the three databases don't coverRouting rules:
    1. If the question asks for a count, list, or specific record from one of the
       three datasets, call the matching DB tool. Never guess numbers yourself.
    2. If the question is general knowledge or not covered by any DB tool, call
       WebSearchTool.
    3. If a question spans more than one dataset, call each relevant tool
       separately and combine the results.
    """agent = create_tool_calling_agent(llm=llm, tools=tools, prompt=prompt)
    executor = AgentExecutor(agent=agent, tools=tools, max_iterations=6, handle_parsing_errors=True)
    

    Es gibt hier nichts, was mit Schlüsselwörtern übereinstimmt, Regulären Ausdrücken angewendet wird oder Phrasen auf Tools abbildet. Das Modell vergleicht die Frage mit jeder Beschreibung und wählt aus. Dadurch werden die Beschreibungen zu den wichtigsten Zeichenketten im Codebase: Wenn sie vage sind oder sich überschneiden, wird der Agent falsche Tools auswählen, und keine Anpassung der Anweisungen an anderer Stelle kann dies vollständig ausgleichen. Die Begrenzung max_iterations=6 sollte ebenfalls beibehalten werden, da sie die Kosten begrenzt und verhindert, dass ein in Schleifen geratener Agent unbegrenzt Tools aufruft.

    Warum der klassische AgentExecutor statt LangGraph

    LangChain entwickelt seine empfohlene Agentenarchitektur zunehmend in Richtung LangGraph weiter, da diese eine genauere Kontrolle über mehrschrittige, zustandsbasierte Workflows ermöglicht. Für diesen Anwendungsfall eignet sich create_tool_calling_agent mit AgentExecutor besser: Jede Anfrage umfasst eine Frage, einen oder zwei Toolaufrufe sowie eine Antwort. Ein Graphenframework würde Strukturen hinzufügen, die das Problem nicht benötigt. Die Wahl der kleinsten Architektur, die das Problem löst, ist eine eigene Fähigkeit – sie unterscheidet sich von dem Wissen darum, dass es leistungsstärkere Optionen gibt. Falls Ihre Routierung später Verzweigungen, Wiederholungsversuche oder persistenten Zustand erfordert, ist das der Punkt, an dem ein Graphen seine Berechtigung hat; der Artikel zu der Gestaltung widerstandsfähiger Agentengraphen behandelt genau dieses Thema.

    Drei Ausfälle, die nichts mit dem Modell zu tun hatten

    Der aufschlussreichste Teil des Aufbaus war nicht das, was beim ersten Versuch funktionierte, sondern das, was schiefging. Keines der drei Probleme betraf Prompte oder die Modellqualität.

    Zwei LangChain-Generationen nebeneinander installiert

    LangChain 1.0 hat sein agents-Modul umorganisiert und das klassische AgentExecutor-Muster in ein separates Paket verlegt. In der Colab-Umgebung waren schließlich beide Generationen installiert, wodurch eine Importierung, die in einer Zelle erfolgreich war, in der nächsten den Fehler ImportError: cannot import name 'AgentExecutor' auslöste.

    Die Lösung bestand aus prozeduralen Schritten statt aus einem Code-Änderung: Deinstallieren Sie die konfliktspezifischen Pakete explizit, legen Sie genaue Versionen anstelle von Versionsbereichen fest und starten Sie den Laufzeitumgebung neu, bevor Sie fortfahren. Eine saubere Abhängigkeitsverwaltung ist echte Ingenieurarbeit. Überprüfen Sie die aktuelle LangChain-Dokumentation, um herauszufinden, wo AgentExecutor in der von Ihnen installierten Version zu finden ist, da sich dies bei verschiedenen Veröffentlichungen geändert hat.

    Spaltenname mit Leerzeichen statt Unterstrich

    Der Dataset-Viewer von Hugging Face zeigte eine Spalte mit dem Namen INSTITUTE_NAME, doch die heruntergeladene Datei verwendete tatsächlich INSTITUTE NAME. Dieses einzige Zeichen verursachte einen KeyError mehrere Funktionsaufrufe entfernt von der eigentlichen Ursache.

    Die Antwort war der zuvor erwähnte Hilfsfunktion _select_and_rename. Sie entfernt alle nicht-alphanumerischen Zeichen, bevor die Spaltennamen verglichen werden, sodass INSTITUTE_NAME, INSTITUTE NAME und institute-name alle übereinstimmen. Wenn eine erforderliche Spalte dennoch nicht gefunden werden kann, wird statt eines stillen Fehlers die tatsächliche Liste der Spalten ausgegeben. Eine gute Fehlermeldung ist an sich bereits ein Debugging-Tool.

    Ein API-Schlüsselformat, das von der Gemini-API abgelehnt wird

    Google führt ein neueres API-Schlüsselformat ein, das mit AQ. beginnt anstelle des bekannten AIzaSy. Zum Zeitpunkt der Erstellung dieses Projekts wurden Schlüssel im neuen Format von der direkten Gemini-API mit dem unklaren Fehler 401 ACCESS_TOKEN_TYPE_UNSUPPORTED abgelehnt. Eine Wiederholungslogik kann keine Zugangsdaten beheben, die vom Endpunkt nicht akzeptiert werden.

    Die Lösung bestand darin, den Authentifizierungsweg vollständig zu ändern: Der Agent authentifiziert sich nun über OAuth bei Google Vertex AI, genauso wie jede andere Google Cloud-Dienstleistung. Dadurch wird das Problem mit der Schlüsselstruktur vermieden, und es handelt sich ohnehin um eine für die Produktion besser geeignete Einstellung. Da sich dieses Verhalten seitdem geändert haben könnte, sollten Sie es vor der Nutzung anhand der aktuellen Google Cloud-Dokumentation überprüfen.

    Das Muster hinter allen drei Fällen: In der heutigen KI-Entwicklung versagen die Infrastrukturkomponenten rund um das Modell – wie Paketversionen, Datenschemata und Authentifizierungsflüsse – weitaus häufiger als das Modell selbst.

    Der Agent in Aktion

    Sobald die Datenbanken erstellt und der Agent verbunden ist, wird eine Anfrage zur Anzahl der Colleges in Rajshahi direkt an das Tools für Institutionen weitergeleitet, welches mit einer aus der Datenbank entnommenen Zahl antwortet:

    You: How many colleges are registered in Rajshahi division?
    > Entering new AgentExecutor chain...
    [Tool: InstitutionsDBTool] → "There are 812 colleges registered in Rajshahi division."
    

    In derselben Sitzung wird eine allgemeinwissensbezogene Frage zu DGHS an die Web-Suche gesendet, während eine Anfrage nach Regierungskrankenhäusern in Chattogramm an die Krankenhausdatenbank weitergeleitet wird:

    You: What is the role of DGHS in Bangladesh?
    > Entering new AgentExecutor chain...
    [Tool: WebSearchTool] → "DGHS (Directorate General of Health Services) is the
    government body responsible for planning, managing, and delivering public
    healthcare services across Bangladesh..."You: List a few government hospitals in Chattogram district.
    > Entering new AgentExecutor chain...
    [Tool: HospitalsDBTool] → "Chattogram district includes several government
    health facilities such as Chattogram Medical College Hospital and Chattogram
    General Hospital, both operating under DGHS..."
    

    Innerhalb eines Gesprächs werden drei verschiedene Datensätze verwendet, und es gibt keinen Code, der dem Agenten mitteilt, welchen er verwenden soll. Das Modell trifft jede Entscheidung ausschließlich auf der Grundlage der Beschreibungen der Tools.

    Lektionen für die nächste Version

    • Prüfen Sie die Daten vor der Erstellung des Schemas. Das Entdecken von Lücken im Datensatz nach der Erstellung der Tools hätte eine Neuimplementierung aller drei Tools bedeutet.
    • Befestigen Sie Abhängigkeiten strenger, als es notwendig erscheint. In einem Ökosystem, in dem alle paar Monate umfangreiche Neuimplementierungen erfolgen, kann ein heute noch sicher funktionierender Versionsbereich in einem neuen Umfeld einige Wochen später völlig anders funktionieren.
  • Lesen Sie externe Fehlermeldungen in voller Länge. Ein Vertex AI 403-Fehler in diesem Projekt enthielt die genaue URL, die zur Behebung benötigt wurde, was erheblich mehr Zeit sparte als Raten.
  • Entwerfen Sie sowohl für Fehlsituationen als auch für den normalen Ablauf. Gehen Sie davon aus, dass generierter SQL fehlerhaft sein könnte, APIs ausgefallen sein könnten und eingehende Daten nicht dem erwarteten Schema entsprechen. Solcher defensiver Code kommt in einer Demo nie zum Vorschein, ist aber entscheidend dafür, dass das System auch beim ersten Fehler vor einem echten Benutzer weiterhin nutzbar bleibt.
  • Zusammenfassung

    Geminis Tool-Calling-Funktion funktionierte sofort korrekt, sobald saubere, klar beschriebene Tools zur Verfügung standen. Das Modell war nicht der schwierige Teil – die Zeit wurde vielmehr durch die zugrundeliegende Infrastruktur aufgebraucht: kollidierende Bibliotheksversionen, ein fehlerhafter Leerzeichen in einem Spaltenkopf sowie ein Anmeldeinformationen-Format, das von der API nicht akzeptiert wurde. Wenn Sie einen Mehrfach-Tool-Agenten entwickeln, investieren Sie frühzeitig in drei Dinge: ehrliche Toolbeschreibungen, strenge Regeln für alles, was das Modell zur Ausführung erzeugt, sowie eine reproduzierbare Umgebung. Diese Faktoren entscheiden darüber, ob der Agent in der Produktion genauso gut funktioniert wie in einer Demo.