Maison  >  Article  >  Java  >  Agent IA fiable en production avec Java Quarkus Langchain - Memory Part

Agent IA fiable en production avec Java Quarkus Langchain - Memory Part

DDD
DDDoriginal
2024-11-18 01:26:02640parcourir

Auteurs

@herbertbeckman - LinkedIn
@rndtavares - LinkedIn

Parties de l'article

  1. Agent IA fiable en production avec Java Quarkus Langchain4j - Partie 1 - IA en tant que service

  2. Agent IA fiable en Java Quarkus Langchain4j prod - Partie 2 - Mémoire (cet article)

  3. Agent IA fiable en production avec Java Quarkus Langchain4j - Partie 3 - RAG (à venir)

  4. Agent IA fiable en production avec Java Quarkus Langchain4j - Partie 4 - Garde-corps (à venir)

Introduction

Lorsque nous créons un agent, nous devons garder à l'esprit que les LLM ne stockent aucun type d'informations, c'est-à-dire qu'ils sont apatrides. Pour que notre agent ait la capacité de « mémoriser » des informations, nous devons mettre en œuvre une gestion de la mémoire. Quarkus nous fournit déjà une mémoire par défaut configurée, mais il peut littéralement mettre hors service votre agent en faisant exploser la mémoire RAM mise à sa disposition, comme décrit dans cette documentation Quarkus, si les précautions nécessaires ne sont pas prises. Pour ne plus avoir ce problème et aussi pour pouvoir utiliser notre agent dans un environnement évolutif, nous avons besoin d'un ChatMemoryStore.

Concepts

Nous utilisons un chat pour interagir avec notre agent et il y a des concepts importants que nous devons connaître pour que notre interaction avec lui puisse se dérouler de la meilleure façon possible et ne provoque pas de bugs en production. Tout d'abord, nous devons connaître les types de messages que nous utilisons lorsque nous interagissons avec lui, ce sont :

  • Messages utilisateur : le message ou la demande envoyé par le client final. Lorsque nous envoyons le message dans Quarkus DevUI, nous envoyons toujours un UserMessage. De plus, il est également utilisé dans les résultats des appels d'outils que nous avons vus précédemment.

  • Messages AI (AiMessage) : le message de réponse du modèle. Chaque fois que LLM répondra à notre agent, il recevra un message comme celui-ci. Ce type de message alterne son contenu entre une réponse textuelle et des requêtes d'exécution d'outil.

  • SystemMessage : ce message ne peut être défini qu'une seule fois et n'est valable qu'au moment du développement.

Maintenant que vous connaissez les 3 types de messages que nous avons, expliquons comment ils doivent se comporter avec quelques graphiques. Tous les graphiques sont tirés de la présentation Java meets AI: Build LLM-Powered Apps with LangChain4j de Deandrea, Andrianakis, Escoffier, je recommande vivement la vidéo.

Le premier graphique démontre l'utilisation des 3 types de messages. UserMessage en bleu, SystemMessage en rouge et AiMessage en vert.

Agente de IA confiável em prod com Java   Quarkus   Langchain- Parte  Memória

Ce deuxième graphique montre comment la « mémoire » doit être gérée. Un détail intéressant est qu'il faut maintenir un certain ordre dans les messages et certains prémisses doivent être respectés.

Agente de IA confiável em prod com Java   Quarkus   Langchain- Parte  Memória

  • Il ne doit y avoir qu'un seul message de type SystemMessage ;
  • Après SystemMessage, les messages doivent toujours basculer entre UserMessage et AiMessage, dans cet ordre. Si nous avons AiMessage après AiMessage, nous lancerons une exception. Il en va de même pour les messages utilisateur consécutifs.

Un autre détail important auquel vous devez prêter attention est la taille de votre ChatMemory. Plus la mémoire de votre interaction est grande, plus les coûts des jetons sont élevés, car le LLM devra traiter plus de texte pour fournir une réponse. Établissez ensuite une fenêtre de mémoire qui correspond le mieux à votre cas d’utilisation. Une astuce consiste à vérifier le nombre moyen de messages de vos clients pour avoir une idée de l’ampleur de l’interaction. Nous montrerons l'implémentation via MessageWindowChatMemory, la classe spécialisée dans la gestion de cela pour nous dans Langchain4j.

Maintenant que nous connaissons tous ces concepts et prémisses, mettons la main à la pâte !

Configuration de notre ChatMemoryStore

Ici, nous utiliserons MongoDB comme ChatMemoryStore. Nous utilisons la documentation MongoDB et téléchargeons une instance sur Docker. N'hésitez pas à le configurer comme vous le souhaitez.

