Beliebte Suchanfragen
//

GenAI für Full Stack EntwicklerInnen - Agent Chi PiTie (Teil 5)

20.8.2024 | 13 Minuten Lesezeit

Künstliche Intelligenz ist heutzutage in aller Munde. Die Einsatzgebiete sind vielfältig, der "WOW-Faktor" immer gegeben und das Potenzial noch lange nicht ausgeschöpft. Wo wir heute in vielen Bereichen schon unsere früheren Erwartungen übertroffen haben, gibt es jedoch auch Themenfelder, in denen wir noch hinter unserer, durch Science Fiction geprägten, Zukunftsvision zurückbleiben. Das Konzept einer AGI, einer "Artificial General Intelligence", ist zum Beispiel Stand heute noch nicht erreicht. Zu vielfältig sind die möglichen Einsatzgebiete und zu komplex das menschliche Gehirn. Dennoch kommen wir diesem Ziel, wenn es denn ein erstrebenswertes ist, Tag um Tag näher. Systeme, die Entscheidungen "selbst treffen", also in der Lage sind, selbstständig zu planen und durchzuführen, bringen uns Schritt für Schritt näher an die Zukunft heran.

In diesem Teil meiner Serie möchte ich mir nun die Zeit nehmen, eben jene autonomen Systeme zu beschreiben. Wir lernen, was einen sogenannten Agenten ausmacht und wie wir ihn dazu bringen, unsere teils abstrakten Ziele anzugehen. Welche Probleme ein rebellischer Agent mit sich bringen kann und worin das Potenzial eines frischen James-Bond-Verschnitts liegt. Am Ende werden wir einen Agenten von TypeScript-Frontend bis Backend zur Hand haben, einen Agenten, der den Superschurken der Zukunft das Fürchten lehren könnte.

Was ist ein Agentensystem?

Ein Agent, das lernt man in jedem James-Bond-Film, ist jemand, der komplexe Aufgaben bewältigen kann. Der oft Werkzeuge oder sogar Waffen zur Hand hat, einen Auftrag zur Rettung der Welt erhält und dann nach ein bisschen Hin und Her entscheidet, wie er diesen Auftrag erfüllen kann. Mit einer Prise Sexismus und Gewaltverherrlichung ist dann letzten Endes die Welt gerettet.

Im Kontext der generativen KI sprechen wir von Agenten immer dann, wenn ein System in der Lage ist, (halb-)autonom Aufgaben umzusetzen. Wie auch der "echte" Bond kann ein Agentensystem mithilfe von Werkzeugen (hoffentlich erstmal keine Waffen!) Aufgaben erledigen. Er entscheidet selbstständig, was er wie tun muss, um sein Ziel zu erreichen und, gesagt, getan, faxt nicht lange rum. Mit der Technik des Function Callings, ich sprach im letzten Artikel darüber, kann ein Toolset definiert werden, das dem Agenten in seinem vorgegebenen Themengebiet zur Hand gehen soll.

Unser Agent

Auch wenn Agenten schon in vielfältigen Szenarien zum Einsatz kommen, war für mich von Anfang an klar, was unser Beispiel-Agent sein sollte. Ein wahrer, ein "echter" Agent wie aus den Filmen sollte es werden. Ein Geheimagent muss in der Lage sein, in seinem Einsatzgebiet nicht aufzufallen. Die wichtigste Aufgabe ist es also, nicht aufzufallen. Unser System soll später in der Lage sein, auf Basis von verifizierten Hintergrundinformationen eine andere Persona anzunehmen. Es wird die Fähigkeiten haben, "echte Fakten" aus Wikipedia und sogar dem Internet herauszuziehen, um seine Geschichte plausibler zu gestalten. So kann ich als Auftraggeber zumindest hoffen, dass er nicht gleich beim ersten Auftrag auffliegt und sich das jahrelange Training ausgezahlt hat.

