Streaming de Respuestas de Agentes: Salida en Tiempo Real para Flujos de Trabajo Multi-Paso
Streaming de Respuestas de Agentes: Salida en Tiempo Real para Flujos de Trabajo Multi-Paso
Tu agente tarda 20 segundos en investigar una pregunta, llamar a tres herramientas y sintetizar una respuesta. El usuario hace clic en “Preguntar” y se queda mirando un spinner durante 20 segundos. No está seguro de si está funcionando. Considera actualizar la página. Se pregunta si debería empezar de nuevo.
Ahora imagina esto: el usuario hace clic en “Preguntar” y en menos de 500 milisegundos ve aparecer las primeras palabras del agente. Lo observa buscar información —”🔍 Buscando en la base de datos de pedidos…”— y ve cómo llegan los resultados en tiempo real. Lee la respuesta mientras se está escribiendo, frase a frase. Los mismos 20 segundos. Una experiencia completamente diferente.
El streaming no es un añadido opcional para los agentes orientados al usuario —es un requisito de UX. El tiempo hasta el primer token es la métrica de latencia más importante en las interfaces de agentes. Los usuarios que ven progreso son pacientes. Los usuarios que no ven nada abandonan. Estudio tras estudio sobre rendimiento web demuestra que la latencia percibida importa más que la latencia real, y el streaming es la herramienta más poderosa que tienes para cerrar la brecha entre ambas.
En este artículo aprenderás cómo implementar streaming en tiempo real para agentes de IA multi-paso —desde la entrega token a token hasta la transparencia en las llamadas a herramientas, actualizaciones de estado, divulgación progresiva, recuperación de errores y elecciones de capa de transporte.
Sección 1: Fundamentos de la API de Streaming de Claude
Antes de poder hacer streaming de un agente, necesitas hacer streaming de una sola respuesta de Claude. Empecemos por los fundamentos.
Streaming vs. Sin Streaming
Una llamada a la API sin streaming se bloquea hasta que se genera la respuesta completa y luego la devuelve toda de una vez. Una llamada con streaming devuelve una secuencia de eventos a medida que se produce la respuesta, comenzando con el primer token.
La diferencia en el código es mínima. La diferencia en la experiencia del usuario es enorme.
Tipos de Eventos
La API de streaming de Claude emite una secuencia estructurada de eventos server-sent:
message_start— Contiene el objetoMessageinicial con metadatos (modelo, rol, uso).content_block_start— Señala el inicio de un bloque de contenido (texto o tool_use).content_block_delta— Contiene contenido incremental: fragmentos de texto o JSON parcial de entrada de herramienta.content_block_stop— Señala el fin de un bloque de contenido.message_delta— Actualizaciones finales del mensaje (razón de parada, uso final).message_stop— El stream ha terminado.
Para respuestas de texto, recibirás muchos eventos content_block_delta, cada uno con un pequeño fragmento de texto (normalmente unos pocos tokens).
Implementación Básica de Streaming
Aquí tienes un ejemplo de streaming síncrono usando el SDK de Python de Anthropic:
import anthropic
client = anthropic.Anthropic()
def stream_basic_response(user_message: str): """Stream a basic Claude response token by token.""" with client.messages.stream( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": user_message}], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) print() # Newline after stream completes
stream_basic_response("Explain quantum entanglement in simple terms.")Y la versión asíncrona, que es la que usarás en servidores web en producción:
import anthropicimport asyncio
async_client = anthropic.AsyncAnthropic()
async def stream_basic_response_async(user_message: str): """Async streaming with the Anthropic SDK.""" async with async_client.messages.stream( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": user_message}], ) as stream: async for text in stream.text_stream: print(text, end="", flush=True) print()
asyncio.run(stream_basic_response_async("Explain quantum entanglement in simple terms."))Manejo de Tokens Parciales y Límites UTF-8
El SDK gestiona la decodificación UTF-8 por ti, pero si trabajas con el stream HTTP crudo, ten en cuenta que los caracteres multibyte pueden dividirse entre fragmentos. Siempre almacena en búfer los bytes crudos y decodifica solo cuando tengas secuencias UTF-8 completas. El iterador text_stream del SDK de anthropic gestiona esto automáticamente —otra razón para usarlo en lugar de parsear tú mismo el stream SSE crudo.
Sección 2: Streaming de Llamadas a Herramientas
El streaming de texto básico es el mínimo indispensable. El verdadero reto —y el verdadero valor— surge cuando tu agente utiliza herramientas. Un agente multi-paso que llama a tres herramientas en secuencia puede parecer muerto durante la ejecución de las herramientas a menos que muestres lo que está pasando.
Cómo Aparecen las Llamadas a Herramientas en el Stream
Cuando Claude decide usar una herramienta, el stream emite un evento content_block_start con type: "tool_use", seguido de eventos content_block_delta que contienen fragmentos del JSON de entrada de la herramienta. Una vez ensamblada la llamada completa, ejecutas la herramienta, inyectas el resultado y continúas la conversación.
La idea clave: conoces el nombre de la herramienta en cuanto llega content_block_start, incluso antes de que el JSON de entrada esté completo. Esto significa que puedes mostrar al usuario inmediatamente algo como ”🔍 Buscando en la base de datos de pedidos…” sin esperar a la llamada completa.
El Bucle Completo del Agente con Streaming
Aquí tienes un bucle completo de agente con streaming que muestra las invocaciones de herramientas en tiempo real:
import anthropicimport json
client = anthropic.Anthropic()
# Define toolstools = [ { "name": "search_orders", "description": "Search customer orders by order ID or customer email.", "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "Order ID or email"}, }, "required": ["query"], }, }, { "name": "get_shipping_status", "description": "Get real-time shipping status for an order.", "input_schema": { "type": "object", "properties": { "order_id": {"type": "string", "description": "The order ID"}, }, "required": ["order_id"], }, },]
def execute_tool(tool_name: str, tool_input: dict) -> str: """Execute a tool and return the result as a string.""" if tool_name == "search_orders": # Simulated database lookup return json.dumps({ "order_id": "ORD-12345", "customer": "jane@example.com", "items": ["Blue Widget x2", "Red Gadget x1"], "total": "$47.99", "status": "shipped", }) elif tool_name == "get_shipping_status": return json.dumps({ "order_id": tool_input["order_id"], "carrier": "FedEx", "tracking": "7891011", "estimated_delivery": "2026-03-10", "current_location": "Memphis, TN", }) return json.dumps({"error": f"Unknown tool: {tool_name}"})
def stream_agent_response(user_message: str): """ Complete streaming agent loop with real-time tool call display. """ messages = [{"role": "user", "content": user_message}]
while True: # Stream the model response tool_calls = [] current_tool = None
with client.messages.stream( model="claude-sonnet-4-20250514", max_tokens=4096, tools=tools, messages=messages, ) as stream: response = None for event in stream: # The SDK exposes raw events via iteration pass
# Use the helper to collect text and tool use response = stream.get_final_message()
# Process the response content blocks for block in response.content: if block.type == "text": print(block.text, end="", flush=True) elif block.type == "tool_use": tool_name = block.name tool_input = block.input tool_id = block.id
# Show the user what's happening print(f"\n⚙️ Calling tool: {tool_name}({json.dumps(tool_input)})")
# Execute the tool result = execute_tool(tool_name, tool_input) print(f"✅ Result received from {tool_name}")
tool_calls.append({ "tool_use_id": tool_id, "tool_name": tool_name, "tool_input": tool_input, "result": result, })
# If the model stopped because it wants to use tools, continue the loop if response.stop_reason == "tool_use": # Add assistant message with all content blocks messages.append({"role": "assistant", "content": response.content})
# Add tool results tool_results = [] for tc in tool_calls: tool_results.append({ "type": "tool_result", "tool_use_id": tc["tool_use_id"], "content": tc["result"], }) messages.append({"role": "user", "content": tool_results})
print("\n--- Continuing agent loop ---") else: # Model is done (stop_reason == "end_turn") print() break
# Run the agentstream_agent_response("Where is my order #ORD-12345? When will it arrive?")Para un streaming token a token verdadero con detección de llamadas a herramientas, puedes iterar sobre los eventos crudos:
def stream_agent_with_live_tokens(user_message: str): """Stream text tokens live while also detecting tool calls.""" messages = [{"role": "user", "content": user_message}]
while True: collected_content = [] current_text = "" current_tool_name = None current_tool_input_json = "" current_tool_id = None stop_reason = None
with client.messages.stream( model="claude-sonnet-4-20250514", max_tokens=4096, tools=tools, messages=messages, ) as stream: for event in stream: if hasattr(event, 'type'): if event.type == 'content_block_start': if event.content_block.type == 'tool_use': current_tool_name = event.content_block.
---
## Artículos Relacionados
- [Patrones de Uso de Herramientas: Interfaces Agente-Herramienta Confiables](/es/blog/agent-tool-use-patterns/)- [Patrones Multi-Agente: Orquestadores, Workers y Pipelines](/es/blog/multi-agent-patterns/)- [Recuperación de Errores en Agentes: 5 Patrones para Fiabilidad en Producción](/es/blog/agent-error-recovery-patterns/)- [Agentes de Automatización Web: Control del Navegador con Claude y Computer Use](/es/blog/web-automation-agents-browser-control-with-claude-and-computer-use/)