View a markdown version of this page

Mémoire sémantique à long terme pour les agents LangGraph - Amazon DynamoDB

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Mémoire sémantique à long terme pour les agents LangGraph

LangGraph sépare deux types d'état d'agent. Short-term state est le fil de conversation lui-même, qu'un pointeur de contrôle persiste pour qu'un fil de discussion puisse reprendre, rejouer et récupérer (voirUtilisation de DynamoDB comme magasin de points de contrôle pour les agents LangGraph). Long-term la mémoire est ce que l'agent connaît à travers les threads et la LangGraph modélise comme un magasin : une surface clé-valeur avec espace de noms sur laquelle l'agent écrit délibérément et relit plus tard.

La DynamoDBStore classe, dans le package langgraph-checkpoint-aws, est l'implémentation DynamoDB de ce magasin. Il gère le côté clé-valeur avec des espaces de noms hiérarchiques, Time to Live pour les mémoires périmées qui expirent et un filtrage de base. Avec un index vectoriel configuré (voirUtilisation d'index vectoriels dans DynamoDB), sa search() méthode effectue une recherche sémantique : les mémoires sont intégrées à l'écriture, indexées de manière asynchrone et rappelées par signification, classées par similarité, à partir de la même table qui les contient. Il n'existe pas de base de données vectorielles distincte à approvisionner et aucun pipeline n'y copiant les données.

