El post anterior definió qué es un agente: un LLM controlando su propio flujo mediante tool calling dentro de un loop. Este post abre la caja negra. Muestra exactamente lo que pasa entre un prompt que entra y la respuesta que sale, con foco en el harness (el código no-LLM que hace girar todo). Toda la complejidad real está aquí. Un modelo bueno con un harness malo hace un agente malo. Lo contrario también es cierto, y con más frecuencia de la que se admite.
Vamos a cubrir: la estructura del loop, cómo se arma el prompt en cada turn, cómo se ejecutan las tool calls (serial vs paralela), cómo devolver un error estructurado, cómo gestionar un context que crece, cómo usar prompt caching para no quebrar, cómo persistir el state, cómo decidir el criterio de parada, y cómo instrumentar todo para la observabilidad. Sin recortar.
1. El loop en pseudocódigo, sin adornos
messages = [{role: 'user', content: user_input}]
turn = 0
while turn < MAX_TURNS:
response = llm.create(
model=MODEL,
system=SYSTEM_PROMPT,
messages=messages,
tools=TOOL_DEFINITIONS,
max_tokens=4096,
)
messages.append({role: 'assistant', content: response.content})
if response.stop_reason == 'end_turn':
return response.content
if response.stop_reason == 'tool_use':
tool_results = execute_tools(response.content.tool_uses)
messages.append({role: 'user', content: tool_results})
turn += 1
continue
if response.stop_reason == 'max_tokens':
raise BudgetExceeded()
raise MaxTurnsExceeded()
Ese es el loop completo. Toda implementación de agente, desde Claude Code hasta Cursor o tu MVP, es una elaboración de esto. La diferencia entre un juguete y algo de producción está en los detalles de cada línea. Vamos a abrir cada una.
2. Armado del prompt: qué va en system, qué va en messages
El system prompt es estático a lo largo de la ejecución. En él va: la identidad del agente, las reglas invariantes, el formato de output esperado, la política de seguridad, la descripción del entorno. Todo lo que no cambia entre turns va en system, porque se cachea.
El array de messages crece en cada turn. Estructura típica en formato Anthropic:
[
{ role: 'user', content: 'pedido inicial' },
{ role: 'assistant', content: [
{ type: 'text', text: 'vou consultar X' },
{ type: 'tool_use', id: 'toolu_01', name: 'search', input: {...} }
]},
{ role: 'user', content: [
{ type: 'tool_result', tool_use_id: 'toolu_01', content: '...' }
]},
{ role: 'assistant', content: [
{ type: 'text', text: 'achei. agora vou processar' },
{ type: 'tool_use', id: 'toolu_02', name: 'process', input: {...} }
]},
...
]
Un detalle que muchas implementaciones se equivocan: el tool_result va con role: 'user', no 'tool'. Anthropic no tiene el rol 'tool'. OpenAI sí (role: 'tool'). Mezclarlos rompe. Cada provider tiene su propio wire format, y esconderlo detrás de un adaptador es higiene mínima.
3. Tool definitions: el schema es un contrato
Cada tool se declara con name, description e input_schema (JSON Schema). El LLM lee esto en cada turn (forma parte del prompt enviado, cuesta tokens). Tres reglas prácticas:
- La description es la UI de la tool para el LLM: escríbela como si fuera un docstring leído por un dev junior. Incluye cuándo usarla, cuándo no usarla, un ejemplo de input válido, el formato esperado de output. La diferencia entre una descripción genérica y una descripción rica es la diferencia entre que el LLM acierte el 60% o el 95% de las veces.
- El schema valida en la entrada, siempre: el LLM va a generar input malformado en algún momento. Valida con Zod/Pydantic y devuelve un error estructurado, no una excepción cruda.
- No expongas una tool que se puede componer en el código: si dos tools siempre se llaman en secuencia, haz una sola. Cada tool extra significa un prompt más grande, más latencia, y una oportunidad más de que el LLM se equivoque.
4. Ejecución de tool calls: paralela o serial
Los LLMs modernos pueden emitir múltiples tool calls en un mismo turn (parallel tool use). Anthropic lo soporta desde Claude 3, OpenAI desde GPT-4o. El harness recibe un array y decide cómo ejecutarlo.
Si las tools son independientes (consultar 3 APIs diferentes), ejecútalas en paralelo con Promise.all/asyncio.gather. Ganancia de latencia directa. Si hay dependencia (raro en el mismo turn, porque el LLM ya lo habría pedido en turns separados), ejecútalas en serial. Ante la duda, en paralelo, y diseña las tools para que sean seguras en concurrencia.
async def execute_tools(tool_uses):
results = await asyncio.gather(*[
execute_one(t) for t in tool_uses
], return_exceptions=True)
return [format_result(t, r) for t, r in zip(tool_uses, results)]
return_exceptions=True es crítico. Sin eso, una tool que falla cancela las demás y el LLM pierde el resultado parcial.
5. Error de tool: nunca propaga, siempre devuelve
Un error de tool no es un error de programa. Es una observación que va al LLM para razonar. Formato Anthropic:
{
type: 'tool_result',
tool_use_id: 'toolu_01',
is_error: true,
content: 'API returned 503: service unavailable. Retry recommended.'
}
Con is_error: true, el LLM entiende que la llamada falló y decide: reintentar (mismo input, misma tool), probar un camino alternativo, pedir ayuda al usuario, o desistir. Si levantas una excepción y rompes el loop, perdiste la oportunidad de que el LLM se recupere.
Errores que deben convertirse en excepción (no en tool_result): la tool no existe (el LLM alucinó el nombre), el schema del input es inválido tras la sanitización, una credencial expirada (necesita intervención externa). Todo lo que es estado transitorio del mundo externo se convierte en un tool_result con error.
6. Context management: el problema que mata a los agentes en producción
En cada turn, el context crece. Cada tool result agrega tokens. En 10 turns puedes tener 50k tokens. En 30, 200k. Tres cosas salen mal.
Costo: pagas input tokens en cada llamada. Un loop de 20 turns con 100k tokens promedio cuesta 20x más que una sola llamada de 100k. Sin prompt caching, es la ruina.
Latencia: el time-to-first-token crece con el tamaño del input. El agente se vuelve progresivamente más lento a lo largo del loop.
Context rot: el modelo pierde precisión a medida que el context se vuelve gigante, incluso dentro de la ventana soportada. El rendimiento se degrada mucho antes del límite teórico.
Cuatro técnicas para mitigarlo.
- Prompt caching: el provider cachea el prefijo estable. Anthropic cobra el 10% del precio normal por los tokens cacheados (1h de TTL con cache extended). Coloca el system prompt, las tool defs y los mensajes estables AL INICIO del prompt. Los mensajes dinámicos al final. Un hit rate decente: 90%+. Reduce el costo entre 5x y 10x.
- Summarización: cada N turns, comprime los mensajes antiguos en un resumen. Pierdes detalle, ganas espacio. Usado en Claude Code con compaction automática.
- Sub-agentes: las tareas paralelizables van a un sub-agente con su propio context. El resultado vuelve como una string corta al agente principal. Un patrón usado por Claude con la Task tool, el deep research de OpenAI, y casi todo agent framework moderno.
- External memory: el state que no cabe en el context va a un storage (Redis, base de datos, filesystem). Una tool de read/write expone el acceso. Un patrón usado en agentes long-running.
7. Prompt caching en detalle: la única feature que decide si puedes pagarlo
Sin prompt caching, un agente es económicamente inviable en volumen. Cómo funciona en Anthropic: marcas un breakpoint en el mensaje con cache_control: { type: 'ephemeral' }. Todo lo que está antes del breakpoint se cachea por 5 minutos (default) o 1 hora (extended). La siguiente petición que empiece con el mismo prefijo paga el 10% del precio.
{
system: [{
type: 'text',
text: SYSTEM_PROMPT,
cache_control: { type: 'ephemeral' }
}],
tools: TOOL_DEFINITIONS, // tools entram no cache automaticamente com system
messages: [
...static_messages,
{ ..., cache_control: { type: 'ephemeral' } },
...dynamic_messages
]
}
Estrategia estándar: dos breakpoints. Uno después de system+tools (cachea el loop entero). Otro después del último mensaje assistant estable (cacheado entre turns cercanos). El hit rate sube a 95%+ en loops largos.
8. Stop reason: cómo sabe el loop que puede terminar
Anthropic devuelve stop_reason con valores: end_turn (el LLM terminó de hablar, sin tool call), tool_use (el LLM pidió una tool), max_tokens (reventó el budget), stop_sequence (encontró una sequence definida), refusal (se negó por política).
Lógica del harness:
end_turn: devuelve la respuesta al usuario, cierra el loop.tool_use: ejecuta, devuelve el resultado, continúa el loop.max_tokens: error. Aumentamax_tokensen la siguiente llamada, o desiste con un mensaje al usuario.refusal: error. Muéstralo al usuario, no reintentes.
El criterio de parada duro del harness: MAX_TURNS (generalmente 30 a 50) y MAX_TOTAL_TOKENS (el budget de ejecución). Sin eso, un agente en loop puede costar decenas de dólares por ejecución.
9. State management y durabilidad
Un agente que dura segundos es stateless: el state vive en memoria, muere con el proceso. Un agente que dura minutos u horas (deep research, refactoring de codebase) necesita ser durable: puede crashear a la mitad y retomar.
Estrategias:
- Event log: cada turn (user message, assistant response, tool result) se agrega a un log persistente (base de datos, Kafka, archivo). Un restart reconstruye los messages del log.
- Checkpoint: cada N turns, serializa el array messages entero. Un restart carga el último checkpoint. Más simple, menos granular.
- Workflow engine: Temporal, Inngest, Restate. El agente se convierte en un workflow durable: cada tool call es una actividad, retry y resume son primitivas del framework. Un patrón usado en agentes de producción serios.
10. Streaming: por qué siempre vale la pena
El LLM genera token a token. Sin streaming, esperas la respuesta completa antes de ver nada. La latencia percibida explota. Con streaming, muestras el texto a medida que sale, y detectas tool_use antes del final para empezar a preparar la ejecución.
El streaming de Anthropic usa SSE con eventos tipados: message_start, content_block_start, content_block_delta (deltas de texto), content_block_stop, message_delta, message_stop. El tool use llega con un input_json_delta parcial, agregado en una string que se convierte en JSON al final.
Un harness serio hace streaming por default. Una latencia de 4 segundos se convierte en una latencia percibida de 800ms.
11. Observabilidad: trace, span, métricas
Cada ejecución de agente es un árbol. Raíz: la request inicial. Hijos: cada turn. Nietos: cada tool call dentro del turn. Instrumentación útil:
- Por turn: tokens input/output, latencia, modelo usado, cache hit ratio, stop_reason.
- Por tool call: nombre, input, output truncado, latencia, éxito/fallo, retry count.
- Por ejecución: total de turns, total de tokens, costo en USD, tiempo total, desenlace (éxito/budget/error).
Herramientas: LangSmith, Langfuse, Helicone, Phoenix Arize, o OTEL directo a Datadog/Honeycomb. Build vs buy depende del volumen. Sin un trace, el debug en producción es arqueología en el log.
12. Sub-agentes: cuándo y cómo
Un sub-agente es un agente invocado como tool por el agente padre. El patrón: el padre decide subdividir, llama a spawn_agent(prompt, tools), el sub-agente corre su propio loop con un context aislado, y devuelve una string. El padre consume la string como tool_result.
Ventajas: paralelismo (varios sub-agentes al mismo tiempo), context isolation (el sub no contamina al padre), especialización (un sub con tools restringidas, un system prompt enfocado). Costo: cada sub-agente es un loop completo con su propio overhead.
Anti-pattern: una cadena profunda de sub-agentes (el padre llama al hijo que llama al nieto que llama al bisnieto). La latencia se apila, el debug se vuelve imposible. Mantén 2 niveles como máximo, salvo en un caso muy justificado.
13. Estructuras que importan: ReAct, Reflexion, Plan-and-Execute
Tres variaciones del loop básico que aparecen en producción.
ReAct (Reasoning + Acting): el loop estándar descrito arriba. El LLM alterna entre razonar (texto) y actuar (tool). Es el default.
Reflexion: después de cada acción, el LLM evalúa el resultado y genera una 'reflexión' que va a la memoria. Las siguientes iteraciones consultan las reflexiones para evitar repetir un error. Útil en tareas de ensayo y error (debug, exploit, optimización).
Plan-and-Execute: se separa en dos fases. Fase 1 (una llamada): el LLM genera un plan en un formato estructurado. Fase 2 (loop): el ejecutor sigue el plan paso a paso, con el LLM revisando si el paso funcionó. Replanifica si es necesario. Menos llamadas que ReAct puro para tareas con un plan claro.
14. Lo que va a salir mal en producción
Una lista honesta de los bugs que aparecen temprano:
- El LLM alucina un tool name que no existe. El harness debe devolver un error estructurado y registrarlo.
- El LLM pasa un input que falla el schema. Valida, y devuelve con un mensaje claro de qué está mal.
- Una tool tiene un side effect y el LLM reintenta tras un timeout, duplicando la acción. Una idempotency key es obligatoria.
- Un loop infinito cuando el LLM insiste en reintentar una tool que falla de forma determinista. Detecta la repetición y rómpela.
- Context overflow en medio de una ejecución. Compacta proactivamente en base al token count, no solo cuando explota.
- Un cache miss porque algún mensaje cambió de orden o de contenido entre turns. Audítalo con una métrica de hit rate.
- Una tool con un timeout default infinito congela todo el agente. Siempre setea un timeout explícito.
- Un error de red en el streaming abandona la respuesta parcial. Implementa resume o reintenta todo.
15. Lo que el harness pequeño NO necesita hacer
Para cerrar con calibración: para empezar, no necesitas un framework. anthropic-sdk + 200 líneas de código resuelven el 80% de los casos. Frameworks como LangGraph, LlamaIndex, CrewAI aportan valor cuando tienes escala (>100 agentes, multi-tenant) o cuando necesitas durabilidad seria.
Todo este post describe lo que implementas desde cero. Entender esta capa es un prerrequisito para elegir un framework con criterio, en vez de adoptar uno y descubrir 6 meses después que estás atrapado en una abstracción que no cabe en tu caso. El harness es tu punto de leverage. Trátalo como infraestructura crítica.