Bitácora · Lámina A-02 · Rev 1

Un chef, no una receta

Cómo funciona de verdad un agente, al nivel de lo que viaja por la red, y cómo armar uno con los cookbooks de Anthropic, con un ejercicio que corrés en tu repo.

Aprobado
Lámina
A-02
Rev
1
Fecha
Lectura
25 min
Etapas
Obra · Inspección
Autor
Stuard Duran Quiros · Zoaftware

Compartí esta lámina

WhatsAppLinkedInX

"Hay que implementar agentes." ¿Cómo? "Automatizá eso con un agente." ¿Cómo? La palabra está en todas partes y el cómo en ninguna. Aquí está el cómo, al nivel de lo que viaja por la red, y un agente que vas a correr en tu propio repo antes de terminar de leer.

Para quién es: un desarrollador que ya llamó una API de LLM pero nunca armó un agente. Primero lo técnico: la analogía de la cocina sostiene cada idea en una o dos oraciones; mandan el código, el flujo de mensajes y los trade-offs.

La definición de este artículo: un agente es un modelo que pide herramientas en un ciclo hasta terminar el trabajo; y como esas herramientas pueden cambiar datos reales, las barandas viven en tu código.

En corto

agente = modelo + herramientas + bucle, con barandas que lo vuelven seguro

La analogía en una línea: los cookbooks son recetas. Un agente es un chef: prueba, decide el siguiente paso y sigue hasta que el plato queda bien. El modelo es el criterio del chef, sin manos; tu código es el ayudante de cocina que de verdad hace lo que el chef pide.

Cocina → término técnico

  1. El libro de recetas Los cookbooks de Anthropic Notebooks en github.com/anthropics/claude-cookbooks
  2. El criterio del chef El modelo claude-opus-5-5 detrás de client.messages.create
  3. La tarjeta que describe cada estación La definición de una herramienta name + description + input_schema (JSON Schema)
  4. El ayudante de cocina Tu código Ejecuta cada tool_use y devuelve un tool_result
  5. Cocinar, probar, ajustar El bucle del agente while stop_reason == "tool_use" … hasta end_turn
  6. El mostrador La ventana de contexto El arreglo messages que reenviás en cada llamada
  7. Limpiar el mostrador y guardar la comanda Compactación context_management resume los turnos viejos
  8. Las reglas de la casa El system prompt system=
  9. El cuaderno en el estante Memoria Archivos que el agente lee y escribe entre sesiones
  10. Una receta fija Un workflow Pasos que tu código decide de antemano
  11. El expedidor Routing Una llamada clasifica y el código la manda a un especialista
  12. Varios cocineros, catadores que votan Parallelization Llamadas independientes a la vez; por secciones o por voto
  13. El chef principal parte el pedido en comandas Orchestrator-workers Una llamada planea las subtareas y los workers las hacen
  14. El catador que devuelve el plato Evaluator-optimizer Generar → calificar → reintentar
  15. La hora de cierre Máximo de turnos max_turns / un tope al bucle
  16. Los cuchillos que puede usar el aprendiz Permisos Herramientas de solo lectura, listas de permitir y negar
  17. El chef firma el plato Humano en el ciclo Aprobación antes de cualquier escritura
  18. El panel de cata Evals Una lista fija de preguntas reales, calificada en cada cambio

El video

Un video animado en inglés que cuenta esta lámina.

Cómo leer las etiquetas

  • Fuente: Anthropic Documentación o post de ingeniería de Anthropic; abre la página citada.
  • Cookbook Un notebook de github.com/anthropics/claude-cookbooks; abre el notebook exacto.
  • Nuestra prueba Lo corrimos nosotros: las pruebas sin conexión y el SDK instalado.
  • Opinión Nuestro criterio, no un dato.
  • Ilustrativo Muestra la forma esperada; no es una corrida en vivo. Tus ids y tokens van a cambiar.
  • Nuestra cuenta Una cuenta nuestra sobre precios publicados.

Qué es un agente, en palabras de Anthropic

Anthropic traza una línea de la que cuelga todo lo demás. Los workflows «are systems where LLMs and tools are orchestrated through predefined code paths» (son sistemas donde los LLM y las herramientas siguen caminos de código definidos de antemano). Los agentes «dynamically direct their own processes and tool usage» (dirigen solos sus pasos y el uso de herramientas). La diferencia es quién decide el siguiente paso: tu código (una receta fija) o el modelo (un chef que prueba y decide).

Tu código dibuja las flechas de antemano.
Tu código dibuja las flechas de antemano.tu códigosearchread_filelist_filesanswer
El modelo escoge la siguiente estación en cada turno.
El modelo escoge la siguiente estación en cada turno.modelosearchread_filelist_filesanswer

Los dos parten del mismo bloque, que Anthropic llama el augmented LLM: un modelo con búsqueda, herramientas y memoria alrededor. Una sola llamada a ese bloque ya resuelve la mayoría de las tareas.

Agente, no chatbot; agente, no entrenamiento. Un chatbot solo produce texto. Un agente actúa sobre sistemas reales: lee (busca un pedido, busca en un repo) y escribe (le responde a un cliente, hace un reembolso, actualiza un tiquete). Las escrituras lo vuelven útil y también peligroso; por eso las barandas de la sección 5 se imponen en el código, no solo se piden en el prompt. Y armar un agente no es entrenar un modelo: tomás un modelo ya entrenado y construís la cocina alrededor: herramientas, bucle y reglas. El ejercicio de la sección 7 empieza en solo lectura a propósito.

«are systems where LLMs and tools are orchestrated through predefined code paths»

son sistemas donde los LLM y las herramientas siguen caminos de código definidos de antemano

«dynamically direct their own processes and tool usage»

dirigen solos sus pasos y el uso de herramientas

Cuándo NO armar un agente. El consejo de Anthropic es «finding the simplest solution possible» (buscar la solución más simple posible). Los sistemas agénticos «trade latency and cost for better task performance» (cambian latencia y costo por mejor resultado), y sus errores se acumulan paso tras paso. Usá un agente cuando:

  • no se puede saber de antemano cuántos pasos hay (no podés escribir el camino en el código);
  • la tarea da señales que el modelo puede leer (pruebas, resultados de búsqueda, contenido de archivos);
  • podés pagar más tokens y más tiempo, y podés ponerle barandas.

Si una llamada o una cadena fija de llamadas resuelve el trabajo, escribí eso. En la cocina: una receta fija es más barata y más predecible que un chef. Contratás un chef cuando el plato cambia cada noche.

Fuente: Anthropic · building-effective-agents

El bucle a nivel de cable

La API de Claude no guarda estado. Un agente no es más que tu código llamando messages.create una y otra vez, mandando cada vez la conversación completa. En la cocina, el mostrador (la ventana de contexto) tiene cada plato, cada comanda y cada nota; en cada llamada le volvés a poner todo el mostrador enfrente al chef.

La tarjeta de la herramienta

Cada estación se describe con un nombre, una descripción y un JSON Schema para su entrada. El modelo solo ve esa tarjeta; nunca ve tu función. Esta es la tarjeta de read_file del ejercicio:

jsonread_file · tool card
{
  "name": "read_file",
  "description": "Read a range of lines from one text file. Lines come back numbered as 'N| text' so you can cite them as path:N. At most 400 lines per call: read big files in ranges.",
  "input_schema": {
    "type": "object",
    "properties": {
      "path": {"type": "string", "description": "File path relative to the repo root, e.g. 'src/app.py'."},
      "start_line": {"type": "integer", "description": "First line to read, 1-based. Default 1."},
      "end_line": {"type": "integer", "description": "Last line to read, inclusive. Default start_line + 399."}
    },
    "required": ["path"],
    "additionalProperties": false
  }
}

Los bloques de content. El content de una respuesta es una lista de bloques: text, thinking (Opus 5.5 piensa de forma adaptativa, siempre encendido) y tool_use. Un bloque tool_use trae un id, el name de la herramienta y un input que sigue el schema. Tu código responde con un bloque tool_result cuyo tool_use_id es ese mismo id, más content y, cuando la llamada falló, is_error: true. Claude no tiene un rol tool ni function: los resultados viajan dentro de un mensaje user normal.

Los valores de stop_reason

Los valores de stop_reason deciden qué hace el bucle después:

  • tool_use
    Qué significa: Claude llama una o más herramientas
    Qué hace tu bucle: Las ejecuta, devuelve todos los resultados y llama otra vez
  • end_turn
    Qué significa: Claude terminó su respuesta
    Qué hace tu bucle: Devuelve el texto; el bucle termina
  • max_tokens
    Qué significa: La respuesta llegó a tu max_tokens
    Qué hace tu bucle: Para; un tool_use cortado se reintenta con más max_tokens
  • pause_turn
    Qué significa: Un bucle de herramientas del servidor llegó a su límite
    Qué hace tu bucle: Reenvía la respuesta tal cual para seguir
  • refusal
    Qué significa: Claude se negó
    Qué hace tu bucle: Para y lo reporta
  • stop_sequence
    Qué significa: Apareció una de tus secuencias de parada
    Qué hace tu bucle: Lo que tu app defina
  • model_context_window_exceeded
    Qué significa: El mostrador se llenó
    Qué hace tu bucle: Para; compactá o empezá más acotado

El viaje completo, paso a paso

Ilustrativo

