Heim  >  Artikel  >  Java  >  Zuverlässiger KI-Agent in Produktion mit Java Quarkus Langchain – Speicherteil

Zuverlässiger KI-Agent in Produktion mit Java Quarkus Langchain – Speicherteil

DDD
DDDOriginal
2024-11-18 01:26:02640Durchsuche

Autoren

@herbertbeckman – LinkedIn
@rndtavares – LinkedIn

Teile des Artikels

  1. Zuverlässiger KI-Agent in der Produktion mit Java Quarkus Langchain4j – Teil 1 – KI als Service

  2. Zuverlässiger KI-Agent in Java Quarkus Langchain4j prod – Teil 2 – Speicher (dieser Artikel)

  3. Zuverlässiger KI-Agent in der Entwicklung mit Java Quarkus Langchain4j – Teil 3 – RAG (bald verfügbar)

  4. Zuverlässiger KI-Agent in der Produktion mit Java Quarkus Langchain4j – Teil 4 – Leitplanken (in Kürze verfügbar)

Einführung

Wenn wir einen Agenten erstellen, müssen wir bedenken, dass LLMs keinerlei Informationen speichern, das heißt, sie sind zustandslos. Damit unser Agent sich Informationen „merken“ kann, müssen wir eine Speicherverwaltung implementieren. Quarkus stellt uns bereits einen konfigurierten Standardspeicher zur Verfügung. Allerdings kann es Ihren Agenten buchstäblich lahmlegen, indem es den ihm zur Verfügung gestellten RAM-Speicher sprengt, wie in dieser Quarkus-Dokumentation beschrieben, wenn nicht die nötige Sorgfalt angewendet wird. Um dieses Problem nicht mehr zu haben und unseren Agenten auch in einer skalierbaren Umgebung nutzen zu können, benötigen wir einen ChatMemoryStore.

Konzepte

Wir nutzen einen Chat, um mit unserem Agenten zu interagieren, und es gibt wichtige Konzepte, die wir kennen müssen, damit unsere Interaktion mit ihm bestmöglich ablaufen kann und keine Fehler in der Produktion verursacht. Zuerst müssen wir die Arten von Nachrichten kennen, die wir verwenden, wenn wir mit ihm interagieren. Dies sind:

  • Benutzernachrichten: Die vom Endkunden gesendete Nachricht oder Anfrage. Wenn wir die Nachricht in Quarkus DevUI senden, senden wir immer eine UserMessage. Darüber hinaus wird es auch in den Ergebnissen von Tool-Aufrufen verwendet, die wir zuvor gesehen haben.

  • KI-Nachrichten (AiMessage): Die Antwortnachricht vom Modell. Immer wenn LLM unserem Agenten antwortet, erhält er eine Nachricht wie diese. Der Inhalt dieser Art von Nachricht wechselt zwischen einer Textantwort und Anforderungen zur Toolausführung.

  • SystemMessage: Diese Nachricht kann nur einmal definiert werden und befindet sich nur zur Entwicklungszeit.

Da Sie nun die drei Arten von Nachrichten kennen, die wir haben, erklären wir Ihnen anhand einiger Grafiken, wie sie sich verhalten sollten. Alle Grafiken stammen aus der Präsentation Java meets AI: Build LLM-Powered Apps with LangChain4j von Deandrea, Andrianakis, Escoffier, ich kann das Video wärmstens empfehlen.

Die erste Grafik zeigt die Verwendung der drei Nachrichtentypen. UserMessage in Blau, SystemMessage in Rot und AiMessage in Grün.

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

Diese zweite Grafik zeigt, wie „Speicher“ verwaltet werden sollte. Ein interessantes Detail ist, dass wir eine bestimmte Reihenfolge in den Nachrichten einhalten und einige Prämissen respektieren müssen.

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

  • Es darf nur 1 Nachricht vom Typ SystemMessage vorhanden sein;
  • Nach SystemMessage sollten Nachrichten immer in dieser Reihenfolge zwischen UserMessage und AiMessage wechseln. Wenn wir AiMessage nach AiMessage haben, lösen wir eine Ausnahme aus. Das Gleiche gilt für aufeinanderfolgende UserMessages.

Ein weiteres wichtiges Detail, auf das Sie achten sollten, ist die Größe Ihres ChatMemory. Je größer der Speicher Ihrer Interaktion, desto höher sind die Token-Kosten, da der LLM mehr Text verarbeiten muss, um eine Antwort bereitzustellen. Richten Sie dann ein Speicherfenster ein, das am besten zu Ihrem Anwendungsfall passt. Ein Tipp ist, die durchschnittliche Anzahl der Nachrichten Ihrer Kunden zu überprüfen, um eine Vorstellung vom Umfang der Interaktion zu bekommen. Wir zeigen die Implementierung durch MessageWindowChatMemory, die Klasse, die darauf spezialisiert ist, dies für uns in Langchain4j zu verwalten.

Jetzt, da wir all diese Konzepte und Prämissen kennen, machen wir uns die Hände schmutzig!

Konfigurieren unseres ChatMemoryStore

Hier verwenden wir MongoDB als ChatMemoryStore. Wir verwenden das MongoDB-Dokument und laden eine Instanz auf Docker hoch. Konfigurieren Sie es gerne nach Ihren Wünschen.

Hinzufügen unserer Verbindung zu MongoDB

Beginnen wir mit dem Hinzufügen der notwendigen Abhängigkeit, um mithilfe von Quarkus eine Verbindung zu MongoDB herzustellen.

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

Nach den Abhängigkeiten müssen wir die Verbindungseinstellungen in unserem src/main/resources/application.properties hinzufügen.

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

Wir können unsere Verbindung zur Basis immer noch nicht testen, da wir zuerst unsere Entitäten und Repositorys erstellen müssen.

Erstellen unserer Entität und unseres Repositorys

Jetzt implementieren wir unsere Interaktionsentität. Diese Entität wird unsere Liste der Nachrichten erstellen lassen. Immer wenn ein neuer Kunde eine Verbindung herstellt, wird eine neue Interaktion generiert. Wenn wir diese Interaktion wiederverwenden müssen, geben wir einfach denselben Interaktionsbezeichner ein.

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);
    }
}

Wir können jetzt unser Repository erstellen.

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

Jetzt werden wir einige langchain4j-Komponenten implementieren, den ChatMemoryStore und den ChatMemoryProvider. ChatMemoryProvider ist die Klasse, die wir in unserem Agenten verwenden werden. Darin werden wir einen ChatMemoryStore hinzufügen, der unser Repository zum Speichern von Nachrichten in unserer MongoDB verwendet. Folgen Sie ChatMemoryStore:

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

Der ChatMemoryProvider sieht folgendermaßen aus:

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);
    }
}

Beachten Sie den MessageWindowChatMemory. Hier implementieren wir das Nachrichtenfenster, das wir am Anfang des Artikels erwähnt haben. In der Methode maxMessages() müssen Sie sie auf die Zahl ändern, die Ihrer Meinung nach für Ihr Szenario am besten geeignet ist. Ich empfehle, die größte Anzahl von Nachrichten zu verwenden, die jemals in Ihrem Szenario vorhanden waren, oder den Durchschnitt zu verwenden. Hier definieren wir die beliebige Zahl 100.

Lassen Sie uns nun unseren Agenten ändern, um unseren neuen ChatMemoryProvider zu verwenden und MemoryId hinzuzufügen. Es sollte so aussehen:

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);
    }

}

Dies sollte unseren AgentWSEndpoint beschädigen. Ändern wir es so, dass es die Interaktionskennung erhält und wir sie als unsere MemoryId verwenden können:

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());
    }
}

Wir können unseren Agenten jetzt erneut testen. Dazu stellen wir einfach eine Verbindung zum Websocket her, indem wir jederzeit eine UUID übergeben. Sie können hier eine neue UUID generieren oder den Befehl uuidgen unter Linux verwenden.

Wenn wir den Test durchführen, erhalten Sie keine Antwort vom Agenten. Dies liegt daran, dass der Agent Probleme beim Schreiben unserer Nachrichten an MongoDB hat und Ihnen dies durch eine Ausnahme anzeigt. Damit wir prüfen können, ob diese Ausnahme auftritt, müssen wir unserer Datei src/main/resources/application.properties eine neue Eigenschaft hinzufügen. Dabei handelt es sich um die Protokollebene, die wir in Quarkus sehen möchten. Fügen Sie dann die folgende Zeile hinzu:

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();
    }
}

Testen Sie jetzt den Agenten. Die Ausnahme sollte folgende sein:

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);
}

Diese Ausnahme tritt auf, weil MongoDB die ChatMessage-Schnittstelle von Langchain4j nicht verarbeiten kann. Daher müssen wir einen Codec implementieren, um dies zu ermöglichen. Quarkus selbst bietet uns bereits einen Codec an, aber wir müssen klarstellen, dass wir ihn nutzen wollen. Anschließend erstellen wir die Klassen ChatMessageCodec und ChatMessageCodecProvider wie folgt:

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

Fertig! Jetzt können wir die Nachrichten in unserer MongoDB testen und verifizieren. Bei der Abfrage können wir die drei Nachrichtentypen im Nachrichtenarray des Dokuments überprüfen.

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

Damit endet der zweite Teil unserer Serie. Wir hoffen, es hat euch gefallen und wir sehen uns in Teil 3.

Das obige ist der detaillierte Inhalt vonZuverlässiger KI-Agent in Produktion mit Java Quarkus Langchain – Speicherteil. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn