Skip to content

Compresión (compression) de contexto (context) y MoA ​

源码版本v2026.7.20

Responsabilidad ​

Dos tareas: ① cuando el historial se acerca al límite de contexto del provider, resumir/podar mensajes viejos para liberar presupuesto (budget) (context_compressor); ② opcionalmente, agregar la salida de varios modelos vía una ruta Mixture-of-Agents (moa_loop). Ambas se invocan dentro del bucle principal; son los componentes de soporte que «permiten que las conversaciones largas sigan corriendo» y «hacen las respuestas más estables».

Archivos clave ​

Flujo de datos ​

  1. El bucle principal evalúa cada turno (turn) si hace falta comprimir (ver _should_run_preflight_estimate:213).
  2. Al alcanzar el umbral, llama a context_compressor:
  3. El «nuevo espacio» liberado por la compresión se reembolsa parcialmente vía iteration_budget.refund() (agent/iteration_budget.py), permitiendo al bucle principal correr unos turnos más.
  4. Si MoA está activo, moa_loop llama en paralelo a varios providers y los agrega, como mejora o árbitro de la respuesta principal.

La estimación del presupuesto es la base de la decisión de comprimir. Aquí está el núcleo de agent/context_compressor.py:419-437:

python
def _estimate_msg_budget_tokens(msg: dict) -> int:
    """Counts the full ``tool_call`` envelope, not just ``function.arguments``
    — counting only the arguments undercounted parallel-tool turns by 2-15x
    (#28053). Also counts provider replay fields (``codex_reasoning_items``)."""
    content_len = _content_length_for_budget(msg.get("content") or "")
    tokens = content_len // _CHARS_PER_TOKEN + 10  # +10 for role/key overhead
    for tc in msg.get("tool_calls") or []:
        if isinstance(tc, dict):
            tokens += len(str(tc)) // _CHARS_PER_TOKEN
    for key in _REPLAY_BUDGET_KEYS:
        tokens += _serialized_length_for_budget(msg.get(key)) // _CHARS_PER_TOKEN
    return tokens

Dos pozos por los que ya se pasó quedan en el comentario: ① contar solo function.arguments subestima entre 2x y 15x en turnos con llamadas paralelas a herramientas; ② no contar campos de replay como codex_reasoning_items deja mensajes assistant «que parecen pequeños pero son enormes» mal protegidos: la compresión termina demasiado cerca del límite y se vuelve a comprimir una y otra vez (#55572).

La limpieza y el stripping de marcas de persistencia están en agent/context_compressor.py:136-160:

python
def _fresh_compaction_message_copy(msg: Dict[str, Any]) -> Dict[str, Any]:
    """Shallow .copy() propagates ``_db_persisted``; new child session then
    skips every row (#57491). Strip at every copy site."""
    fresh = msg.copy()
    fresh.pop(_DB_PERSISTED_MARKER, None)
    return fresh


def _strip_persistence_markers(messages: List[Dict[str, Any]]) -> None:
    """Terminal sweep — invariant holds regardless of intermediate copy sites."""
    for msg in messages:
        if isinstance(msg, dict):
            msg.pop(_DB_PERSISTED_MARKER, None)

«Dos líneas de defensa»: _fresh_compaction_message_copy elimina la marca en cada punto de copia; _strip_persistence_markers hace un sweep completo al final de compress(). La primera declara intención; la segunda es un respaldo estructural. El recorte de imágenes (en agent/context_compressor.py:497-515, _strip_image_parts_from_parts) funciona igual: reemplaza los parts de image/image_url/input_image por un placeholder de texto; cuando no hay imagen, devuelve None para que el llamador se salte la sustitución y no genere un list nuevo sin sentido en cada mensaje.

Motivo de diseño ​

¿Por qué «estimación» en lugar de un conteo exacto de tokens? El tokenizer del provider no es público (sobre todo los campos ocultos de los modelos de reasoning); tiktoken es lento y no precisa. El módulo usa una fórmula fija de estimación por caracteres + 10 caracteres de overhead, suficiente para decidir «¿comprimir o no?» y «¿cómo proteger la cola?» de forma conservadora — no necesita ser exacto, solo usar la misma regla que la estimación preflight (ver _should_run_preflight_estimate en turn_context). Si las dos reglas divergen, aparece el deadlock de «preflight dice que no hace falta, compresión interna dice que no progresa».

¿Por qué conservar el nombre de la llamada a herramienta y las menciones de ruta? Cuando un LLM resume mensajes viejos, suele tragarse operaciones clave del estilo «se llamó a write_file(/tmp/foo.py)». El turno siguiente lee «se habló de un archivo», pero no sabe a qué ruta se escribió, y repite la creación o lee en el lugar equivocado. _extract_tool_call_name_and_args / _collect_path_mentions pegan la información dura junto al summary; el coste son unas líneas extra de tokens, el beneficio es que las llamadas a herramienta posteriores no se quedan amnésicas.

El refund se devuelve a iteration_budget y no se añade directamente al presupuesto para que max_iterations no quede en pura decoración — si cada compresión sumara turnos, una conversación larga podría correr indefinidamente. El refund solo devuelve las iteraciones de llamada a herramienta que la propia compresión ahorró — «el tope sigue siendo el tope».

Límites y fallos ​

  • Compresión que no progresa: _compression_made_progress exige que el conteo de tokens baje >5% para considerar que hay progreso. Un turno que solo ahorra 2% se marca como «sin progreso» para evitar dar vueltas en vacío; el bucle principal en ese caso hace auto-reset y abre sesión nueva.
  • Marca de persistencia residual: si la compresión produce un objeto con _db_persisted, el state.db de la child session nueva lo salta y el transcript compactado se pierde por completo en la DB (#57491). El sweep terminal al final de compress() lo cubre.
  • El summary se lee como nueva instrucción: un modelo débil interpreta el ## Active Task del summary como una petición de usuario a la que responder (#11475, #14521). _SUMMARY_END_MARKER añade un separador explícito; en casos límite de alternancia se envuelve además con _MERGED_PRIOR_CONTEXT_HEADER / _MERGED_SUMMARY_DELIMITER para evitar ambigüedad.
  • Revivir un prefijo histórico: _HISTORICAL_SUMMARY_PREFIXES guarda prefijos de versiones anteriores. Al hacer resume a un lineage nuevo, los prefijos viejos se eliminan a la vez, para que una directiva stale no se quede empotrada en el body y siga secuestrando las respuestas.

Resumen ​

El compresor es el salvavidas de las conversaciones largas; la decisión central es «qué descartar, qué conservar»: prioriza las llamadas a herramienta y las menciones de ruta, porque perderlas dejaría «amnésicos» a los turnos siguientes. MoA es una mejora ortogonal, activable. Ninguno de los dos cambia la estructura del bucle principal; solo se insertan en el momento adecuado.

Sitio de aprendizaje comunitario no oficial. Basado en el código fuente de NousResearch/hermes-agent (licencia MIT). · Privacidad · Términos · Acerca de