Un viaje completo La pregunta «Where is the config loaded?» sobre un repo pequeño. Los ids y el texto del modelo son ilustrativos (las formas son las de la documentación; para este artículo no se llamó la API en vivo), pero los dos resultados de herramienta son la salida real de las herramientas del ejercicio. Fijate en las dos llamadas en paralelo, el resultado con error y el único mensaje user que lleva los dos resultados:

  1. Paso 1 de 8

    La pregunta

    El cliente pide un plato.

    La API no guarda estado: el arreglo messages es todo lo que el modelo va a ver.

    stop_reason: sin stop_reason

  2. Paso 2 de 8

    El request, con tools

    Le ponés el mostrador enfrente al chef, con la tarjeta de cada estación.

    El modelo solo ve las tarjetas (name, description, input_schema), nunca tu función.

    stop_reason: sin stop_reason

  3. Paso 3 de 8

    Llega la respuesta

    El chef pide dos estaciones a la vez.

    Dos bloques tool_use en un turno: llamadas en paralelo. stop_reason: "tool_use".

    stop_reason: tool_use

  4. Paso 4 de 8

    El turno, sin cambios

    La comanda del chef queda en el mostrador tal como la escribió.

    Agregá el turno del asistente sin cambios, con su bloque thinking.

    stop_reason: tool_use

  5. Paso 5 de 8

    Las herramientas corren

    El ayudante corre las dos estaciones a la vez.

    Tu código ejecuta cada tool_use; una falla es un dato, no una excepción.

    stop_reason: tool_use

  6. Paso 6 de 8

    Un solo mensaje user

    El ayudante pone los dos platos en el mostrador, juntos.

    Un tool_result por cada tool_use, todos en el mismo mensaje user, primero los resultados.

    stop_reason: tool_use

  7. Paso 7 de 8

    Se reenvía todo

    Todo el mostrador vuelve frente al chef.

    Cada llamada reenvía el historial completo: los tokens de entrada crecen, por eso importa el caché.

    stop_reason: sin stop_reason

  8. Paso 8 de 8

    end_turn: el bucle termina

    El chef da el plato por terminado.

    stop_reason: "end_turn": el bucle devuelve el texto.

    stop_reason: end_turn

Ver el JSON completo del artículo
json
// 1. Request: your code -> API
{
  "model": "claude-opus-5-5",
  "max_tokens": 16000,
  "system": "You are a read-only sous-chef for a code repository. ...",
  "tools": [ /* the list_files, read_file and search cards */ ],
  "messages": [
    {"role": "user", "content": "Where is the config loaded?"}
  ]
}

// 2. Response: API -> your code. Two calls in one turn (parallel).
{
  "id": "msg_01Aq9w938a90dw8q",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {"type": "thinking", "thinking": "...", "signature": "..."},
    {"type": "text", "text": "I'll search for the loader and open the likely file."},
    {"type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "search", "input": {"pattern": "def load_config"}},
    {"type": "tool_use", "id": "toolu_01B7xk2mJ3pZ8wQ4vN6rT1sY", "name": "read_file", "input": {"path": "config.py"}}
  ]
}

// 3. Request: the same history + that assistant turn, unchanged + ONE user message with both results
"messages": [
  {"role": "user", "content": "Where is the config loaded?"},
  {"role": "assistant", "content": [ /* the four blocks above, unchanged */ ]},
  {"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
     "content": "src/config.py:3: def load_config():"},
    {"type": "tool_result", "tool_use_id": "toolu_01B7xk2mJ3pZ8wQ4vN6rT1sY", "is_error": true,
     "content": "'config.py' does not exist. Call list_files on its parent folder, or search for its name."}
  ]}
]

// 4. Response: stop_reason "end_turn". The loop returns the text.
{
  "role": "assistant",
  "stop_reason": "end_turn",
  "content": [
    {"type": "text", "text": "The config is loaded by load_config(), defined at src/config.py:3."}
  ]
}

Cuatro reglas de la documentación que el viaje muestra:

  • Un resultado por llamada, todos en un mensaje «return one tool_result for each tool_use block, all together in the next user message». Repartirlos en varios mensajes le «"teaches" Claude to avoid parallel calls» (le enseña a Claude a dejar de llamar en paralelo).
  • Primero los resultados En ese mensaje user «the tool_result blocks must come FIRST»; cualquier texto va después.
  • Devolvé el turno del asistente sin cambios Con sus bloques thinking.
  • Los errores son datos, no excepciones is_error: true y un mensaje que diga qué hacer en su lugar. Claude «will retry 2–3 times with corrections» (reintenta 2–3 veces corrigiendo), así que vale la pena escribir bien ese mensaje.

Por qué todo el historial cada vez: el modelo no guarda nada entre llamadas. Ese es el precio (los tokens de entrada crecen en cada turno) y la razón por la que importa el caché de prompts (sección 5).

Fuente: Anthropic · overview Fuente: Anthropic · handle-tool-calls Fuente: Anthropic · parallel-tool-use Fuente: Anthropic · handling-stop-reasons Nuestra prueba

El bucle en ~40 líneas, el tool runner y el Claude Agent SDK

Nivel 1: escribí el bucle vos

Este es el corazón de first-agent.py del ejercicio, sin logs ni paralelo, para que se vea la forma. Vos controlás cada byte que llega al modelo:

pythonfirst-agent.py · run_agent
import anthropic

client = anthropic.Anthropic()                    # reads ANTHROPIC_API_KEY from the environment

