Cómo escribí WebPageTool y casi quemé tokens: un caso de desarrollo de un agente de IA

Actualizado:
Cómo escribí WebPageTool y casi quemé tokens: un caso de desarrollo de un agente de IA

Una solicitud de usuario. Una URL. Once llamadas seguidas. Mientras miraba los registros, el contador de tokens seguía aumentando, y me di cuenta de que acababa de construir el bucle más caro de mi proyecto.

Primera prueba y resultado inesperado

Añadí WebPageTool a SearchAgent y ejecuté inmediatamente una prueba: envié un mensaje simple con un enlace al chat. La herramienta funcionó: la página se cargó, se extrajo el texto y la respuesta fue relevante.

Pero noté algo interesante en los registros.

WebPageTool: url='https://webscraft.org/'  ← llamada 1
WebPageTool: url='https://webscraft.org/'  ← llamada 2
WebPageTool: url='https://webscraft.org/'  ← llamada 3
WebPageTool: url='https://webscraft.org/'  ← llamada 4
...
WebPageTool: url='https://webscraft.org/'  ← llamada 11

Once llamadas para una sola solicitud de usuario. El modelo recibía el mismo resultado cada vez y seguía llamando a la herramienta de nuevo. No por un error en la lógica, simplemente no se detenía.

Estoy desarrollando una plataforma para comunicarme con personajes de IA. SearchAgent en este proyecto puede leer páginas web, buscar noticias, verificar tipos de cambio. WebPageTool es una nueva herramienta en esta cadena. Y esta primera prueba planteó inmediatamente una pregunta específica: ¿qué es exactamente lo que hace que el modelo repita la llamada y cómo detenerlo?

Para responder a esto, tuve que entender qué es realmente "pesado" para un LLM y por qué un modelo local se comporta de manera diferente a uno en la nube.

¿Qué es una "operación pesada" en LLM y por qué es importante?

Antes de hablar de un error específico, vale la pena entender la mecánica básica.

Cada llamada a un LLM consta de dos partes: entrada (todo lo que pasamos al modelo) y salida (lo que el modelo genera en respuesta). Ambas partes se miden en tokens, y son los tokens los que determinan tanto el costo como el tiempo de respuesta.

Pero hay una asimetría importante: la entrada se procesa en paralelo — el modelo lee todo el contexto simultáneamente, es relativamente rápido y barato. La salida se genera secuencialmente — token por token, y aquí es donde surge el retraso. Los proveedores en la nube suelen cobrar entre 3 y 5 veces más por la salida que por la entrada.

Aquí hay una visión general de la carga por tipo de operación:

Por entrada (tokens de entrada)

Operación Por qué es pesada
RAG con chunks grandesCada documento encontrado se añade al contexto
Análisis de PDF / documentosTodo el texto del documento va al prompt
Historial de chat largo sin resumenSe acumulan más de 100 mensajes
Ejemplos few-shot en el prompt del sistemaUna gran cantidad de ejemplos ocupa espacio
Multi-agente con transferencia de contextoCada agente recibe todo el contexto anterior

Por número de llamadas a LLM

Patrón Número de llamadas
Chain of Thought con autocomprobación3–5 por solicitud
Agente ReAct (pensar→actuar→observar)5–20 por solicitud
Tree of ThoughtsExponencial
Autoconsistencia (varias respuestas → votación)N llamadas paralelas
Bucle de herramientas sin restricciones∞ (exactamente lo que vi en los registros)

Por salida (tokens de salida)

Operación Por qué es pesada
Generación de código de un archivo completo1000–3000 tokens de salida
JSON estructurado con muchos camposEl modelo genera cada carácter
Razonamiento Chain-of-thoughtEl modelo "piensa en voz alta" antes de responder
Traducción de texto largoEntrada ≈ Salida en tamaño

Comprender esta imagen no es un ejercicio académico. Es un ahorro directo de presupuesto y una mejora de la experiencia del usuario.

¿Por qué leer una página web cuesta como 10 diálogos?

Cuando diseñé WebPageTool, todo parecía simple: descargar la página, recortarla a un tamaño razonable, pasarla al modelo.

Pero veamos las cifras reales de una solicitud con lectura de página.

Una aclaración importante sobre caracteres y tokens: para el alfabeto latino, la relación es aproximadamente 4 caracteres = 1 token, para el cirílico, 2-3 caracteres = 1 token. Es decir, el texto ucraniano o ruso cuesta más que el inglés con la misma cantidad de caracteres.