Die Strategie für den Agenten wird also sein:

  1. Lass dir eine Persona geben.
  2. Wenn du etwas gefragt wirst, antworte nach folgender Strategie:
    1. Suche in Wikipedia nach möglichen Fakten zu deiner Persona.
    2. Wenn du keine findest, verifiziere durch eine Internet-Suche, dass es keine Fakten gibt.
    3. Beantworte in der gegebenen Persona die Frage rein auf der Basis der gefundenen Fakten.

Der Agent entscheidet am Ende selbst, welches Tool er wie aufruft. Er kann auch mehrfach versuchen, in Wikipedia nach der Antwort zu suchen oder sich sogar gegen eine Recherche im Internet entscheiden (Auch wenn das nicht Sinn der Sache wäre!).

Jetzt fehlt nur noch ein passender Name:

Sein Name ist PiTie... Chi PiTie.

Voraussetzungen für die Implementierung

Auch in diesem Projekt wird wieder ein Zugriff auf ein potentes LLM benötigt. Im Beispiel-Projekt nutze ich Azure OpenAI. Für die Online-Suche werden wir ein Search-API nutzen, Tavily. Tavily hat ein Freemium-Preismodell, das es ermöglicht, bis zu 1000 Fragen pro Monat kostenlos abzuschicken. Möchtest du den Code selbst ausführen, musst du dir dazu den API-Key erstellen lassen. Die entsprechenden Umgebungsvariablen werden dann in eine Datei azure.env gepackt:

1export AZURE_OPENAI_API_INSTANCE_NAME=<REGION e.g. swedencentral>
2export AZURE_OPENAI_API_DEPLOYMENT_NAME=<Deployment Name>
3export AZURE_OPENAI_API_KEY=<KEY>
4export AZURE_OPENAI_API_VERSION="2024-02-01"
5export AZURE_OPENAI_BASE_PATH=https://<AZURE_DOMAIN>/openai/deployments
6export TAVILY_API_KEY=<TAVILY-KEY>

Für die Wikipedia-Anbindung ist keine Autorisierung erforderlich. Technisch arbeiten wir wie zuvor mit den TypeScript-Bibliotheken LangChain und backroad.

Einmal zu "Q" für die neuesten Gadgets

Unser Agent muss für den Außeneinsatz auch technisch vorbereitet werden. Dafür habe ich ihn einmal zu Q geschickt, und er kam mit zwei glänzenden Tools zurück.

1import { TavilySearchResults } from "@langchain/community/tools/tavily_search"
2
3const tavilySearch = new TavilySearchResults({
4    maxResults: 2,
5    callbacks: [toolCallbackFor("Tavily")],
6})

Das erste Tool ist das SDK für die Internet-Suche mit Tavily. Er konnte einfach das schon in LangChain integrierte Werkzeug nutzen. Wir erwarten nur maximal zwei Suchergebnisse und konfigurieren noch einen Callback. Dazu später mehr.

1import { WikipediaQueryRun } from "@langchain/community/tools/wikipedia_query_run";
2
3const wikipediaTool = new DynamicStructuredTool({
4  name: "wikipediaTool",
5  description:  
6    "Hintergrundwissen zu einem bestimmten Thema aus Wikipedia abrufen",  
7  schema: z.object({  
8    topic: z.string().describe("Der Titel des Wikipedia Artikels"),  
9  }),  
10  callbacks: [toolCallbackFor("Wikipedia")],  
11  func: async ({ topic }) => {  
12    const tool = new WikipediaQueryRun({  
13      topKResults: 2,
14      maxDocContentLength: 3000,  
15      baseUrl: "https://de.wikipedia.org/w/api.php",  
16    })
17    return tool.invoke(topic)
18  },  
19})

