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?”.
En términos técnicos, cada interacción requiere:
- Pausa del contexto: El modelo detiene procesamiento
- Serialización/deserialización: Datos van y vienen por la API
- Contaminación del contexto: Resultados crudos llenan la ventana de tokens
- 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.
El flujo es así:
- El modelo genera código: Escribe un script que define toda la lógica (bucles, condiciones, filtros)
- Ejecución en sandbox: El código corre en un contenedor aislado con acceso a tus herramientas
- Procesamiento interno: Las herramientas se ejecutan, los datos se filtran y agregan dentro del código
- 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?
| Escenario | Enfoque | Justificación |
|---|---|---|
| Consulta única simple | Directo | Sin overhead de sandbox |
| Procesamiento de dataset grande | Programático | Filtra datos antes del contexto |
| Bucles/iteraciones múltiples | Programático | Evita N round-trips |
| Herramienta que requiere UI/confirmación | Directo | Supervisión humana antes de acciones irreversibles |
| Análisis condicional complejo | Programático | Lógica de control en código |
| Herramienta crítica que debe funcionar siempre | Híbrido | Dos 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ística | Estado | Descripción |
|---|---|---|
strict: true | No soportado | Herramientas con outputs estructurados estrictos |
tool_choice | No soportado | No puedes forzar modo programático de herramienta específica |
disable_parallel_tool_use: true | No soportado | Conflicto con ejecución programática paralela |
| Herramientas MCP | No soportado | Conectores 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_atpara evitar timeouts de contenedor - Reutilización de contenedores via
containerfield para sesiones relacionadas - Tests que verifican que solo el output final llega al contexto (no datos intermedios)
Fuentes
- Improved Web Search with Dynamic Filtering — Claude Blog — datos sobre mejoras en precisión y reducción de tokens en búsquedas web.
- 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.
- 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.