El contexto prolongado no es gratuito: construí una capa segura de poda rápida que hace que los sistemas LLM funcionen

He trabajado en que el estado de la conversación tiende a crecer rápidamente con el tiempo. Es común reenviar grandes porciones del historial en cada turno, incluidos resultados de herramientas más antiguas, recuperaciones repetidas de RAG y contexto que ya no es relevante. A medida que esto se acumula, las indicaciones pueden volverse significativamente más grandes, lo que puede aumentar el costo de inferencia y la latencia y, en algunos casos, afectar el rendimiento del razonamiento.

Construí una canalización determinista que elimina este estado redundante antes de que el mensaje llegue al modelo. La versión que implementé evita llamadas LLM, incrustaciones y dependencias externas. Depender estrictamente de los componentes estándar de la biblioteca garantiza que cada decisión de poda siga siendo totalmente determinista y reproducible.

El seguimiento de estado se ejecuta en tres pasos distintos: eliminación de contexto caducado, eliminación de contexto duplicado y restauración de dependencia. El tercer pase es lo que ayuda a que los dos primeros sean más seguros en la práctica. Garantiza que nada de lo que dependa un mensaje posterior se elimine accidentalmente.

Mientras lo construía, me encontré con dos errores que cambiaron el diseño. Mi primer corpus de referencia utilizó una cantidad fija de duplicados y llamadas a herramientas obsoletas, lo que hizo que los porcentajes de reducción se redujeran a medida que crecían las conversaciones. Eso no reflejaba lo que esperaría del comportamiento en el mundo real.

Al principio, mi lógica de restauración de dependencia tampoco se probó en absoluto porque mis datos sintéticos nunca crearon un caso en el que se eliminara un mensaje requerido. Ambos problemas se tratan aquí, junto con cómo solucionarlos cambió los resultados.

Después de corregir la canalización, la comparé con tres cargas de trabajo: chat simple, un asistente RAG y un agente con muchas herramientas. Cada uno fue probado en cinco tamaños de conversación, para un total de 15 configuraciones en dos máquinas diferentes. En estas ejecuciones, se conservaron todos los datos requeridos etiquetados. El sistema también alcanzó un punto fijo estable después de una sola pasada, lo que significa que podar un mensaje ya podado no produce más cambios.

La reducción de tokens depende de la carga de trabajo. Es aproximadamente del 2 al 4 por ciento para el chat simple, del 27 al 32 por ciento para un asistente de RAG y del 33 al 34 por ciento para un agente con muchas herramientas. Incluso con 2.000 vueltas y 131.000 tokens, el preprocesamiento se mantuvo por debajo de los 50 milisegundos.

El código completo, las 35 pruebas y la salida sin formato del terminal se incluyen a continuación para que pueda ejecutar el proceso usted mismo.

Código completo: https://github.com/Emmimal/prompt-pruning-layer/

Cada mensaje que envío se vuelve más pesado

Seguí viendo exactamente el mismo patrón aparecer en cada agente de larga duración que construí. Una conversación comienza perfectamente limpia. Cincuenta vueltas después, es un completo desastre.

En ese quincuagésimo turno, la carga útil que está enviando en cada solicitud incluye el aviso del sistema, el historial de chat completo, cuatro resultados de la herramienta (dos de los cuales están completamente obsoletos porque la herramienta se ejecutó dos veces), seis fragmentos recuperados (tres son casi duplicados porque el usuario volvió a un tema antiguo), un resultado SQL de hace veinte turnos que simplemente está ahí, y una única preferencia de usuario declarada una vez y nunca volvió a aparecer.

Nada de esto es técnicamente incorrecto. Cada pieza tenía sentido en el momento exacto en que fue inyectada. El verdadero problema es que nunca se aclara nada. El mensaje se convierte en un registro de solo agregar de cada evento histórico, y usted está volcando todo en el modelo, en cada turno, de manera indefinida.

Esto no es sólo un problema de almacenamiento. El rendimiento del razonamiento se degrada de manera mensurable a medida que aumenta la longitud de la entrada, incluso cuando el contenido agregado es irrelevante para la tarea.[1], y los modelos son peores a la hora de utilizar información enterrada en medio de un contexto largo que información cerca de los bordes[2].

