Skip to content

English · Español

02 — Memoria: scratchpad vs long-term

🇪🇸 La palabra "memoria" en literatura de agentes confunde dos cosas muy distintas: lo que el agente está pensando ahora (scratchpad, efímero) y lo que el agente quiere recordar entre conversaciones (long-term, persistente). Mantenerlas separadas evita errores estructurales.

Dos tipos, mantenidos aparte

Tipo Vida Formato Qué guarda Leída por
Scratchpad Una corrección Lista append-only de triples (thought, action, observation) La traza actual El planificador, cada paso
Long-term Entre correcciones Almacén estructurado (archivo JSON) Hechos agregados: contadores de errores por aprendiz, overrides de regularidad por verbo Traída a mano cuando es relevante

El scratchpad es la memoria de trabajo del agente. El almacén long-term es una herramienta que el agente puede leer. Mezclarlas — por ejemplo, volcar contenido long-term al prompt del scratchpad a ciegas — infla el contexto y confunde al planificador.

El scratchpad

Forma concreta (src/miniagent/memory.py):

@dataclass
class ScratchpadMemory:
    steps: list[Step] = field(default_factory=list)

    def append(self, thought: str, action: ToolCall, observation: ToolResult) -> None: ...

    def format_for_prompt(self) -> str:
        """Render the trace as the 'TRACE:' block in the planner prompt."""

    def clear(self) -> None:
        """Reset to empty. Called at the start of every correct() call."""

Ciclo de vida:

NEW → ACCUMULATING → FROZEN_FOR_ANSWER → DISCARDED
  • NEW: ScratchpadMemory() recién creado, cero pasos.
  • ACCUMULATING: el planificador añade tras cada tool call.
  • FROZEN_FOR_ANSWER: el planificador va a emitir FinalAnswer; no más pasos.
  • DISCARDED: tras devolver la corrección, se descarta el scratchpad.

Crítico: el scratchpad nunca sobrevive a una corrección. Si accidentalmente lo conviertes en un atributo de clase que persiste entre llamadas a correct(), verás que las correcciones "recuerdan" el verbo de la frase anterior — confuso e incorrecto.

Por qué append-only

Dos razones:

  1. Replay. Con una traza append-only, puedes reproducir la corrección paso a paso para depurar o auditar.
  2. El planificador ve un historial consistente. Mutar pasos anteriores en mitad del bucle confunde al planificador — re-condicionaría sobre un historial que en realidad no ocurrió.

Por qué estructurado, no texto libre

Un "scratchpad" ingenuo es la prosa que el modelo escribió entre acciones ("Déjame pensar... debería llamar a lookup_irregular_verb..."). Podríamos usar esto, pero es lossy y fácil de manipular vía prompt injection.

Usamos una traza estructurada: (thought_summary, action: ToolCall, observation: ToolResult). El thought_summary es un resumen de 1–2 frases extraído del JSON del planificador si está presente, o vacío. La acción y la observación están tipadas y validadas. Esto hace que la traza sea legible por máquina — podemos serializarla a JSON y recargarla para replay o análisis.

Memoria a largo plazo

LongTermMemory es un pequeño almacén persistente. Backend por defecto: un archivo JSON en experiments/32-tutor-demo/longterm.json. (SQLite sería excesivo a esta escala.)

Se persisten tres clases de hechos:

@dataclass
class LongTermMemory:
    # Per-learner mistake counters.
    # Key: (learner_id, verb, error_type) → count
    mistake_counts: dict[tuple[str, str, str], int]

    # Per-verb regularity overrides (e.g., "spelled" vs "spelt" — both acceptable).
    # Key: verb → list of acceptable past forms
    verb_overrides: dict[str, list[str]]

    # Per-learner per-tense accuracy.
    # Key: (learner_id, tense) → {correct, total}
    accuracy_stats: dict[tuple[str, str], dict[str, int]]

Estos son pequeños (el alcance de la Fase 32 es 20 verbos × 1 aprendiz × 5 tiempos), así que el almacén entero cabe en RAM y serializa a unos pocos KB.

Lo que no va en long-term

  • Frases en bruto. Privacidad + ruido. Guarda estadísticas agregadas, no la entrada del usuario.
  • Feedback en texto libre. Difícil de consultar, fácil de derivar. Fuerza la estructura.
  • Índices de embeddings / bases de datos vectoriales. El RAG de la Fase 29 es una herramienta de recuperación separada que el agente puede invocar; no es la memoria propia del agente. (Ver theory/00-motivation.md para por qué.)

Leyendo de long-term hacia el prompt

Cuando el agente corrige una frase con verbo V, puede elegir inyectar:

LEARNER HAS PREVIOUSLY ERRED ON THIS VERB 3 TIMES.
Most common error: incorrect past tense (used "goed" instead of "went").

Esto se convierte en parte del prompt de sistema del planificador para esta corrección. La inyección es explícita y acotada — nunca volcamos el almacén long-term entero. El bound es, digamos, los 3 hechos más relevantes (por simple recencia o conteo).

Cuándo inyectar:

  • La corrección actual involucra un verbo en el que el aprendiz ha errado antes → inyecta el conteo.
  • La corrección actual está en un tiempo en el que el aprendiz tiene baja precisión → inyecta la estadística.
  • En cualquier otro caso → no inyectes nada.

Esta clase de inyección selectiva es el verdadero propósito de la memoria long-term en un agente tutor: no "recordar todo", sino "personalizar la explicación".

Garantías de persistencia

El almacén long-term es solo append-o-update. Los errores pueden incrementarse pero no borrarse (queremos el historial). Los overrides pueden añadirse pero no quitarse (el borrado requiere intervención manual).

Al arrancar el agente: cargar longterm.json si existe; si no, vacío. Al terminar la corrección: actualizar contadores, guardar a disco.

Para la Fase 32 esto es suficiente. Un sistema real de producción añadiría: versionado, migraciones de esquema, lock files para escrituras concurrentes, backups. Nada de eso está en alcance.

Una trampa común: fuga de memoria entre correcciones

Es muy fácil escribir:

class GrammarTutorAgent:
    def __init__(self):
        self.scratchpad = ScratchpadMemory()  # <-- BUG: shared across corrections

    def correct(self, sentence):
        ...

Esto convierte el scratchpad en un atributo de clase, lo que significa que la segunda corrección hereda la traza de la primera. Resultado: el planificador del agente ve contexto obsoleto y se comporta de forma errática.

El fix:

class GrammarTutorAgent:
    def __init__(self): ...

    def correct(self, sentence):
        scratchpad = ScratchpadMemory()  # fresh per call
        ...

tests/test_memory.py debería testear explícitamente que dos llamadas consecutivas a correct() producen trazas independientes. El test es dead simple pero pilla un bug crítico.

Memoria ≠ ventana de contexto

Una confusión común: "la ventana de contexto del modelo es su memoria". Esto es técnicamente cierto a nivel de llamada al LM — pero el bucle del agente convierte al modelo en un sistema con estado al:

  • Construir el prompt desde el scratchpad (que es almacenamiento persistente real entre llamadas al LM).
  • Persistir hechos long-term en disco entre correcciones.

El modelo no tiene estado; el agente sí. La memoria vive en el bucle, no en el modelo.

Lo que este archivo NO cubre

  • Recuperación con base de datos vectorial como memoria. Eso es el RAG de la Fase 29, disponible para el agente como herramienta, no como su memoria central.
  • Memoria episódica vs semántica. Distinción de la ciencia cognitiva. Fuera de alcance.
  • Consolidación de memoria, ciclos de sueño, etc. Fuera de alcance (y algo especulativo para agentes LLM de todos modos).

Siguiente: 03-sandboxing.md