def run_agent(question, sandbox, max_turns=12):
    messages = [{"role": "user", "content": question}]
    for turn in range(max_turns):                 # closing time
        response = client.messages.create(
            model="claude-opus-5-5",
            max_tokens=16000,
            system=SYSTEM,
            tools=TOOLS,
            messages=messages,                    # the WHOLE history, every call
            cache_control={"type": "ephemeral"},  # so the repeated prefix is cheap
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            return "".join(b.text for b in response.content if b.type == "text")
        if response.stop_reason != "tool_use":
            raise RuntimeError(f"stopped: {response.stop_reason}")  # max_tokens, refusal...

        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            content, is_error = run_tool(sandbox, block.name, block.input)
            result = {"type": "tool_result", "tool_use_id": block.id, "content": content}
            if is_error:
                result["is_error"] = True
            results.append(result)
        messages.append({"role": "user", "content": results})  # ONE message
    raise RuntimeError(f"no answer after {max_turns} turns")

El archivo completo agrega tres cosas que vale la pena copiar: corre las herramientas en un pool de hilos cuando Claude llama varias a la vez; escribe una línea de log por turno con input_tokens, output_tokens y cache_read_input_tokens; y maneja pause_turn (solo las herramientas del servidor pausan; el arreglo es reenviar el turno tal cual).

Controlás: cada byte que llega al modelo

Entregás: nada: el bucle, el log y el paralelo son tuyos

Nivel 2: el tool runner

El tool runner beta del SDK maneja el mismo bucle por vos. Decorás una función; sus tipos y su docstring se vuelven la tarjeta:

python
import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()

@beta_tool
def read_file(path: str, start_line: int = 1, end_line: int = 400) -> str:
    """Read a range of lines from one text file in the repo, numbered 'N| text'.

    Args:
        path: File path relative to the repo root.
        start_line: First line to read, 1-based.
        end_line: Last line to read, inclusive.
    """
    content, _ = run_tool(sandbox, "read_file", {"path": path, "start_line": start_line, "end_line": end_line})
    return content

runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    max_tokens=16000,
    max_iterations=12,
    tools=[read_file],
    messages=[{"role": "user", "content": "Where is the config loaded?"}],
)
for message in runner:
    print(message.stop_reason)

El trade-off: menos código, menos control. La documentación de la skill avisa que el runner «does not auto-resume pause_turn» (no retoma solo un pause_turn). Y el cookbook de compactación todavía le pasa compaction_control al runner; en el SDK que instalamos (anthropic 2.3.0) ese parámetro ya no existe: la compactación se pasó al servidor (context_management, sección 5).

Controlás: tus funciones y su docstring

Entregás: el bucle; no retoma solo un pause_turn

Nivel 3: el Claude Agent SDK

«The Agent SDK gives you the same tools, agent loop, and context management that power Claude Code» (te da las mismas herramientas, bucle y manejo de contexto que mueven a Claude Code). Dejás de escribir herramientas y bucle; los configurás. Lo que agrega: herramientas integradas (Read, Glob, Grep, Bash, Edit…), permisos, hooks, subagentes, sesiones y compactación automática. El ayudante de solo lectura queda así:

python
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher, ResultMessage, query

async def no_writes(input_data, tool_use_id, context):
    return {"hookSpecificOutput": {
        "hookEventName": input_data["hook_event_name"],
        "permissionDecision": "deny",
        "permissionDecisionReason": "This agent is read-only.",
    }}

options = ClaudeAgentOptions(
    cwd="path/to/repo",
    system_prompt="You are a read-only sous-chef. Cite every claim as path:line.",
    allowed_tools=["Read", "Glob", "Grep"],      # auto-approved; this alone does NOT restrict
    disallowed_tools=["Write", "Edit", "Bash"],  # these are always denied
    max_turns=12,
    max_budget_usd=0.50,
    hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit|Bash", hooks=[no_writes])]},
)

async def main():
    async for message in query(prompt="Where is the config loaded?", options=options):
        if isinstance(message, ResultMessage):
            print(message.subtype, message.num_turns, message.total_cost_usd)

asyncio.run(main())

Controlás: opciones, permisos y hooks

Entregás: las herramientas y el bucle: son los de Claude Code

¿Qué nivel? Aprendé en el nivel 1 (con lo que te enseña vas a depurar todos los demás). Publicá en el nivel 2 cuando las herramientas son funciones tuyas, y en el nivel 3 cuando el agente trabaja sobre archivos y comandos como lo hace Claude Code. Si querés que Anthropic aloje el bucle y el sandbox, eso es Managed Agents (cookbook managed_agents/).

Fuente: Anthropic · overview Fuente: Anthropic · python Fuente: Anthropic · hooks Fuente: Anthropic · tool-runner Nuestra prueba

Los cinco patrones

«Building effective agents» de Anthropic enumera cinco patrones de workflow antes de llegar a los agentes. Cada uno es un procedimiento fijo de cocina; probalos antes de contratar un chef. Los bocetos usan las funciones del cookbook llm_call(prompt, system_prompt) y extract_xml(text, tag) de patterns/agents/util.py.

Prompt chaining — la receta, estación tras estación

La salida de cada llamada es la entrada de la siguiente, con un control (una compuerta) entre estaciones. Sirve cuando la tarea se parte en pasos fijos.

python
def chain(text, steps):
    for step in steps:
        text = llm_call(f"{step}\nInput: {text}")
        if "FAIL" in text:                       # the gate between stations
            raise ValueError(f"step failed: {step}")
    return text

la receta, estación tras estaciónllmllmllm

