Pytania dotyczące routingu w narzędziach SQL i wyszukiwarkach internetowych za pomocą agenta Gemini
Jak agent do wywoływania narzędzi LangChain w Vertex AI dokonuje wyboru pomiędzy trzema narzędziami SQLite do konwersji tekstu na SQL a wyszukiwaniem w sieci w czasie rzeczywistym, oraz jakie błędy związane z danymi, zależnościami i autoryzacją należy się spodziewać.
Źródła: Building a Multi-Tool AI Agent for Bangladesh: Teaching Gemini to Pick the Right Tool for the Job
Bot rozmówczy oparty na jednym źródle danych dobrze radzi sobie z jednym rodzajem pytań, ale zawodzi, gdy użytkownik zadaje pytanie związane z innym tematem: albo wymyśla odpowiedź, albo rezygnuje. Bardziej przydatnym celem inżynieryjnym jest system, który rozpoznaje granice własnej wiedzy i przekazuje każde pytanie komponentowi, który faktycznie potrafi na nie odpowiedzieć. Ten przewodnik buduje dokładnie takie rozwiązanie: pojedynczego agenta, który odpowiada na pytania dotyczące Bangladeszu (szkoły, szpitale, restauracje oraz wiedza ogólna), wybierając dla każdego pytania jeden z czterech specjalistycznych narzędzi. W trakcie nauki zobaczysz, jak przekształcić bazy danych SQLite w bezpieczne narzędzia do konwersji tekstu na SQL, dlaczego opisy narzędzi są ważniejsze od kodu routingu oraz jakie błędy niezwiązane z modelem najczęściej mogą sparaliżować taki projekt.
Dlaczego orkiestrator jest lepszy od jednego modelu, który wie wszystko
Najbardziej zaawansowane systemy AI obecnie mają wspólną strukturę: model językowy, który dobrze rozumie intencje użytkownika, łączy się z kilkoma niezawodnymi narzędziami o wąskim zakresie działania, które skutecznie pozyskują fakty. Asystenci obsługi klienta przechodzą między systemem rozliczeń a bazą wiedzy; asystenci badawczy natomiast przechodzą między wyszukiwarką internetową a wewnętrznymi dokumentami. Model nie musi przechowywać odpowiedzi – musi jedynie określić, jaką funkcję należy wykorzystać.
W tym projekcie ten schemat jest stosowany z udziałem czterech specjalistów i jednego koordynatora:
┌──────────────────────────┐
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)
InstitutionsDBToolobejmuje szkoły, kolegia oraz madrasy z oficjalnego rejestru EIIN.HospitalsDBToolobejmuje publiczne placówki zdrowotne zarejestrowane w DGHS.RestaurantsDBToolobejmuje restauracje, których dane pochodzą z Google Places.
WebSearchTool przeprowadza wyszukiwanie w czasie rzeczywistym (przez API Tavily) w przypadku pytań, na które bazy danych nie potrafią odpowiedzieć.Wszystko działa dzięki AgentExecutor z LangChain, przy czym Gemini w Google Vertex AI pełni rolę silnika rozumowania. Każdy narzędzie bazodanowe przekształca pytanie w języku naturalnym na zapytanie SQL, je wykonywa i zamienia uzyskane wyniki z powrotem na czytelne zdania.
Poniższe przykłady decyzji o kierowaniu zapytania ilustrują ich cel:
- Zapytanie „Ile jest kolegiów w dystrykcie Rajshahi?” trafia do
InstitutionsDBTool, ponieważ chodzi o ustrukturyzowaną liczbę z danych EIIN. - Zapytanie „Wymień szpitale rządowe w dystrykcie Chattogram” trafia do
HospitalsDBTool, ponieważ chodzi o ustrukturyzowaną listę placówek DGHS. - "Znajdź restauracje o nazwie zawierającej słowo biryani w pobliżu Pirojpur" kieruje się do
RestaurantsDBTool, ponieważ chodzi o wyszukiwanie w tabeli restauracji. - ">Jaka jest rola DGHS w Bangladeszu?" kieruje się do
WebSearchTool, ponieważ żadna baza danych nie przechowuje tego rodzaju ogólnej wiedzy.
Agent nigdy nie sprawdza samych baz danych. Widzi jedynie cztery opisy narzędzi napisane prostym językiem angielskim, a jakość kierowania zależy od tego, jak dobrze są one napisane, a nie od żadnych z góry ustalonych reguł.
Zbadaj zestawy danych przed zaprojektowaniem czegokolwiek
Zanim istniał jakikolwiek kod agenta, trzy zestawy danych zostały pobrałe z Hugging Face i przeanalizowane wiersz po wierszu. Ta godzina przyniosła większe korzyści niż jakikolwiek inny krok, ponieważ krótka specyfikacja projektu okazała się błędna co do zawartości danych. Oczekiwano informacji o liczbie łóżek w danych dotyczących szpitali oraz programach studiów na uczelniach w danych o instytucjach. Rzeczywistość wyglądała inaczej:
all-bangladeshi-hospitalsto katalog placówek i biur zarejestrowanych w DGHS/MOPA: nazwa, typ placówki, agencja, dział, dystrykt oraz flaga wskazująca na sektor prywatny lub rządowy. Nie zawiera on żadnych danych dotyczących liczby łóżek ani pracowników.Institutional-Information-of-Bangladeshobejmuje szkoły, kolegia, madrasze oraz instytuty techniczne posiadające rejestrację EIIN. Nie zawiera on informacji o uniwersytetach ani żadnych danych o programach studiów.
Bangladeshi-Restaurant-Data pochodzi z wykopywania danych z Google Places: nazwa, adres, współrzędne, ocena oraz liczba recenzji. Nie ma kolumny z rodzajem kuchni.Taka luka pomiędzy opisem a rzeczywistym zawartością jest normalna. Prawidłowym rozwiązaniem nie jest dostosowywanie danych do krótkiego opisu, lecz projektowanie w oparciu o to, co faktycznie istnieje, oraz zapisywanie tych niezgodności, aby nikt później nie uznał tego za błąd. W tym przypadku zapytanie dotyczące „biryani” doprowadziło do dopasowania tekstowego nazwy restauracji, a filtr lokalizacyjny do dopasowania tekstowego adresu, ponieważ nie ma ani pola z rodzajem kuchni, ani z dzielnicą. Kompromis jest niewielki i jasno udokumentowany.
Warto wcześnie przyjąć szerszy nawyk: traktować zbiór danych jako źródło prawdy, a wszelkie schematy lub krótkie opisy jako przypuszczenia kogoś innego. To, co wyświetla .head(), stanowi rzeczywistą specyfikację.
Ładowanie nieuporządkowanych plików CSV do typizowanych tabel SQLite
Każdy zbiór danych jest oczyszczany, przekształcany na odpowiednie typy i zapisywany do osobnego pliku SQLite. Schemat jest wyraźnie deklarowany, aby SQLite nie domyślał się typów kolumn oraz aby późniejszy proces konwersji tekstu na SQL mógł korzystać z sensownych nazw kolumn. Zwróć uwagę na ograniczenie NOT NULL dla pola name oraz na flagę całkowitą is_private o domyślnej wartości zero:
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
);
"""
Funkcja oczyszczania mapuje surowe nagłówki pliku CSV na nazwy zdefiniowane w schemacie, przekształca identyfikatory oraz flagę prywatności na liczby (nieprawidłowe wartości stają się nullami zamiast powodować awarię), usuwa wiersze bez identyfikatora lub nazwy oraz eliminuje duplikatowe obiekty:
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)
Zwróć uwagę na wywołanie _select_and_rename – można by oczekiwać zwykłego df.rename(). Ten helper został dodany dopiero po rzeczywistym błędzie, opisanym w sekcji z błędami poniżej. Jest to dobry przykład kodu obronnego, którego cel staje się widoczny dopiero wtedy, gdy dowiadujemy się, co poszło nie tak.
Jedna fabryka narzędzi do konwertowania tekstu na SQL
Wszystkie trzy narzędzia do bazy danych używają identycznego procesu, dlatego jest on napisany raz jako fabryka i wywoływany trzy razy. Fabryka otwiera bazę danych za pomocą SQLDatabase.from_uri, buduje łańcuch do generowania zapytań SQL za pomocą create_sql_query_chain, przygotowuje narzędzie do wykonywania zapytań za pomocą QuerySQLDatabaseTool, a także tworzy łańcuch do formatowania odpowiedzi na podstawie promptu, modelu oraz parsera ciągów znakowych:
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()
Funkcja wewnętrzna wykonywa faktyczną pracę dla każdego pytania. Żąda od modelu kodu SQL, wyodrębnia pierwsze polecenie SELECT, wprowadza ograniczenia dotyczące dostępu tylko do odczytu oraz liczby wierszy, je wykonuje, a następnie przekazuje pytanie, zapytanie oraz surowe wyniki do łańcucha formatowania. Każda wyjątek zamienia się w czytelną wiadomość zamiast śladu stosu, co także daje agencji coś sensownego do rozważenia:
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)
Trzy decyzje projektowe w tym fragmencie można łatwo pominąć w pierwszej wersji:
- Model generuje kod SQL za pomocą funkcji
create_sql_query_chain. Nie ma ręcznie utrzymywanych szablonów zapytań, które musiałyby być zsynchronizowane ze schematem.
_enforce_read_only_and_limit odrzuca wszystko, co nie jest zapytaniem SELECT, oraz ogranicza liczbę wierszy, dzięki czemu zdezorientowany lub sfałszowany model nie może modyfikować danych ani wyświetlać całej tabeli w oknie kontekstowym.Cenną dodatkową ochroną, jeśli adaptujesz ten wzorzec, jest otwarcie pliku SQLite również w trybie tylko do odczytu. Sprawdzenia ciągów znakowych w SQL można obejść subtelnymi sposobami, więc połączenie z bazą danych, które fizycznie nie może zapisać danych, stanowi drugą linię obrony.
Pozwolenie opisom narzędzi na sterowanie routingiem
Tutaj skrypt staje się agentem. Komenda systemowa najpierw określa rolę asystenta:
SYSTEM_PROMPT = """You are a multi-tool research assistant for Bangladesh.
Następnie wymienia cztery narzędzia i podaje trzy zasady routingu: używać odpowiedniego narzędzia bazodanowego do liczeń, wykazów lub konkretnych rekordów, nigdy nie szacować liczb; używać wyszukiwarki internetowej do uzyskiwania ogólnej wiedzy; a gdy pytanie dotyczy kilku zbiorów danych, wywoływać każde odpowiednie narzędzie i łączyć wyniki. Poniżej promptu create_tool_calling_agent łączy model, narzędzia oraz prompt, natomiast AgentExecutor uruchamia pętlę z ograniczeniem do sześciu iteracji oraz tolerancyjnym radzeniem sobie z nieprawidłowymi wywołaniami narzędzi:
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)
Tutaj nic nie odpowiada słowom kluczowym, nie stosuje się wyrażeń regularnych ani nie mapuje się fraz na narzędzia. Model porównuje pytanie z każdym opisem i dokonuje wyboru. Dlatego opisy stanowią najważniejsze ciągi znaków w bazie kodu: jeśli są niejasne lub się pokrywają, agent zacznie wybierać niewłaściwe narzędzie, a żadna modyfikacja instrukcji w innych miejscach nie będzie w stanie tego w pełni skompensować. Warto również zachować ograniczenie max_iterations=6, ponieważ ogranicza to koszty i zapobiega temu, by agent w pętli bez końca wywoływał narzędzia.
Dlaczego klasyczny AgentExecutor zamiast LangGraph
LangChain stopniowo przenosi swoją zalecaną architekturę agentów na LangGraph, który umożliwia lepszą kontrolę nad wieloetapowymi, stanowymi procesami. W tym przypadku create_tool_calling_agent w połączeniu z AgentExecutor jest bardziej odpowiedni: każda prośba składa się z jednego pytania, jednej lub dwóch wywołań narzędzi oraz jednej odpowiedzi. Framework grafowy dodałby strukturę, której problem nie wymaga. Wybór najprostszej architektury, która rozwiązuje problem, to odrębna umiejętność, różniąca się od znajomości istnienia potężniejszych opcji. Jeśli później Twoja logika kierowania ruchem będzie wymagać rozgałęzień, ponownych prób lub utrzymywania stanu, wtedy właśnie graf staje się przydatny; artykuł na temat projektowania odpornych grafów agentów omawia tę kwestię.
Trzy awarie, które nie miały nic wspólnego z modelem
Najbardziej pouczającą częścią procesu budowy nie było to, co zadziałało od razu przy pierwszej próbie, lecz to, co się zepsuło. Żaden z trzech problemów nie dotyczył promptów ani jakości modelu.
Dwie wersje LangChain zainstalowane obok siebie
LangChain 1.0 przearanżował swój moduł agents i przeniósł klasyczny wzorzec AgentExecutor do odrębnego pakietu. Środowisko Colab miało zainstalowane obie wersje, więc import, który zadziałał w jednej komórce, powodował błąd ImportError: cannot import name 'AgentExecutor' w następnej.
Rozwiązanie polegało na procedurach, a nie na zmianie kodu: należy wyraźnie odinstalować sprzeczne pakiety, ustalić dokładne wersje zamiast zakresów oraz ponownie uruchomić środowisko przed kontynuacją. Dobry stan zależności to prawdziwa praca inżynierska. Sprawdź aktualną dokumentację LangChain, aby dowiedzieć się, gdzie znajduje się AgentExecutor w wersji, którą instalujesz, ponieważ to się zmieniało pomiędzy różnymi wydaniami.
Nazwa kolumny z przestrzenią zamiast podkreślnika
Przeglądarka zbiorów danych Hugging Face pokazywała kolumnę o nazwie INSTITUTE_NAME, ale pobraany plik faktycznie używał INSTITUTE NAME. Ten jeden znak spowodował błąd KeyError kilka wywołań funkcji od rzeczywistej przyczyny.
Odpowiedzią był wcześniej omawiany pomocnik _select_and_rename. Usuwa on wszystkie znaki inne niż alfanumeryczne przed porównaniem nazw kolumn, dzięki czemu INSTITUTE_NAME, INSTITUTE NAME oraz institute-name są traktowane jako identyczne. Gdy wymagana kolumna nadal nie może zostać znaleziona, program wyświetla rzeczywisty listę kolumn zamiast cicho zawieść się. Dobry komunikat błędu sam w sobie stanowi narzędzie do debugowania.
Format klucza API odrzucony przez Gemini API
Google wprowadza coraz nowszy format kluczy API, który zaczyna się od AQ. zamiast znajomego AIzaSy. W momencie tworzenia tego projektu klucze w nowym formacie były odrzucane przez bezpośrednie Gemini API z niejasnym błędem 401 ACCESS_TOKEN_TYPE_UNSUPPORTED. Logika ponawiania prób nie może naprawić danych uwierzytelniających, których endpoint nie akceptuje.
Rozwiązaniem było całkowite zmianienie ścieżki autoryzacji: agent teraz autoryzuje się w Google Vertex AI za pomocą OAuth, tak jak każda inna usługa Google Cloud. Dzięki temu unika się problemu z formatem kluczy, a taka konfiguracja jest bez wątpienia bardziej odpowiednia do użycia w produkcji. Ponieważ to zachowanie mogło się od tamtej pory zmienić, sprawdź je w aktualnej dokumentacji Google Cloud przed jej wykorzystaniem.
Wzorzec wspólny dla wszystkich trzech przypadków: we współczesnej inżynierii sztucznej inteligencji elementy otaczające model (wersje pakietów, schematy danych i procesy autoryzacji) psują się znacznie częściej niż sam model.
Agent w działaniu
Gdy bazy danych zostaną utworzone, a agent podłączony, pytanie dotyczące liczby kolegiów w Rajshahi jest kierowane bezpośrednio do narzędzia obsługującego te instytucje, które odpowiada podając wartość z bazy danych:
You: How many colleges are registered in Rajshahi division?
> Entering new AgentExecutor chain...
[Tool: InstitutionsDBTool] → "There are 812 colleges registered in Rajshahi division."
W tej samej sesji pytanie z zakresu wiedzy ogólnej o DGHS jest wysyłane do wyszukiwarki internetowej, natomiast zapytanie o szpitale rządowe w Chattogram trafia do bazy danych szpitali:
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..."
W ramach jednej rozmowy wykorzystywane są trzy różne źródła danych, a nigdzie nie ma kodu wskazującego agentowi, który z nich wybrać. Model podejmuje każdą decyzję na podstawie samych opisów narzędzi.
Lekcje do przejęcia przy następnej wersji
- Należy zweryfikować dane przed stworzeniem schematu. Odkrycie braków w zestawie danych po stworzeniu narzędzi oznaczałoby konieczność przepisania wszystkich trzech.
- Zaleca się bardziej ścisłe określenie zależności niż to konieczne. W ekosystemie, w którym co kilka miesięcy dokonuje się dużych przeróbek, zakres wersji bezpiecznie działający dziś może zachowywać się zupełnie inaczej w nowym środowisku kilka tygodni później.
Zakończenie
Narzędzie Gemini działało poprawnie, gdy tylko otrzymało dokładne i jasno opisane narzędzia. Model nie stanowił trudności – czas pochłaniała otaczająca infrastruktura: sprzeczne wersje bibliotek, błąd w nagłówku kolumny oraz format danych, którego API nie akceptowało. Jeśli budujesz agenta wielonarzędziowego, zainwestuj od razu w trzy rzeczy: szczere opisy narzędzi, ścisłe zasady dotyczące tego, co model generuje w celu wykonania zadań, oraz środowisko umożliwiające powtarzalne testy. To one decydują o tym, czy agent będzie działał tak dobrze w środowisku produkcyjnym, jak w demonstracji.
Literatura pokrewna
- Wzmocnienie agenta Python LangChain siedmioma wbudowanymi middleware — Dowiedz się, jak middleware LangChain 1.0 dodaje funkcje takie jak streszczanie, ograniczenia liczby wywołań, powtórki, alternatywne modele, maskowanie danych osobowych oraz zatwierdzenie przez człowieka do agenta Gemini, nie wpływając na jego podstawową logikę.
- Wbudowanie agenta LangChain w FastAPI: narzędzia, manualne wyszukiwanie, strumieniowanie — Stwórz asystenta wewnątrz aplikacji za pomocą FastAPI i LangChain: podręcznik w formacie PDF przechowywany w ChromaDB dostępny jako narzędzie, kontekst dla poszczególnych użytkowników, zapisywana historia działań oraz odpowiedzi w formie strumienia.