La reacción instintiva inmediata cuando ves esta hinchazón es simplemente un truncamiento básico. Corta los últimos N mensajes y descarta todo lo demás. Es literalmente una línea de código. El problema es que rompe silenciosamente tus cadenas de conversación de maneras que ni siquiera te darás cuenta hasta que explote ante un usuario real:

Turno 3: Usuario: Mi formato de salida preferido es CSV. Turno 4: Asistente: Entendido. … Turno 47: Usuario: Exportar los resultados.

Si su ventana está limitada en los últimos 20 turnos, el turno 3 desaparece en el turno 47. El asistente pierde el contexto de que el usuario solicitó un CSV. Esos datos no estaban obsoletos; era una dependencia dura, y el simple truncamiento posicional no puede diferenciar entre un contexto antiguo y prescindible y un contexto antiguo del que todavía depende un giro posterior.

Esa es la restricción de diseño exacta que aborda este proyecto. Cualquier mecanismo que elimine el estado redundante debe distinguir entre esas dos categorías. La actualidad por sí sola no es suficiente.

Tomar prestada una idea de los sistemas operativos

Aquí está el replanteamiento sobre el que construí el resto de esto: un sistema operativo decide constantemente qué páginas permanecen residentes en la RAM y cuáles son desalojadas. Una conversación LLM de larga duración tiene exactamente el mismo problema, excepto que nada desempeña el papel de administrador de memoria. El contexto simplemente se acumula para siempre porque ningún proceso tiene la tarea de decidir qué deja de ganar su lugar en el mensaje.

La podadora rápida de este artículo es la pieza que falta.

La arquitectura de poda contextual de múltiples pasos utilizada para eliminar la sobrecarga de tokens redundantes y resolver dependencias estructurales antes de la compilación de LLM. Imagen por autor

Cada estado de conversación (un turno de usuario, un turno de asistente, una salida de herramienta, un fragmento recuperado) es un objeto Mensaje con una función, un número de turno y algunos metadatos de contabilidad:

@dataclass clase Mensaje: id: str rol: str contenido: str giro: int tool_call_key: Opcional[str] = Ninguno expires_after_turn: Opcional[int] = Ninguno define_keys: lista = campo(default_factory=list)

Tres pases recorren esa lista en orden. Cada uno es una función pura: mensajes que entran, una lista filtrada que sale. Ningún modelo está involucrado en ninguna parte del paso de poda en sí.

¿Por qué no hay ningún modelo dentro de la podadora?

Podría haber usado un modelo de incrustación para calificar la relevancia del mensaje, lo que habría hecho que esto fuera más confuso y mucho más fácil de escribir. No lo hice, y no es por el bien de la pureza.

Una vez que una decisión de poda depende del juicio de un modelo, se pierde la capacidad de razonar sobre cómo será una conversación en el siguiente turno. El mismo insumo ya no garantiza el mismo resultado. Una capa de poda destinada a hacer más predecible un sistema de producción no debería ser la parte menos predecible del mismo. Aquí todo se ejecuta en búsquedas de clases de datos, expresiones regulares y dictados, que es exactamente el conjunto de herramientas que necesita este problema.

Paso 1: Eliminación del contexto caducado

Si se llama a una herramienta más de una vez con la misma clave (la misma consulta de búsqueda, la misma búsqueda de SQL, la misma lectura de archivo), solo el resultado más reciente sigue siendo confiable. Todo lo anterior bajo esa clave ha caducado.

def _pass1_expired_context_elimination(self, mensajes): last_occurrence = {} para m en mensajes: if m.tool_call_key: last_occurrence[m.tool_call_key] = m.id mantenido, eliminado =[],[]para m en mensajes: si m.tool_call_key y last_occurrence[m.tool_call_key] != m.id: eliminado.append(m) else: keep.append(m) return mantenido, eliminado

Paso 2: Eliminación de contexto duplicado

Los canales de recuperación constantemente obtienen pasajes idénticos o casi duplicados, especialmente cuando un usuario regresa a un tema anterior. Este pase normaliza los espacios en blanco y las mayúsculas, mantiene solo la primera aparición y descarta todos los duplicados posteriores.