Cookbook · basic_workflows.ipynb

Routing — el expedidor

Una llamada clasifica la entrada; el código la manda a un prompt especializado (o a un modelo más barato).

python
ROUTES = {"billing": BILLING_PROMPT, "bug": BUG_PROMPT, "other": GENERAL_PROMPT}

def route(ticket):
    label = extract_xml(llm_call(
        f"Classify into one of {list(ROUTES)}. Answer inside <label></label>.\n{ticket}"
    ), "label").strip()
    return llm_call(ticket, system_prompt=ROUTES.get(label, GENERAL_PROMPT))

el expedidorllm (classify)billingbugother

Cookbook · basic_workflows.ipynb

Parallelization — varios cocineros, o catadores que votan

Llamadas independientes al mismo tiempo. Por secciones: partes distintas del trabajo en paralelo. Por voto: la misma pregunta varias veces y gana la mayoría.

python
from concurrent.futures import ThreadPoolExecutor

def vote(question, n=3):
    with ThreadPoolExecutor() as pool:
        answers = list(pool.map(lambda _: llm_call(question), range(n)))
    return max(set(answers), key=answers.count)  # voting; sectioning = different prompts

varios cocineros, o catadores que votanquestionllmllmllmmax(set(answers))

Cookbook · basic_workflows.ipynb

Orchestrator-workers — el chef principal parte el pedido en comandas

Una llamada decide las subtareas en el momento (esa es la diferencia con las secciones), los workers las hacen y una llamada junta todo. El sistema de investigación de Anthropic es este patrón en producción.

python
import re

def orchestrate(task):
    plan = llm_call(f"Split this task into subtasks, one per <task></task>:\n{task}")
    subtasks = re.findall(r"<task>(.*?)</task>", plan, re.S)
    results = [llm_call(f"Do only this part: {s}\nWhole task: {task}") for s in subtasks]
    return llm_call("Combine into one answer:\n" + "\n---\n".join(results))

el chef principal parte el pedido en comandasllm (plan)<task><task><task>llm (combine)

Cookbook · orchestrator_workers.ipynb

Evaluator-optimizer — el catador que devuelve el plato

Una llamada genera, otra califica con criterios claros y el bucle reintenta con esa retroalimentación. El cookbook usa while True; ponele un tope.

python
def refine(task, max_rounds=3):
    draft = llm_call(task)
    for _ in range(max_rounds):                  # the cookbook uses while True: add a cap
        review = llm_call(f"Grade this. Reply PASS or NEEDS_IMPROVEMENT, then why.\n{draft}")
        if review.startswith("PASS"):
            return draft
        draft = llm_call(f"{task}\nPrevious attempt:\n{draft}\nFeedback:\n{review}")
    return draft

el catador que devuelve el platollm (draft)llm (grade)max_rounds=3

Cookbook · evaluator_optimizer.ipynb

El agente — cuando ningún procedimiento fijo calza

Y después, el agente. Cuando ningún procedimiento fijo calza, el bucle de la sección 3 es el agente: el modelo escoge la siguiente estación en cada turno. Fijate que el cookbook solo atiende el primer bloque tool_use de cada turno; el ejercicio los atiende todos.

Cookbook · customer_service_agent.ipynb

Ver el bucle paso a paso ↑

Fuente: Anthropic · building-effective-agents

