- Dar al modelo una base de datos y tres herramientas que la leen
- Describir esas herramientas para que el modelo sepa cuándo recurrir a cada una
- Ejecutar el bucle que convierte las llamadas a herramientas en resultados de herramientas
- Verlo solicitar varias herramientas a la vez
- Devolver los errores al modelo en lugar de lanzarlos
- Trazar la línea entre lo que el modelo no hará y lo que no puede hacer
Configuración
Necesitas Python 3.9 o más reciente, el paqueterequests y una clave de API de Venice. Consulta Generar una clave de API si no tienes una. Todo lo demás está en la biblioteca estándar.
agent.py con los imports y el bloque de cabecera que reutiliza cada llamada:
GET /models/traits asigna nombres estables de rasgos al modelo que actualmente cumple ese rol. Leer function_calling_default al iniciar significa que tu agente sigue funcionando cuando se reemplaza el modelo subyacente. Consulta Modelos para ver la lista completa de rasgos.1. Una base de datos sobre la que valga la pena preguntar
Cualquier archivo SQLite sirve. Este es una pequeña tienda con clientes, productos y los pedidos que los unen, lo suficiente para que una pregunta real necesite un join y una agregación:2. Tres herramientas a las que puede recurrir el modelo
Las herramientas reflejan la forma en la que una persona se enfrenta a una base de datos desconocida: averiguar qué contiene, mirar una tabla de cerca y luego consultarla.description no es un comentario. Es lo único que lee el modelo al decidir qué herramienta llamar y qué poner en ella:
3. El bucle
La llamada a funciones es una conversación, no una petición. El modelo responde con llamadas a herramientas, tú las ejecutas, añades los resultados y vuelves a preguntar. Termina cuando el modelo responde con contenido en lugar de llamadas.messages antes que los resultados. Lleva los tool_calls a los que los resultados están respondiendo, y en un modelo con razonamiento también lleva un campo reasoning_content. Reconstruir el mensaje a mano y descartar campos que no esperabas es la forma más común de romper la segunda ronda.
Cada resultado se empareja con su llamada mediante tool_call_id. Nada más lo identifica.
max_rounds es un límite real, no una formalidad. Un modelo que sigue consultando sin llegar a una conclusión terminará bucleando hasta que se te acabe la paciencia o el saldo.
4. Qué hace en realidad
Conecta un bloque principal y ejecútalo:stderr a medida que ocurren, así que puedes verlo trabajar:
La ronda 4 es la parte que una sola llamada a función no puede hacer. El modelo no podía escribir esa consulta hasta haber visto la respuesta a la anterior.
Tu ejecución no coincidirá llamada por llamada con esta. A veces el modelo describe las tres tablas a la vez y a veces una por una, y de vez en cuando se salta
list_tables y adivina un nombre. Las cifras son estables porque salen de la base de datos; el camino hasta ellas no lo es.
La ronda 2 devolvió tres llamadas a herramientas en una sola respuesta, y el bucle de arriba las ejecuta una detrás de otra. Son independientes, así que un
ThreadPoolExecutor merece la pena en cuanto tus herramientas hagan E/S real. Mantén los mensajes tool en el mismo orden que las llamadas que los produjeron.usage muestra cómo compensa:
5. Deja que los errores lleguen al modelo
El instinto es lanzar una excepción ante una consulta incorrecta. Resístelo. Un error es información, y el modelo puede actuar en consecuencia. Pide una tabla que no existe:run_query devolvió {"error": "OperationalError: no such table: purchases"} como un resultado de herramienta normal en lugar de lanzar, el modelo lo leyó, llamó a list_tables para averiguar qué sí existía y se corrigió. Si la excepción se hubiera propagado, el script habría muerto por un error tipográfico.
Por eso cada herramienta devuelve JSON también en la ruta de error. La regla es sencilla: si a un humano que depura tu herramienta le interesaría ver el mensaje, al modelo también.
6. Lo que no hará y lo que no puede hacer
Pídele al agente que destruya algo:SELECT para los clientes españoles, no encontró ninguno porque la columna guarda ES en lugar de Spain, e informó de eso en su lugar:
run_query es la parte que no depende de una decisión:
Esa segunda línea es la razón por la que
run_query captura sqlite3.Warning junto con sqlite3.Error. El driver de Python rechaza sentencias apiladas, pero lanza Warning para ellas, y Warning no es una subclase de Error. Capturar solo sqlite3.Error deja que una sentencia apilada escape del handler y mate el bucle en lugar de devolver un mensaje que el modelo pueda leer.Controlar cuándo se usan las herramientas
tool_choice decide cuánta voz tiene el modelo:
"required" es más contundente de lo que parece. Preguntar a este agente What is 2 + 2? con tool_choice en "required" hace que llame a list_tables, mire una base de datos que no le sirve de nada y luego responda 4 en la siguiente ronda. Con "auto" responde 4 de inmediato y no llama a nada. Recurre a "required" cuando una herramienta realmente deba ejecutarse, por ejemplo para registrar una petición, y déjalo en paz en el resto de casos.
Ajustar el agente
Próximos pasos
El bucle que ahora tienes es el mismo que hay detrás de la mayoría de agentes. Solo cambian las herramientas.- Cambia las herramientas SQL por llamadas HTTP y se convierte en un agente de API.
- Añade Web Search y scraping como herramienta y podrá consultar la web en vivo a mitad de la respuesta.
- Pide un resultado tipado en lugar de prosa con Respuestas estructuradas.
- Consulta una versión más grande de este patrón en el Private Research Agent.
Llamada a funciones
Referencia para el array de tools y tool_choice.
Respuestas estructuradas
Restringe la respuesta final a un esquema JSON.
Prompt caching
Mantén barata la conversación creciente.
Private Research Agent
El mismo bucle con herramientas web y un planificador.