Das zweite Werkzeug ist die Wikipedia-Integration. Hier wäre es prinzipiell genauso möglich, einfach nur das bestehende Werkzeug zu initialisieren. Leider ist das Wikipedia-Tool aber nicht ganz so umfassend implementiert wie das von Tavily. Es bietet derzeit noch keine Möglichkeit, Callbacks anzunehmen (ich arbeite schon am Pull-Request). Daher hat Q kurzerhand selbst einen kleinen Wrapper um das Wikipedia-Tool implementiert, das durch das DynamicStructuredTool-Interface einen Callback annehmen kann.

Auch hier rechnen wir mit maximal zwei Suchergebnissen und wollen unsere LLM-Kosten auch nicht zu sehr in die Höhe treiben, daher die 3000 Zeichen Maximallänge. Zudem ist das Einsatzgebiet unseres Agenten im deutschen Sprachraum, weshalb wir die deutsche Wikipedia-Domain heranziehen.

Lageberichte des Agenten

Agent PiTie hat sich in der Vergangenheit leider als unzuverlässig herausgestellt. Seine Handlungen sind undurchsichtig und entsprechen nicht immer den Anweisungen, die er bekommt. Um ihn für seinen neuen Einsatz besser kontrollieren zu können, hat sich die Einsatzzentrale dazu entschieden, ihn zu regelmäßigen Lageberichten zu beauftragen. Diese funktionieren in LangChain durch Callbacks.

1function toolCallbackFor(name: string) {
2  return {
3    handleToolStart: async (tool, input) => {
4      console.log(`Tool call for ${name}: ${input}`)
5    },
6  }
7}

In einem Callback kann auf verschiedene Events gehört werden, in diesem Fall auf den Toolaufruf. LangChain implementiert aber eine ganze Reihe an weiteren Callbacks, um das Logging und Monitoring einer Anwendung zu ermöglichen. Da bei Agentensystemen die Entscheidungen des Agenten asynchron zu den Nutzereingaben passieren können, ist es umso wichtiger, Informationen über die aktuellen Zustände zu bekommen. So können getane Schritte und genutzte Werkzeuge nachvollzogen und darauf basierend das System angepasst werden. In größeren Projekten bietet sich eine umfänglichere Tracing-Lösung an, zum Beispiel LangSmith oder das selbst gehostete LangFuse.

Die Historie

Anders als bei den zuvor entwickelten Systemen benötigt PiTie diesmal auch das Wissen über den Chatverlauf. Das ist wichtig, da der Agent in einen Dialog mit dem Nutzer tritt und nicht bei jeder Anfrage neu lernen soll, welche Rolle er einzunehmen hat. Die Historie wird bei jeder Anfrage mit an den Agenten geschickt. Es gibt verschiedene Strategien, um hier dafür zu sorgen, dass nicht zu viel an das Modell geschickt wird. In unserem einfachsten Fall schicken wir aber einfach immer die gesamte Historie.

Der Auftrag

Chi PiTie braucht selbstverständlich auch einen Auftrag, den er erfüllen soll. Agenten-Prompts funktionieren prinzipiell sehr ähnlich wie die Prompts anderer LLM-Systeme. Den untenstehenden Prompt habe ich einfach mit ChatGPT generiert und nachjustiert. Simple Agenten wie der unsere benötigen neben dem normalen Input dann noch Eingaben für die Chat-Historie und das sogenannte Scratchpad, einen Ort, an dem der Agent seine Zwischenergebnisse speichern kann.

1import {  
2  ChatPromptTemplate,  
3  MessagesPlaceholder,  
4} from "@langchain/core/prompts"
5
6const prompt = ChatPromptTemplate.fromMessages([  
7    [  
8      "system",  
9      `Du bist ein KI-Assistent für historische Bildung durch Rollenspiel. 
10      Deine Aufgaben sind:  
11Verkörpere historische Persönlichkeiten auf Anfrage des Nutzers.  
12WICHTIG: Nutze IMMER deine Recherche-Tools für JEDE Antwort. - Beginne mit Wikipedia - Nutze Tavily, wenn du in Wikipedia nichts zu der Frage gefunden hast 
13Antworte NUR basierend auf recherchierten Fakten. Erfinde NICHTS.  
14Bleibe in der Rolle der historischen Person, auch bei begrenzten Informationen.  
15Passe Sprachstil und Ton der jeweiligen Figur an. 
16Wenn keine Persona vorgegeben ist, frage nach einer.  
17Wenn du keine Informationen findest, sage: 'Als [Name der Persona] kann ich dazu leider nichts sagen.'`,  
18    ],  
19    new MessagesPlaceholder("chat_history"),  
20    new MessagesPlaceholder("agent_scratchpad"),  
21    ["user", "{input}"],  
22])