def _pass2_duplicate_context_elimination(self, mensajes): visto, mantenido, eliminado = {},[],[]para m en mensajes: si m.role == ROLE_RETRIEVED_DOC: norma = " ".join(m.content.lower().split()) si norma se ve: eliminado.append(m) continuar visto[norma] = m.id mantenido.append(m) regresar mantenido, eliminado

Paso 3: Restauración de dependencia (y el error que me hizo construirlo correctamente)

Este pase es la razón por la que los dos primeros son seguros para correr. Si el Paso 1 o el Paso 2 arrojan un mensaje que resulta ser el único lugar donde se definió un hecho aún referenciado, este pase lo captura y lo devuelve.

El mecanismo es intencionalmente simple: un mensaje se marca a sí mismo con una etiqueta DEFINE: literal, y un mensaje posterior hace referencia a él usando REF:. Si un REF sobrevive en el conjunto final conservado pero su DEFINE coincidente se eliminó en sentido ascendente, Dependency Restoration restaura ese mensaje faltante en la lista.

def _pass3_dependency_restoration(self, all_messages, Keep_messages, remove_messages): keep_ids = {m.id para m en keep_messages} by_id = {m.id: m para m en all_messages} key_definer = {} para m en all_messages: para clave en m.defines_keys: key_definer[key] = m.id referenced_keys = set() para m en keep_messages: referenced_keys.update(m.references()) restaurado =[]para clave en claves_referidas: definer_id = key_definer.get(key) si definer_id y definer_id no están en keep_ids: restaurado_msg = by_id[definer_id] keep_messages.append(restored_msg) keep_ids.add(definer_id) restaurado.append(restored_msg) keep_messages.sort(key=lambda m: (m.turn, m.id)) devuelve mensajes_mantenidos, restaurado

Aquí está el error. Ejecuté mi primer punto de referencia completo en las tres cargas de trabajo y cinco tamaños, y cada fila imprimió "Restaurado (deps): 0". Todos. Mi primera reacción fue que esto parecía fantástico: un historial de seguridad perfecto. No lo fue. Significaba que el Paso 3 nunca había restaurado un solo mensaje en ninguna ejecución de ningún tamaño.

Regresé a mi generador de corpus para descubrir por qué, y la respuesta fue vergonzosa una vez que la vi. Mis conversaciones sintéticas solo adjuntaron marcadores DEFINE a mensajes simples de usuario, mientras que el Paso 1 y el Paso 2 solo eliminan los resultados de las herramientas y los documentos recuperados. Las dos categorías nunca se superpusieron. Dependency Restoration estaba en el código base, completamente escrito, pero no probado en absoluto por mi propio punto de referencia porque nada de lo que generé le dio una razón para activarse.

La solución fue permitir que algunas salidas de herramientas también definieran una dependencia, de la misma manera que una llamada real a la herramienta "obtener configuración de usuario" podría revelar un hecho del que depende la conversación más adelante, aunque esa llamada a la herramienta exacta podría ser reemplazada y marcada como caducada por el Paso 1. Una vez que agregué eso, los números cambiaron inmediatamente: 2 restauraciones en la conversación herramienta-agente más pequeña, subiendo a 127 en la más grande. Los datos requeridos permanecieron preservados al 100 por ciento todo el tiempo, pero ahora ese número en realidad significaba algo, porque el pase que se estaba probando era capaz de fallar y no lo hizo.

Quiero ser directo sobre la limitación que sigue presente incluso después de la solución: la detección de dependencia es una coincidencia literal de identificadores, no una comprensión. Capta una REF exacta que coincide con una DEFINE exacta. No detectará a un usuario parafraseando y preguntando "qué formato mencioné antes" sin una etiqueta coincidente. Un solucionador de dependencias semánticas necesitaría un modelo de incorporación o una llamada LLM, y eso está explícitamente fuera de lo que hace este canal determinista. Prefiero ofrecer una garantía más limitada que puedo probar que una más amplia que no puedo.

Objetivos de diseño y por qué existe cada uno.

Determinista. La misma entrada siempre produce la misma salida. Mantener el modelo fuera del circuito elimina la variación entre ejecuciones y cualquier riesgo de alucinar lo que debería permanecer en el mensaje.

Seguro para dependencias. Nunca deja caer en silencio un hecho que todavía necesita un turno posterior. El truncamiento posicional carece por completo de esta propiedad, lo que lo hace no negociable aquí. Un podador que ahorra el 40 por ciento de los tokens y que ocasionalmente interrumpe una conversación es una compensación peor que uno que ahorra el 4 por ciento y nunca rompe nada.

Idempotente. Hacer funcionar la podadora por segunda vez no hace nada. Si eso no es cierto, no se puede volver a podar de manera segura cada giro de una conversación en crecimiento sin correr el riesgo de agravar la deriva.

Ligero. El paso de poda nunca debe convertirse en el cuello de botella exacto para el que fue creado.

El punto de referencia: tres cargas de trabajo y un error que casi cometí

Mi primera versión del generador de corpus sintético seleccionó un número fijo de pasajes duplicados y llamadas repetidas a herramientas (seis llamadas a herramientas y ocho duplicados) independientemente de la duración de la conversación. Lo ejecuté en cinco tamaños y el porcentaje de reducción disminuyó a medida que la conversación se hizo más larga: 9,9 por ciento a 50 vueltas, cayendo a 0,3 por ciento a 2000 vueltas. Esto se remonta al tráfico de producción real y no es defendible. Si la cantidad de desperdicio en un punto de referencia es solo una constante que seleccioné personalmente, cualquiera que lo lea tiene razón al preguntar si construí el punto de referencia para demostrar que el algoritmo funciona.

Así que descarté ese generador y lo reconstruí en torno a un modelo de carga de trabajo explícito: recuperación por turno, tasa de superposición de recuperación, tasa de llamadas de herramientas y tasa de repetición de herramientas. Todos estos se arreglaron antes de ejecutar un punto de referencia único, basado en lo que parecía un comportamiento de producción plausible, en lugar de ajustarse después para alcanzar un número que me gustaba. De ahí surgieron tres cargas de trabajo:

Charla normal. Sin recuperación, llamadas de herramientas ocasionales, en su mayoría intercambios ordinarios de ida y vuelta.

Asistente del RAG. Recupera documentos en cada paso, con una posibilidad real de que cualquier pasaje determinado se superponga a algo recuperado recientemente, porque los usuarios vuelven a visitar los temas y la recuperación vuelve a mostrar los mismos fragmentos.

Agente de herramientas. Llamadas frecuentes a través de cinco tipos de herramientas (búsqueda, SQL, calculadora, sistema de archivos, recuperación web), alta tasa de repetición, modelado de algo que vuelve a planificar y consultar constantemente.

Cada corpus sintético también incluye información básica: cada mensaje del que depende algún mensaje posterior se etiqueta como requerido desde el principio. Entonces, "¿la poda mantuvo todo lo necesario?" es una verificación de etiquetas conocidas, no una suposición.

Aquí está el resultado completo. Las 15 configuraciones, ni una sola porción:

Carga de trabajoTurnosTokens antesTokens despuésReducciónHechos mantenidosIdempotenteGastos generalesChat normal501,1751,1531.87%SíSí0.17 msChat normal2004,8204,6293.96%SíSí0.79 msChat normal50012,07811,6603.46%SíSí1.82 msNormal chat100024,37923,3814.09%SíSí3.79 msChat normal200048,24146,5143.58%SíSí8.27 msAsistente RAG503,4942,55126.99%SíSí0.60 msAsistente RAG20014,0099,59931.48%SíSí2.18 Asistente msRAG50035,34724,13331.73%SíSí6.19 Asistente msRAG100070,35847,95031.85%SíSí11.89 Asistente msRAG2000140,76695,08732.45%SíSí28.95 msTool agente503,2792,17633.64%SíSí0.47 agente msTool20012,9558,58533.73%SíSí2.04 agente msTool50032,41221,67733.12%SíSí6.46 agente msTool100065,36643,35133.68%SíSí15.02 msTool agente2000131,59187,62533.41%SíSí43.04 ms
Gráfico de líneas que compara el porcentaje de reducción de tokens en tres cargas de trabajo de LLM (chat normal, asistente de RAG, agente de herramientas) a medida que el tamaño de la conversación aumenta de 50 a 2000 turnos.
Reducción de tokens por carga de trabajo y tamaño de la conversación: la poda rápida elimina del 2 al 4 por ciento de los tokens en el chat simple, pero del 27 al 34 por ciento una vez que la recuperación o las llamadas repetidas a herramientas entran en escena. Imagen por autor

