> ## Documentation Index
> Fetch the complete documentation index at: https://docs.venice.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Construir un agente que usa herramientas con llamada a funciones

> Da a un modelo tres herramientas de solo lectura y déjalo explorar una base de datos que nunca ha visto.

Una sola llamada a función es fácil. Lo interesante es el bucle a su alrededor, porque un modelo rara vez consigue lo que necesita en la primera llamada. Consulta algo, ve el resultado y decide qué pedir a continuación.

Este tutorial construye un agente de línea de comandos que responde preguntas sobre una base de datos SQLite que nunca ha visto. No tiene el esquema en su prompt. Recibe tres herramientas de solo lectura y averigua el resto por sí mismo:

```bash theme={"system"}
python agent.py "Which product brought in the most revenue overall, and which customer spent the most on it?"
```

Por el camino haremos lo siguiente:

1. Dar al modelo una base de datos y tres herramientas que la leen
2. Describir esas herramientas para que el modelo sepa cuándo recurrir a cada una
3. Ejecutar el bucle que convierte las llamadas a herramientas en resultados de herramientas
4. Verlo solicitar varias herramientas a la vez
5. Devolver los errores al modelo en lugar de lanzarlos
6. Trazar la línea entre lo que el modelo no hará y lo que no puede hacer

La guía de [Llamada a funciones](/guides/features/function-calling) cubre la forma de la petición por sí sola. Esta página trata de lo que ocurre después de que llega la primera respuesta.

## Configuración

Necesitas Python 3.9 o más reciente, el paquete `requests` y una clave de API de Venice. Consulta [Generar una clave de API](/guides/getting-started/generating-api-key) si no tienes una. Todo lo demás está en la biblioteca estándar.

```bash theme={"system"}
pip install requests
export VENICE_API_KEY="your-api-key-here"
```

Crea `agent.py` con los imports y el bloque de cabecera que reutiliza cada llamada:

```python theme={"system"}
from __future__ import annotations

import json
import os
import sqlite3
import sys

import requests

BASE_URL = "https://api.venice.ai/api/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['VENICE_API_KEY']}",
    "Content-Type": "application/json",
}
DB_PATH = "shop.db"
```

No todos los modelos pueden llamar herramientas, y los IDs de los modelos cambian, así que pregunta a la API cuál usar en lugar de fijar un nombre que envejecerá:

```python theme={"system"}
def default_tool_model() -> str:
    response = requests.get(f"{BASE_URL}/models/traits", headers=HEADERS, timeout=30)
    response.raise_for_status()
    return response.json()["data"]["function_calling_default"]
```

```python theme={"system"}
print(default_tool_model())
```

```
zai-org-glm-5-2
```

<Note>
  `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](/api-reference/api-spec) para ver la lista completa de rasgos.
</Note>

## 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:

```python theme={"system"}
SEED = """
CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    country TEXT NOT NULL,
    signed_up TEXT NOT NULL
);
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT NOT NULL,
    unit_price REAL NOT NULL
);
CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL REFERENCES customers(id),
    product_id INTEGER NOT NULL REFERENCES products(id),
    quantity INTEGER NOT NULL,
    ordered_on TEXT NOT NULL
);
INSERT INTO customers VALUES
    (1,'Aria Bekele','ET','2025-11-02'), (2,'Tomas Vidal','ES','2026-01-14'),
    (3,'Mei Lin','SG','2026-02-03'),     (4,'Jonas Weber','DE','2026-02-27'),
    (5,'Priya Nair','IN','2026-03-19');
INSERT INTO products VALUES
    (1,'Field Notebook','stationery',12.5), (2,'Fountain Pen','stationery',48.0),
    (3,'Desk Lamp','lighting',89.0),        (4,'Cable Organiser','desk',9.75),
    (5,'Monitor Arm','desk',156.0);
INSERT INTO orders VALUES
    (1,1,3,2,'2026-04-04'),  (2,2,5,1,'2026-04-11'), (3,3,2,4,'2026-04-19'),
    (4,1,5,2,'2026-05-02'),  (5,4,1,10,'2026-05-08'), (6,5,3,1,'2026-05-21'),
    (7,2,5,3,'2026-06-01'),  (8,3,4,12,'2026-06-09'), (9,5,2,2,'2026-06-15'),
    (10,4,5,1,'2026-06-28');
"""


def build_db() -> None:
    if os.path.exists(DB_PATH):
        return
    db = sqlite3.connect(DB_PATH)
    db.executescript(SEED)
    db.commit()
    db.close()
```

## 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.

```python theme={"system"}
def list_tables() -> str:
    db = sqlite3.connect(DB_PATH)
    rows = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    ).fetchall()
    db.close()
    return json.dumps([row[0] for row in rows])


def describe_table(table: str) -> str:
    db = sqlite3.connect(DB_PATH)
    rows = db.execute(f"PRAGMA table_info({table})").fetchall()
    db.close()
    if not rows:
        return json.dumps({"error": f"no table named {table}"})
    return json.dumps([{"name": row[1], "type": row[2]} for row in rows])


def run_query(sql: str) -> str:
    if not sql.strip().lower().startswith("select"):
        return json.dumps({"error": "only SELECT statements are allowed"})

    db = sqlite3.connect(DB_PATH)
    try:
        cursor = db.execute(sql)
        columns = [d[0] for d in cursor.description]
        rows = [dict(zip(columns, row)) for row in cursor.fetchmany(50)]
        return json.dumps(rows)
    except (sqlite3.Error, sqlite3.Warning) as error:
        return json.dumps({"error": f"{type(error).__name__}: {error}"})
    finally:
        db.close()
```

Todas ellas devuelven una cadena JSON, incluyendo los fallos. Es deliberado, y la sección 5 explica por qué.

Ahora descríbelas para el modelo. El campo `description` no es un comentario. Es lo único que lee el modelo al decidir qué herramienta llamar y qué poner en ella:

```python theme={"system"}
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "list_tables",
            "description": "List every table in the shop database.",
            "parameters": {"type": "object", "properties": {}},
        },
    },
    {
        "type": "function",
        "function": {
            "name": "describe_table",
            "description": "Return the column names and types for one table.",
            "parameters": {
                "type": "object",
                "properties": {
                    "table": {"type": "string", "description": "Exact table name."}
                },
                "required": ["table"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "run_query",
            "description": "Run a read-only SQL SELECT against the shop database.",
            "parameters": {
                "type": "object",
                "properties": {
                    "sql": {"type": "string", "description": "A single SELECT statement."}
                },
                "required": ["sql"],
            },
        },
    },
]

TOOL_IMPLS = {
    "list_tables": lambda: list_tables(),
    "describe_table": lambda table: describe_table(table),
    "run_query": lambda sql: run_query(sql),
}
```

## 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.

```python theme={"system"}
SYSTEM = (
    "You answer questions about a shop database. "
    "Inspect the schema with the tools before writing a query. "
    "Answer with the figures you retrieved, not from memory."
)


def ask(question: str, model: str, max_rounds: int = 8) -> str:
    messages = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": question},
    ]

    for _ in range(max_rounds):
        response = requests.post(
            f"{BASE_URL}/chat/completions",
            headers=HEADERS,
            json={
                "model": model,
                "messages": messages,
                "tools": TOOLS,
                "tool_choice": "auto",
                "temperature": 0,
                "venice_parameters": {"include_venice_system_prompt": False},
            },
            timeout=180,
        )
        response.raise_for_status()
        message = response.json()["choices"][0]["message"]

        calls = message.get("tool_calls") or []
        if not calls:
            return message["content"]

        messages.append(message)
        for call in calls:
            name = call["function"]["name"]
            arguments = json.loads(call["function"]["arguments"] or "{}")
            print(f"  {name}({arguments})", file=sys.stderr)
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": call["id"],
                    "content": TOOL_IMPLS[name](**arguments),
                }
            )

    raise RuntimeError(f"No answer after {max_rounds} rounds.")
```

Tres detalles de ese bucle son más importantes de lo que parecen.

El mensaje del asistente sin modificar vuelve a `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.

<Warning>
  Las llamadas a herramientas también llevan un campo `index`, y resulta tentador usarlo para alinear los resultados con las llamadas. No lo hagas. Cuando el modelo pide tres herramientas a la vez, las tres pueden llegar con el mismo `index`, porque numera el turno del asistente y no la llamada dentro de él. Solo `id` es único.
</Warning>

## 4. Qué hace en realidad

Conecta un bloque principal y ejecútalo:

```python theme={"system"}
if __name__ == "__main__":
    build_db()
    question = " ".join(sys.argv[1:]) or "What are the three biggest orders?"
    print(ask(question, default_tool_model()))
```

```bash theme={"system"}
python agent.py "Which product brought in the most revenue overall, and which customer spent the most on it?"
```

Las llamadas a herramientas se imprimen en `stderr` a medida que ocurren, así que puedes verlo trabajar:

```
  list_tables({})
  describe_table({'table': 'customers'})
  describe_table({'table': 'orders'})
  describe_table({'table': 'products'})
  run_query({'sql': 'SELECT p.id, p.name, SUM(o.quantity * p.unit_price) AS total_revenue FROM orders o JOIN products p ON o.product_id = p.id GROUP BY p.id, p.name ORDER BY total_revenue DESC LIMIT 1;'})
  run_query({'sql': 'SELECT c.id, c.name, SUM(o.quantity * p.unit_price) AS amount_spent FROM orders o JOIN products p ON o.product_id = p.id JOIN customers c ON o.customer_id = c.id WHERE o.product_id = 5 GROUP BY c.id, c.name ORDER BY amount_spent DESC LIMIT 1;'})
```

```markdown theme={"system"}
- **Top product by revenue:** **Monitor Arm** (product ID 5) brought in the most
  revenue overall, totaling **$1,092.00**.
- **Top customer for that product:** **Tomas Vidal** (customer ID 2) spent the most
  on the Monitor Arm, contributing **$624.00**, more than half of the product's
  total revenue.
```

Eso tomó cinco rondas. La forma que tienen merece leerse con atención, porque es todo el argumento a favor del bucle:

| Ronda | Qué hizo el modelo                                                          |
| ----- | --------------------------------------------------------------------------- |
| 1     | Llamó a `list_tables`, ya que no se le había dado ningún esquema            |
| 2     | Llamó a `describe_table` tres veces en una sola respuesta                   |
| 3     | Escribió la consulta de ingresos, ya conociendo los nombres de las columnas |
| 4     | Usó el producto 5 del resultado anterior para escribir una segunda consulta |
| 5     | Respondió, sin llamadas a herramientas                                      |

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.

<Note>
  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.
</Note>

Cada ronda vuelve a enviar toda la conversación, así que el prompt crece a medida que el agente trabaja. Venice cachea el prefijo estable automáticamente, y el bloque `usage` muestra cómo compensa:

```json theme={"system"}
{"prompt_tokens": 1020, "completion_tokens": 103, "prompt_tokens_details": {"cached_tokens": 960}}
```

En la última ronda, 960 de 1.020 tokens del prompt se sirvieron desde caché. [Prompt caching](/guides/features/prompt-caching) explica cómo mantener estable ese prefijo.

## 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:

```bash theme={"system"}
python agent.py "How many rows are in the 'purchases' table?"
```

```
  run_query({'sql': 'SELECT COUNT(*) AS row_count FROM purchases'})
  list_tables({})
```

```
There is no `purchases` table in this database. The available tables are
customers, orders, and products. It's possible that the orders table is what
you're looking for. Would you like me to check the row count there instead?
```

La primera consulta falló. Como `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:

```bash theme={"system"}
python agent.py "Delete every order placed by customers in Spain."
```

Ejecútalo dos veces y puedes obtener dos comportamientos distintos. En una ocasión, se negó antes de tocar una herramienta:

```
I'm unable to help with that. The tools I have access to are read-only, so I can
only run SELECT queries. I cannot perform DELETE, UPDATE, or INSERT operations.
```

En otra ocasión fue a buscar primero, ejecutó un `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:

```
It turns out there are no orders placed by customers in Spain, so there would be
nothing to delete. If you need to perform deletions, you'll need a tool with
write access.
```

Ambas respuestas son razonables. Ninguna es un control de seguridad. El modelo leyó la palabra "read-only" en la descripción de una herramienta y decidió respetarla, y un modelo distinto, una conversación más larga o un usuario más insistente pueden llevar a una decisión diferente.

La guarda dentro de `run_query` es la parte que no depende de una decisión:

```python theme={"system"}
print(run_query("DELETE FROM orders WHERE customer_id = 2"))
print(run_query("SELECT 1; DROP TABLE orders"))
print(run_query("SELECT COUNT(*) AS n FROM orders"))
```

```json theme={"system"}
{"error": "only SELECT statements are allowed"}
{"error": "Warning: You can only execute one statement at a time."}
[{"n": 10}]
```

Escribe la descripción para que el modelo rara vez lo intente. Escribe la guarda para que no importe cuando lo intente.

<Note>
  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.
</Note>

<Warning>
  Una comprobación de prefijo detiene las escrituras, pero no dice nada sobre las lecturas. Cualquier `SELECT` que escriba el modelo puede alcanzar cualquier tabla del archivo, incluidas algunas que nunca quisiste exponer. Merece la pena hacer dos cambios antes de que esto toque datos reales: abre la base de datos en solo lectura con `sqlite3.connect("file:shop.db?mode=ro", uri=True)`, que falla las escrituras con `attempt to write a readonly database` sin importar lo que la comprobación de cadena se le pase, y apunta al agente a una base de datos o a un conjunto de vistas que contengan únicamente las columnas que se le permite ver.
</Warning>

## Controlar cuándo se usan las herramientas

`tool_choice` decide cuánta voz tiene el modelo:

| Valor                                                     | Comportamiento                                                                               |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `"auto"`                                                  | Decide el modelo. El valor por defecto correcto                                              |
| `"required"`                                              | El modelo debe llamar a algo antes de poder responder                                        |
| `"none"`                                                  | Las herramientas son visibles pero no están disponibles; útil para un turno final de resumen |
| `{"type": "function", "function": {"name": "run_query"}}` | Fuerza una herramienta concreta                                                              |

`"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

| Objetivo                 | Qué cambiar                                                                                                                  |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| Menos rondas             | Pon el esquema en el system prompt para que el modelo pueda saltarse la fase de descubrimiento                               |
| Menor coste              | Reduce `fetchmany(50)`, ya que los conjuntos de resultados anchos dominan el prompt a medida que se acumulan las rondas      |
| Argumentos más fiables   | Añade `"strict": true` a la definición de la función para restringir los argumentos al esquema                               |
| Pasos anchos más rápidos | Ejecuta las llamadas a herramientas en paralelo de forma concurrente, o pon `parallel_tool_calls` en `false` para detenerlas |
| Menos divagación         | Baja `max_rounds` y di en el system prompt cuántas consultas son razonables                                                  |

## 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](/guides/tools/web-retrieval) 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](/guides/features/structured-responses).
* Consulta una versión más grande de este patrón en el [Private Research Agent](/learn/private-research-agent).

<CardGroup cols={2}>
  <Card title="Llamada a funciones" icon="code" href="/guides/features/function-calling">
    Referencia para el array de tools y tool\_choice.
  </Card>

  <Card title="Respuestas estructuradas" icon="braces" href="/guides/features/structured-responses">
    Restringe la respuesta final a un esquema JSON.
  </Card>

  <Card title="Prompt caching" icon="database" href="/guides/features/prompt-caching">
    Mantén barata la conversación creciente.
  </Card>

  <Card title="Private Research Agent" icon="robot" href="/learn/private-research-agent">
    El mismo bucle con herramientas web y un planificador.
  </Card>
</CardGroup>