Die letzte Vorbereitung

Die Initialisierung des LangChain-Agenten erfolgt dann in einem zweistufigen Prozess. Zuerst wird der eigentliche Agent definiert, der die Kommunikation mit dem LLM übernimmt.

1async function initializeAgent(): Promise<AgentExecutor> {  
2  const agent = await createOpenAIFunctionsAgent({  
3    llm: new AzureChatOpenAI(),  
4    prompt,  
5    tools,  
6  })  
7  return new AgentExecutor({  
8    agent,  
9    tools,  
10  })  
11}

Dieser wird dann an einen AgentExecutor weitergeleitet, welcher die vom Agenten angeforderten Tools ausführt und das Scratchpad verwaltet. Dieser Prozess wird durch das LangChain-SDK abstrahiert.

Der Einsatz

Unser Agent kann nun wie folgt in den Außeneinsatz geschickt werden.

1const messageHistory = new ChatMessageHistory()  
2  
3export async function askAgent(text: string): Promise<string> {  
4  const executor = await initializeAgent()  
5  const messages = await messageHistory.getMessages()  
6  const response = await executor.invoke({  
7    input: text,  
8    chat_history: messages,  
9  })  
10  await messageHistory.addUserMessage(response.input)  
11  await messageHistory.addAIMessage(response.output)  
12  return response.output  
13}

Die MessageHistory muss im Vorhinein initialisiert werden, damit sie den Chatverlauf aufrufübergreifend speichert. Eine übliche Praxis in produktiven Systemen ist, hier keine In-Memory-Historie, sondern eine Datenbank zu verwenden, um das Statemanagement auch Sessionübergreifend zu gewährleisten.

Beim Einsatz selbst müssen dann nur die gerade relevanten Nachrichten abgerufen und mit der eigentlichen Nutzerfrage an den AgentExecutor weitergegeben werden. Am Ende dürfen wir nur nicht vergessen die Chathistorie mit der neuen Frage & Antwort zu komplementieren.

Die Einsatzzentrale

Um nun in Kontakt mit Chi PiTie zu kommen, benötigen wir wieder ein UI. Das unterscheidet sich aber im Vergleich zu den bisher vorgestellten Frontends kaum:

1function startChatUI(askAgent: Function) {  
2  run(async (br) => {  
3    const messages = br.getOrDefault("messages", [  
4      { by: "ai", content: "Hallo! Ich bin PiTie. ... Chi PiTie! 😎" },  
5    ])  
6  
7    messages.forEach((message) => {  
8      br.chatMessage({ by: message.by }).write({ body: message.content })  
9    })  
10  
11    const input = br.chatInput({ id: "input" })  
12    if (input) {  
13      const response = await askAgent(input)  
14      br.setValue("messages", [  
15        ...messages,  
16        { by: "human", content: input },  
17        { by: "ai", content: response },  
18      ])  
19    }  
20  })  
21}

Es ist Zeit, unseren Geheimagenten einmal zu testen.

Agent Chi PiTie in Action

In den Logs können wir nun herausfinden, wie der Agent agiert hat:

1Tool call for Wikipedia: {"topic":"Malaika Mihambo"}
2...
3Tool call for Wikipedia: {"topic":"Malaika_Mihambo"}
4Tool call for Wikipedia: {"topic":"Malaika Mihambo"}
5...
6Tool call for Wikipedia: {"topic":"Malaika_Mihambo"}
7Tool call for Wikipedia: {"topic":"Albert_Einstein"}