Cada fila dice Hechos guardados: Sí e Idempotente: Sí. No la mayoría de las filas. Los quince.

El patrón por carga de trabajo tiene sentido una vez que se observa de dónde provienen realmente los desechos. El chat normal apenas recupera nada y rara vez repite una llamada de herramienta, por lo que el Paso 1 o el Paso 2 no tienen casi nada que captar; permanece alrededor del 4 por ciento sin importar cuánto tiempo dure la conversación. El asistente RAG recupera cada giro con una superposición real, por lo que la eliminación de contexto duplicado lleva la mayor parte del peso, aterrizando alrededor del 32 por ciento. El agente Tool combina ambos problemas (repetición frecuente de herramientas y superposición de recuperación) y alcanza la reducción más alta del 33 al 34 por ciento.

Diferentes cargas de trabajo acumulan diferentes tipos de residuos. La podadora responde directamente a cualquier desperdicio que se encuentre frente a ella, en lugar de producir un número plano que sugeriría que el punto de referencia fue sometido a ingeniería inversa para alcanzar un objetivo.

Gráfico de tres paneles que compara el recuento de mensajes antes y después de la eliminación de avisos para cargas de trabajo de chat normal, asistente RAG y agente de herramientas, en cinco tamaños de conversación.
Recuento de mensajes antes y después de la poda, por carga de trabajo: la brecha entre las dos líneas es la firma visual directa de cuánto contexto redundante realmente acumula cada tipo de carga de trabajo. Imagen por autor

Si sólo tengo que quedarme con un resultado de todo este punto de referencia, es la propiedad de seguridad: 15 de 15 configuraciones conservaron el 100 por ciento de los datos requeridos. Cero dependencias faltantes, en tres cargas de trabajo estructuralmente diferentes y un rango de duración de conversación 40 veces mayor. El truncamiento por posición no puede ofrecer eso. Para mí, esta fue la señal más importante a la hora de evaluar si el enfoque podría utilizarse en producción.

Ese número también se calcula, no se cuenta manualmente. El propio script de referencia cuenta cuántos de los 15 pares (carga de trabajo, tamaño) conservaron cada hecho requerido y cuántos alcanzaron el punto fijo idempotente, imprimiendo un bloque de resumen agregado después de las 15 ejecuciones detalladas:

============================================================= RESUMEN =========================================================================== Configuraciones ejecutadas: 15 (3 cargas de trabajo x 5 tamaños) Datos requeridos preservados: 15/15 Punto fijo alcanzado (idempotente): 15/15 Rango de reducción de tokens de carga de trabajo Hechos preservados Idempotente Chat normal 1,9-4,1% SI SI Asistente de trapo 27,0-32,5% SI SI Agente de herramientas 33,1-33,7% SI SI

Escribí este cheque porque me sorprendí contando a mano las filas de la tabla para un borrador inicial. Esa métrica pertenece a la salida del script, no a mis propios ojos entrecerrando los ojos ante un registro de terminal. Si una ejecución de prueba alguna vez llega a menos de 15/15, significa que rompí la podadora y tengo una regresión que buscar, no un error tipográfico que editar en la publicación.

Dejé una métrica específica fuera de los números finales: tokens eliminados por milisegundo de sobrecarga de ejecución. El código lo calcula: alcanza un máximo de alrededor de 4000 tokens por milisegundo en pequeñas ejecuciones de agentes de herramientas y alcanza entre 900 y 1700 tokens en escalas más grandes. Permanece en el código base como un campo interno porque ayuda a realizar un seguimiento de los costos de escalado, pero pertenece fuera de la tabla principal. Los lectores no pueden actuar en consecuencia como lo harían con el recuento de tokens sin procesar, el porcentaje de reducción o la sobrecarga de milisegundos. Tres métricas directas que muestran una clara compensación son mejores que una cuarta que actúa como una novedad.