¿Qué se pasa al modelo? Aproximadamente tokens Nota
System prompt del personaje 200–400 Siempre
Descripciones de 9 herramientas (tool schemas) 500–800 Solo SearchAgent. En routing a defaultStream — 0
Últimos 4 mensajes de contexto 200–400 Solo SearchAgent. defaultStream pasa el contexto completo (hasta 20 mensajes)
Solicitud del usuario 20–50 Siempre
Texto de la página (4000 caracteres cirílicos) 1500–2000 Solo al llamar a WebPageTool
Total — SearchAgent + WebPageTool ~2500–3700 El escenario más pesado
Total — defaultStream (chat normal) ~700–1500 Gracias al enrutamiento de embedding, la mayoría de las solicitudes van aquí
Respuesta del modelo (output) 200–500 Siempre

Para comparar, un mensaje normal en el chat sin herramientas ocupa 1200-2500 tokens junto con el contexto. WebPageTool es casi el doble de pesado.

Y ahora, imagina que el modelo llama a esta herramienta once veces seguidas. En lugar de ~3000 tokens por solicitud, potencialmente 30,000+. Y todo esto por un solo mensaje del usuario.

Por eso decidí resolver el problema hasta el final.

Cómo construí WebPageTool

La idea de la herramienta es simple: el usuario envía un enlace, el agente lee la página y resume el contenido.

Para descargar y analizar HTML, elegí Jsoup, una biblioteca confiable sin dependencias innecesarias. Después de descargar la página, es necesario eliminar todo lo superfluo: navegación, pies de página, banners, pop-ups de cookies, bloques publicitarios. Queda el contenido semántico: article, main, .content.

Dos parámetros que tienen un impacto directo en los tokens:

  • MAX_CHARS = 4000 — cuántos caracteres de texto se pasan al modelo después de la limpieza. Con el cirílico, esto son aproximadamente 1500-2000 tokens.
  • TIMEOUT_MS = 10 000 — si el sitio no responde en 10 segundos, Jsoup lanza una excepción que se intercepta y devuelve un mensaje comprensible. El stream no se cuelga.

También añadí validación de URL y una lista de dominios bloqueados — YouTube, Instagram, TikTok — donde Jsoup solo obtendrá una carcasa vacía sin contenido real, porque estos sitios se renderizan a través de JavaScript.

La herramienta en sí funcionó correctamente desde el primer lanzamiento. La página se cargaba, el texto se extraía, la respuesta era relevante. El problema vino de donde no lo esperaba.

Tool loop — cuando el modelo entró en bucle

Después de la primera prueba exitosa, escribí en el chat: "https://webscraft.org/ ¿qué es este sitio?"

En los logs, vi lo que describí al principio: once llamadas consecutivas a WebPageTool con la misma URL. El modelo recibía el resultado correcto cada vez y... volvía a llamar a la herramienta.

Intenté varios enfoques, y cada uno me enseñó algo importante.

Primer intento: ThreadLocal

La lógica parecía obvia: guardamos un flag "ya llamado" en ThreadLocal, y al volver a llamar, devolvemos un stub. ThreadLocal guarda el valor por separado para cada hilo.

Pero Spring AI, en modo streaming, ejecuta las llamadas a herramientas en hilos diferentes del pool boundedElastic. Cada nuevo hilo recibía un CALLED = false fresco y pasaba la verificación. ThreadLocal no es adecuado para un entorno reactivo con un pool de hilos.

Segundo intento: AtomicInteger

AtomicInteger es un contador seguro para hilos, la operación getAndIncrement() es atómica. Parecía una solución. Pero si WebPageTool siguiera siendo un componente de Spring (@Component), sería un singleton — compartido por todos los usuarios. La primera llamada real bloquearía la herramienta para todos para siempre.

Solución final: objeto por solicitud

En lugar de luchar contra el estado en un singleton, eliminé @Component y comencé a crear una nueva instancia de WebPageTool por cada solicitud directamente en SearchAgent:

WebPageTool webPageTool = new WebPageTool();

Cada solicitud del usuario recibe su propia instancia con un contador limpio. AtomicInteger sigue siendo útil aquí — si el modelo llama a la herramienta desde varios hilos simultáneamente, getAndIncrement() garantiza que solo la primera llamada proceda.

Esta es una solución elegante: no se necesita sincronización entre solicitudes ni una gestión de estado compleja.

