Skip to content

ADR-0021: Streaming de tokens

  • Estado: Accepted
  • Fecha: 2026-05-28

El nodo de generación de respuesta del agente almacenaba en búfer la respuesta completa del LLM antes de que se dispararan el análisis posterior, la verificación de citaciones y el evento SSE de nodo completado. La latencia visible para el usuario era, por lo tanto, el tiempo de reloj desde el primer token hasta el último token, más el paso de análisis, antes de que apareciera algo en la SPA. En una respuesta de modo JSON de 500 tokens a los típicos 250 tok/s de Groq, eso son ~2 segundos de silencio entre el evento de nodo iniciado y el primer carácter del mensaje del asistente.

Un proveedor con capacidad de streaming en la superficie compatible con OpenAI (chat/completions con stream: true) emite deltas por token en la red a medida que el modelo los genera; Groq es uno de esos proveedores. La SPA ya ejecuta un EventSource contra el endpoint de chat y consume los eventos del ciclo de vida SSE por nodo (ADR-0009). La infraestructura para hacer aflorar los deltas existe; este ADR registra las decisiones de diseño tomadas al conectar los dos extremos.

Tres decisiones acopladas:

  1. Mecanismo entre límites: stream personalizado de LangGraph. El nodo de generación de respuesta emite un evento de stream personalizado ({"event": "token_delta", "delta": "..."}) por cada bloque de tokens que recibe del adaptador. El manejador de streaming de FastAPI solicita el modo de stream custom de LangGraph junto a los modos existentes; los bloques de custom se enrutan a registros SSE token_delta mediante un pequeño ayudante de mapeo.

  2. API del adaptador: Protocolo de cliente de streaming aparte. Un Protocolo de cliente de streaming extiende el Protocolo base de cliente de LLM con un método asíncrono stream(messages, params) que devuelve un iterador asíncrono de deltas de tokens. Ambos Protocolos son comprobables en tiempo de ejecución. Los adaptadores que exponen streaming implementan ambos Protocolos; los adaptadores que no lo hacen se quedan solo con el Protocolo base. La rama de streaming del agente enruta mediante una comprobación isinstance y recurre a la llamada de completado en búfer para los adaptadores sin streaming.

  3. Alcance: solo proveedor con capacidad de streaming en el lanzamiento. La rama de streaming se publicó para el cliente de Groq y el stub en proceso (para las pruebas). Los clientes de OpenAI, Anthropic y de respaldo se quedaron deliberadamente solo en modo búfer. Por lo tanto, un turno transmite token por token solo cuando un proveedor con capacidad de streaming (Groq) es el proveedor activo; bajo el primario OpenAI desplegado, que es solo de búfer, la respuesta se entrega en una sola pieza. OpenAI usa la misma forma SSE compatible con OpenAI y puede añadirse con cambios de adaptador de una línea cuando se justifique; la forma SSE de Anthropic difiere y necesita su propio trabajo de adaptador. El cliente de respaldo no es consciente del streaming: ante un fallo del stream primario a mitad de vuelo, el consumidor hace aflorar un evento de error SSE y la SPA muestra un reintento. Un estado futuro de stream en cascada está documentado en “Trabajo futuro”.

A1: Cola fuera de banda en lugar del stream personalizado de LangGraph

Section titled “A1: Cola fuera de banda en lugar del stream personalizado de LangGraph”

Pasar una cola desde el manejador de FastAPI al grafo mediante la configuración del runnable. El nodo empuja los deltas a la cola; el manejador lee de la cola de forma concurrente con el stream del grafo.

  • Pro: Funciona en cualquier versión de LangGraph; es un patrón preexistente en bases de código asíncronas de Python.
  • Contra: Introduce un estado fuera de banda que la capa de pruebas tiene que configurar; el orden entre los deltas de la cola y los bloques de stream existentes no es nativo del grafo y tiene que imponerlo el manejador.
  • Rechazada: la versión fijada de LangGraph ya proporciona una API nativa documentada de escritura de stream.

A2: Extender el método de completado con una retrollamada por token

Section titled “A2: Extender el método de completado con una retrollamada por token”

Un método de Protocolo; el streaming es una retrollamada que el adaptador invoca por token.

  • Pro: Sin nueva clase de Protocolo.
  • Contra: Cambia la firma de cada adaptador; el tipado estático no puede distinguir adaptadores con streaming de los que no lo tienen; el tipo de la retrollamada es más difícil de razonar que un iterador asíncrono.
  • Rechazada: el tipado estructural mediante un Protocolo aparte es el patrón de Python más idiomático.

A3: Hacer streaming para cada adaptador que lo soporte (OpenAI incluido)

Section titled “A3: Hacer streaming para cada adaptador que lo soporte (OpenAI incluido)”

