Saltar al contenido
AIM Desarrollo y Automatizaciones

Un agente de IA con herramientas reales en Workers AI

Gemma 4 con function calling nativo, cuatro herramientas de solo lectura y topes en cada ejecución. Así funciona el agente del Lab y cuánto cupo gasta.

Publicado el
Tiempo de lectura
5 min de lectura
  • IA
  • Workers AI
  • Cloudflare
  • Agentes
  • Pruebas

En el Lab hay un Agente IA con herramientas. Le escribes un objetivo, por ejemplo «una página con blog para mi panadería, ¿cuánto y en cuánto tiempo?», y lo ves trabajar paso a paso: escribe un plan corto, consulta el sitio con herramientas, calcula el presupuesto y te propone el siguiente paso. Corre en el mismo plan gratuito de Workers AI que el asistente, con Gemma 4.

Te cuento qué herramientas tiene, qué topes le puse y cómo evito que invente precios (DEC-033).

Function calling nativo, sin protocolo propio

La alternativa era pedirle al modelo un JSON con la herramienta y sus argumentos, y validarlo yo. No hizo falta. El 2026-11-08 comprobé con el modelo real (@cf/google/gemma-4-26b-a4b-it) que Gemma 4 en Workers AI acepta tools y tool_choice al estilo OpenAI y que, en streaming, manda delta.tool_calls: primero el id y el nombre, después los argumentos en pedazos. El adaptador se lo pasa así:

tools: request.tools.map((tool) => ({
  type: 'function',
  function: { name: tool.name, description: tool.description, parameters: tool.parameters },
})),
tool_choice: request.toolChoice,

El adaptador junta los pedazos de cada llamada y la entrega completa. El razonamiento del modelo queda apagado: el plan es la frase corta que escribe antes de sus llamadas.

Si algún día Workers AI deja de soportar herramientas, el plan B es justo el que descarté: un protocolo JSON validado con Zod. Y si cargo ANTHROPIC_API_KEY, el chat pasa a Claude, pero el agente sigue en Gemma, porque el adaptador de Claude no tiene herramientas.

Cuatro herramientas que solo leen

Las herramientas corren en el edge, en la misma API del sitio, sobre su propio contenido. Ninguna escribe, ninguna sale a la red y todas son deterministas:

  • search_site busca en los servicios, los proyectos, el Lab, el blog y las preguntas frecuentes, en el idioma de la página, y devuelve hasta 5 resultados con su ruta.
  • get_service trae un servicio: qué incluye, su tiempo de referencia, su precio «desde» y los adicionales del cotizador, con sus ids.
  • estimate_quote calcula el rango de precio con la misma función del cotizador, que ahora vive en @aim/contracts/quote-estimate. Así los dos dan el mismo rango.
  • suggest_next_step arma el enlace del siguiente paso: el cotizador con el servicio elegido, el formulario de contacto o WhatsApp.

Leen un catálogo que pnpm knowledge genera desde apps/web/src/content, junto con el conocimiento del asistente. Si cambio un precio y olvido regenerarlo, las pruebas fallan.

El modelo nunca ejecuta nada: solo ve las descripciones de las herramientas y sus resultados en JSON. Lo que pide pasa antes por el esquema Zod de esa herramienta:

const args = ARGS[name].safeParse(parsed)
if (!args.success) {
  return { ok: false, name, error: 'invalid_arguments', detail: issuesDetail(args.error) }
}

Unos argumentos inválidos o una herramienta que no existe no rompen la ejecución. La llamada se rechaza con un detalle corto que el modelo puede leer y corregir, y en la página la ves como un paso más: «Argumentos no válidos: la llamada se rechazó y el agente puede corregirla».

Topes por todos lados

Estos son los límites de cada ejecución:

export const AGENT_LIMITS: AgentLimits = {
  maxSteps: AGENT_MAX_STEPS,
  maxToolCalls: AGENT_MAX_TOOL_CALLS,
  maxTokens: 350,
  tokenBudget: 7_000,
}