El resultado de la idempotencia es la parte que más me gustó seguir. Probar prune(prune(x)) == prune(x) significa que la tubería llega a un punto fijo estable en la primera pasada. Ejecutarlo nuevamente en un mensaje ya eliminado no cambia nada:

Diagrama de flujo de idempotencia que muestra un
Representación visual de la naturaleza idempotente del algoritmo de poda, lo que demuestra que las operaciones secuenciales en un mensaje previamente comprimido producen estados idénticos sin mayor degradación estructural. Imagen por autor

Eso descarta la oscilación. También descarta la contracción acumulativa entre turnos si vuelve a podar cada mensaje de una conversación en crecimiento, que es exactamente como se ejecuta esto en producción.

Gráfico de líneas que muestra la sobrecarga de poda rápida en milisegundos versus el tamaño de la conversación en turnos, para tres tipos de cargas de trabajo de LLM, manteniéndose por debajo de 50 milisegundos incluso con 2000 turnos.
La reducción de gastos generales aumenta con el tamaño de la conversación, pero se mantiene por debajo de 50 ms incluso en una conversación de 131 000 tokens y 2000 turnos. Imagen por autor

Reproduciéndolo en una segunda máquina.

Ejecuté el punto de referencia completo en un contenedor de Linux que ejecuta Python 3.12.3, luego nuevamente en Windows 11 en PyCharm usando Python 3.12 en un venv separado. Cada recuento de tokens y mensajes coincidió exactamente en ambas máquinas. Solo se movieron los tiempos de milisegundos, lo cual es estándar para diferentes hardware, e incluso esos se mantuvieron por debajo de los 50 milisegundos para la conversación más grande y con más herramientas en ambas configuraciones.

Una cosa que noté al comparar las dos ejecuciones y quiero ser claro: el tiempo de compilación rápida (el tiempo para serializar la lista de mensajes final en una cadena) ocasionalmente resultó más lento después de la poda que antes en la carga de trabajo de Chat Normal. No por mucho, menos de un milisegundo, pero la dirección era hacia atrás.

Mi lectura es que el Chat Normal sólo elimina del 2 al 4 por ciento de los mensajes, por lo que los tamaños de las listas de antes y después son casi idénticos. En operaciones de menos de dos milisegundos, la fluctuación del sistema y las pausas en la recolección de basura inundan fácilmente la señal real. Agregar una llamada de calentamiento y usar la mediana de 30 carreras estabilizó en gran medida la métrica, pero la anomalía aún aparece en esa carga de trabajo. Es ruido en una escala donde el delta es menor que el error de medición, así que lo dejé sin editar en lugar de manipular los datos.

Lo que este punto de referencia no mide deliberadamente

Este punto de referencia aísla la reducción de tokens y los gastos generales de poda porque esas son las métricas que realmente controla el canal. La latencia LLM de un extremo a otro es una variable completamente separada. Depende de la arquitectura del proveedor, el procesamiento por lotes, el almacenamiento en caché regional y las condiciones de la red que este proyecto no puede ver. Intentar convertir un porcentaje de reducción de token directamente en un delta de latencia significa inventar una constante de conversión arbitraria.

La realidad básica es simple: recortar entre el 30 y el 34 por ciento de los tokens de entrada significa que el modelo hace menos trabajo por llamada. En general, el costo de inferencia y la latencia tienden a aumentar con el tamaño del mensaje.[4], lo que lo convierte en una palanca de costos útil. Pero un número de latencia real requiere un pase de validación en vivo con su proveedor específico. Publicar una cifra de latencia genérica aquí significaría hacer afirmaciones sobre la infraestructura que no controlo, en lugar de evaluar la capa de poda en sí.

Cómo encaja esto en un bucle de agente real

El podador vive exactamente en un lugar: justo después de que el historial de conversaciones se reúne durante un turno y justo antes de que se serialice en la cadena de mensajes final.

de Prompt_pruning importar PromptPruner, PromptBuilder podadora = PromptPruner() constructor = PromptBuilder() def handle_turn(estado_conversación, mensaje_nuevo_usuario): estado_conversación.append(mensaje_nuevo_usuario) mensajes_podados, informe = podadora.prune(estado_conversación) solicitud = constructor.build(mensajes_podados) respuesta = call_llm(prompt) conversacion_state.append(make_assistant_message(respuesta)) devolver respuesta

