Inicio / Artículos / Memoria de hilo para un agente de soporte en LangGraph con checkpoints en SQLite

Memoria de hilo para un agente de soporte en LangGraph con checkpoints en SQLite

Cómo un agente de soporte en LangGraph conserva el contexto de varios turnos con SqliteSaver e identificadores de hilo, y por qué la memoria de hilo a corto plazo no es memoria a largo plazo.

1330 palabras

Un agente de soporte capaz de buscar en la documentación de LEPA es útil, pero aun así se tropieza con el segundo mensaje de un chat. Imagina que un usuario escribe una sola palabra y el agente le pregunta a qué área se refiere.

Turn 1: "help"
Agent: Which area — login, grades, reports…?

El usuario responde con el problema real y, si nada guardó el primer intercambio, el agente empieza desde cero.

Turn 2: "Why aren't grades showing?"
Agent: …starts from zero unless something remembers Turn 1

El soporte es un diálogo, así que el agente necesita una forma de recordarlo. A continuación se explica la memoria que se construyó de verdad para el agente de soporte de LEPA y también la que se dejó fuera a propósito.

Por qué un endpoint sin estado rompe las aclaraciones

Cada petición HTTP llega sin historial. Para el servidor, cada llamada parece un visitante nuevo.

User → Agent → answer → forget
User → Agent → answer → forget
User → Agent → answer → forget

Eso arruina cualquier flujo en el que el agente pregunta primero. Una respuesta a «¿qué área de LEPA?» solo tiene sentido si el sistema sigue sabiendo que ambos mensajes pertenecen a la misma conversación.

Dos tipos de memoria, solo uno construido

Al principio del proyecto la división quedó así:

  • La memoria a corto plazo abarca una conversación, es decir, un hilo.
  • La memoria a largo plazo abarca hechos que sobreviven a la sesión.

La primera versión del agente tiene el primer tipo. El segundo todavía no existe. La distinción importa tanto que se repite más abajo allí donde pueda colarse la confusión.

Estado durante una ejecución frente a estado entre ejecuciones

El estado de LangGraph ya funciona como una pequeña memoria mientras una ejecución está en curso. El objeto de estado de soporte lleva cuatro cosas.

messages
user_role
issue_category
clarification_needed
retrieved_documents
final_answer
…

El campo messages está conectado al reductor add_messages, de modo que cada turno nuevo se añade al final y no se sobrescribe nada. Dentro de una llamada a invoke, ese estado compartido es la forma en que los nodos se pasan el contexto.

Pero una llamada a invoke termina. En cuanto llega la siguiente petición HTTP, todo lo de la anterior desaparece salvo que el estado se haya guardado en algún sitio. Guardarlo es justo lo que significa hacer checkpointing.

Cómo funciona un checkpointer

En pocas palabras, un checkpointer guarda el estado del grafo cuando termina el trabajo y lo archiva bajo un identificador de conversación. Es como cerrar la pestaña de un ticket en un servicio de soporte y volver a abrirla más tarde: el número de ticket es el mismo y el historial también.

El proyecto usa SqliteSaver del paquete langgraph-checkpoint-sqlite. El flujo empieza con un identificador que se pasa en la configuración de la ejecución.

thread_id = "abc-123"
        ↓
invoke graph with config { configurable: { thread_id } }
        ↓
state is saved to SQLite
        ↓
next invoke with same thread_id
        ↓
prior messages / fields load again

En la parte de la API, el manejador toma el thread id del cuerpo de la petición, o genera un UUID si no se envió ninguno, y lo envuelve en el diccionario de configuración.

thread_id = body.thread_id or str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

Esa configuración se entrega al grafo junto con el nuevo mensaje del usuario.

result = graph.invoke(
    {"messages": [HumanMessage(content=body.message)]},
    config=config,
)

Un thread id equivale a un carril de memoria.

Qué se guarda

Para un hilo concreto, el checkpointer guarda todo el estado del grafo. Eso incluye la lista de mensajes, que va creciendo, y campos como el rol del usuario y la categoría del problema, tal como se fijaron en pasos anteriores.