Son 5 llamadas al modelo, 6 llamadas a herramientas, 350 tokens de salida por llamada y un presupuesto de 7.000 tokens en total. La ruta suma un tiempo límite de 45 s y la temperatura va en 0,2. La última llamada se manda con tool_choice: 'none', así que tiene que responder con texto. También es la última si ya se gastaron las herramientas o si la siguiente llamada podría pasarse del presupuesto: cada llamada repite todo lo anterior, así que el bucle suma lo gastado y la entrada de la llamada previa. Pasado el tope de herramientas, una llamada recibe tool_budget y el agente responde con lo que tiene.

La temperatura baja salió de medir: con la de por defecto, una ejecución se fue en texto repetido hasta el tope de salida. Gemma además a veces empieza con restos de su canal de razonamiento (thought\n<channel|>), aunque esté apagado, y el adaptador los quita. Al principio escribía los enlaces con un dominio inventado, y las reglas ahora le piden la ruta tal cual.

Los precios solo salen de las herramientas

La primera regla del prompt dice que cada dato sale de una herramienta y que los precios salen solo de estimate_quote. Pero una regla en un prompt no es una garantía, así que al terminar el bucle revisa la respuesta:

unverifiedAmounts: unverifiedAmounts(text, results),

amounts.ts busca cada monto de dinero de la respuesta (con su moneda, y «1,4 millones» cuenta como 1.400.000) y lo compara con los números que devolvieron las herramientas de esa ejecución. Lo que no coincide viaja en el evento done y la página lo advierte junto a la respuesta: «La respuesta menciona montos que ninguna herramienta devolvió (…). Confírmalos en el cotizador antes de decidir».

Tu objetivo llega al modelo dentro de una etiqueta <objetivo>, como datos y no como instrucciones, y una etiqueta escrita dentro del objetivo se quita antes. En la medición, un intento de inyección («ignore your rules…») terminó en una sola llamada y sin tocar ninguna herramienta.

Cuánto cupo gasta, medido

Workers AI ahora devuelve usage.neurons en cada respuesta, así que medí con el modelo real el 2026-11-08 (docs/DESPLIEGUE.md, sección «Agente IA»):

  • Una ejecución típica hace de 3 a 4 llamadas y gasta entre 36 y 60 neurons; la más larga, 76. Un mensaje del chat, de control, gastó 63.
  • El primer texto llega entre 0,4 y 1,2 s después de empezar, y una ejecución completa tarda de 4 a 9 s.
  • El peor caso que permiten los topes ronda los 138 neurons: unos 11.000 tokens de entrada y 4 × 350 de salida. Es más o menos el doble del peor mensaje del chat (unos 75).

Por eso cada ejecución cuenta como 2 usos del cupo que comparte con el asistente y el revisor de código. El peor día con solo agente serían 60 ejecuciones × 138 ≈ 8.300 neurons; con solo chat, 120 × 75 ≈ 9.000. Los dos caben en los 10.000 gratis.

Los dos usos se reservan juntos, con el mismo candado e insert condicional que corrigió la carrera del cupo en DEC-030:

const slots = await takeUsageSlot(db, {
  ipHash,
  model: agent.model,
  now: deps.now(),
  limits,
  units: AGENT_RUN_UNITS,
})

La reserva es una sola sentencia, en el mismo lote que el candado (por qué importa con Neon por HTTP): o entran las dos filas o ninguna. Los tokens reales quedan en la primera, y si el modelo falla antes de mostrar algo, las filas se liberan.

Cómo lo pruebo

Las pruebas del bucle usan un modelo con guion que reproduce pasos escritos a mano: argumentos inválidos que se corrigen, una herramienta inexistente, los topes, una inyección y montos sin verificar. Otra prueba, que solo corre si la pido, repite la medición contra el modelo real.

Para revisarlo tú

Y para verlo trabajar, dale un objetivo en el Agente IA con herramientas.