Debido a que la canalización es idempotente, es seguro llamar a prune() en cada turno de una conversación en crecimiento. Ejecutar la podadora diez veces sobre un historial podado nueve veces produce exactamente el mismo resultado que una ejecución limpia desde cero. Esto hace que "ejecutarlo en cada turno" sea un valor predeterminado seguro, eliminando la necesidad de realizar un seguimiento del estado o el razonamiento sobre pases anteriores.

La única decisión de integración que queda es cómo se adjuntan las etiquetas REF y DEFINE a los mensajes. Aquí son marcadores literales dentro del contenido del mensaje, que es el mecanismo más simple para un prototipo. Un sistema de producción probablemente los adjuntaría como metadatos estructurados en el objeto del mensaje para que las etiquetas nunca se filtren en el texto sin formato que lee el modelo. La lógica del paso 3 sigue siendo la misma en ambos sentidos. Un proceso ascendente todavía tiene que determinar qué cuenta como una dependencia que vale la pena etiquetar, porque el Paso 3 solo puede restaurar lo que se le indica explícitamente que rastree.

Lo que esto no cubre

La detección de dependencias es literal, no semántica. Si una referencia está parafraseada y carece de una etiqueta coincidente, el script la omitirá.

Estas cargas de trabajo también son completamente sintéticas. Elegí los tres conjuntos de parámetros basándome en un comportamiento de producción plausible, no en una telemetría real. Si tiene registros de producción, el siguiente paso obvio es regenerar estas tres categorías de cargas de trabajo a partir del uso real. Los números cambiarán dependiendo de cómo sea su tráfico real.

Este canal omite la compresión semántica, las incrustaciones y la poda con puntuación LLM. Estos son enfoques alternativos válidos, pero evitarlos mantiene esta implementación completamente determinista y libre de dependencia. LLMLingua es un excelente ejemplo de alternativa aprendida; Utiliza un modelo de lenguaje pequeño para puntuar y soltar tokens, alcanzando índices de compresión mucho más altos que este script.[3]. Elegir entre ellos es una compensación directa: se intercambia determinismo y ejecución sin dependencia por una compresión más estricta.

Los recuentos de tokens también son aproximaciones. El script utiliza una heurística de límites de puntuación y espacios en blanco en lugar de un tokenizador de subpalabras de producción como tiktoken. Debido a que la heurística se ejecuta consistentemente antes y después de la poda, los porcentajes de reducción relativa siguen siendo precisos, incluso si los números absolutos no coinciden perfectamente con un tokenizador oficial.

Finalmente, no existe una medición directa de la latencia por las razones de infraestructura detalladas anteriormente.

¿Dónde tomaría esto a continuación?

Aquí tienen sentido dos ampliaciones en lugar de ampliar los tres pasos existentes.

El primero es un enfoque híbrido. Mantenga estos tres pases deterministas como una primera etapa rápida y segura, luego entregue el resultado a una herramienta de compresión con reconocimiento de incrustación o con puntuación LLM como LLMLingua. Esto detecta la redundancia semántica que la coincidencia de identificadores literales pasa por alto, como dos pasajes que dicen lo mismo con palabras diferentes.

Ejecutar primero los pases deterministas preserva la garantía de seguridad. Si la etapa aprendida se comporta mal, la conversación vuelve a la base determinista en lugar de fallar de manera impredecible. Desde el punto de vista arquitectónico, el pase aprendido solo funciona en la salida del Paso 3. Un error de modelo en el paso de compresión podría reducir un mensaje de manera demasiado agresiva, pero no puede reintroducir una falla de dependencia que los pases deterministas ya eliminaron.

La segunda extensión es cerrar la brecha entre los datos sintéticos y la realidad de la producción. El siguiente paso es regenerar estas mismas tres categorías de carga de trabajo a partir de seguimientos de producción reales. Mantener idéntica la metodología de referencia (los mismos parámetros fijos, etiquetas de dependencia de verdad sobre el terreno y barrido de 15 configuraciones) garantiza que los resultados sigan siendo directamente comparables con estas cifras publicadas y, al mismo tiempo, se reemplazan las conjeturas con telemetría real.

