Mostrar la respuesta según llega
Una página de chat en flujo muestra la respuesta de un chatbot unas pocas palabras cada vez. El servidor las envía según las tiene. Importa porque una respuesta larga puede tardar muchos segundos, y una página en blanco hace pensar que la web falla.
El servidor envía trozos pequeños. La página añade cada trozo a la pantalla. El terminal del laboratorio no tiene navegador, así que aquí construyes la página y el flujo como texto. En tu propio equipo la abrirías en un navegador.
Al final de esta lección
Nota
Puedo mostrar en una página web una respuesta en flujo según va llegando.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Flujo | una respuesta enviada en trozos pequeños |
| Eventos del servidor | una forma de que un servidor empuje líneas a una página |
| Manejador | la función que responde a una petición |
| Escapar | cambiar caracteres para que se vean como texto |
| Trozo | una parte pequeña de una respuesta en flujo |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un servidor puede enviar una respuesta en trozos
Con eventos del servidor, el servidor mantiene la conexión abierta y escribe líneas. Cada evento empieza por data: , lleva un trozo en JSON y acaba con una línea en blanco. El tipo de la respuesta es text/event-stream.
Un último evento, llamado done, lleva una línea data: end y avisa a la página de que la respuesta ha terminado.
Ejemplo resuelto: las líneas de un flujo
| Línea enviada | Lo que hace la página |
|---|---|
| data: {"piece": "Hola"} y una línea en blanco | Añade Hola a la pantalla |
| data: {"piece": " y bienvenido"} y una línea en blanco | Añade las palabras siguientes detrás |
| event: done, luego data: end y una línea en blanco | Cierra la conexión |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
La página añade y puede parar
La página lleva un pequeño script. Abre el flujo y, por cada evento, añade el trozo a un párrafo. Un botón de parar cierra el flujo, así que ya no llegan más trozos.
Añadir un trozo con append y una cadena añade texto simple. Nunca añade marcado.
Escapa el texto del modelo
Un modelo puede repetir lo que se le dio, incluido <script>. Si ese texto entra en una página como HTML, el navegador puede ejecutarlo.
Escapar convierte < en < y los demás caracteres especiales en códigos seguros. Escapa todo texto de modelo que vaya a HTML. Haz & primero, o escaparás tus propios códigos.
Un error frecuente
Advertencia
Mucha gente cree que el texto del modelo es seguro porque tú escribiste las reglas. No lo es. El modelo puede repetir las palabras de un usuario o de una fuente. Trata cada respuesta como no fiable: añádela como texto, o escápala antes.
Escribir la ayuda del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"estado \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la ayuda del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' const list = [].concat(sample);' ' const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return list[Math.min(turn, list.length - 1)];' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la página y el flujo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'function quoted(text) { return JSON.stringify(text); }' 'function escapeHtml(text) {' ' return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")' ' .replace(/\"/g, \""\").replace(/\x27/g, \"'\");' '}' > ui4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la página y el flujo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function pageHtml() {' ' const script = [' ' \"const out = document.getElementById(\" + quoted(\"answer\") + \");\",' ' \"const source = new EventSource(\" + quoted(\"/stream\") + \");\",' ' \"source.onmessage = function (e) { out.append(JSON.parse(e.data).piece); };\",' ' \"source.addEventListener(\" + quoted(\"done\") + \", function () { source.close(); });\",' ' \"document.getElementById(\" + quoted(\"stop\") + \").onclick = function () { source.close(); };\",' ' ].join(\"\n\");' ' return \"<html><body><p id=answer></p><button id=stop>Parar</button><script>\" + script + \"</script></body></html>\";' '}' >> ui4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la página y el flujo, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function sseLine(piece) { return \"data: \" + JSON.stringify({ piece: piece }) + \"\n\n\"; }' 'function streamBody(pieces, stopAfter) {' ' let body = \"\";' ' pieces.slice(0, stopAfter).forEach(function (piece) { body = body + sseLine(piece); });' ' return stopAfter >= pieces.length ? body + \"event: done\ndata: end\n\n\" : body;' '}' >> ui4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la página y el flujo, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function handle(request, pieces) {' ' if (request.url === \"/\") return { status: 200, type: \"text/html\", body: pageHtml() };' ' if (request.url === \"/stream\") {' ' return { status: 200, type: \"text/event-stream\", body: streamBody(pieces, request.stopAfter || 99) };' ' }' ' return { status: 404, type: \"text/plain\", body: \"No encontrado\" };' '}' >> ui4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la página y el flujo, parte 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function show(pieces) {' ' const home = handle({ url: \"/\" }, pieces);' ' console.log(\"GET / ->\", home.status, home.type, home.body.length + \" caracteres\");' ' const live = handle({ url: \"/stream\" }, pieces);' ' console.log(\"GET /stream ->\", live.status, live.type);' ' console.log(live.body.split(\"\n\n\").join(\" | \"));' ' const stopped = handle({ url: \"/stream\", stopAfter: 2 }, pieces);' ' console.log(\"Parar tras 2 trozos:\", stopped.body.split(\"\n\n\").length - 1, \"trozos enviados, sin línea done\");' ' console.log(\"Escapado:\", escapeHtml(\"<b>hola</b> & gracias\"));' '}' >> ui4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir la página y el flujo, parte 6
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' // Muestra grabada, usada solo si ningún modelo responde.' ' const sample = \"Hola y bienvenido a bordo, ¿adónde quieres ir?\";' ' const reply = await ask([{ role: \"user\", content: \"Saluda a un viajero de autobús en una frase corta.\" }], sample, 40);' ' show(reply.split(\" \").map(function (word, at) { return at === 0 ? word : \" \" + word; }));' '}' 'main();' >> ui4.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Llamar al manejador
docker exec -w /home/node/project agent node ui4.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Una salida posible
| Línea | Lo que puede mostrar |
|---|---|
| GET / | 200 text/html y el tamaño de la página |
| GET /stream | 200 text/event-stream, y luego los trozos de una respuesta separados por | |
| Parar | 2 trozos enviados, sin línea done |
| Escapado | <b>hola</b> & gracias, siempre igual |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.