Streaming de respuestas
Muestra la respuesta del modelo a medida que se genera, en consola y en un endpoint web con Server-Sent Events, sin perder el mensaje final ni el stop_reason.
- Tiempo
- ~15 min
- Tema
- Producción
- Stack
- python · anthropic · fastapi
Problema
Tu asistente tarda varios segundos en contestar y el usuario mira una pantalla vacía. La respuesta completa tarda lo mismo con o sin streaming, pero con streaming el primer texto aparece enseguida y la espera se siente distinta.
Quieres mostrar el texto conforme llega, tanto en un script como en una web, y seguir teniendo al final el mensaje completo con su stop_reason y su usage.
Ingredientes
- Python 3.10 o superior.
pip install anthropic(yfastapi uvicornpara el paso 3).- La variable de entorno
ANTHROPIC_API_KEY.
Pasos
-
Usa
messages.streamcomo gestor de contexto.stream.text_streamva entregando trozos de texto. Al terminar,stream.get_final_message()devuelve el mismo objetoMessageque tendrías sin streaming: contenido, stop_reason yusage. -
Mide el tiempo hasta el primer token. Es la métrica que nota el usuario. El tiempo total apenas cambia.
import sys
import time
import anthropic
MODEL = "claude-sonnet-5-5"
client = anthropic.Anthropic()
def responder(pregunta: str) -> anthropic.types.Message:
inicio = time.monotonic()
primer_token = None
with client.messages.stream(
model=MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": pregunta}],
) as stream:
for texto in stream.text_stream:
if primer_token is None:
primer_token = time.monotonic() - inicio
print(texto, end="", flush=True)
final = stream.get_final_message() # el mensaje completo, igual que sin streaming
print()
print(f"\n[primer token: {(primer_token or 0):.2f} s | total: {time.monotonic() - inicio:.2f} s | "
f"stop_reason: {final.stop_reason} | salida: {final.usage.output_tokens} tokens]", file=sys.stderr)
if final.stop_reason == "max_tokens":
print("[aviso: la respuesta se cortó por max_tokens]", file=sys.stderr)
return final
if __name__ == "__main__":
responder(" ".join(sys.argv[1:]) or "Explica en tres párrafos qué es una ventana de contexto.")Ejecútalo: python streaming.py "¿Qué es un token?".
- Llévalo a una web con Server-Sent Events. El navegador recibe una línea
data:por cada trozo. Al final, un evento confiny elstop_reasonpara que la interfaz sepa que ha terminado y si la respuesta se cortó.
import json
import anthropic
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
MODEL = "claude-sonnet-5-5"
client = anthropic.Anthropic()
app = FastAPI()
class Pregunta(BaseModel):
texto: str
def eventos(pregunta: str):
with client.messages.stream(
model=MODEL, max_tokens=1024, messages=[{"role": "user", "content": pregunta}]
) as stream:
for texto in stream.text_stream:
yield f"data: {json.dumps({'texto': texto})}\n\n"
final = stream.get_final_message()
yield f"data: {json.dumps({'fin': True, 'stop_reason': final.stop_reason})}\n\n"
@app.post("/preguntar")
def preguntar(p: Pregunta):
# una función generadora normal: FastAPI la recorre en un hilo aparte
return StreamingResponse(eventos(p.texto), media_type="text/event-stream")Arráncalo con uvicorn web:app --reload.
- Consúmelo desde el navegador.
EventSourcesolo admite GET, así que para un POST lee el cuerpo como flujo:
const resp = await fetch("/preguntar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ texto: "¿Qué es un token?" }),
});
const lector = resp.body.pipeThrough(new TextDecoderStream()).getReader();
let resto = "";
while (true) {
const { value, done } = await lector.read();
if (done) break;
resto += value;
const eventos = resto.split("\n\n");
resto = eventos.pop(); // el último puede estar a medias
for (const ev of eventos) {
const datos = JSON.parse(ev.replace(/^data: /, ""));
if (datos.texto) document.querySelector("#respuesta").textContent += datos.texto;
}
}Cómo comprobar que funciona
# salida de ejemplo
Un token es la unidad mínima de texto con la que trabaja el modelo…
[primer token: 0.62 s | total: 4.80 s | stop_reason: end_turn | salida: 212 tokens]- El texto aparece por partes, no de golpe al final.
- El tiempo hasta el primer token es una fracción pequeña del total.
- Con
curl -N -X POST localhost:8000/preguntar -H 'Content-Type: application/json' -d '{"texto":"hola"}'ves llegar los eventos uno a uno. Si llegan todos juntos, algo en medio (un proxy, la compresión) está acumulando la respuesta. - Baja
max_tokensa 20: debe aparecer el aviso demax_tokensy el evento final con esestop_reason.
Variaciones
- TypeScript. Con
@anthropic-ai/sdk:const stream = client.messages.stream({...}); stream.on("text", (t) => process.stdout.write(t)); const final = await stream.finalMessage();. - Herramientas. Con herramientas, el stream también trae bloques
tool_usecuyoinputllega por partes. No ejecutes nada hasta tener el mensaje final: usaget_final_message()y sigue el bucle normal (Uso de herramientas). - Eventos completos. Itera
for evento in stream:para recibir todos los eventos (message_start,content_block_delta,message_delta,message_stop…). Útil para mostrar progreso de herramientas o razonamiento. - Asíncrono.
anthropic.AsyncAnthropicconasync with client.messages.stream(...) as stream:yasync for texto in stream.text_stream:. - Cancelar. Si el usuario cierra la pestaña, sal del bucle: al salir del
withse cierra la conexión y dejas de generar tokens.
Errores típicos
- Olvidar el mensaje final. Sin
get_final_message()no tienesstop_reasonniusage, y no sabrás si la respuesta se cortó ni cuánto costó. - Proxies que acumulan. Algunos servidores y CDN guardan la respuesta entera antes de enviarla. Desactiva el buffering para esa ruta (en nginx,
proxy_buffering off) y no comprimas el flujo. - Validar JSON trozo a trozo. Si pides salida estructurada, el JSON solo es válido al final. Parsea el mensaje completo.
- Ejecutar herramientas con argumentos a medias. Espera al final del bloque
tool_use. - Pensar que el streaming abarata. Cuesta lo mismo. Mejora la latencia percibida, no la total (Latencia).
- Errores a mitad. Si la conexión se corta a mitad, el texto mostrado queda incompleto. Indícalo en la interfaz y ofrece reintentar (Reintentos y límites de tasa).