Es probable que los registros de uso real cambien las métricas de RAG y del agente de herramientas en cualquier dirección. El tráfico del mundo real no significa automáticamente una mayor compresión. Por ejemplo, un sistema de producción con una agresiva deduplicación ascendente podría ya eliminar el desperdicio que supone este modelo sintético. Sería un hallazgo muy útil por derecho propio, más que un fracaso.

Un tercer punto más pequeño a tener en cuenta: la convención REF/DEFINE utilizada aquí es sólo un marcador de posición. Un sistema de producción debería derivar estas etiquetas automáticamente a partir de datos estructurados, argumentos de llamada de herramientas, variables de sesión o configuraciones explícitas del usuario, en lugar de depender de marcadores de texto manuales.

Si bien la lógica determinista en el Paso 3 sigue siendo idéntica en ambos sentidos, el valor real de esta canalización depende completamente de qué tan limpiamente se pueden generar etiquetas de dependencia precisas en sentido ascendente. Se trata de un problema de integración específico de la arquitectura, más que algo que una biblioteca de poda de uso general pueda resolver de forma inmediata.

Los sistemas de servicio de producción ya tratan la hinchazón rápida como un problema de administración de memoria en la capa de infraestructura, desalojando y compartiendo páginas de caché KV de manera muy similar a como un sistema operativo maneja la memoria física.[4]. Puede pensar en este proyecto como si estuviera operando una capa por encima de eso, en la fase de construcción inmediata, antes de que la solicitud llegue a la infraestructura.

Las dos capas no compiten. Un mensaje más pequeño y deduplicado proporciona una mejor entrada a un caché bien administrado en lugar de actuar como un reemplazo.

La conclusión principal

Si se preserva un hecho crítico, la canalización debe demostrar esa retención en lugar de asumirla basándose en la ausencia de errores obvios. Este principio guió el diseño del sistema.

Las conversaciones de larga duración no siempre requieren un modelo más amplio e inteligente para decidir qué recordar. A menudo, la solución más sólida es un sistema predecible de tres pasos que puede demostrar mediante programación lo que no perdió.

Fuentes

[1]Levy, M., Jacoby, A. y Goldberg, Y. (2024). Misma tarea, más tokens: el impacto de la longitud de la entrada en el rendimiento del razonamiento de modelos de lenguaje grandes. En Actas de la 62ª Reunión Anual de la Asociación de Lingüística Computacional (Volumen 1: Artículos extensos) (págs. 15339-15353). Asociación de Lingüística Computacional. https://doi.org/10.18653/v1/2024.acl-long.818

[2]Liu, NF, Lin, K., Hewitt, J., Paranjape, A., Bevilacqua, M., Petroni, F. y Liang, P. (2024). Perdido en el medio: cómo los modelos de lenguaje utilizan contextos largos. Transacciones de la Asociación de Lingüística Computacional, 12, 157-173. https://doi.org/10.1162/tacl_a_00638

[3]Jiang, H., Wu, Q., Lin, C.-Y., Yang, Y. y Qiu, L. (2023). LLMLingua: indicaciones de compresión para inferencia acelerada de modelos de lenguaje grandes. En Actas de la Conferencia de 2023 sobre métodos empíricos en el procesamiento del lenguaje natural (págs. 13358-13376). Asociación de Lingüística Computacional. https://doi.org/10.18653/v1/2023.emnlp-main.825

[4]Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, CH, González, JE, Zhang, H. y Stoica, I. (2023). Gestión eficiente de la memoria para modelos de lenguaje grandes que sirven con PagedAttention. En Actas del 29º Simposio sobre principios de sistemas operativos (SOSP '23) (págs. 611–626). Asociación de Maquinaria de Computación. https://doi.org/10.1145/3600006.3613165

Todo el código, los números de referencia y los resultados de las pruebas de este artículo son míos, se generan ejecutando el código base incluido directamente y se reproducen en dos máquinas independientes. No se utilizaron conjuntos de datos propietarios, textos protegidos por derechos de autor ni códigos de terceros para crear o comparar este sistema. Todo el código, el generador de corpus, el podador, el arnés de referencia y las 35 pruebas están disponibles en el repositorio vinculado a continuación.

https://github.com/Emmimal/prompt-poning-layer