¿Te resulta más fácil leer con fondo crema, un tono de color, más contraste o un texto más grande? Elige tus colores y tu texto una sola vez, para todas las páginas.

Lección gratuita

Respuestas en streaming de una API

Después de esta lección: Puedo leer una respuesta que llega en trozos y unirla en un solo texto.

Lección de 60 min4 comandosLaboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

Una respuesta que llega palabra a palabra

Una respuesta en streaming es una respuesta que un modelo de lenguaje envía en trozos pequeños mientras escribe. No llega de golpe al final. Las aplicaciones de chat lo usan para que las palabras aparezcan enseguida.

Es útil porque una respuesta larga puede tardar varios segundos. Con un flujo, quien lee ve el principio casi al instante. Tu programa tiene que leer los trozos y unirlos.

Al final de esta lección

Nota

Puedo leer una respuesta que llega en trozos y unirla en un solo texto.

Palabras clave

PalabraQué significaEjemplo
Flujouna respuesta enviada en trozos mientras se escribe
Eventos del servidoruna forma de que un servidor empuje líneas a un cliente
Línea de datosuna línea del flujo que empieza por data: y lleva JSON
Deltael trozo nuevo de texto de una línea de datos
Marcador de finla última línea de datos, que dice que el flujo terminódata: [DONE]

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Un flujo es una lista de líneas

Para pedir un flujo añades "stream": true al cuerpo de la petición. El servidor responde entonces con eventos enviados por el servidor. Es texto sencillo, una línea cada vez, con una línea vacía entre eventos.

Cada línea que empieza por data: lleva un trozo de JSON. El texto está en choices[0].delta.content. La última línea es data: [DONE].

Las líneas de un flujo

La línea empieza porQué contiene
data: {"choices":[{"delta":{"role":"assistant"}}]}La apertura: un rol y todavía sin texto
data: {"choices":[{"delta":{"content":"Uno"}}]}Un trozo de texto que añadir
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}La razón por la que terminó la respuesta
data: [DONE]El marcador de fin, que no es JSON
: keep-aliveUna línea de comentario que se ignora

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Mira los paquetes de bytes que llegan

Comando
docker exec -w /home/node/project agent node -e 'const lab = require("./lab.js"); const event = (text) => "data: " + JSON.stringify({ choices: [{ delta: { content: text } }] }) + "\n\n"; const whole = event("Uno") + event(", dos") + event(", tres.") + "data: [DONE]\n\n"; (async () => { let chunks = []; let label = "Flujo"; try { const response = await lab.send({ stream: true, max_tokens: 30, messages: [{ role: "user", content: "Cuenta del uno al cinco." }] }); if (!response.ok) throw new Error("HTTP " + response.status); const decoder = new TextDecoder(); for await (const bytes of response.body) chunks.push(decoder.decode(bytes, { stream: true })); } catch (error) { label = "Muestra grabada"; chunks = whole.match(/[^]{1,45}/g); } console.log(label + ": primeros paquetes de bytes"); chunks.slice(0, 4).forEach((one, i) => console.log((i + 1) + " " + JSON.stringify(one))); console.log("... " + chunks.length + " paquetes en total."); })();'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Un paquete de red no es una línea

Los bytes llegan a tu programa en paquetes, y un paquete puede acabar a mitad de una línea. Por eso guarda un búfer. Añade cada paquete, corta cada línea completa y guarda el resto para el paquete siguiente.

Solo una línea completa se puede leer sin riesgo como JSON. Después añade cada trozo de texto a la respuesta.

Escribe stream.js, un lector de líneas completas

Comando
docker exec -w /home/node/project agent node -e 'require("fs").writeFileSync("stream.js", ["// stream.js: lee una respuesta en flujo como líneas completas, se corten como se corten los paquetes.", "async function* linesOf(response) {", "  let held = \"\";", "  const decoder = new TextDecoder();", "  for await (const bytes of response.body) {", "    held += decoder.decode(bytes, { stream: true });", "    const parts = held.split(\"\\n\");", "    held = parts.pop();", "    yield* parts;", "  }", "  if (held !== \"\") yield held;", "}", "", "module.exports = { linesOf };"].join("\n") + "\n")'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Añade piecesOf, que da el texto

Comando
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("stream.js", ["", "// Parte 2: envía una llamada en flujo y devuelve los trozos de texto, uno a uno.", "async function* piecesOf(body) {", "  const response = await require(\"./lab.js\").send({ ...body, stream: true });", "  if (!response.ok) throw new Error(\"HTTP \" + response.status);", "  for await (const line of linesOf(response)) {", "    if (!line.startsWith(\"data: \") || line === \"data: [DONE]\") continue;", "    const piece = JSON.parse(line.slice(6)).choices[0].delta.content;", "    if (piece) yield piece;", "  }", "}", "", "module.exports = { linesOf, piecesOf };"].join("\n") + "\n")'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Un error frecuente

Advertencia

Mucha gente cree que cada paquete es un evento. No lo es. Un paquete puede traer tres eventos o la mitad de uno. Tu programa es correcto cuando da el mismo texto se corten como se corten los bytes. Primero guarda en el búfer, luego lee las líneas.

Imprime y une los trozos

Comando
docker exec -w /home/node/project agent node -e 'const { piecesOf } = require("./stream.js"); const sample = ["Uno", ", dos", ", tres", ", cuatro", ", cinco."]; (async () => { const started = Date.now(); let first = null, count = 0, text = "", label = "Respuesta del modelo"; try { const asking = { max_tokens: 40, messages: [{ role: "user", content: "Cuenta del uno al cinco." }] }; for await (const piece of piecesOf(asking)) { if (first === null) first = Date.now() - started; text += piece; count++; process.stdout.write(piece); } } catch (error) { label = "Muestra grabada"; text = sample.join(""); count = sample.length; first = null; process.stdout.write(text); } console.log("\n" + label + ": " + count + " trozos, " + text.length + " caracteres."); if (first !== null) console.log("El primer trozo llegó a los " + first + " ms y todo a los " + (Date.now() - started) + " ms."); })();'

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Ejemplo resuelto: de las líneas al texto

Líneas de datos, en ordenTrozo añadidoTexto hasta ahora
Apertura, solo el rolNingunoVacío
content "Uno"UnoUno
content ", dos", dosUno, dos
finish_reason "stop"NingunoUno, dos
data: [DONE]NingunoUno, dos

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Para ir más lejos: mide el primer trozo

Ejecuta tres veces el script de unir y anota el tiempo hasta el primer trozo y hasta la respuesta entera.

Después di cuál de los dos nota más quien lee, y por qué un flujo ayuda sobre todo cuando la respuesta es larga.

Sobre este laboratorio y su código: Laboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

¿Enseñas a una clase? Asigna este módulo a tus alumnos con guía del profesor, tareas con corrección automática y un laboratorio para cada alumno.