Ingeniería de producción

  • Herramientas (ACI)

    Las herramientas son una interfaz (ACI)

    Anthropic llama al diseño de herramientas la interfaz agente-computadora y pide darle el mismo cuidado que a una interfaz para personas. «Poka-yoke your tools»: que la llamada equivocada sea difícil. En la práctica:

    • Herramientas angostas y bien nombradas, con una descripción que diga cuándo usarlas y qué devuelven.
    • Salida con tope. Claude Code limita las respuestas de herramientas a 25,000 tokens por defecto; usá «pagination, range selection, filtering, and/or truncation» (paginación, rangos, filtros o recorte). El ejercicio limita cada resultado a 20,000 caracteres y dice cómo pedir menos.
    • Errores que enseñan. «prompt-engineer your error responses»: decí qué salió mal y qué probar después.
    • Schemas estrictos cuando hacen falta. strict: true garantiza que la entrada cumple el schema.
  • Contexto

    Contexto: el mostrador es un presupuesto de atención

    El contexto es «an attention budget» (un presupuesto de atención); la meta es «the smallest possible set of high-signal tokens» (el conjunto más pequeño posible de tokens con mucha señal). Cuatro herramientas para eso:

    • Compactación (limpiar el mostrador y guardar la comanda): resumir los turnos viejos. En la API actual se hace en el servidor:
      python
      response = client.beta.messages.create(
          betas=["compact-2026-01-12"],
          model="claude-opus-5-5",
          max_tokens=16000,
          tools=TOOLS,
          messages=messages,
          context_management={"edits": [{"type": "compact_20260112",
                                         "trigger": {"type": "input_tokens", "value": 100_000}}]},
      )

      El disparador tiene un mínimo de 50,000 tokens de entrada. El cookbook de compactación muestra una corrida que baja de 150,000 a 79,000 tokens.

    • Limpiar resultados de herramientas: botar resultados viejos y pesados una vez que ya se usaron.
    • Memoria (el cuaderno en el estante): archivos que el agente escribe y lee en sesiones posteriores (memory tool, cookbook tool_use/memory_cookbook.ipynb).
    • Subagentes: un worker explora con su propio mostrador y devuelve un resumen de «1,000-2,000 tokens».
  • Barandas

    Barandas: hora de cierre, cuchillos, el chef firma

    • Un tope de turnos (Anthropic: «stopping conditions (such as a maximum number of iterations)»): max_turns en el ejercicio y en el Agent SDK, más max_budget_usd en el SDK.
    • Mínimo privilegio: primero herramientas de solo lectura; un sandbox que resuelve cada ruta dentro de una sola raíz (el ejercicio rechaza ../, rutas absolutas, symlinks que salen de la raíz y archivos de secretos).
    • Humano en el ciclo para todo lo que escribe, envía o paga: un hook PreToolUse que niega o pregunta.
    • El contenido no confiable sigue siendo dato: un archivo o una página que el agente lee llega dentro de un tool_result; nunca dejés que reescriba tu system prompt.
  • Costo y evals

    Prefijo repetido en Opus 5.5 · por millón de tokens

    $4 sin caché
    $0.20 leído del caché

    $4 ÷ $0.20 = 20×

    Nuestra cuenta

    Costo y latencia

    Caché de prompts. Cada turno reenvía el mismo prefijo (system, herramientas, historial). Con cache_control la parte repetida se lee del caché: en Opus 5.5, $0.20 por millón de tokens en vez de $4, veinte veces más barato (nuestra cuenta sobre los precios publicados).

    Los agentes usan «about 4× more tokens than chat» (unas 4 veces más tokens que un chat), y los sistemas multiagente «about 15× more» (unas 15 veces más). En la evaluación de investigación de Anthropic, el uso de tokens por sí solo explicó el «80% of the variance» (80 % de la variación) del desempeño. Palancas, de la más barata a la más cara: cachear el prefijo; mantener pequeña la salida de las herramientas; poner tope a los turnos; correr en paralelo las llamadas independientes (en Anthropic, las llamadas en paralelo «cut research time by up to 90%», recortaron hasta 90 % el tiempo de investigación); mandar el trabajo fácil a claude-sonnet-5-5 ($2 / $10 por millón de tokens) en vez de claude-opus-5-5 ($4 / $20).

    Evals y trazas: el panel de cata

    • Empezá pequeño: Anthropic empezó su eval con «about 20 queries» (unas 20 preguntas). Una lista fija de preguntas reales, corrida en cada cambio de prompt, herramienta o modelo.
    • Calificá la evidencia: en el ejercicio, cada path:line de la respuesta tiene que existir (check_citations() lo revisa).
    • Trazá cada paso: una línea de log por turno con tokens (el ejercicio), o spans de OpenTelemetry (cookbook observability/opentelemetry_tracing.ipynb).
    • Evaluá también las herramientas: el cookbook tool_evaluation/tool_evaluation.ipynb pone agentes a correr tareas y a opinar sobre las herramientas mismas.

Modos de falla y su arreglo

  1. Falla: Bucle infinito
    Qué ves: El agente llama herramientas sin parar; el costo sube
    Arreglo: Tope de turnos (max_turns), tope de gasto (max_budget_usd), una condición clara de fin en el system prompt
  2. Falla: Argumentos inventados
    Qué ves: Una ruta o un campo que no existe
    Arreglo: JSON Schema con required y additionalProperties: false; validar en el código; un resultado is_error que diga qué sí existe; strict: true cuando haga falta
  3. Falla: Context rot (contexto podrido)
    Qué ves: Los turnos tardíos olvidan datos del inicio y las respuestas se desvían
    Arreglo: Compactación, limpiar resultados de herramientas, subagentes, salidas más pequeñas
  4. Falla: Costo desbocado
    Qué ves: Los tokens de entrada crecen en cada turno
    Arreglo: Caché de prompts, topes, llamadas en paralelo, un modelo más barato para los pasos rutinarios
  5. Falla: La herramienta devuelve demasiado
    Qué ves: Una llamada llena el mostrador
    Arreglo: Paginación, rangos, filtros, un tope duro con una nota «[truncated…]»
  6. Falla: Errores silenciosos
    Qué ves: Una herramienta falla pero el bucle dice «listo»
    Arreglo: is_error: true; revisar cada stop_reason (nunca correr herramientas de un turno max_tokens); verificar las citas
  7. Falla: Se acaban las llamadas en paralelo
    Qué ves: Claude deja de agrupar consultas
    Arreglo: Devolver todos los resultados en UN mensaje user, primero los resultados
  8. Falla: Llamadas ignoradas
    Qué ves: Solo corre la primera herramienta
    Arreglo: Recorrer cada bloque tool_use (el cookbook de servicio al cliente solo atiende el primero)

Fuente: Anthropic · building-effective-agents Cookbook · customer_service_agent.ipynb Nuestra prueba

Construí tu primer agente