En la práctica, el primer turno es así: el usuario dice algo vago, el agente hace una pregunta aclaratoria y el estado se guarda.

Turn 1: "help"
  → clarification question
  → state saved

El segundo turno continúa con los mensajes anteriores todavía disponibles, así que el clasificador lee el último texto del usuario con contexto y los pasos de conocimiento y soporte pueden responder como es debido.

Turn 2: "Why aren't grades showing?"
  → prior messages still there
  → classify sees the latest user text
  → knowledge + support can answer

Las pruebas confirman el comportamiento. Envían un primer mensaje vago, luego la pregunta sobre las notas con el mismo thread id, y comprueban que el segundo resultado tiene un historial de mensajes más largo que el primero.

Qué abarca aquí la «memoria a corto plazo»

Cuando se dice que el agente tiene memoria a corto plazo, la afirmación se limita a esta pila.

SQLite checkpoint
  + thread_id
  + SupportState.messages (append)

Es la memoria de un único hilo de soporte. Permite:

  • una aclaración seguida de una respuesta
  • recordar turnos anteriores dentro del mismo chat
  • un endpoint de chat que se comporta como una sesión en lugar de como una serie de preguntas sin relación

No ofrece de forma automática:

  • un perfil permanente de cada docente
  • conocimiento de todo el centro más allá de los documentos en Markdown
  • una afirmación como «el agente sabe que esta persona prefiere el modo oscuro en todas las aplicaciones»

Eso corresponde a los diseños de memoria a largo plazo.

Cómo sería la memoria a largo plazo

La memoria a largo plazo es información que se conserva a propósito más allá de un hilo y se recupera después. En LEPA, los candidatos podrían ser que un usuario siempre es docente, que un centro llama a sus periodos «Semester A» o que un problema al publicar notas se resolvió la semana pasada.

Una analogía aproximada ayuda, y no es más que eso.

Short-term ≈ RAM for this conversation
Long-term  ≈ saved notes you can look up next month

Una transcripción guardada del hilo abc-123 sigue siendo memoria de hilo. El historial de la conversación no es memoria a largo plazo por defecto; para eso harían falta un almacén aparte y una política de recuperación. El objeto de estado incluso reserva un campo para un resumen de la conversación en el futuro. Ningún nodo actual lo rellena, así que es una marca de trabajo pendiente, no una funcionalidad.

La respuesta del chat y el contrato del thread id

El endpoint de chat devuelve un cuerpo JSON con la respuesta, la categoría y un indicador de memoria, entre otros campos.

{
  "answer": "...",
  "category": "grades",
  "memory_updated": true,
  "thread_id": "...",
  "clarification_needed": false
}

Se espera que los clientes devuelvan el thread id en el turno siguiente. Si lo omiten, la API genera un UUID nuevo y empieza una conversación nueva. Esa única regla hace que la memoria sea explícita y no mágica.

Una advertencia sobre la respuesta: en la API actual el indicador de memoria siempre vale true. No significa que se haya escrito un resumen a largo plazo. Solo indica que la petición pasó por el grafo con checkpoints.

Una pequeña trampa operativa

SqliteSaver.from_conn_string es un gestor de contexto, así que cierra la base de datos en cuanto la ejecución sale del bloque with. Eso funciona en un script y falla en un proceso de FastAPI de larga duración. Por eso, una función auxiliar llamada get_sqlite_checkpointer abre una conexión, llama a setup y mantiene la conexión durante toda la vida de la aplicación.

Es un detalle mínimo y fácil de pasar por alto, pero cobra importancia en cuanto la memoria tiene que vivir fuera de un notebook.

La pila de memoria en una línea

La memoria del agente es el estado durante la ejecución más los checkpoints en SQLite entre ejecuciones.

State during a run
  +
SQLite checkpoints across runs
  +
thread_id to find the right conversation

Basta para conversaciones de soporte de varios turnos. No llega a ser un sistema real de memoria a largo plazo, y decirlo con claridad forma parte de la lección. El siguiente paso amplía la vista más allá de una herramienta con memoria para explicar por qué el flujo de trabajo se divide en las etapas de recepción, conocimiento y soporte.