Conditions préalables

  • Et Compte AWS avec l'autorisation de créer des tables DynamoDB et d'invoquer des modèles Amazon Bedrock

  • Accès à un modèle d'intégration dans Amazon Bedrock dans votre région. Ces exemples utilisent Amazon Titan Text Embeddings V2

  • Python 3.10 ou version ultérieure, avec langgraph-checkpoint-aws 1.2.2 ou version ultérieure et boto3 1.43.64 ou version ultérieure (les boto3 versions antérieures n'ont aucune opération) SearchVectors

Installez les bibliothèques à l'aide de pip :

pip install langgraph langgraph-checkpoint-aws langchain-aws

Configurer la boutique

Configurez le magasin avec un index bloc et appelez setup() :

from langchain_aws import BedrockEmbeddings from langgraph_checkpoint_aws import DynamoDBStore store = DynamoDBStore( table_name="support-agent-memory", region_name="us-east-1", index={ "embed": BedrockEmbeddings(model_id="amazon.titan-embed-text-v2:0"), "dims": 1024, "fields": ["text"], "distance_function": "COSINE", }, ) store.setup()

Quatre paramètres du index bloc méritent d'être compris :

  • embedprend n'importe LangChain quel objet d'intégration ou un simple appelable qui mappe une liste de chaînes à une liste de vecteurs.

  • dimsdoit correspondre à la taille de sortie de votre modèle. Titan Text Embeddings V2 renvoie 1 024 dimensions par défaut. En cas de désaccord, le magasin génère une erreur de non-concordance de dimensions en nommant les deux nombres lors de la première écriture, plutôt que d'écrire des vecteurs que l'index ne peut pas utiliser.

  • fieldssélectionne les parties de la valeur à intégrer. Ici, seul le text champ est intégré. La valeur par défaut["$"], sérialise la valeur entière au format JSON et l'intègre, ce qui est pratique, mais intègre également vos attributs de comptabilité.

  • distance_functionest défini par défaut surCOSINE, et accepte également EUCLIDEAN et. DOT_PRODUCT

setup()crée la table si elle est absente, joint l'index vectoriel et active Time to Live si vous l'avez configuré. À un tableau existant, il ajoute uniquement ce qui manque, afin que les DynamoDBStore déploiements existants puissent adopter la recherche sémantique sans recréer le tableau ni migrer des éléments. Gardez à l'esprit qu'il setup() ne revient pas tant que l'index n'est pas renvoyé ACTIVE et qu'il n'est plus remplacé : un index qui génère des rapports ACTIVE tout en continuant à remplir rejette SearchVectors les appels. Sur une nouvelle table vide, l'attente est courte. Le réaménagement d'une table avec des objets existants prend autant de temps que le remblayage.

Écrivez des souvenirs

L'écriture est une chose ordinaireput(). L'intégration se fait pour vous :

namespace = ("memories", "acme-corp", "user-8812") store.put(namespace, "mem-1", { "text": "Account runs a proxy that closes idle sockets after 60 seconds", "source": "ticket-4471", }) store.put(namespace, "mem-2", { "text": "Customer prefers email, asked not to be called by phone", "source": "ticket-4471", }) store.put(namespace, "mem-3", { "text": "Uses a custom build of the SDK pinned to version 2.14", "source": "ticket-4502", })

Rappeler par le sens

Le client ouvre une nouvelle conversation et indique que sa connexion ne cesse de s'arrêter au bout d'une minute environ :

results = store.search( namespace, query="connection drops after a minute of inactivity", limit=3, ) for item in results: print(round(item.score, 3), item.value["text"])

Sortie :

0.324 Account runs a proxy that closes idle sockets after 60 seconds 0.039 Customer prefers email, asked not to be called by phone 0.031 Uses a custom build of the SDK pinned to version 2.14

La mémoire concernée occupe la première place et aucune clé de la requête ne correspond à quoi que ce soit. SearchItem.scoresuit la LangGraph convention selon laquelle le niveau le plus élevé est le plus pertinent. DynamoDB renvoie une distance, où la valeur la plus faible est la plus proche, de sorte que le magasin convertit : COSINE car le score est1 - distance, car EUCLIDEAN il est1 / (1 + distance), et les DOT_PRODUCT scores sont transmis inchangés. Si vous vous basez sur un score absolu pour décider si une mémoire est suffisamment pertinente pour être injectée dans une invite, calibrez ce seuil par rapport à vos propres données et à la fonction de distance que vous avez choisie.

Étendue de la mémoire avec le schéma de recherche

Lors de DynamoDBStore la création de l'index vectoriel, il déclare la clé de partition de la table en tant qu'HASHélément du schéma de recherche d'index. Comme cet élément existe, chaque recherche doit fournir une condition, et le magasin fournit l'espace de noms : le tuple d'espace de noms est joint pour former la valeur de la clé de partition, de sorte que chaque recherche sémantique est épinglée à exactement un espace de noms. Trois conséquences s'ensuivent :

  • L'isolation est structurelle, il ne s'agit pas d'un filtre que vous pensez à écrire. Une recherche ne peut pas atteindre une mémoire dans un espace de noms différent. Par conséquent, un magasin desservant de nombreux locataires n'a aucune forme de requête qui renvoie les souvenirs d'un autre locataire. Il s'agit de la portée de la requête plutôt que de l'autorisation : un principal disposant d'une dynamodb:SearchVectors autorisation sur la table peut rechercher directement n'importe quelle valeur d'espace de noms. C'est pourquoi le choix des espaces de noms qu'un appelant peut lire revient toujours à IAM et à la couche d'autorisation de votre application.

  • Recall Cost suit la mémoire d'un utilisateur, et non l'ensemble de votre table. Le travail de recherche est limité par la quantité contenue dans cet espace de noms, et non par le nombre de mémoires que contient l'ensemble de votre produit, ce qui permet de réduire la latence et les coûts de recherche vectorielle à un niveau faible et stable au fur et à mesure de votre croissance.

  • La recherche sémantique est un espace de noms exact, pas un préfixe. Une recherche atteint exactement l'espace de noms que vous transmettez et rien en dessous. La recherche ("memories", "acme-corp") n'aboutit pas("memories", "acme-corp", "user-8812"), car il s'agit de valeurs de clé de partition différentes. Votre espace de noms est votre étendue de rappel, alors choisissez-le en fonction de l'étendue que vous souhaitez voir apparaître lors d'une seule recherche.

Le tableau suivant explique comment choisir une forme d'espace de noms.

Forme de l'espace de noms

On search() atteint

Choisissez-le quand

("memories", user_id)

Les souvenirs de cet utilisateur

Single-tenant produit avec rappel par utilisateur

("memories", tenant_id, user_id)

Cet utilisateur, chez ce locataire

Multi-tenant, le cas courant

("memories", tenant_id, user_id, agent_name)

Notes d'un agent concernant cet utilisateur

Plusieurs agents spécialisés qui ne devraient pas lire les notes des uns et des autres

("account_facts", tenant_id)

Tenant-wide connaissances

Informations qui s'appliquent à chaque utilisateur d'un compte

Évitez de placer chaque mémoire dans un seul espace de noms et de filtrer en fonction des métadonnées : les filtres sont appliqués une fois que DynamoDB a déjà sélectionné les correspondances les plus proches dans l'ensemble de l'espace de noms, et une seule recherche renvoie au maximum les 100 meilleures réponses. Ainsi, lorsqu'un locataire occupé remplit le top 100 pour une requête courante, la recherche d'un locataire plus silencieux renvoie moins de résultats que ceux demandés, même si ses mémoires sont présentes. Préférez la portée de l'espace de noms aux filtres pour tout ce qui détermine l'exactitude. Aller au-delà de votre limite de rappel est également une conception légitime : un agent qui a besoin d'un contexte à la fois spécifique à l'utilisateur et à l'échelle du compte recherche les deux espaces de noms et fusionne les résultats.

Considérations

  • L'indice est finalement cohérent, le même modèle qu'un indice secondaire mondial. A search() émis immédiatement après a put() peut ne pas inclure la nouvelle mémoire pour le moment. Pour un agent qui écrit un souvenir puis le rappelle au cours du même tour, relisez-le à l'aide d'une touche.

  • Une seule recherche renvoie au maximum les 100 meilleurs résultats. Une demande dont le limit plus offset dépasse ce seuil est rejetée avec une erreur claire au lieu de ne rien renvoyer silencieusement au-delà du plafond.

  • Si vous utilisez Time to Live pour faire expirer des mémoires périmées et activer l'actualisation lors de la lecture, la date d'expiration d'une mémoire rappelée par l'agent est repoussée, de sorte que les mémoires en cours d'utilisation ne sont pas celles qui disparaissent discrètement.

  • Les échecs de recherche vectorielle surviennent, ce qui permet à votre application de réagir. Un accélérateur, un problème d'autorisations et un résultat réellement vide auraient l'air identiques si le magasin renvoyait une liste vide en cas d'échec.

  • Les opérations vectorielles sont facturées dans leurs propres unités, mesurées séparément des unités de demande de lecture et d'écriture de la table de base, et les deux évoluent en fonction du nombre de dimensions. Une dimension d'intégration plus petite est moins coûteuse à chaque écriture et à chaque recherche. Utilisez donc la plus petite dimension qui garantit la qualité de votre mémorisation.

  • Les mémoires écrites sans texte dans la configuration fields sont stockées sans intégration et n'apparaîtront pas dans les résultats sémantiques. Le magasin enregistre un avertissement lorsque cela se produit.

Ressources supplémentaires