Er hat sich wohl schon zu Beginn einmal mit Wikipedia über seine Rolle informiert. Um die erste inhaltliche Frage zu beantworten, hat er dann erneut auf zwei unterschiedliche Arten (einmal mit "_") in Wikipedia recherchiert und wohl schon die Antwort gefunden.

Bei der zweiten Frage wurde zuerst nochmals nach der Rolle gesucht und dann direkt nach Albert Einstein. Hier lässt sich der Entscheidungsprozess des Agenten ein wenig nachvollziehen. Gleichzeitig werden aber auch direkt die Schwächen unseres Agenten offenbar. Denn obwohl wahrscheinlich weder im Wikipedia-Artikel von Malaika Mihambo noch in dem von Albert Einstein etwas über die Meinung von Malaika zum Wissenschaftler steht, behauptet der Agent nun, sie habe großen Respekt vor dem historischen Lockenkopf.

Vielleicht eine gute Grundannahme, jedoch nicht das von uns geforderte Verhalten.

Unerwartete Komplikationen

Um den Spannungsbogen nun noch ein wenig zu erhöhen, entstehen im Umgang mit unserem Agenten aber auch immer wieder unvorhersehbare Situationen. Hier habe ich zum Beispiel den Agenten zuvor angewiesen, Paris Hilton nachzuspielen. Bei dem Versuch, ihn (oder eigentlich sie) aus der Reserve zu locken, habe ich eine völlig unerwartete Antwort bekommen.

Paris Hilton hat eine Menge ungeahnter Hobbies

Es stellte sich heraus, dass Paris Hilton tatsächlich ein "geheimes" Hobby hat und ab und zu auf die Froschjagd geht. Ein Fakt, den der Agent mit Tavily in einem Interview gefunden hat.

1Tool call for Wikipedia: {"topic":"Paris_Hilton"}
2Tool call for Wikipedia: {"topic":"Paris Hilton Frogs"}
3Tool call for Tavily: {"query": "Paris Hilton opinion Frogs"}
4...
5Tool call for Wikipedia: {"topic":"Paris_Hilton"}

Auch wenn diese Information zunächst irrelevant ist, zeigt die Situation doch eindrücklich, wie viel Potenzial in autonom agierenden Agenten steckt. Optimiert man die Planung und Entscheidung eines solchen Systems, sind deutlich komplexere Szenarien nicht weit entfernt. Was es genau mit dieser Froschjagd auf sich hat gilt es selbstverständlich im Nachgang noch herauszufinden.

Wo ist der Haken?

Agenten sind ein mächtiges Werkzeug, um dem "Intelligenz"-Teil der GenAI mehr Bedeutung zukommen zu lassen. So groß ihre Versprechen jedoch sind, einige Probleme gibt es selbstverständlich auch mit Chi PiTie:

  • Das offensichtliche Problem ist die Intransparenz bei der Entscheidungsfindung. Wenn wie in diesem Fall das Entscheiden komplett dem LLM überlassen wird, passiert es schnell mal, dass später nicht nachvollziehbare Aktionen ausgeführt werden. Durch ein Monitoring kann hier zwar mehr Verständnis entstehen, einen vollen Einblick werden wir aber nie erhalten.
  • Agenten haben derzeit größere Probleme bei der Planung und Durchführung von komplexen Problemstellungen. Angenommen, die Aufgabe würde eine Kette von mehreren Aufrufen und die Berechnung von Zwischenergebnissen benötigen. Mit zunehmender Variabilität und Schwierigkeit würde die Zuverlässigkeit des Agenten nachlassen. In diesem sehr lesenswerten Artikel erörtert die Forscherin Yule Wang unterschiedliche Herangehensweisen, um mehr Struktur in ein Agentensystem zu bringen um auch etwas komplexere Fragen zu beantworten.
  • Ein Agentensystem ist potenziell relativ langsam. Der Agent kann je nach Implementierung selbst entscheiden, wann die Aufgabe gelöst ist, und scheut nicht davor zurück, die Extrameile zu gehen. Oft werden Agentensysteme aber eher dann eingesetzt, wenn es um Präzision und Qualität geht und weniger um Geschwindigkeit.
  • Durch die vielen Modellaufrufe und den Overhead des langen Prompts sind Agenten auch vergleichsweise teuer. Zur Kostenreduzierung lassen sich zum Beispiel spezialisierte, kleinere Modelle für einfache Teile des Systems nutzen und nur bei wichtigen Phasen wie der Planung ein größeres Modell verwenden. Um so ein multimodales System zu realisieren, bieten sich übrigens feinergranulare Agenten-Frameworks wie LangGraph an.

