Programmatic Tool Calling: El Fin del Ping-Pong de la IA

Patrón avanzado de Anthropic que reduce tokens hasta un 37% ejecutando herramientas mediante código Python en sandbox, eliminando el bucle pregunta-respuesta tradicional.

Si has trabajado con herramientas de IA que necesitan hacer múltiples consultas — buscar datos, procesarlos, filtrarlos — sabes que el problema no es la capacidad individual, sino el constante ping-pong entre el modelo y tus servicios. Cada consulta es un round-trip completo que consume tokens, añade latencia y llena el contexto con basura irrelevante. En este post explico cómo el Programmatic Tool Calling de Anthropic convierte ese becario preguntón en un programador autónomo que orquesta tareas complejas sin molestarte.

¿Por qué el modelo tradicional de herramientas es insostenible?

El tool calling convencional funciona así: el modelo pide una herramienta, esperas la respuesta, la procesas, se la devuelves, y repite. Es como contratar a alguien para cocinar que te llama por cada ingrediente: “¿Abro el refrigerador?”, “¿Saco los huevos?”, “¿Los rompo?”.

Flujo de herramientas tradicionales mostrando: Usuario → Modelo → pausa y llama herramienta → recibe datos → procesa → Modelo → pausa y llama herramienta → recibe datos → procesa, repitiéndose múltiples veces, ilustrando el ciclo de ping-pong ineficiente
El patrón tradicional genera múltiples round-trips: cada consulta requiere pausa del modelo, serialización de datos, y contaminación del contexto con resultados crudos

En términos técnicos, cada interacción requiere:

  1. Pausa del contexto: El modelo detiene procesamiento
  2. Serialización/deserialización: Datos van y vienen por la API
  3. Contaminación del contexto: Resultados crudos llenan la ventana de tokens
  4. Round-trip completo: Latencia de red en cada paso

Cuando necesitas procesar 100 registros de una base de datos, esto significa 100 llamadas separadas. El resultado: lentitud, el coste escala con cada round-trip adicional, y un contexto saturado de datos que el modelo no necesita para razonar.

¿Cómo funciona Programmatic Tool Calling?

Anthropic cambió las reglas completamente. En lugar del ping-pong tradicional, el modelo escribe código Python que orquesta múltiples herramientas dentro de un sandbox seguro.

Flujo de Programmatic Tool Calling mostrando: Usuario → Modelo genera código Python → Sandbox (contiene bucles, condicionales, múltiples llamadas a herramientas sin round-trips) → procesamiento y filtrado interno → solo resultado final retorna a Modelo
Programmatic Tool Calling ejecuta toda la orquestación en sandbox: el modelo genera código una sola vez, las herramientas se llaman internamente sin interrupciones, y solo el resultado final limpio retorna al modelo

El flujo es así:

  1. El modelo genera código: Escribe un script que define toda la lógica (bucles, condiciones, filtros)
  2. Ejecución en sandbox: El código corre en un contenedor aislado con acceso a tus herramientas
  3. Procesamiento interno: Las herramientas se ejecutan, los datos se filtran y agregan dentro del código
  4. Resultado limpio: Solo el output final llega al contexto del modelo
# Código que el modelo genera automáticamente
async def analyze_sales_by_region():
    regions = ["West", "East", "Central", "North", "South"]
    results = {}

    for region in regions:
        # Cada llamada se ejecuta sin round-trip al modelo
        # Claude debería generar consultas parametrizadas para seguridad
        data = await query_database("SELECT revenue FROM sales WHERE region = ?", [region])
        results[region] = sum(row["revenue"] for row in data)

    # Solo este resultado llega al contexto del modelo
    top_region = max(results.items(), key=lambda x: x[1])
    return f"Top region: {top_region[0]} with ${top_region[1]:,}"

# El modelo ve: "Top region: West with $125,000"
# No ve: 500 filas de datos crudos de la base de datos

¿Qué herramientas se pueden llamar programáticamente?

El campo allowed_callers en tu definición de herramienta controla este comportamiento:

// Solo llamadas directas (comportamiento tradicional)
{
  name: "search_emails",
  description: "Search user emails by keyword",
  input_schema: { /* ... */ },
  allowed_callers: ["direct"]  // Default si se omite
}

// Solo llamadas programáticas (desde código)
{
  name: "process_expense_report",
  description: "Process expense data and return JSON objects",
  input_schema: { /* ... */ },
  allowed_callers: ["code_execution_20260120"]
}

// Ambos modos disponibles
{
  name: "query_database",
  description: "Execute SQL query and return results",
  input_schema: { /* ... */ },
  allowed_callers: ["direct", "code_execution_20260120"]
}

Regla práctica: Usa ["code_execution_20260120"] para herramientas que devuelven datos estructurados grandes o cuando anticipes múltiples llamadas secuenciales.

¿Cuándo conviene programático vs directo vs híbrido?

EscenarioEnfoqueJustificación
Consulta única simpleDirectoSin overhead de sandbox
Procesamiento de dataset grandeProgramáticoFiltra datos antes del contexto
Bucles/iteraciones múltiplesProgramáticoEvita N round-trips
Herramienta que requiere UI/confirmaciónDirectoSupervisión humana antes de acciones irreversibles
Análisis condicional complejoProgramáticoLógica de control en código
Herramienta crítica que debe funcionar siempreHíbridoDos herramientas separadas: una ["direct"], otra ["code_execution_20260120"]

Los tres “superpoderes” complementarios

Anthropic no se limitó al Programmatic Tool Calling. Introdujeron tres mejoras que potencian el patrón:

Dynamic Filtering en búsquedas web

Antes, si Claude leía una página web, se tragaba anuncios, menús y HTML basura. Ahora genera código que “limpia” el contenido antes de procesarlo [1].

Habilitación: Usa las herramientas web_search_20260209 o web_fetch_20260209 con el header beta code-execution-web-tools-2026-02-09.

Resultado: 24% menos tokens de entrada y 11% mejor precisión [1]. En BrowseComp, Sonnet 4.6 saltó del 33.3% al 46.6% de precisión [1].

Tool Search interno

Ya no necesitas cargar el manual de todas tus herramientas “por si acaso”. Claude busca la herramienta que necesita cuando la necesita, aprende su interfaz al vuelo, y la ejecuta. El patrón: cuando tienes un catálogo grande y no quieres cargar todas las definiciones por adelantado, Claude puede consultar un registro de herramientas bajo demanda.

Resultado: 85% menos tokens al arrancar [2]. De ~77K tokens a ~8.7K tokens en configuración inicial.

Tool Use Examples

En lugar de instrucciones robóticas sobre formularios complejos, das ejemplos reales de uso. Claude aprende por pattern matching.

Resultado: Precisión en manejo de parámetros del 72% al 90% [2].

Implementación práctica con TypeScript

Paso 1: Envío inicial

import { Anthropic } from "@anthropic-ai/sdk";

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

async function analyzeCustomerData() {
  const response = await anthropic.messages.create({
    model: "claude-opus-4-6",
    max_tokens: 4096,
    messages: [{
      role: "user",
      content: "Analiza los ingresos por región del último trimestre y identifica los 3 clientes top"
    }],
    tools: [
      {
        type: "code_execution_20260120",
        name: "code_execution"
      },
      {
        name: "query_sales_db",
        description: "Execute SQL query. Returns JSON array of rows with columns: customer_id, region, revenue, date",
        input_schema: {
          type: "object",
          properties: {
            sql: { type: "string", description: "SQL query to execute" }
          },
          required: ["sql"]
        },
        allowed_callers: ["code_execution_20260120"]
      }
    ]
  });

  return response;
}

Paso 2: Bucle de respuesta a tool calls

async function handleToolResults(response, conversationHistory) {
  // Si hay tool_use blocks pendientes, responder con tool_result
  while (response.stop_reason === "tool_use") {
    const toolUseBlocks = response.content.filter(block => block.type === "tool_use");

    // Ejecuta todas las herramientas en paralelo
    const results = await Promise.all(
      toolUseBlocks.map(toolUse => executeYourTool(toolUse.name, toolUse.input))
    );

    // Responde SOLO con tool_result blocks (sin texto adicional)
    const toolResponse = await anthropic.messages.create({
      model: "claude-opus-4-6",
      max_tokens: 4096,
      container: response.container?.id, // Reutiliza contenedor si existe
      messages: [
        ...conversationHistory,
        { role: "assistant", content: response.content },
        {
          role: "user",
          content: toolUseBlocks.map((toolUse, index) => ({
            type: "tool_result",
            tool_use_id: toolUse.id,
            content: results[index]
          }))
        }
      ],
      tools: [/* mismas herramientas */]
    });

    response = toolResponse;
  }

  return response; // stop_reason: "end_turn"
}

Punto clave: La descripción detallada del formato de salida (Returns JSON array of rows with columns...) es crítica. Claude usa esta info para escribir código que procesa correctamente los resultados.

Errores comunes

Respuestas con contenido mixto en tool calls programáticos

Cuando hay tool calls programáticos pendientes, tu respuesta debe contener solo bloques tool_result. Incluir texto causa error de API.

// ❌ Inválido: Mezclar texto con tool_result
{
  "role": "user",
  "content": [
    {"type": "tool_result", "tool_use_id": "toolu_01", "content": "[{\"customer_id\": \"C1\"}]"},
    {"type": "text", "text": "¿Qué sigue?"}  // Esto causa error
  ]
}

// ✅ Válido: Solo tool_result para calls programáticos
{
  "role": "user",
  "content": [
    {"type": "tool_result", "tool_use_id": "toolu_01", "content": "[{\"customer_id\": \"C1\"}]"}
  ]
}

El orquestador que también hace consultas directas

Si defines una herramienta con ["direct", "code_execution_20260120"], Claude puede usar ambos modos en la misma conversación. Esto rompe la predicibilidad del flujo.

Síntoma: A veces ves datos crudos en el contexto, otras veces no. Solución: Elige un modo por herramienta. Si necesitas ambos, crea dos herramientas distintas.

Herramientas sin esquema de salida detallado

Claude necesita saber exactamente qué formato devuelve tu herramienta para escribir código que la procese.

// ❌ Malo: Descripción vaga
description: "Get user data"

// ✅ Bueno: Formato específico
description: "Returns user object with fields: id (string), name (string), email (string), created_at (ISO date)"

Ignorar la expiración del contenedor

Los contenedores caducan a los ~4.5 minutos de inactividad. Si tu herramienta tarda más en responder, el código recibe un TimeoutError.

Solución: Monitorea el campo expires_at en las respuestas y implementa timeouts en tus herramientas.

Tool results con contenido ejecutable no validado

Los tool results se procesan en el sandbox Python, lo que expone amenazas específicas: uso de eval() sobre resultados no validados, construcción de SQL dinámico desde datos externos, shell injection si se pasan outputs a subprocesos, y prompt injection desde contenido web malicioso.

Solución: Usa queries parametrizadas para SQL (ya aplicado en línea 63), valida estructuras de datos antes de procesarlas, y sanitiza contenido web. Evita eval(), exec() o subprocess con datos de tool results sin validación previa.

Herramientas con efectos secundarios sin idempotencia

Tool calling incluye retries automáticos que pueden duplicar efectos. Herramientas que escriben en bases de datos, envían emails o procesan pagos pueden ejecutarse múltiples veces sin el conocimiento del desarrollador.

Solución: Diseña herramientas críticas como idempotentes o implementa deduplicación vía request_id únicos para prevenir efectos duplicados.

Incompatibilidades conocidas

CaracterísticaEstadoDescripción
strict: trueNo soportadoHerramientas con outputs estructurados estrictos
tool_choiceNo soportadoNo puedes forzar modo programático de herramienta específica
disable_parallel_tool_use: trueNo soportadoConflicto con ejecución programática paralela
Herramientas MCPNo soportadoConectores MCP no pueden llamarse programáticamente

Checklist de implementación

  • Herramientas de datos masivos configuradas con allowed_callers: ["code_execution_20260120"]
  • Descripción detallada del formato de salida en cada herramienta (tipos, campos, estructura)
  • Timeouts implementados en herramientas que pueden tardar >30 segundos
  • Validación de tool results para prevenir inyección de código
  • Monitoreo del campo expires_at para evitar timeouts de contenedor
  • Reutilización de contenedores via container field para sesiones relacionadas
  • Tests que verifican que solo el output final llega al contexto (no datos intermedios)

Fuentes

  1. Improved Web Search with Dynamic Filtering — Claude Blog — datos sobre mejoras en precisión y reducción de tokens en búsquedas web.
  2. Advanced Tool Use Performance — Anthropic Engineering — métricas de reducción de tokens y mejoras de precisión en Tool Search y Tool Use Examples.
  3. Programmatic Tool Calling — Claude API Docs — documentación técnica oficial sobre implementación y casos de uso.

Preguntas Frecuentes

¿Qué pasa si mi herramienta falla durante la ejecución programática?

El código Python recibe el error como string y Claude puede manejarlo programáticamente — retry, logging, fallbacks. Es más resiliente que el modo directo porque el error no interrumpe toda la conversación.

¿Puedo mezclar herramientas programáticas y directas en la misma request?

Sí, pero no es recomendable. El patrón híbrido confunde al modelo sobre cuándo usar cada modo. Mejor separar claramente: herramientas de datos masivos → programáticas, herramientas de UI/confirmación → directas.

¿Cómo debug el código que genera Claude internamente?

El campo stdout en code_execution_tool_result muestra prints del código. Usa print() statements en tu lógica de herramientas para debugging. También puedes inspeccionar el código en response.content[n].input.code.

¿Vale la pena el overhead del sandbox para tareas simples?

No. Para una sola consulta con respuesta pequeña, el overhead de crear el contenedor supera el beneficio. Como heurística empírica: usa el modo programático cuando anticipes 3+ llamadas o datasets >10KB. Mide latencia p95 y coste de tokens en tu caso específico.

¿Los contenedores mantienen estado entre requests?

Sí, si reutilizas el container ID. Variables, imports, archivos temporales persisten durante ~4.5 minutos. Útil para análisis multistep donde necesitas mantener datasets en memoria.

Advertencia crítica de seguridad: Solo reutiliza contenedores dentro del mismo usuario o sesión. En aplicaciones multi-tenant, reutilizar un contenedor entre usuarios distintos expone variables, archivos temporales y datos de un usuario a otro. Siempre invalida el container ID al cambiar de contexto de seguridad.

Advertencia crítica de seguridad: Solo reutiliza contenedores dentro del mismo usuario o sesión. En aplicaciones multi-tenant, reutilizar un contenedor entre usuarios distintos expone variables, archivos temporales y datos de un usuario a otro. Siempre invalida el container ID al cambiar de contexto de seguridad.

¿Cómo implemento el bucle de conversación completo?

El patrón es simple: mientras stop_reason === "tool_use", responde con tool_result blocks y continúa hasta stop_reason === "end_turn". Ver el ejemplo completo de TypeScript en la sección “Implementación práctica” que muestra el bucle while para manejar múltiples tool calls programáticos.