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.
"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
- El libro de recetas Los cookbooks de Anthropic Notebooks en github.com/anthropics/claude-cookbooks
-
El criterio del chef
El modelo
claude-opus-5-5detrás declient.messages.create -
La tarjeta que describe cada estación
La definición de una herramienta
name+description+input_schema(JSON Schema) -
El ayudante de cocina
Tu código
Ejecuta cada
tool_usey devuelve untool_result -
Cocinar, probar, ajustar
El bucle del agente
while stop_reason == "tool_use"… hastaend_turn -
El mostrador
La ventana de contexto
El arreglo
messagesque reenviás en cada llamada -
Limpiar el mostrador y guardar la comanda
Compactación
context_managementresume los turnos viejos -
Las reglas de la casa
El system prompt
system= - El cuaderno en el estante Memoria Archivos que el agente lee y escribe entre sesiones
- Una receta fija Un workflow Pasos que tu código decide de antemano
- El expedidor Routing Una llamada clasifica y el código la manda a un especialista
- Varios cocineros, catadores que votan Parallelization Llamadas independientes a la vez; por secciones o por voto
- El chef principal parte el pedido en comandas Orchestrator-workers Una llamada planea las subtareas y los workers las hacen
- El catador que devuelve el plato Evaluator-optimizer Generar → calificar → reintentar
-
La hora de cierre
Máximo de turnos
max_turns/ un tope al bucle - Los cuchillos que puede usar el aprendiz Permisos Herramientas de solo lectura, listas de permitir y negar
- El chef firma el plato Humano en el ciclo Aprobación antes de cualquier escritura
- 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).
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.
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:
{
"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_useQué significa: Claude llama una o más herramientasQué hace tu bucle: Las ejecuta, devuelve todos los resultados y llama otra vez -
end_turnQué significa: Claude terminó su respuestaQué hace tu bucle: Devuelve el texto; el bucle termina -
max_tokensQué significa: La respuesta llegó a tumax_tokensQué hace tu bucle: Para; untool_usecortado se reintenta con másmax_tokens -
pause_turnQué significa: Un bucle de herramientas del servidor llegó a su límiteQué hace tu bucle: Reenvía la respuesta tal cual para seguir -
refusalQué significa: Claude se negóQué hace tu bucle: Para y lo reporta -
stop_sequenceQué significa: Apareció una de tus secuencias de paradaQué hace tu bucle: Lo que tu app defina -
model_context_window_exceededQué significa: El mostrador se llenóQué hace tu bucle: Para; compactá o empezá más acotado
El viaje completo, paso a paso
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:
tu código
tu código → API
API → tu código
"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 */ ],{"role": "user", "content": "Where is the config loaded?"}-
{"role": "assistant", "content": [ /* the four blocks above, unchanged */ ]},{"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"}}
{"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."} ]}
Tu código corre las herramientas
searchsrc/config.py:3: def load_config():read_file'config.py' does not exist. Call list_files on its parent folder, or search for its name.
{
"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"}}
]
}{
"role": "assistant",
"stop_reason": "end_turn",
"content": [
{"type": "text", "text": "The config is loaded by load_config(), defined at src/config.py:3."}
]
}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
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
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
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
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
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
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
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
-
Paso 1 de 8
La pregunta
El cliente pide un plato.
La API no guarda estado: el arreglo
messageses todo lo que el modelo va a ver.stop_reason: sin stop_reason
-
Paso 2 de 8
El request, con
toolsLe 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
-
Paso 3 de 8
Llega la respuesta
El chef pide dos estaciones a la vez.
Dos bloques
tool_useen un turno: llamadas en paralelo.stop_reason: "tool_use".stop_reason:
tool_use -
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 -
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 -
Paso 6 de 8
Un solo mensaje
userEl ayudante pone los dos platos en el mostrador, juntos.
Un
tool_resultpor cadatool_use, todos en el mismo mensajeuser, primero los resultados.stop_reason:
tool_use -
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
-
Paso 8 de 8
end_turn: el bucle terminaEl 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
// 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_resultfor eachtool_useblock, 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«thetool_resultblocks 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: truey 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:
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:
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í:
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.
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 textRouting — el expedidor
Una llamada clasifica la entrada; el código la manda a un prompt especializado (o a un modelo más barato).
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))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.
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 promptsOrchestrator-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.
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))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.
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 draftEl 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.
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: truegarantiza 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:
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».
- Compactación (limpiar el mostrador y guardar la comanda): resumir los turnos viejos. En la API actual se hace en el servidor:
-
Barandas
Barandas: hora de cierre, cuchillos, el chef firma
- Un tope de turnos (Anthropic: «stopping conditions (such as a maximum number of iterations)»):
max_turnsen el ejercicio y en el Agent SDK, másmax_budget_usden 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
PreToolUseque 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.
- Un tope de turnos (Anthropic: «stopping conditions (such as a maximum number of iterations)»):
-
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×
Costo y latencia
Caché de prompts. Cada turno reenvía el mismo prefijo (system, herramientas, historial). Con
cache_controlla 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 declaude-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:linede 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.ipynbpone agentes a correr tareas y a opinar sobre las herramientas mismas.
Modos de falla y su arreglo
-
Falla: Bucle infinitoQué ves: El agente llama herramientas sin parar; el costo subeArreglo: Tope de turnos (
max_turns), tope de gasto (max_budget_usd), una condición clara de fin en el system prompt -
Falla: Argumentos inventadosQué ves: Una ruta o un campo que no existeArreglo: JSON Schema con
requiredyadditionalProperties: false; validar en el código; un resultadois_errorque diga qué sí existe;strict: truecuando haga falta -
Falla: Context rot (contexto podrido)Qué ves: Los turnos tardíos olvidan datos del inicio y las respuestas se desvíanArreglo: Compactación, limpiar resultados de herramientas, subagentes, salidas más pequeñas
-
Falla: Costo desbocadoQué ves: Los tokens de entrada crecen en cada turnoArreglo: Caché de prompts, topes, llamadas en paralelo, un modelo más barato para los pasos rutinarios
-
Falla: La herramienta devuelve demasiadoQué ves: Una llamada llena el mostradorArreglo: Paginación, rangos, filtros, un tope duro con una nota «[truncated…]»
-
Falla: Errores silenciososQué ves: Una herramienta falla pero el bucle dice «listo»Arreglo:
is_error: true; revisar cadastop_reason(nunca correr herramientas de un turnomax_tokens); verificar las citas -
Falla: Se acaban las llamadas en paraleloQué ves: Claude deja de agrupar consultasArreglo: Devolver todos los resultados en UN mensaje
user, primero los resultados -
Falla: Llamadas ignoradasQué ves: Solo corre la primera herramientaArreglo: 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.
Preparación
python3 -m venv .venv && . .venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY=... # set it in your shell, never in codeSin llave y sin llamar la API. El sandbox tiene que negarse a salir del repo:
python first-agent.py --selftestSalida esperada (nuestra corrida):
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 passedPreguntale sobre su propio archivo. Cada turno imprime una línea en stderr: el turno, el
stop_reason, los tokens y las herramientas que corrió.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):
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:243o líneas cercanas. Revisá quecache_readcrezca después del turno 1: es el historial repetido que se lee del caché.Preguntá algo que necesite dos consultas independientes:
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.turn 1 | tool_use | in=… out=… cache_read=0 | list_files({"path": "."}), search({"pattern": "MAX_TURNS"})La hora de cierre, y un cuchillo que el aprendiz no puede usar:
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 questiony sale con2(ese mensaje exacto está cubierto por las pruebas sin conexión). En el segundo, el log muestraread_file({"path": "../.bashrc"}) ERRORy el modelo responde que el archivo está fuera del repo.
0 de 5 listos
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.
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.
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.", TrueLa 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.
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):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í
yesy después revisá el registro:tail -1 refunds.log2026-10-25T07:27:49-06:00 | A-1001 | 12.50 | cold food | approved by a human at turn 2
0 de 5 listos
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_toolyclient.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-sdky después el fragmento del nivel 3 conallowed_tools=["Read", "Glob", "Grep"],disallowed_toolsy un hookPreToolUse. Comparátotal_cost_usdcon 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:
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:
- Building effective agents — el mapa
- Tool use overview y handle tool calls — el cable
- patterns/agents/ — los cinco patrones, listos para correr
- Writing tools for agents — la interfaz
- Effective context engineering — el mostrador
- Claude Agent SDK overview y el cookbook claude_agent_sdk/ — el nivel 3
- 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.
Anthropic
Documentación de la API de Claude
Cookbooks
- patterns/agents/basic_
workflows - patterns/agents/orchestrator_
workers - patterns/agents/evaluator_
optimizer - patterns/agents/util.py
- tool_
use/customer_ service_ agent - tool_
use/parallel_ tools - tool_
use/tool_ choice - tool_
use/memory_ cookbook - tool_
use/automatic-context-compaction - tool_
use/context_ engineering - claude_
agent_ sdk - managed_
agents - observability
- tool_
evaluation
Nuestra prueba
- selftest de
first-agent.pyy 19 pruebas sin conexión - selftest de
refund-agent.pyy 36 pruebas sin conexión - firmas del SDK
anthropic2.3.0