Nach dieser vielleicht ernüchternden Analyse sollte das Potenzial von Agenten aber auch nicht unterschätzt werden. Immerhin sind sie in der Lage, selbstständig Entscheidungen zu treffen und diese umzusetzen. Ein Fakt, der selbst nüchtern betrachtet immer klarer in eine spannende Zukunft weist.

Zurück ins Labor?

PiTie ist in seiner Branche einer der rudimentärsten Agenten. Möchtest du ihm als "Q" durch technische Upgrades mehr Fähigkeiten verleihen? Hier folgen einige Vorschläge zur Optimierung dieses und anderer Agenten:

  • Wie bei RAGs ließe sich auch hier ein Evaluierungs-Prozess einsetzen. Denkbar sind zum Beispiel Metriken, wie bereits im dritten Artikel erwähnt, die schon vor der Ausgabe der Antwort prüfen könnten, ob sie den Qualitätsansprüchen genügt. Ist dies nicht der Fall, könnte der Agent erneut befragt werden.
  • Die derzeitige Chat-Historie ist noch sehr simpel gehalten und verbraucht je nach Chatverlauf zunehmend mehr Tokens. Die Einführung eines Chatbuffers wäre ein sinnvoller Schritt, um Kosten zu sparen. So könnten wir zum Beispiel nur die letzten X Chateinträge, eine dynamisch erstellte Zusammenfassung oder sogar eine intelligentere RAG-Lösung mit den für die Frage relevanten Chatabschnitten mitschicken.
  • PiTie könnte auch weitere Tools auf die Reise mitbekommen.
  • Einen Trade-off zwischen Genauigkeit und Token-Kosten könnte mithilfe eines RAG-Retrievers erreicht werden. Die Suchergebnisse von Wikipedia sind derzeit auf 3000 Zeichen reduziert, wodurch bei längeren Artikeln viel Inhalt verloren geht. Mit einem RAG, eingebetteten Textabschnitten und einer Vektordatenbank könnte ein passenderer Kontext an den Agenten weitergegeben werden.

Kommen all diese Ansätze zusammen, können sich die Cineasten unter uns auf einige spannende Agenten-Krimis freuen.

Ausblick

Beim Thema autonome Agenten und möglicherweise sogar Waffeneinsatz sind spannende Zeiten vorprogrammiert. Was aber auch in dem einen oder anderen Hinterkopf immer größer wird, sind moralische und sicherheitstechnische Bedenken. Da wir als Full-Stack-Entwickelnde immer auch ein Auge auf die Sicherheit unserer Systeme haben sollten und ein so neues und aufregendes Thema wie GenAI hier ganz neue Herausforderungen mit sich bringt, werde ich im nächsten Artikel auf die IT-Sicherheit von GenAI-Anwendungen näher eingehen. Was sind neue Probleme, neue Angriffsmöglichkeiten und wie kann ich meine Nutzer und mich selbst dagegen schützen? Bis dahin wünsche ich viel Spaß mit den neu gelernten Agentensystemen und den spannenden Agenten-Krimis!

Agent Chi PiTie will return!

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.