Кэшаванне запыткаў у роутарах між інструментамі SQL і сервісамі веб-шукання за дапамою агента Gemini
Як агент для вызова інструментав LangChain на Vertex AI выбірае між трыма інструментамі SQLite для перакладу тексту ў SQL та жывым пошукам у інтэрнете, а таксама якія проблемы з дадзеннямі, залежнасцямі та аутэнтыкацыёй можна спакаваць.
Крыніцы: Building a Multi-Tool AI Agent for Bangladesh: Teaching Gemini to Pick the Right Tool for the Job
Чат-бот, створаны на адзінам источніку дадзейнаў, добра справляецца з аднаго типу запытанняў, але трапляецца у працоўных трудносцях, калі корыстнік запытае пра ўсё іншае: ён альбо выдумвае адпаведзь, альбо здаёцца. Болей корыстным інжынерным цялевым пунктам є система, якая ведае прыграніцы своих знаёмасцей і перадае кожны запытанне той часткі, якая можа яго адпаведзець. У данай інструкцыі показана самэ гэта: адзіны агент, які адпаведзае на запытанні пра Бангладэш (школы, лікарні, рестараны і загальныя знаёмасці), выбіраючы для кожнага запытання адны з чатырох спецыялізаваных інструментаў. У ходзе інструкцыі вы пазнаёсце, як ператварыць базы дадзейнаў SQLite на безпечныя інструменты для перакладу текста ў SQL, чаму апісанні інструментаў маюць большое значэнне, чым код для направлення запытанняў, і якія з не-модельных проблем часта спрачынаюць затрымкі ў такых проектах.
Чаму оркестрабіліст пераважае адну модель, якая ведае ўсё
Сягодні большасць з самых серйозных систем AI маюць аднаковую структуру: лінгвістычны модель, які добра ў розумеўцы намеру, знаходзіцца паўстаўленым пры канэлькі надзеяных інструментаў з вузкаю сферай дзеяння, якія добра справляюцца з выкарыстоўванням фактов. Памочнія асистэнты пераходзяць между бэкендам для вылічэння платы і базай знаёмых; дапаможнія асистэнты для даследжэнняў пераходзяць между пошукам у інтэрнете і внутранімі дакументамі. Модэлю не трэба зберагаць адпаведныя адказы; яму трэба вырашыць, калькі з можнасцей выкарыстаць.
У даным проекте гэты патэрн застосоўваецца з чатырма спецялістамі і адним коардынаторам:
┌──────────────────────────┐
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аб’ёднае школы, колледжы і мадрасы, якія знаходзяцца ў офіцыйным реестры EIIN.HospitalsDBToolаб’ёднае дзяржавныя медычныя установы, зарэгістраваныя ў DGHS.RestaurantsDBToolаб’ёднае заклады гарманірнага абеду, дакументы пра якія былі засоблены з Google Places.
WebSearchTool адзьюмоўна выканае пошук (чэрез API Tavily) для таго, на што базы дадзеных не можуць даўаць адказ.Усё працуе за дапамогай AgentExecutor з LangChain, пры чым Gemini у Google Vertex AI выступае як механізм аналізу. Кожны інструмент базы дадзеных ператварае запит на нейтральнай мове ў SQL, выканае яго і ператварае рэзультаты знадоў у чытальныя аб’екты.
Дзеякія прыклады рашэнняў па направленні запитоў уточняюць ўсё бол яшчэ:
- Запит „Скількі колледжаў ёсць у аддзеле Раджшахі?“ направляецца да
InstitutionsDBTool, таму што характэрзуецца структураваным пільненням на адной з баз дадзеных EIIN. - Запит „Спіс уральнічых лікарняў у районе Чаттаграм“ направляецца да
HospitalsDBTool, таму што ўключае структураваны спіс установ DGHS.
RestaurantsDBTool, таму што цэта аперацыя являеся пошукам у табліцы рэстаранаў.WebSearchTool, таму што жаданая база дадзеных не зберагае такія загальныя ведамы.Агент ніколі не адказвае на самыя базы дадзеных. Ён бачыць толькі чатыры опису інструментаў на простай англійскай мове, і якосць направлення залежыць ад таго, насколькі добра напісаныя гэтыя описы, а не ад якога-небудзь жорсткая правіла розпаду.
Абавязкова перагляньце наборы дадзеных прыямо перш чым працаваць над чым-небудзь
Даўнае раней, чым існавалі коды агентаў, тры наборы дадзеных былі завантажаны з Hugging Face і аналізаваны роў за роў. Гэты крок прынёс больш практычных наследкаў, чым будзь-які іншы, адтолькі што у краткай інструкцыі да проекту былі хыбныя данні пра тое, што мясткіце дадзеных. У інструкцыі спакульвалася інформацыя пра колькасць ложакоў у больніцах і пра програмы выдавання абразоў у вузах. Рэальнасць была іншая:
all-bangladeshi-hospitals— это каталог установ і аддзеў, зарэгістраваных у DGHS/MOPA: назва, тип установы, агентства, падразделэння, район і пазначка пра тое, чыя ўстанова — прыватная чы правядомая. У яму няма даных пра колькасць ложакоў чы персоналу.Institutional-Information-of-Bangladeshаб’ёмлівае школы, колледжы, мадрасы і тэхнічныя інстытуты, якія маюць рэгістрацыю EIIN. У яму няма інформаціі пра вузы чы пра програмы выдавання абразоў.
Bangladeshi-Restaurant-Data атрыбуты падаюць з выкарабквання мясоў Google Places: назва, адреса, координаты, рэйтынг і колькасць адзінакоў. Колонка з назвай кухні не існуе.Такая разліка межа апісаннем і фактычным зместам ёсць звычайная. Правы падход — не прымусваць данні паслухацца стисленаму апісанню, а спроекаваць структуру на аднойчынны змест і зафіксаваць неасоцыябельнасць, каб пазней ніхто не прыймаў гэта за баг. У гэтым случае запыт на "biryani" стаў падазрэнням на адпаведнасць з назвай рестарана, а фільтр па месцазнаходзенні — на адпаведнасць з адресай, адколі не існуе ні поля з назвай кухні, ні поля з районам. Компрас паўзлівы і чытра задокументаваны.
Варта як можно раней адкупіць шырэйшую прычыну: спрыяваць да таго, каб набор дадзеных вважалі аднакова правдападобным джэрелам, а любыя схематыкі чыста простыяя апісанні — толькі чыёй-небудзь здагадцай. Тое, што выдае .head(), — гэта справжняя спецыфікацыя.
Завантажэнне нераспоряджальных CSV-файлаў у структураваныя табліцы SQLite
Кожны набор дадзеных адчышваны, перакладваюцца у правільныя типы і запісваны ў свой сабэйт SQLite. Схема прызначаецца явна, ў тым ліку, каб SQLite не выважала типы столбцоў, і каб пазнейшы процес перакладу текста ў SQL бачыў значымыя назвы столбцоў. Зверніце ўвагу на абмежэнне NOT NULL для name і на цілочысленны флаг is_private з стандартным значэнням нуля:
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
);
"""
Функцыя адчышвання перакладае первісныя заголовкі CSV у назвы столбцоў схемы, перакладвае ідэнтыфікаторы та флагі прыватнасці у числы (некоректныя значэння стаюць null-амі заместо таго, каб вызвалі крах), адбраковвае рядкі, у якіх не ўказаны id чы імя, а таксама адмаўляе дублікаты об’ектаў:
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)
Зверніце увагу на вызов _select_and_rename — тут можна бачыць звычайны df.rename(). Штучка гэта была дадзена толькі пасля рэальнай проблемы, якая описана ў раздзеле зі багамі нижэй. Це хорашы прыклад захоўнага коду, мета якога становіцца зрозумелай толькі пасля таго, як стане вядома, што зламалася.
Одна фабрыка інструментаў для перакладу тексту ў SQL
Усі тры інструментаў для баз дадзенаў выконваюць аднаковы процес, таму ён запісываецца аднойчы як фабрыка і вызываецца тры разы. Фабрыка ачынае базу дадзенаў за дапамогою SQLDatabase.from_uri, стварае ланцоўку для запісу SQL за дапамогою create_sql_query_chain, готавіць інструмент для выканання запытак за дапамогою QuerySQLDatabaseTool, а таксама стварае ланцоўку для форматавання адпаведзі з падказкі, модэлю і парсера страк:
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()
Внутрэняя функцыя выконвае саму роботу па кожнам запиту. Яна прасіць модель падготавіць SQL-запит, выдлічае першы оператыўнай запіт SELECT, застаўляе толькі чытальны доступ і ліміт роўнаў, выконвае яго, а пасля перадае запит, сам запіт і необработаныя рэзультаты ў ланцоўку форматавання. Будзь-якая асаблівасць ператвараецца на чытальныя паведамленні заместа стэк-трэйсу, што таксама дае агенту ўсё неабходнае для рашэння:
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)
У гэтым фрагменте тры выборы дизайну можна легка праігнораваць у першай версіі:
- Модель генеруе SQL через
create_sql_query_chain. Не існуе шаблонаў запытаў, якія трэба ручна падтрымваць, каб яны былі сынхронізаваны з схемай.
_enforce_read_only_and_limit адхоўляе ўсё, што не являецца запытам SELECT, і обмежвае колькість рэядкоў, таму паспяшны або маніпульаваны модэль не можа зменіць данні чыста вывесіць цэлую табелю ў вікно контексту.Корыстны дапаможні захоў, якщо вы адаптаваеце гэты патэрн, — гэта таксама ачыніць файл SQLite у режыме толькі для чытання. Перакрыцця перагляду строек у SQL можа адбывацца супільнымі спосабамі, і з’ўязак з базай дадзенаў, які фізычна не можа выканаць запіс, являе сабою другую лінію захавы.
Выварканне маршрутаў за дапамогою апісаў інструментаў
Хто тут скрыпт становіцца агентам. Системны запрос пачынаецца з абмовкі ролі асистента:
SYSTEM_PROMPT = """You are a multi-tool research assistant for Bangladesh.
Пасля чаго пераказваеся апаведанне пра чатыры інструмента та задаюцца тры правілы маршрутацыі: для падрахунку, супакладу чы або спецыфічных записей неабяжна выкорыстоўваць адпаведны інструмент базы дадзенаў, а не працаваць з адгадкамі; для загальных ведамасцей — выкорыстоўваць пошук у інтэрнете; а калі запыт стосуецца канкрэтных набораў дадзенаў, неабяжна вызваць кожны адпаведны інструмент та з’яеднаць яго рэзультаты. Пад запытом create_tool_calling_agent спаўнаюць з’еднанне модэля, інструментаў та запыту, а AgentExecutor запускае цикл з максімум шасць ітерацый та дазволяе працаваць з некоректнымі вызывамі інструментаў:
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)
Жадно з тут не падае під кантэкст ключоўых слов, рэгулярных выразаў чы супарабоцтва фраз з інструментамі. Модель паўставляе запыт з кожным апісаннем і выбирае. Чырвоныя апісанні стаюць найважлівейшымі стрэнгамі ў базе коду: якщо яны нечыяры або перакрываюцца, агент пачне выбіраць некоректны інструмент, і жадна змяна запыту ў іншых частках не зможа гэта выправіць. Таксама варта застаўці ліміт max_iterations=6, калі ўжо таму, што ён обмежвае колькасць выконанняў і не дазволяе агенту з ціклам безканечна вызываць інструменты.
Чаму класычны AgentExecutor заместо LangGraph
LangChain пераводзіць свою рэкамендаваную архітектуру агента на LangGraph, яка дае большую контроль над багатыядернымі, з настаўленнем стану процесамі. Для такога випадку карэктнае ўжыццё create_tool_calling_agent з AgentExecutor: кожны запит — это адна запытка, адна чы две вызовы інструментаў і адна адпаведзь. Фрэймворк у вобразе графа дадаў бы структуру, якой проблема не патрабуе. Выбір самай маленькай архітектуры, якая рашае проблему, — это сама по сабе навык, адмінстрацыйны да ведаў, што існуе моцнейшы варыянт. Якщо пазней ваша сістэмы распадзелу запыткаў будзе патрабаваць розгалужэння, перапрыбуткі чы стойкага настаўлення, тады граф становіцца карысным; стаття на дизайне стойкіх графаў агентаў раскрывае гэты направленні.
Тры абыекты, якія не мелі нічынага з модэлем
Найболей цікавым аспектам практычнага тэставання было не тое, што запрацавало з першай спробы, а тое, што не запрацавало. Жадна з трох проблем не стосвалася запытак чырпання дадзеных або якосці модэлю.
Две версіі LangChain, установленыя параду
LangChain 1.0 перарганізаваў свой модуль agents і перыявіў класычны шаблон AgentExecutor у окремы пакет. У сераўеры Colab выйшло, што установіліся обе версіі, таму імпорт, які успяў у аднай клетцы, вызваў памылку ImportError: cannot import name 'AgentExecutor' у наступной.
Рашэнне было процедурным, а не змянайом коду: явнаа выключыце канфліктуючыя пакеты, задаць точныя версіі заместо дыапазонаў, і пачаць рунтайм занова пры продажчэнні. Адпаведнае кераванне залежнасцям — гэта справжняі інжынерны работа. Пераканайцеся ў чынной дакументацыі LangChain, дзе знаходзіцца AgentExecutor у версіі, яую вы інсталюеце, таму што гэта мянялася ў разных выданнях.
Назва столбца з прасловам уместо апострофа
Прыводчык набора дадзеных Hugging Face паказваў столбец пад назвай INSTITUTE_NAME, але выкарануты файл на самай працоўваў з INSTITUTE NAME. Гэты адзін симвал спрычыніў KeyError за калькі функцый да рэальнай прычыны.
Адаптаваннем была выкарыстоўвана памочная функцыя _select_and_rename, якая была паказана раней. Яна адзьмяе ўсі не-алфавітно-чыслаўныя символы пры порэванні імен столбцоў, таму INSTITUTE_NAME, INSTITUTE NAME і institute-name усе супадаюць. Калі неякі неабходны столбець так і не можа быць знайдзены, програма выводзіць рэальны список столбцоў, а не проста мовчкі выключаецца. Хорашая паведамленне пра адзінак ёсць сама по сабе інструментам для адлагоджэння.
Фармат ключа API, які быў адхоўлены Gemini API
Компанія Google запускае новыя фарматы ключаў API, якія пачынаюцца з AQ. уместа звычайнага AIzaSy. Калі быў створаны гэты проект, ключы новага фармату былі адхоўлены безпасэроднім Gemini API з незрозумелай памылкай 401 ACCESS_TOKEN_TYPE_UNSUPPORTED. Логіка павторных спроб не можа вырашыць проблему з аутентифікацыйнымі даннымі, якія канцэнтрык не прымае.
Рашэнь было выконуць цэлыя зміны ў шляху аутентыкацыі: агент тепер аутентыфікуецца ў Google Vertex AI за дапамогою OAuth, так сама як і будзь-яка іншая служба Google Cloud. эта мера пазбегае проблемы з форматам ключа, і, без сумневу, ўсё-такі являецца болей падходячым рашэньм для працы ў продакшене. Адказна, гэта правілле могло змяніцца з таго часу, таму пераканайцеся ў яго актуальнасці, апытавшыся ў найновейшай дакументацыі Google Cloud, прычым выкорыстоўваючы яе.
Асновная законамірнасць усіх трох: у сучаснай інжынерыі ІІ, элементы, якія стосуюцца модэлю (версіі пакетаў, схемы дадзеных і процесы аутентыкацыі), ламаюцца набліжна частае, чым сам модэль.
Агент у дзейнасці
Калі базы дадзеных створеныя, а агент падключаны, запит пра колькість колледжаў у Раджшахі направляецца безпасова да інструмента, які керуе гэтыми установамі, і той адпавядае цифрамі, якія беруцца з базы дадзеных:
You: How many colleges are registered in Rajshahi division?
> Entering new AgentExecutor chain...
[Tool: InstitutionsDBTool] → "There are 812 colleges registered in Rajshahi division."
У той самай сесіі запит працоўніка з загальных ведамасяпра аб DGHS надаецца системе веб-шукання, тады як запит пра уральскія лікарні ў Чаттаграме надаецца базе данных лікарняў:
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..."
У рамках адной розмовы викорыстоўваюцца тры разныя джерэлы дадзеных, і нідзе нема коду, які б паведаміў агента, каторае з іх выбраць. Модель прымечае кожнае рашэнне на адной ляжыці з описамі інструментаў.
Урокі, якія трэба выкарыстоўваць падчас наступнай рэалізацыі
- Пераканацца ў правядлівасці дадзеных пры стварэнні схемы. Адкрыць недастаткі набора дадзеных пасля стварэння інструментаў значыла б перапісваць усе тры.
- Фіксавацыя залежнасцэй трэба робіць строгае, чым можа здавацца неабходным. У экасістэме, дзе кожыя калькі перапісваюцца кожныя калісцы, дыапазон версій, які сьогодні працюе стабільна, можа працаваць зусім інакш у новай средзе за калькісяць.
Заключэнне
Інструмент Gemini працюе правільна, як толькі йому даюць чыстыя, чыткая апісаваныя інструменты. Сама модель не была складным элементам. Час займала абыходная інфраструктура: суперасунекі версый бібліятэк, зайвый прасоўк у заглавцы столбца і формат аутентифікацыйных данных, які API не мог прыняць. Якща вы ствараеце агента з калькольніка інструментоў, рана інвестыруйце у тры рэчі: чыстыя апісаванні інструментоў, строгія правіла для всього, што модель генеруе для выканання, і сяродавы сераўак. Яны вялікай меры вплываюць на тое, чы робіць агент таксама добра ў працэйнай сіткі, як і пад час дамавання.