Preguntas de enrutamiento entre herramientas SQL y búsquedas en la web con un agente Gemini
Cómo un agente de llamadas a herramientas de LangChain en Vertex AI elige entre tres herramientas de texto a SQL de SQLite y la búsqueda web en tiempo real, además de los errores relacionados con datos, dependencias y autenticación que se pueden esperar.
Fuentes: Building a Multi-Tool AI Agent for Bangladesh: Teaching Gemini to Pick the Right Tool for the Job
Un chatbot basado en una sola fuente de datos maneja bien un tipo específico de preguntas, pero falla en cuanto el usuario pregunta sobre algo relacionado: o inventa una respuesta o se rinde. El objetivo técnico más útil es contar con un sistema que reconozca los límites de sus propios conocimientos y dirija cada pregunta al componente capaz de responderla realmente. Esta guía crea exactamente eso: un único agente que responde preguntas sobre Bangladesh (escuelas, hospitales, restaurantes y conocimientos generales) eligiendo, para cada pregunta, una de cuatro herramientas especializadas. A lo largo del proceso verá cómo encapsular bases de datos SQLite como herramientas seguras de texto a SQL, por qué las descripciones de las herramientas son más importantes que el código de enrutamiento, y cuáles son los fallos no relacionados con los modelos que con mayor probabilidad pueden detener un proyecto como este.
Por qué un orquestador es mejor que un modelo que lo sabe todo
La mayoría de los sistemas de IA más avanzados de hoy comparten una estructura similar: un modelo de lenguaje capaz de entender la intención se encuentra frente a varias herramientas fiables y de alcance limitado que son eficaces para recuperar datos. Los asistentes de soporte alternan entre un backend de facturación y una base de conocimientos; los asistentes de investigación alternan entre búsquedas en la web y documentos internos. El modelo no necesita almacenar las respuestas; solo debe decidir qué capacidad utilizar.
El proyecto aquí aplica ese patrón con cuatro especialistas y un coordinador:
┌──────────────────────────┐
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)
InstitutionsDBToolabarca escuelas, colegios y madrazas extraídos del registro oficial EIIN.HospitalsDBToolabarca establecimientos de salud gubernamentales registrados en DGHS.RestaurantsDBToolabarca restaurantes cuyos datos fueron recopilados de Google Places.
WebSearchTool realiza búsquedas en tiempo real (a través de la API de Tavily) para cualquier pregunta a la que las bases de datos no puedan responder.Todo funciona con AgentExecutor de LangChain, siendo Gemini en Google Vertex AI el motor de razonamiento. Cada herramienta de base de datos convierte una pregunta en lenguaje natural a SQL, la ejecuta y transforma los resultados en oraciones legibles.
Algunas decisiones de enrutamiento ejemplares ilustran cómo se determina la intención:
- "¿Cuántas universidades hay en la división de Rajshahi?" se dirige a
InstitutionsDBTool, ya que se trata de un conteo estructurado a partir de los datos EIIN. - "Listar hospitales gubernamentales en el distrito de Chattogram" se dirige a
HospitalsDBTool, porque corresponde a una lista estructurada de las instalaciones del DGHS. - “Encontrar restaurantes con ‘biryani’ en el nombre cerca de Pirojpur” dirige a
RestaurantsDBTool, ya que se trata de una búsqueda en la tabla de restaurantes. - “¿Cuál es el papel del DGHS en Bangladesh?” dirige a
WebSearchTool, porque ninguna base de datos almacena ese tipo de conocimiento general.
El agente nunca inspecciona las bases de datos en sí. Lo único que ve son cuatro descripciones de herramientas en inglés sencillo, y la calidad del enrutamiento depende de cuán bien estén redactadas esas descripciones y no de ningún mecanismo de ramificación predefinido.
Inspeccione los conjuntos de datos antes de diseñar cualquier cosa
Antes de que existiera cualquier código de agente, se descargaron los tres conjuntos de datos desde Hugging Face y se examinaron fila por fila. Esa hora resultó ser más valiosa que cualquier otro paso, ya que la descripción del proyecto resultó estar equivocada respecto a lo que contenían los datos. Se esperaba encontrar información sobre la capacidad de camas en los datos hospitalarios y sobre programas universitarios en los datos de las instituciones. La realidad era diferente:
all-bangladeshi-hospitalses un directorio de establecimientos y oficinas registradas en DGHS/MOPA: nombre, tipo de establecimiento, agencia, división, distrito y una marca que indica si es privado o gubernamental. No contiene cifras sobre la capacidad de camas ni del personal.Institutional-Information-of-Bangladeshabarca las escuelas, colegios, madrazas e institutos técnicos que tienen registro en EIIN. No incluye universidades ni información sobre programas de grado alguno.
Bangladeshi-Restaurant-Data proviene del escaneo de Google Places: nombre, dirección, coordenadas, calificación y cantidad de reseñas. No existe una columna para la cocina.Este desfase entre la descripción y el contenido es algo habitual. Lo adecuado no es adaptar los datos a un resumen, sino diseñar el sistema en función de lo que realmente está presente y anotar las discrepancias para que nadie posteriormente lo considere un error. En este caso, una búsqueda de “biryani” generó una coincidencia textual en el nombre del restaurante, y un filtro de ubicación produjo una coincidencia textual en la dirección, ya que no existen campos para la cocina ni para el distrito. El compromiso es menor y está claramente documentado.
Vale la pena adoptar desde temprano la costumbre de tratar el conjunto de datos como la fuente de verdad, y cualquier diagrama de esquema o resumen como una simple suposición. Lo que muestra .head() es la especificación real.
Cargar CSVs desordenados en tablas SQLite tipadas
Cada conjunto de datos se limpia, se convierte a los tipos adecuados y se guarda en su propio archivo SQLite. El esquema se declara explícitamente para que SQLite no infiera los tipos de columna, y para que la cadena de conversión de texto a SQL pueda utilizar nombres de columna significativos posteriormente. Observe la restricción NOT NULL en name y la bandera entera is_private con un valor por defecto de cero:
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
);
"""
La función de limpieza mapea los encabezados brutos del CSV a esos nombres de esquema, convierte el identificador y la bandera de privacidad a números (los valores inválidos se convierten en nulos en lugar de causar un error), elimina las filas que faltan un id o un nombre, y quita las instalaciones duplicadas:
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)
Preste atención a la llamada a _select_and_rename, donde podría esperarse un simple df.rename(). Ese complemento se añadió únicamente después de un fallo real, descrito en la sección de errores a continuación. Es un buen ejemplo de código defensivo cuyo propósito solo se hace evidente una vez que se sabe qué falló.
Una fábrica de herramientas reutilizables de texto a SQL
Todas las tres herramientas para bases de datos siguen un flujo idéntico, por lo que se escribe una sola vez como fábrica y se llama tres veces. La fábrica abre la base de datos mediante SQLDatabase.from_uri, crea una cadena que escribe SQL con create_sql_query_chain, prepara una herramienta que ejecuta consultas con QuerySQLDatabaseTool, y compone una cadena para formatear las respuestas a partir de un prompt, el modelo y un analizador de cadenas:
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()
La función interna realiza el trabajo real para cada pregunta. Pide al modelo que genere SQL, extrae la primera instrucción SELECT, aplica un acceso solo de lectura y un límite de filas, la ejecuta, y pasa la pregunta, la consulta y el resultado bruto a la cadena de formato. Cualquier excepción se convierte en un mensaje legible en lugar de una traza de pila, lo que también proporciona al agente algo concreto sobre lo cual razonar:
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)
Tres decisiones de diseño en este fragmento son fáciles de omitir en una primera versión:
- El modelo genera el SQL a través de
create_sql_query_chain. No existen plantillas de consulta mantenidas manualmente para mantenerse sincronizadas con el esquema.
_enforce_read_only_and_limit rechaza todo lo que no sea un SELECT y limita la cantidad de filas, de modo que un modelo confundido o manipulado no pueda modificar datos ni exportar toda una tabla a la ventana de contexto.Otra medida de protección útil, si se adapta este patrón, es abrir el archivo SQLite también en modo solo lectura. Las verificaciones de cadenas en SQL pueden ser evadidas de formas sutiles, y una conexión a la base de datos que físicamente no pueda escribir constituye una segunda línea de defensa.
Dejar que las descripciones de la herramienta se encarguen del enrutamiento
Aquí es donde un script se convierte en un agente. El mensaje del sistema comienza definiendo el papel del asistente:
SYSTEM_PROMPT = """You are a multi-tool research assistant for Bangladesh.
Luego enumera las cuatro herramientas y establece tres reglas de enrutamiento: utilizar la herramienta de base de datos correspondiente para contar, listar o obtener registros específicos, y nunca estimar números; usar búsquedas en la web para obtener conocimientos generales; y cuando una pregunta abarca varios conjuntos de datos, llamar a cada herramienta relevante y combinar los resultados. Debajo del prompt, create_tool_calling_agent une el modelo, las herramientas y el prompt, mientras que AgentExecutor ejecuta el bucle con un límite de seis iteraciones y manejo tolerante de llamadas a herramientas mal formadas:
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)
Aquí no hay nada que coincida con palabras clave, aplique expresiones regulares ni asocie frases a herramientas. El modelo compara la pregunta con cada descripción y elige una. Por eso las descripciones son las cadenas más importantes en la base de código: si son vagas o se superponen, el agente comenzará a elegir la herramienta incorrecta, y ningún ajuste en los prompts en otros lugares podrá compensarlo por completo. También vale la pena mantener el límite max_iterations=6, ya que controla los costos y evita que un agente en bucle llame a las herramientas indefinidamente.
¿Por qué el clásico AgentExecutor en lugar de LangGraph?
LangChain ha ido pasando su arquitectura de agente recomendada a LangGraph, que permite un control más preciso sobre flujos de trabajo multietapa con estado. Para este caso de uso, create_tool_calling_agent con AgentExecutor es más adecuado: cada solicitud consiste en una pregunta, una o dos llamadas a herramientas y una respuesta. Un marco basado en grafos añadiría estructura que el problema no necesita. Elegir la arquitectura más sencilla que resuelva el problema es una habilidad en sí misma, distinta de saber que existe una opción más potente. Si posteriormente su sistema de enrutamiento necesita ramificaciones, intentos repetidos o estado persistente, ese es el momento en que un grafo resulta útil; el artículo sobre diseño de grafos de agentes resilientes aborda ese tema.
Tres fallos que no tuvieron nada que ver con el modelo
La parte más instructiva del proceso de desarrollo no fue lo que funcionó a la primera, sino lo que falló. Ninguno de los tres problemas estuvo relacionado con las instrucciones o con la calidad del modelo.
Dos versiones de LangChain instaladas una al lado de la otra
LangChain 1.0 reorganizó su módulo agents y trasladó el patrón clásico AgentExecutor a un paquete separado. El entorno de Colab terminó con ambas versiones instaladas, por lo que una importación que funcionaba en una celda generaba el error ImportError: cannot import name 'AgentExecutor' en la siguiente.
La solución fue de tipo procedural y no una modificación en el código: desinstalar explícitamente los paquetes que causaban conflictos, fijar versiones exactas en lugar de rangos, y reiniciar el entorno de ejecución antes de continuar. El mantenimiento adecuado de las dependencias es un trabajo real de ingeniería. Consulte la documentación actual de LangChain para saber dónde se encuentra AgentExecutor en la versión que instale, ya que esto ha cambiado entre distintas versiones.
Un nombre de columna con espacio en lugar de guion bajo
El visor de datasets de Hugging Face mostraba una columna llamada INSTITUTE_NAME, pero el archivo descargado en realidad utilizaba INSTITUTE NAME. Ese único carácter provocó un KeyError a varias llamadas de función de distancia de la causa real.
La respuesta fue la función auxiliar _select_and_rename mencionada anteriormente. Esta elimina todos los caracteres que no son alfanuméricos antes de comparar los nombres de las columnas, por lo que INSTITUTE_NAME, INSTITUTE NAME y institute-name coinciden todos. Cuando aún no se puede encontrar una columna requerida, muestra la lista real de columnas en lugar de fallar silenciosamente. Un buen mensaje de error es, por sí mismo, una herramienta de depuración.
Formato de clave API rechazado por la API Gemini
Google ha ido introduciendo un formato más reciente de clave API que comienza con AQ. en lugar del familiar AIzaSy. En el momento en que se desarrolló este proyecto, las claves con el nuevo formato eran rechazadas por la API Gemini directamente con un error poco claro de tipo 401 ACCESS_TOKEN_TYPE_UNSUPPORTED. La lógica de reintentos no puede solucionar un credencial que el endpoint no acepta.
La solución fue cambiar por completo la ruta de autenticación: ahora el agente se autentica en Google Vertex AI mediante OAuth, al igual que cualquier otro servicio de Google Cloud. Esto evita el problema relacionado con el formato de las claves y, de hecho, es la configuración más adecuada para entornos de producción. Dado que este comportamiento podría haber cambiado desde entonces, confírvelo en la documentación actual de Google Cloud antes de depender de él.
El patrón común en los tres casos: en la ingeniería de IA actual, los aspectos relacionados con el modelo (versiones de paquetes, esquemas de datos y flujos de autenticación) fallan con mucha más frecuencia que el propio modelo.
El agente en acción
Una vez que se crean las bases de datos y se conecta el agente, una consulta sobre los colegios de Rajshahi se dirige directamente a la herramienta correspondiente a las instituciones, la cual responde con una cifra extraída de la base de datos:
You: How many colleges are registered in Rajshahi division?
> Entering new AgentExecutor chain...
[Tool: InstitutionsDBTool] → "There are 812 colleges registered in Rajshahi division."
Dentro de la misma sesión, se envía una pregunta de conocimientos generales sobre DGHS a la búsqueda en la web, mientras que una solicitud de hospitales gubernamentales en Chattogram se dirige a la base de datos de hospitales:
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..."
Se utilizan tres fuentes de datos diferentes dentro de una misma conversación, y no existe ningún código que indique al agente cuál elegir. El modelo toma cada decisión únicamente a partir de las descripciones de las herramientas.
Lecciones para la próxima versión
- Validar los datos antes de diseñar el esquema. Descubrir las deficiencias en el conjunto de datos después de crear las herramientas habría significado tener que reescribir las tres.
- Fijar las dependencias de manera más estricta de lo necesario. En un ecosistema donde se realizan reescrituras importantes cada pocos meses, un rango de versiones que funciona bien hoy podría comportarse de forma muy distinta en un entorno nuevo unas semanas después.
Conclusión
La herramienta de Gemini funcionó correctamente en cuanto se le proporcionaron herramientas claras y bien descritas. El modelo no fue la parte difícil; lo que consumió tiempo fue la infraestructura circundante: versiones conflictivas de bibliotecas, un espacio extra en el encabezado de una columna y un formato de credenciales que la API no aceptaba. Si construye un agente multiherramientas, invierta desde el principio en tres cosas: descripciones honestas de las herramientas, restricciones estrictas sobre todo lo que el modelo genere para su ejecución, y un entorno reproducible. Estos elementos determinan si el agente funciona tan bien en producción como lo hace en una demostración.
Lecturas relacionadas
- Fortaleciendo un agente de Python LangChain con siete middlewares incorporados — Aprenda cómo los middlewares de LangChain 1.0 añaden funcionalidades como resumen, límites de llamadas, reintentos, solución de fallback para modelos, eliminación de información personal sensible y aprobación humana a un agente Gemini sin modificar su lógica principal.
- Incorporando un agente de LangChain en FastAPI: Herramientas, búsqueda manual y transmisión en tiempo real — Cree un asistente integrado en la aplicación con FastAPI y LangChain: un manual en PDF almacenado en ChromaDB que se expone como herramienta, contexto por usuario, historial con puntos de control y respuestas transmitidas en tiempo real.