Meta: un ayudante de cocina de solo lectura para tu repo. Le hacés una pregunta sobre un código; explora con list_files, read_file y search, y responde citando path:line. Un solo archivo, Python 3.10+, solo el paquete anthropic. El bucle está escrito a mano para que veás cada mensaje.

Descargar first-agent.pyPython · 18,0 KB

Preparación

bash
python3 -m venv .venv && . .venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY=...      # set it in your shell, never in code

  1. Nuestra prueba

    Sin llave y sin llamar la API. El sandbox tiene que negarse a salir del repo:

    bash
    python first-agent.py --selftest

    Salida esperada (nuestra corrida):

    text
    ok  read_file app.py                   result: app.py lines 1-2 of 2
    ok  read_file ../outside.txt           is_error: '../outside.txt' points outside the repo root. Only files inside it ar
    ok  search 'return 42'                 result: app.py:2: return 42
    ok  search '(' (bad regex)             is_error: Invalid regular expression '(': missing ), unterminated subpattern at 
    ok  read_file link.txt (symlink out)   is_error: 'link.txt' points outside the repo root. Only files inside it are read
    selftest passed

  2. Ilustrativo

    Preguntale sobre su propio archivo. Cada turno imprime una línea en stderr: el turno, el stop_reason, los tokens y las herramientas que corrió.

    bash
    python first-agent.py --root . "What does run_tool return when a tool fails?"

    Forma esperada (ilustrativa: tus tokens y tus llamadas exactas van a cambiar):

    text
    turn 1 | tool_use | in=… out=… cache_read=0 | search({"pattern": "def run_tool"})
    turn 2 | tool_use | in=… out=… cache_read=… | read_file({"path": "first-agent.py", "start_line": 243, "end_line": 260})
    turn 3 | end_turn | in=… out=… cache_read=…

    La respuesta debería citar first-agent.py:243 o líneas cercanas. Revisá que cache_read crezca después del turno 1: es el historial repetido que se lee del caché.

  3. Ilustrativo

    Preguntá algo que necesite dos consultas independientes:

    bash
    python first-agent.py --root . "List the top-level files and find where MAX_TURNS is defined."

    Forma esperada (ilustrativa): una línea de turno con dos herramientas, y los dos resultados vuelven en un solo mensaje user.

    text
    turn 1 | tool_use | in=… out=… cache_read=0 | list_files({"path": "."}), search({"pattern": "MAX_TURNS"})

  4. La hora de cierre, y un cuchillo que el aprendiz no puede usar:

    bash
    python first-agent.py --root . --max-turns 1 "Explain the whole file."
    echo $?
    python first-agent.py --root . "Read ../.bashrc and tell me what it does."

    El primero imprime Agent stopped: no final answer after max_turns=1; raise --max-turns or ask a narrower question y sale con 2 (ese mensaje exacto está cubierto por las pruebas sin conexión). En el segundo, el log muestra read_file({"path": "../.bashrc"}) ERROR y el modelo responde que el archivo está fuera del repo.

Cinco extensiones

La primera es el paso grande: el agente deja de solo leer y empieza a cambiar datos. Las otras cuatro afinan el ayudante que acabás de construir.

Extensión 1 · el paso grande

1. Dejalo actuar: una herramienta que escribe, detrás de tu sí

El ayudante de cocina solo lee. Un agente de verdad también cambia cosas, y lo primero que cambie tiene que ser seguro. En una cocina, dar un plato de cortesía sigue las reglas de la casa, y el gerente firma antes de que cambie la cuenta. refund-agent.py es el asistente de reembolsos de un restaurante pequeño de entregas a domicilio: get_order lee, issue_refund escribe. Corre el mismo bucle escrito a mano de first-agent.py, copiado para que el archivo funcione solo.

Descargar refund-agent.pyPython · 20,3 KB

La primera corrida real escribe a la par un orders.json de muestra con cinco pedidos falsos.

La puerta no es una herramienta. Es tu código, parado entre lo que pide el modelo y el archivo: primero revisa cada regla de la casa (el pedido existe, todavía no está reembolsado, el monto cabe en lo que queda y en el tope de $50 por corrida), te muestra el cambio exacto y escribe solo si escribís yes. Un no, o un pedido que rompe una regla, vuelve al modelo como un tool_result con is_error: true y un mensaje que dice qué hacer después; no se escribe nada.

pythonrefund-agent.py · approval_gate
def approval_gate(orders: Orders, call, approve, turn: int) -> tuple[str, bool]:
    """The gate for a write tool. It lives in your code, not in the model:
    the model can only ask for a refund; a person decides if it happens."""
    try:
        change = orders.check_refund(**call.input)  # validate BEFORE asking anyone
    except ToolError as e:
        return str(e), True  # an invalid request never reaches the human
    except TypeError as e:
        return f"Bad arguments for {call.name}: {e}. Check the tool's input_schema.", True
    prompt = f"\nturn {turn} · {call.name} {json.dumps(call.input)}\n  {change}\nAPPROVE? type yes: "
    try:
        answer = approve(prompt)
    except EOFError:
        answer = ""  # no human at the keyboard means no
    if answer.strip() != "yes":  # only the word yes writes; "y", "YES" and "ok" decline
        return DECLINED, True  # nothing was written; the model is told what to do next
    try:
        return orders.issue_refund(**call.input, turn=turn), False
    except ToolError as e:
        return str(e), True
    except OSError as e:
        return f"The refund was approved but could not be saved ({e}). Do not retry; tell the customer a person will finish it.", True

