Streaming de respuestas en agentes de IA: SSE sin romper tools
Resumen
Cómo streamear la respuesta de un agente con SSE (OpenAI stream=true, Claude messages.stream, Gemini streaming) sin pintar JSON a medias ni ejecutar una tool dos veces. Eventos, timeouts, errores mid-stream y checklist para el canal de chat.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Streamear no es “más rápido el modelo”. Es mostrar el principio mientras el resto se genera, para que el usuario no mire un spinner de 20 segundos. El costo en tokens es el mismo. El costo en ingeniería sube: eventos a medias, tools a medias, errores a mitad de SSE y un proxy que corta la conexión.
Esta guía fija el contrato HTTP (SSE, stream=true) de OpenAI, Claude y Gemini, y las reglas para no ejecutar una tool o parsear JSON hasta que el bloque esté cerrado. Fuentes oficiales del 3 de septiembre de 2026.
Si el agente vive detrás de un webhook de Telegram/WhatsApp, streaming hacia el proveedor sigue sirviendo (time-to-first-token, cancelación), pero el canal a menudo no pinta token a token. Distingue las dos piernas.
Qué es SSE aquí
Por defecto las APIs esperan a tener toda la respuesta y la mandan en un HTTP. Con streaming, el servidor emite server-sent events: una secuencia de eventos mientras el modelo sigue generando.
OpenAI documenta stream=true (o stream=True en Python) en Responses. El cliente itera eventos:
stream = client.responses.create(
model="gpt-4o-mini",
input=[{"role": "user", "content": "Resume el ticket en 3 líneas."}],
stream=True,
)
for event in stream:
print(event)
Claude: "stream": true en Messages, o client.messages.stream(...) / .text_stream en el SDK. Eventos con nombre (message_start, content_block_delta, message_stop). Un error mid-stream llega como event: error (por ejemplo overloaded_error, el equivalente de un 529).
Gemini documenta streaming de interactions en su guía de Streaming: el SDK entrega chunks de texto; no asumas el mismo set de eventos que Claude.

La regla que evita el 90 % de bugs
No ejecutes una tool ni parsees JSON hasta el evento que cierra el bloque.
- Texto al usuario: sí puedes pintar cada
text_delta/ chunk. Es lo único seguro de ir a medias. - Tool call: los argumentos llegan en deltas. Un
{ "to": "ana@no es un mail. Espera elcontent_block_stop(Claude) o el evento equivalente que marca el tool call completo (OpenAI documenta streaming también con function/tool calls). - Structured output: el JSON válido existe al final. Mid-stream
JSON.parseva a fallar o, peor, a “funcionar” con un objeto incompleto.
Si mezclas streaming con HITL, la aprobación mira el tool call cerrado, no el delta 3 de 12.
Dónde se rompe en producción
- El proxy corta SSE. Nginx, Cloudflare, una función serverless con timeout de 10 s. El usuario ve media frase y silencio. Alinea timeout del host con el del cliente; si no llega, usa un runtime largo (VPS) o parte la tarea.
- Reintentar el stream entero. Un retry a mitad pinta el texto dos veces o re-ejecuta la tool. Idempotency key + “si ya mandé tokens, no reintento el mismo run”.
- Guardar solo el texto streameado. El objeto final del SDK trae usage, tool calls y stop reason. Persiste eso, no el concatenado a ojo.
- Errores como texto. Claude mete
event: erroren el stream. Si lo ignoras, tu UI cree que el modelo “dijo” el JSON del error. - WhatsApp/Telegram como si fueran un browser. Esos canales no son SSE hacia el usuario. Streamea hacia tu backend; al canal manda 1–2 mensajes (typing + final) o edita un mensaje si la API lo permite.

OpenAI vs Claude vs Gemini (lo que sí está en docs)
| Transporte | Cómo se prende | Detalle útil | |
|---|---|---|---|
| OpenAI | SSE (stream=true) en Responses; WebSocket aparte con previous_response_id | stream=True / stream: true | Guía enfocada en HTTP SSE; el SDK itera eventos |
| Claude | SSE en Messages | stream: true o messages.stream | Eventos nombrados; thinking deltas; error mid-stream |
| Gemini | Streaming de interactions (SDK) | Ver guía Streaming | Chunks de texto; no copies el state machine de Claude |
No inventes nombres de eventos de Gemini copiando a Claude. Lee el objeto que te da tu SDK.
Checklist
- Streaming encendido solo donde el usuario ve tokens (web/app). En bots, streaming interno + un mensaje final está bien.
- Pintar texto delta a delta; tools y JSON al cierre del bloque.
- Timeout del reverse proxy > tiempo peor de una corrida, o runtime que no mate SSE.
- Handler de
event: error/ excepción del iterator. No tragar el error como texto. - Persistencia del mensaje final (usage, tools, stop), no del buffer sucio.
- Cancelación: si el usuario se va, abortas el HTTP. Dejas de gastar tokens.
- Un test: cortar el stream a mitad y verificar que no corre la tool.
- Logs de time-to-first-token y duración total, para observabilidad.
FAQ
¿Streaming baja el costo? No. Puede subirlo si cancelas tarde o reintentas. El win es latencia percibida.
¿Puedo streamear y usar JSON schema a la vez? Depende del proveedor. Aunque se pueda, no uses el JSON hasta el evento final. El schema no hace mágicamente parseable un prefijo.
¿Y el “thinking” de Claude? Llega como deltas aparte. No lo mandes al usuario del bot a menos que quieras filtrar. La guía de Claude lista thinking deltas junto a text y tool use.
¿WebSockets en vez de SSE? OpenAI documenta WebSocket en Responses para input incremental (previous_response_id). Para “ir pintando la respuesta” SSE alcanza y es lo que Nginx/CDNs ya entienden. No cambies de transporte por moda.
Empieza por streamear solo texto a un frontend. Cuando eso sea aburrido (reconnect, abort, logs), recién streameas un agente con tools.
Lecturas relacionadas
Sigue explorando Agentes en Producción y otras piezas para builders.



