Медыцынскі агент, які выбірае паміж базамі SQLite і вэб-пошукам
Будуем агента LangChain, які вырашае, ці патрэбен для медыцынскага пытання SQL-запыт да трох набораў даных ці жывы вэб-пошук, і дзве паломкі, што змянілі дызайн.
Крыніцы: Building a Multi-Tool AI Agent That Knows When to Query a Database and When to Search the Web
Большая частка чат-ботаў на базе ШЫ ёсць простымі інструментамі з аднай функцыяй. Як толькі задаеце запіт, яны альбо выдумваюць адпаведную адказ з памяці, альбо вадзіраюцься, таму што не можаюць доступаць да вашых дадзэнняў. Рэальныя сістэмы павінны быць розумнейшымі і выбіраць правы істочнік інформацыі для кожнага запіту. Гэты праект адносна самэ гэтага рашыяе для медычных запытаў: цэўкі-агент, які чытае запіт і сам адлучвае, чы розтлумачэнне знаходзіцца ў структураванай базе дадзэнняў чы на відкрытым інтэрнэце. Ён пераводзіць запыты между тромя базамі дадзэнняў SQLite і інструментам жываго пошуку ў інтэрнэце, а пасля таго, як, за прыпускам, быў завершаны, стаўся два рэальных проблемы.
Мета
У задачы на гэты курс быў запрошаны агент, які мог бы выкарыстоўваць тры функцыі:
- Адпавядаць на статыстычныя запыты на адной з трох рэальных медычных баз дадзэнняў, якія абходзяцься серцовымі захворэннямі, раком і дзяброўным diабетам; кожная з яых зберагаецца у сваёй сабэйной базе дадзэнняў SQLite.
Адна «галава», чатыры інструментаў, а таксама здольнасць падбіраць для кожнай задачы адпаведны інструмент.
Архітэктура
Ментальны мадэль являе сабою дыстрабіцэра:
┌────────────────────────┐
│ Main AI Agent │
│ (LangChain create_agent) │
└────────────┬─────────────┘
│ routes based on question type
┌────────────────┬──────────┼──────────┬────────────────┐
▼ ▼ ▼ ▼ ▼
HeartDiseaseDBTool CancerDBTool DiabetesDBTool MedicalWebSearchTool
│ │ │ │
▼ ▼ ▼ ▼
heart_disease.db cancer.db diabetes.db Tavily Search API
(SQLite) (SQLite) (SQLite) (general knowledge)
Главны агент ніколі не працюе безпасоверху з самымі дадзеннямі. Яго задача — прачытаць запіт, выбраць спецяліста і перадаць задачу яму. Запыт пра сярэдній рывень холестэруў у наборы дадзенняў пра серцавыя захворованні належыць HeartDiseaseDBTool, а запыт пра прычыны дзяброўага diабету 2-га тыпу — MedicalWebSearchTool. Калі запыт супакоюе аба аспекты, агент вызывае оба інструмента і з’едынае ўсі яхныя рэзультаты ў адну цэлеспрамованую адказ.
Наборы дадзенняў
Тры настоямыя, публічныя медыцынскія наборы дадзеных Kaggle былі перакананы ў базы дадзеных SQLite з стоварамі з правільным тыпам. Дадзеныя пра серцавыя захворэння знаходзяцца ў heart_disease.db і складаюця з 303 рэядоў, якія паводзяцься пра возраст, пол, холестэрын, артырыяльны тлак і канечны рэзультат. Меры клетак раку зберагаюцца ў cancer.db і складаюця з 101 рэяду, якія паводзяцься пра тычынку кластэра, розмер клетакі, голыя ядры і мітоз. Дадзеныя пра дыябет у індэйцаў Піма знаходзяцца ў diabetes.db і складаюця з 768 рэядоў, якія паводзяцься пра глюказу, КІМ, вагітнасці і канечны рэзультат.
Пераканаць CSV у SQLite здаецца простым, але правільнае выкананне значыць выбіранне зместовых назв табелей і тыпаў стоўароў (INTEGER, REAL, TEXT) правільна, а не кінуць усё як тэкст. Логіка пераканання пачынаецца з адпаведнасці між тыпамі дадзеных у pandas і тыпамі ў SQLite:
DTYPE_MAP = {"int64": "INTEGER", "float64": "REAL", "object": "TEXT", "bool": "INTEGER"}
Пасля чаго дапаможнік стварае апісаннея столбцоў у запаведзенні CREATE TABLE на адвесе dtypes з DataFrame:
def infer_sqlite_schema(df: pd.DataFrame) -> str:
"""I build a CREATE TABLE column definition string from a DataFrame's dtypes."""
columns = []
for col in df.columns:
sqlite_type = DTYPE_MAP.get(str(df[col].dtype), "TEXT")
safe_col = col.replace(" ", "_").replace("-", "_")
columns.append(f'"{safe_col}" {sqlite_type}')
return ", ".join(columns)
Калі базы дадзэных вучынутыя, можна быць вялікім спакойным, што схема адпраўна відбівае рэальныя даны, што мае вельмі важны значэнне, калі LLM атрымлівае задачу напісаць SQL-запыт на адной з іх.
Стварэнне інструментаў для баз дадзэных
Усе тры інструменты для баз дадзэных выконваюць тыя ж тры крокі, таму адна спяльная функцыя заменяе тры версіі логікі:
- Прыём запытання корыстніка на нейтральнай мове.
- Запуск LLM, каб той ператворыў запытанне на SQL-запыт, які выконвае толькі оперэйшн SELECT, выкарыстоўваючы схему табелі як кантэкст.
- Выконанне запыту, а пасля паўтарны запуск LLM, каб той перапісаў первісныя рэкорды у адпаведны адказ на англійскай мове.
У запытанні на генераванне мадэль прыродна аднойчы прыроджваецца экспертом па SQLite і выкарыстоўваецца тэкст, які выкарыстоўвае сінтаксычна правільны, толькі для чытання SELECT-запыт.
SQL_GENERATION_PROMPT = ChatPromptTemplate.from_template(
"""You are a SQLite expert. Given an input question and a table schema,
write a syntactically correct, READ-ONLY SQLite SELECT query that answers
the question.
У рэшце шаблона паводзіцца схема і запыт, пры чым выклікаецца наявнасць толькі чыстага SQL, без жадных адказоў і без маркдауна. Пасля гэтага следуе функцыя для чысткі:
Table schema:
{table_info}Question: {question}Return ONLY the raw SQL query. No explanation, no markdown formatting.
SQL Query:"""
)def _clean_sql(raw: str) -> str:
"""Strips markdown fences (if any) and blocks write operations."""
sql = raw.strip()
if sql.startswith("```"):
sql = sql.strip("`")
if sql.lower().startswith("sql"):
sql = sql[3:]
sql = sql.strip() forbidden = ("insert", "update", "delete", "drop", "alter", "create")
if sql.lower().startswith(forbidden):
raise ValueError("Write operations are not permitted on this read-only tool.")
return sql
Той функцыяны _clean_sql выканае тыхое, але важнае заведзенне. Незалежна ад таго, што гэнеруе LLM, агент ніколі не можа зменіць базовыя даны, таму доступ застаецца толькі для чытання.
Кожны з трох абразоў ёсць простым кэйсам, у яком указваецца назва базы дадзенаў, табліца, а таксама чыткая апісанне, якая паведамляе агента для маршрутацыі, калі яго трэба выкарыстоўваць:
heart_disease_tool = make_db_tool(
db_path=HEART_DB_PATH,
table_name="heart_disease_records",
tool_name="HeartDiseaseDBTool",
description=(
"Use this tool to answer STATISTICAL or DATA questions about the "
"Heart Disease dataset (columns: age, sex, cp, trestbps, chol, "
"fbs, restecg, thalach, exang, oldpeak, slope, ca, thal, target). "
"Do NOT use this tool for general medical definitions or symptoms."
),
)
Пасляпэўныя заставкі кожнага апісання, чыстая згадка пра тое, для чаго не трэба вжываць даны інструмент, маюць большое значэнне, чым можна было б спадзявацца. Эфектывнасць механізма направлення на аднойчынных нейронных мерэжах залежыць ад таго, насколькі точна опісана функцыя кожнага інструмента.
Інструмент для пошуку ў інтэрнэце
Для загальных медычных ведамаў API Tavily Search выкорыстоўваецца ў інструменце, апісанне якога таксама рэштырана. Ён обмежвае можлівасці інструмента загальнымі ведамаўмі, каб яго не плуталі з запытамі пра статыстыку:
medical_web_search_tool = StructuredTool.from_function(
func=_run_web_search,
name="MedicalWebSearchTool",
description=(
"Use this tool ONLY for GENERAL medical knowledge questions such "
"as definitions, symptoms, causes, risk factors, treatments, or "
"cures. Do NOT use this tool for statistics or numeric dataset "
"questions."
),
args_schema=WebSearchInput,
)
Складанне агента
Пасля стварэння чатырох інструментаў, апошнім элементам являецца рутэр: сістэмны запрос, які простым мовам апісвае логіку прыняцця рашэння, плюс канстрактар агента LangChain, які з’едынае усё разам. Запрос пачынаецца з інформацыі для мадэлі пра тое, што яна ёсць професыяльным асістэнтам па медычныя даны з чатырма інструментамі:
SYSTEM_PROMPT = """You are a professional medical data assistant with access to four tools.
Правіла маршрутацыі павінны прыменявацца строго. Запыткі пра статыстыку, даны, цыфры, колькасці, сярэгі або конкрэтныя запісі з набора дадзеных направляюцца да відпаведнага інструмента базы дадзеных, а запыткі пра адзначэнне, сімптамы, прычыны або способы лечэння — да пошуку ў інтэрнэце:
Routing rules (follow strictly):
1. If the question asks about STATISTICS, DATA, NUMBERS, COUNTS, AVERAGES,
or specific RECORDS from a dataset -> use the matching DB tool.
2. If the question asks for a DEFINITION, SYMPTOM, CAUSE, RISK FACTOR, or
CURE/TREATMENT -> use MedicalWebSearchTool.
3. If a question mixes both, call both tools and combine the answers clearly.
4. Never invent numbers. Always answer in clear, professional natural language.
"""TOOLS = [heart_disease_tool, cancer_tool, diabetes_tool, medical_web_search_tool]def build_agent_executor():
llm = get_llm()
return create_agent(model=llm, tools=TOOLS, system_prompt=SYSTEM_PROMPT)
Запытайцеся пра сярэгожы рэвень холестэрулу ў наборэ дадзеных па серцовыя хворобы, і адпаведзь будзе працягам 246,26. Запытайцеся, каліясныя сімптомы серцовых хвороб, і агент зменіць направленне, шукае ў Інтэрнэце і вяртае структураваны спіс, які аб’ёмляе бол у грудзях, задышку, вялікую вымоц і іншае, з адказамі на канкрэтныя запытанні і без выдумак.
Калі змянілася сітуацыя
Найболей павучальная частка стварэння не была запланавана. Два элемента зламаліся пасля таго, як агент быў створаны.
Перша проблема полягала у тым, што безкоштовній бэкенд LLM знік. Агент быў створаны на базе GitHub Models — безкоштовнага інтэрфейсу для апрацоўкі даных, сумеснага з OpenAI. У процэсе тэставання кожны запыт почаў падводзіць з прычыны github_models_retirement_brownout, адколі GitHub перманентна закрыў цю службу. Ніякое налагоджэнне власнага коду праекту не магло гэта выправіць. Рашэнням стала перанесенне всего бэкенду на Google Gemini, што прабачалася перэработкай файла config.py, каб падтрымаць кілька постачальнікаў (Gemini, Azure OpenAI і прамы OpenAI) і такім чынам усунуць можлівасць аднародзовага бягу.
Далей, найновейшыя модэлі Gemini зламалі фрэймворк агента. Новейшыя модэлі для працы з мыслямі Google (семья 3.x) прадаюць кожны вызов інструменту внутрашнім токенам thought_signature, які кліент павінен адправіць знову ў наступным раундзе. Пачатковы агент выкарыстоўваў старэйшыя функцыі LangChain create_tool_calling_agent і AgentExecutor; гэта поўнажанне пераскладвала сабее сообшчэння между крокамі, асабліва не павядамляючы пра відсутнасць падпісу. У рэзультаце кожны вызов інструменту завершаўся з незрозумелай памылкай Function call is missing a thought_signature.
Жадны патч гэта не вылечыў. Рашэннем было перайсці на ныяшнюю функцыю LangChain create_agent, якая работае на базе LangGraph і перадае первісныя об’екты сообшчэння між кожным раундам, замест таго каб іх перзбудаваць. Падпіс автаматычна застаецца пры сообшчэнні, і вся гэта група памылак знікае.
Гэтыя падыткі паказуюць, што агентныя сістэмы базуюцца на інфраструктурэ, якая змінюецца пад іх. Прадаўцы прыпіняют падтрымку певных канцэнтраў, а новыя версіі модэляў прыносяць новыя трэбаванні да протакалаў. Настоямыя навыкі тут — гэта з’ясоўваць, чаму выступае памылка 400, а не проста чытаць ўсё, што пішана ў ўспамене, больш чым будзь-якае адзінлічнае рэчыце коду.
Спагляданне прыняцтва рашэння раутэрам
Задовольнае вражанне даў момент, калі можна было спаглядаць, як агент задае правільныя запытанні за запытаннем, не атрымваючы інструкцый. Прамер касэты паказвае гэта.
Калі агента папросілі даць сярэжню велічыну адной эпітэліяльной клеткі ў даных пра рак, ён перадаў запыткі на CancerDBTool, сгенараваў SELECT AVG(single_epithelial_cell_size) FROM cancer_records; і адпаведзеў, што сярэжнае значэнне становіць прыблізна 2,37.
Калі было запытана прасэвая вякосць паціўнікаў, яным дыягноставана дзябет, сістэма перадала запыт у DiabetesDBTool і правільна фільтравала толькі тых паціўнікаў, яным дыягноставана хвороба (Outcome = 1), перш чым вырачыць прасэвую вякосць, а не на основе всей базы дадзеных. Адпаведны адказ складаўся прыблізна з 37 гадоў.
Паслядні запыт, пра тое, што такое дзябет і якая ўсерэгу прасэвая роўень глюказы ў базе дадзеных, быў справжнім тэстам. Агент разгледзеў сумешаны запыт: для адпаведзення на пытанне пра вялічыну дзябета быў выкорыстан MedicalWebSearchTool, а для пытання пра роўень глюказы — DiabetesDBTool; пасля чаго агент з’едыніў оба рэзультаты ў адны цяглы параграф, хоць ніхто яго пра гэта не прасіў. Такая поведзенне вынікае з адной лініі інструкцый для сістэмы: калі запыт сумешаны, трэба выкарыстаць обе інструменты і чытна з’едыніць адпаведзення. Рулікаўванне — гэта не магія, а толькі точныя інструкцыі плюс модэль, які ўсё правільна выконвае.
Інжынерыя інструкцый для рулікаўвання інструментамі
Сама найбольша вплывучая рашынка пад час стварэння не была жоўтком коду на Python. Гэта было тое, як пісалася галоўка description кожнага інструмента. Пачатковыя апісанні былі занадта нечысткія, больш схожыя на адпаведзі на запытанні пра даны пра хворобы серца, і агент часам выкарыстоўваў пошук у інтэрнэце для статыстычных запытанняў, таму што нічога не забараняла гэтага.
Рашынкай было змусіць кожнае апісанне выконваць два завадзі: чыстка адзначыць, для чаго праглядаецца інструмент, і чыстка адзначыць, для чаго ён не праглядаецца.
"Use this tool to answer STATISTICAL or DATA questions about the Heart
Disease dataset ... Do NOT use this tool for general medical definitions
or symptoms — use MedicalWebSearchTool for those."
Запісваець таго другога рэчынкі мае прыблізна такую ж значымасць для направлення запіта, як і сам прампт сістэмы. Чатыры інструмента, з якіх толькі адзін прызначаны для загальных знанняў, значыць, будзь-яя нечыстасць у апісах негайна прыводзіць да таго, што запіт патрапляе не туды. Самэй канкрэтныя, взаімна абыходныя апісы інструментоў былі тым, што на самай справе пераканалася задача, а SQL і API-функцыі былі на другай пазі.
Безпека з моменту праектавання
Усюды дэйствуе калькі прынцыпаў:
- Всегда толькі чытанне. Запіты, якія маюць
INSERT,UPDATE,DELETE,DROPабоALTER, адхіляюцца кожным SQL-інструментам, таму ніхто не можа дасягнуць базы дадзеных. - Ніякіх выдуманых цыфраў. Прампт сістэмы забараняе ствараць статыстыку, і калі інструмент нічога не вяртае, агент гэта пазначае.
.env, які існэўчуе паляваны з кантролю версій за дапамогою .gitignore, таму нічога супернаватковага не прабіваеся да рэпазітарыя.Тэхнічная структура
- LangChain (
create_agent, створаны на базе LangGraph) як механізм керавання - Google Gemini (
gemini-3.5-flash-lite) як галоўны LLM - SQLite для лёгкага, на базе файлоў, зберагання ўсіх трохоў набораў дадзеных
- API Tavily Search для атрымання інформацыі з рэальнага часу
- Python з SQLAlchemy і Pandas як з’еднальны элемент
Урокі з працэсу стварэння
Ёнклудаванне правільнага падчыння ў працэз работы зайняло адны полудзень. Яго зміцнэнне было самым повольным этапам: агенту трэба было перазьісцеўваць пасля завершэння апошніх службаў однага прадавця і змян у службах іншага, што вымагало атлітавання крэтынгу выходных дапамог і гатовасці пераструктураваць праект у замест на падбор спосабоў адмахнуцца базовай несумеснасці. Для тых, хто стварае агентныя AI-сістэмы, галоўны урок — прыймаць, што прадавец модэля, фармат API і нават фрэймворк агента могу змяніцца ў ходзе стварэння. Толькі тыя праекты, якія вытрымаюць такія змены, варыяць выпускаваць.
З гэтага выліваюцца тры меншыя урокі:
- З самага пачатку абстрагуйце прадавца LLM. Функцыя
get_llm(), якая пераглядае калькі можлівых аўтэнтыфікацыйных даных, у замест на жорсткую прыкладку аднаго прадавця ў всіх месцах, ператворыла міграцыю на Gemini ў змяну ў адным файле, а не на цэлы перапіс.
github_models_retirement_brownout, і Function call is missing a thought_signature на першы погляд выглядаюць як звычныя падазлы 400. Тое, чыры хвілінкі чыра паўдзяна прыйшлося на рашэнне проблемы, залежала ад таго, што было напісана ў тэксце падазлу, а заглавны тэкст ніколі не рассказваў всю історыю.create_agent, коментар у файле requirements.txt чытальна пояснюе, чаму старэйшая версія 0.3.x больш не працюе, тады ніхто не будзе змушаны шукаты рашэнне сам.