Maximiza la cobertura de streaming extendiendo el adaptador de OpenAI al mismo tiempo.

  • Pro: más beneficios para el usuario por incremento.
  • Contra: El streaming de OpenAI bajo el cambio de nivel del respaldo en cascada no está validado; podría filtrar respuestas a medio transmitir en un respaldo; el diseño en cascada en sí mismo es solo de búfer (según el diseño de respaldo en ADR-0002).
  • Diferida: anotada como trabajo futuro más abajo.

A4: La SPA renderiza los deltas en un panel aparte; el área principal del mensaje aún almacena en búfer

Section titled “A4: La SPA renderiza los deltas en un panel aparte; el área principal del mensaje aún almacena en búfer”

Dos destinos de renderizado: un panel de stream en bruto al estilo de depuración, más el área principal del mensaje que espera la finalización del turno.

  • Pro: Sin el fallo de reemplazo al completar.
  • Contra: Incumple el objetivo visible para el usuario (todo el sentido del streaming de tokens es la materialización al estilo de ChatGPT en el área principal).
  • Rechazada: el renderizado progresivo en el área principal es la recompensa de cara al usuario.
  • Reduce la latencia percibida hasta el primer token de ~2s (búfer de respuesta completa) a ~80ms (primer token en la red) cuando un proveedor con capacidad de streaming (Groq) está activo; bajo un primario solo de búfer la latencia percibida no cambia.
  • La superficie de streaming es puramente aditiva en la red SSE: los clientes que no se suscriben al nuevo nombre de evento lo ignoran silenciosamente según la semántica SSE del W3C.
  • La ruta de completado en búfer se preserva sin cambios para cada adaptador sin streaming, cada solicitud de chat JSON y la CLI de evaluación. La compatibilidad hacia atrás se mantiene para esas rutas.
  • La nueva superficie de Protocolo es pequeña (un método, un tipo); los dobles de prueba son simples.
  • El respaldo en cascada se omite al hacer streaming. Ante un fallo del stream primario de Groq, el consumidor ve un evento de error SSE y debe reintentar. El respaldo en cascada para turnos transmitidos es un diseño de estado futuro al que este ADR no se compromete.
  • El renderizado progresivo de la SPA debe manejar estados de JSON parcial (el LLM está en modo JSON según ADR-0019; los tokens llegan como {"kind":"...","text":"..."} carácter por carácter). La capa de renderizado descarta la envoltura JSON y renderiza solo el valor interno text; un "text":"... parcial requiere una lógica de UI cuidadosa.
  • La cifra de latencia del bloque de streaming es el tiempo acumulado desde que se abrió el stream; el delta de tiempo de reloj por bloque no se reporta porque rara vez es significativo para la contabilidad posterior.
  • El formato de la red SSE añade un nuevo nombre de evento (token_delta); los nombres de evento existentes (graph_topology, node_started, node_completed, paused, turn_completed, error, interaction_logged, cost_updated) quedan intactos.
  • La carga útil del token-delta lleva la cadena del delta, una razón de finalización, los conteos de tokens de entrada / salida, una cifra de latencia acumulada, el nombre del modelo y un pequeño mapa de metadatos. Los bloques intermedios llevan una razón de finalización nula y conteos de tokens en cero; el bloque terminal lleva la contabilidad por turno.
  • El reintento del stream cubre solo el establecimiento de la conexión inicial (3 intentos, retroceso exponencial). Los fallos a mitad de stream se lanzan de inmediato.
  • La solicitud de streaming compatible con OpenAI establece stream=True más stream_options={"include_usage": True} para que el bloque terminal lleve el uso en todos los proveedores compatibles con OpenAI.
  • El analizador de bloque a token-delta es agnóstico del proveedor y compartido, de modo que los futuros adaptadores de streaming de OpenAI lo reutilizan.
  • Streaming de OpenAI: un cambio de adaptador de una línea reutilizando los ayudantes de transporte compartidos. Diferido hasta que surja una necesidad medida.
  • Streaming de Anthropic: forma de red SSE distinta (bloques con tipo de evento); necesita su propia implementación de adaptador.
  • Respaldo de stream en cascada: el reto de diseño es que ante un fallo del primario a mitad de stream el consumidor ya ha almacenado en búfer tokens parciales; reintentar en el respaldo retransmite desde el principio, rompiendo el orden. Un diseño posible es “detener y reproducir desde el principio en el respaldo” con la SPA descartando el búfer parcial en el primer bloque del secundario. Fuera del alcance en el lanzamiento; se incorporaría por su cuenta.

Revertir el cambio de la SPA restaura el renderizado en búfer sin ningún otro cambio de código. La infraestructura de streaming queda en su sitio (es inofensiva cuando la SPA no consume el nuevo evento). La superficie de Protocolo y los ayudantes de transporte pueden quedarse porque las rutas de código en búfer no los usan.

  • ADR-0002 (abstracción de proveedor de LLM): la superficie de Protocolo que este ADR extiende.
  • ADR-0009 (grafo de ejecución con streaming): el marco SSE al que este ADR añade un evento.
  • ADR-0019 (respuesta estructurada del agente): el contrato de modo JSON que los deltas transmitidos llevan a través sin cambios.