Ajout de notre connexion à MongoDB

Commençons par ajouter la dépendance nécessaire pour se connecter à MongoDB à l'aide de Quarkus.

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-mongodb-panache</artifactId>
</dependency>

Après les dépendances, nous devons ajouter les paramètres de connexion dans notre src/main/resources/application.properties.

quarkus.mongodb.connection-string=mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@localhost:27017
quarkus.mongodb.database=chat_memory

Nous ne pourrons toujours pas tester notre connexion à la base, car nous devons d'abord créer nos entités et référentiels.

Création de notre entité et de notre référentiel

Implémentons maintenant notre entité Interaction. Cette entité aura notre liste de messages faite. Chaque fois qu'un nouveau client se connecte, une nouvelle interaction sera générée. Si nous devons réutiliser cette Interaction, nous saisissons simplement le même identifiant d'Interaction.

package <seupacote>;

import dev.langchain4j.data.message.ChatMessage;
import io.quarkus.mongodb.panache.common.MongoEntity;
import org.bson.codecs.pojo.annotations.BsonId;

import java.util.List;
import java.util.Objects;

@MongoEntity(collection = "interactions")
public class InteractionEntity {

    @BsonId
    private String interactionId;
    private List<ChatMessage> messages;

    public InteractionEntity() {
    }

    public InteractionEntity(String interactionId, List<ChatMessage> messages) {
        this.interactionId = interactionId;
        this.messages = messages;
    }

    public String getInteractionId() {
        return interactionId;
    }

    public void setInteractionId(String interactionId) {
        this.interactionId = interactionId;
    }

    public List<ChatMessage> getMessages() {
        return messages;
    }

    public void setMessages(List<ChatMessage> messages) {
        this.messages = messages;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        InteractionEntity that = (InteractionEntity) o;
        return Objects.equals(interactionId, that.interactionId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(interactionId, messages);
    }
}

Nous pouvons maintenant créer notre référentiel.

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-mongodb-panache</artifactId>
</dependency>

Nous allons maintenant implémenter certains composants langchain4j, le ChatMemoryStore et le ChatMemoryProvider. ChatMemoryProvider est la classe que nous utiliserons dans notre agent. Nous y ajouterons un ChatMemoryStore qui utilisera notre référentiel pour stocker les messages dans notre MongoDB. Suivez ChatMemoryStore :

quarkus.mongodb.connection-string=mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@localhost:27017
quarkus.mongodb.database=chat_memory

Le ChatMemoryProvider ressemblera à ceci :

package <seupacote>;

import dev.langchain4j.data.message.ChatMessage;
import io.quarkus.mongodb.panache.common.MongoEntity;
import org.bson.codecs.pojo.annotations.BsonId;

import java.util.List;
import java.util.Objects;

@MongoEntity(collection = "interactions")
public class InteractionEntity {

    @BsonId
    private String interactionId;
    private List<ChatMessage> messages;

    public InteractionEntity() {
    }

    public InteractionEntity(String interactionId, List<ChatMessage> messages) {
        this.interactionId = interactionId;
        this.messages = messages;
    }

    public String getInteractionId() {
        return interactionId;
    }

    public void setInteractionId(String interactionId) {
        this.interactionId = interactionId;
    }

    public List<ChatMessage> getMessages() {
        return messages;
    }

    public void setMessages(List<ChatMessage> messages) {
        this.messages = messages;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        InteractionEntity that = (InteractionEntity) o;
        return Objects.equals(interactionId, that.interactionId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(interactionId, messages);
    }
}

Remarquez la MessageWindowChatMemory. C'est ici que nous implémentons la fenêtre de message que nous avons mentionnée au début de l'article. Dans la méthode maxMessages(), vous devez le remplacer par le nombre que vous jugez le mieux adapté à votre scénario. Ce que je recommande, c'est d'utiliser le plus grand nombre de messages ayant jamais existé dans votre scénario, ou d'utiliser la moyenne. Ici, nous définissons le nombre arbitraire 100.

Changeons maintenant notre agent pour utiliser notre nouveau ChatMemoryProvider et ajoutons MemoryId. Cela devrait ressembler à ceci :

package <seupacote>;

import dev.langchain4j.data.message.ChatMessage;
import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase;

import java.util.List;

public class InteractionRepository implements PanacheMongoRepositoryBase<InteractionEntity, String> {

    public InteractionEntity findByInteractionId(String interactionId) {
        return findById(interactionId);
    }