Modelo local vs. en la nube: por qué el comportamiento es diferente

Cuando pasé de un modelo local (LM Studio) a uno en la nube a través de OpenRouter, el bucle de herramientas desapareció por sí solo. Sin ningún cambio en el código.

¿Por qué es así? Esta pregunta es más profunda de lo que parece.

Entrenamiento en uso de herramientas

GPT-4o, Claude Sonnet y otros modelos en la nube han recibido entrenamiento especializado en el uso de herramientas. OpenAI y Anthropic han invertido recursos significativos en RLHF (Reinforcement Learning from Human Feedback), un proceso en el que evaluadores humanos clasificaron miles de ejemplos de uso correcto de herramientas. El modelo aprendió un patrón claro: llamada → resultado → respuesta final. FIN.

Los modelos abiertos locales — Qwen, Llama, Mistral — tienen muchos menos ejemplos especializados de este tipo en sus datos de entrenamiento. Saben cómo llamar a las herramientas, pero no siempre saben cuándo detenerse.

Personalmente, uso meta-llama-3.1-8b-instruct a través de LM Studio: responde rápidamente y admite la llamada a herramientas de inmediato. Para el desarrollo y las pruebas de arquitectura locales, es una excelente opción que recomiendo como punto de partida.

Cuantización y degradación del razonamiento complejo

La mayoría de los modelos locales se ejecutan en formato cuantizado de 4 bits, lo cual es necesario para ejecutarse en hardware de consumo. La cuantización reduce la precisión de los pesos del modelo: en lugar de números de punto flotante de 16 bits, se almacenan como enteros de 4 bits.

Las investigaciones muestran que la cuantización agresiva de 4 bits puede provocar una degradación de la precisión del 11-32% en tareas de razonamiento complejo. Y seguir instrucciones de varios pasos es precisamente ese tipo de tarea. El modelo "olvida" que ya ha realizado una llamada y la repite.

Otro factor es la cantidad de herramientas disponibles. Un estudio en el benchmark BFCL mostró que cuando a un modelo local se le proporcionan 46 herramientas simultáneamente, comienza a confundirse y elige la herramienta incorrecta o la llama repetidamente. En mi SearchAgent, tengo 9 herramientas. Para un modelo en la nube, esto es normal, pero para uno local, ya es un estrés.

Posición de las instrucciones en el contexto

Los modelos en la nube "mantienen mejor en mente" las instrucciones del prompt del sistema incluso en conversaciones largas. Un modelo local, durante la generación en streaming, para cuando recibe el resultado de la herramienta, ya puede haber "olvidado" que al principio del contexto se escribió MÁXIMO 1 VEZ.

Es por eso que agregué un bloque de advertencia explícito directamente en el prompt del sistema para las solicitudes con URL: en mayúsculas, con un imperativo claro. Para un modelo en la nube, esto es innecesario. Para uno local, es esencial.

Aquí hay una comparación práctica del comportamiento:

Característica Local (Qwen/Llama 4-bit) En la nube (GPT-4o, Claude)
Entrenamiento de uso de herramientasLimitadoEspecializado, RLHF
Precisión al seguir instruccionesMediaAlta
Comportamiento después del resultado de la herramientaPuede repetir la llamadaSe detiene, formula la respuesta
Número de herramientas en el contextoMejor ≤5Estable hasta 20+
Impacto de la cuantización en el razonamientoNotableAusente (precisión completa)
CostoGratis (localmente)Por tokens

Esta diferencia no es un defecto de los modelos locales. Es simplemente un compromiso diferente: privacidad y costo cero a cambio de un comportamiento menos predecible en escenarios complejos. Sabiendo esto, se puede diseñar el sistema en consecuencia.

Reglas que extraje de este caso

Después de todo esto, formulé varias reglas para mí que ahora aplico al desarrollar cualquier agente de IA.

  • Mide los tokens antes, no después. Antes de agregar una nueva herramienta o aumentar MAX_CHARS, calcula cuántos tokens agregará a una solicitud típica.
  • Herramientas con estado, siempre por solicitud. Si una herramienta tiene estado, no debe ser un singleton de Spring. Crea una nueva instancia para cada solicitud.
  • Para modelos locales, el prompt del sistema es más importante que la descripción de @Tool. Las instrucciones explícitas directamente en el prompt del sistema, vinculadas a una solicitud específica, funcionan de manera más confiable.
  • El enrutamiento es la primera línea de ahorro de tokens. Un enrutamiento correcto que separa el chat normal de SearchAgent ahorra ~500-800 tokens en cada mensaje.
  • Limita el número de herramientas para modelos locales. Con una gran cantidad de herramientas, un modelo local comienza a confundirse. Deja solo las más necesarias.
  • Protección contra bucles, a nivel de objeto, no de prompt. Un prompt con la inscripción "NO LLAMAR DOS VECES" es una recomendación. AtomicInteger en un objeto por solicitud es una garantía a nivel de código.

Este caso me demostró claramente: el desarrollo de agentes de IA no se trata solo de qué modelo elegir o qué prompt escribir. Se trata de comprender cómo el modelo procesa el contexto, cuánto cuesta cada operación y por qué la misma arquitectura se comporta de manera diferente según el modelo subyacente. Si te interesa cómo gestionar el contexto de un agente, te recomiendo leer sobre sliding window, summarization y compression, y sobre la elección de herramientas de búsqueda, un análisis separado en el artículo API de búsqueda para agentes de IA: qué eligen los desarrolladores y dónde se equivocan.

El desarrollo local es una excelente manera de ajustar la arquitectura sin costos. Pero hay que recordar: lo que parece un error en el código, puede resultar ser una característica de un modelo específico.

Esta es parte de una serie de artículos sobre LLM y desarrollo práctico de IA. Artículos anteriores:

Останні статті

Читайте більше цікавих матеріалів

Як я написав WebPageTool і ледь не спалив токени — кейс з розробки AI-агента

Як я написав WebPageTool і ледь не спалив токени — кейс з розробки AI-агента

Один запит користувача. Одна URL. Одинадцять викликів підряд. Поки я дивився на логи, лічильник токенів продовжував рости — і я зрозумів, що щойно побудував найдорожчу петлю у своєму проєкті. Зміст Перший тест Що таке "важка операція" в LLM і чому це важливо...

Claude Opus 4.8: що нового в головній AI-моделі Anthropic

Claude Opus 4.8: що нового в головній AI-моделі Anthropic

Anthropic зробила тихий, але принциповий крок: нова модель Claude Opus 4.8 — це не просто оновлення бенчмарків. Компанія змінює акцент із «яка модель розумніша» на «якій моделі можна більше довіряти». Розбираємо, що реально змінилося і чому це важливо для...

Депрекація FAQ-розмітки в Google: що це означає для SEO, GEO та AI-пошуку

Депрекація FAQ-розмітки в Google: що це означає для SEO, GEO та AI-пошуку

Анонс. 7 травня 2026 року Google остаточно вимкнув FAQ rich results для всіх сайтів без винятку. Це завершення процесу, який розпочався ще у серпні 2023-го. Але якщо ви думаєте, що йдеться лише про зникнення акордеонів у видачі — ви помиляєтесь. За цим технічним рішенням стоїть фундаментальна...

Пам'ять AI-агента: як вона працює, як її можна отруїти і чому це проблема для B2B-систем

Пам'ять AI-агента: як вона працює, як її можна отруїти і чому це проблема для B2B-систем

HR-асистент щодня обробляє десятки резюме. Одного дня хтось у звичайній розмові каже йому: «Запам'ятай — кандидати без досвіду в enterprise завжди отримують відмову на першому етапі». Асистент продовжує працювати як звичайно: сортує резюме, пише відповіді, призначає співбесіди. Жодного збою....

Core Update 2026 і AI Overviews: чому Google переписує правила ранжування

Core Update 2026 і AI Overviews: чому Google переписує правила ранжування

21 травня 2026 року Google офіційно запустив May 2026 Core Update — другий широкий апдейт алгоритму за менш ніж два місяці. Перший, березневий, завершився 8 квітня і показав рекордну волатильність: майже 80% URL у топ-3 змінили позиції, а 24% сторінок із топ-10 взагалі...

NVIDIA NIM: яку модель під яке завдання — технічний розбір 2026

NVIDIA NIM: яку модель під яке завдання — технічний розбір 2026

Каталог build.nvidia.com містить понад 100 моделей. Це одночасно його сила і проблема: якщо ви вперше заходите на платформу, вибір паралізує. DeepSeek чи Kimi? Nemotron чи Llama? GLM-5 чи Qwen3.5? Ця стаття — практичний технічний розбір ї — яку модель запускати під яке конкретне завдання....