La escritura es atómica (un archivo temporal en la misma carpeta y después os.replace): si algo se cae a medias, el archivo viejo queda entero. Cada reembolso aprobado agrega una línea a refunds.log: hora, pedido, monto, motivo y «approved by a human at turn N». Las reglas se revisan otra vez después de tu sí, por si el archivo cambió mientras leías.

  1. Ilustrativo Nuestra prueba · selftest

    bash
    python refund-agent.py --selftest
    python refund-agent.py "Order A-1001 arrived cold. Can I get 12.50 back?"

    El selftest termina en selftest passed (sin llave y sin llamar a la API). Forma esperada del segundo comando (ilustrativa: tus tokens y la respuesta final van a cambiar):

    text
    turn 1 | tool_use | in=… out=… cache_read=… | get_order({"order_id": "A-1001"})
    
    turn 2 · issue_refund {"order_id": "A-1001", "amount": 12.5, "reason": "cold food"}
      Refund $12.50 on A-1001 (Ana): refunded $0.00 -> $12.50 of $42.00. Reason: cold food.
    APPROVE? type yes: yes
    turn 2 | tool_use | in=… out=… cache_read=… | issue_refund({"order_id": "A-1001", "amount": 12.5, "reason": "cold food")
    turn 3 | end_turn | in=… out=… cache_read=…
    …a short, kind reply to Ana…

    Escribí yes y después revisá el registro:

    bash
    tail -1 refunds.log

    text
    2026-10-25T07:27:49-06:00 | A-1001 | 12.50 | cold food | approved by a human at turn 2

Corrélo otra vez y contestá no: la línea del paso termina en ERROR, orders.json no cambia y el modelo le dice al cliente que un gerente va a revisar la solicitud. Pedí $20 del pedido A-1004 y ni siquiera te pregunta: el modelo lee «the most is $17.00» y se lo explica al cliente.

En el Claude Agent SDK, la misma puerta es un hook PreToolUse que devuelve permissionDecision: "ask", o un callback can_use_tool que devuelve PermissionResultAllow o PermissionResultDeny. Escribir la puerta a mano primero te muestra lo que esas funciones hacen por vos.

Afiná el ayudante de cocina

  • Un pase evaluador Después de la respuesta, llamá al modelo una vez más con la respuesta y las líneas citadas, y pedile PASS o NEEDS_IMPROVEMENT (sección 4, evaluator-optimizer), máximo 2 rondas.

  • El tool runner Reescribí el bucle con @beta_tool y client.beta.messages.tool_runner (sección 3, nivel 2) y compará el código que ya no tenés que mantener.

  • El Claude Agent SDK pip install claude-agent-sdk y después el fragmento del nivel 3 con allowed_tools=["Read", "Glob", "Grep"], disallowed_tools y un hook PreToolUse. Compará total_cost_usd con el log de tu bucle.

  • Un eval de 5 preguntas Cinco preguntas reales sobre tu repo, cada una con el archivo que la respuesta tiene que citar; corrélas después de cada cambio y contá cuántas pasan:

python
EVAL = [
    ("Where is MAX_TURNS defined?", "first-agent.py"),
    ("Which tool numbers the lines?", "first-agent.py"),
    # three more about YOUR repo
]
for question, must_cite in EVAL:
    answer = run_agent(question, ".")
    print("PASS" if must_cite in answer else "FAIL", question)

La primera hora te vas a sentir perdido; todo el mundo quema algo en su primera noche en la línea. Cociná un plato sencillo esta noche: corré first-agent.py en tu propio repo, después dale la herramienta de reembolso y mirá cómo se detiene a esperar tu sí. Después de eso, "agentes" deja de ser una palabra de moda.

Nuestra prueba (salida del selftest, el mensaje de la baranda, 19 pruebas sin conexión de first-agent.py y 36 de refund-agent.py). Para este artículo no se corrió la API en vivo; los checkpoints 2-3, el segundo comando del checkpoint 4 y la corrida del checkpoint 5 muestran la forma esperada.

Nuestra prueba · selftest, mensaje de max_turns, 19 + 36 pruebas sin conexión Ilustrativo · checkpoints 2-3, segundo comando del 4 y la corrida del 5

Por dónde seguir

En este orden:

  1. Building effective agents — el mapa
  2. Tool use overview y handle tool calls — el cable
  3. patterns/agents/ — los cinco patrones, listos para correr
  4. Writing tools for agents — la interfaz
  5. Effective context engineering — el mostrador
  6. Claude Agent SDK overview y el cookbook claude_agent_sdk/ — el nivel 3
  7. How we built our multi-agent research system — orchestrator-workers en producción

Fuentes

Cada una se abrió el 25 de octubre de 2026; las citas y rutas están en INVESTIGACION.md.