    public void updateMessages(String interactionId, List<ChatMessage> messages) {
        persistOrUpdate(new InteractionEntity(interactionId, messages));
    }

    public void deleteMessages(String interactionId) {
        deleteById(interactionId);
    }

}

Cela devrait casser notre AgentWSEndpoint. Modifions-le pour qu'il reçoive l'identifiant d'interaction et que nous puissions l'utiliser comme notre MemoryId :

package <seupacote>;

import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;

import java.util.List;
import java.util.Objects;

public class MongoDBChatMemoryStore implements ChatMemoryStore {

    private InteractionRepository interactionRepository = new InteractionRepository();

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        var interactionEntity = interactionRepository.findByInteractionId(memoryId.toString());
        return Objects.isNull(interactionEntity) ? List.of() : interactionEntity.getMessages();
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        interactionRepository.updateMessages(memoryId.toString(), messages);
    }

    @Override
    public void deleteMessages(Object memoryId) {
        interactionRepository.deleteMessages(memoryId.toString());
    }
}

Nous pouvons désormais tester à nouveau notre agent. Pour ce faire, on se connecte simplement au websocket en passant un UUID quand on le souhaite. Vous pouvez générer un nouvel UUID ici ou utiliser la commande uuidgen sous Linux.

Lorsque nous effectuons le test, vous ne recevrez aucune réponse de l'agent. Cela se produit parce que l'agent rencontre des problèmes pour écrire nos messages dans MongoDB et il vous le montrera via une exception. Afin que nous puissions vérifier que cette exception se produit, nous devons ajouter une nouvelle propriété à notre src/main/resources/application.properties, qui est le niveau de journalisation que nous voulons voir dans Quarkus. Ensuite, ajoutez-y la ligne suivante :

package <seupacote>;

import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;

import java.util.function.Supplier;

public class MongoDBChatMemoryProvider implements Supplier<ChatMemoryProvider> {

    private MongoDBChatMemoryStore mongoDBChatMemoryStore = new MongoDBChatMemoryStore();

    @Override
    public ChatMemoryProvider get() {
        return memoryId -> MessageWindowChatMemory.builder()
                .maxMessages(100)
                .id(memoryId)
                .chatMemoryStore(mongoDBChatMemoryStore)
                .build();
    }
}

Maintenant, testez l'agent. L'exception devrait être la suivante :

package <seupacote>;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.quarkiverse.langchain4j.ToolBox;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
@RegisterAiService(
        chatMemoryProviderSupplier = MongoDBChatMemoryProvider.class
)
public interface Agent {

    @ToolBox(AgentTools.class)
    @SystemMessage("""
            Você é um agente especializado em futebol brasileiro, seu nome é FutAgentBR
            Você sabe responder sobre os principais títulos dos principais times brasileiros e da seleção brasileira
            Sua resposta precisa ser educada, você pode deve responder em Português brasileiro e de forma relevante a pergunta feita

            Quando você não souber a resposta, responda que você não sabe responder nesse momento mas saberá em futuras versões.
            """)
    String chat(@MemoryId String interactionId, @UserMessage String message);
}

Cette exception se produit car MongoDB ne peut pas gérer l'interface ChatMessage de Langchain4j, nous devons donc implémenter un codec pour rendre cela possible. Quarkus lui-même nous propose déjà un codec, mais nous devons préciser que nous souhaitons l'utiliser. Nous allons ensuite créer les classes ChatMessageCodec et ChatMessageCodecProvider comme suit :

package <seupacote>;

import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.inject.Inject;

import java.util.Objects;
import java.util.UUID;

@WebSocket(path = "/ws/{interactionId}")
public class AgentWSEndpoint {

    private final Agent agent;

    private final WebSocketConnection connection;

    @Inject
    AgentWSEndpoint(Agent agent, WebSocketConnection connection) {
        this.agent = agent;
        this.connection = connection;
    }

    @OnTextMessage
    String reply(String message) {
        var interactionId = connection.pathParam("interactionId");
        return agent.chat(
                Objects.isNull(interactionId) || interactionId.isBlank()
                        ? UUID.randomUUID().toString()
                        : interactionId,
                message
        );
    }

}
quarkus.log.level=DEBUG

Prêt ! Nous pouvons maintenant tester et vérifier les messages dans notre MongoDB. Lors de l'interrogation, nous pouvons vérifier les 3 types de messages dans le tableau messages du document.

Agente de IA confiável em prod com Java   Quarkus   Langchain- Parte  Memória

Cela termine la deuxième partie de notre série. Nous espérons que vous avez apprécié et à bientôt dans la partie 3.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn