Reason+Act (ReAct) Agenten[1] können komplexe Denkaufgaben lösen, indem sie kettenartiges Denken mit dem Zugriff auf externe Werkzeuge und der Fähigkeit zur iterativen Lösungsfindung kombinieren.
Eine wichtige Komponente des Agenten ist der Systemprompt, der das Gesamtverhalten des Agenten definiert (wir werden gleich ein Beispiel sehen).
Die Verarbeitung beginnt mit einer Benutzereingabe, die eine Lösung für ein Problem anfordert. Der Agent wird durch den Systemprompt angewiesen, über die Frage nachzudenken und gegebenenfalls ein externes Werkzeug auszuwählen, das bei der Lösung hilft.
Der Agent ruft das Werkzeug auf und entscheidet nach Erhalt einer Antwort, ob weitere Verarbeitung erforderlich ist. Wenn ja, kann er erneut um Hilfe bitten. Der Agent durchläuft Denk- und Handlungsschritte (Aufruf eines Werkzeugs), bis er das Problem gelöst hat und gibt dann ein Ergebnis an den Benutzer zurück.
Der Prozess wird in der folgenden Abbildung veranschaulicht.
Der einfachste Weg, die Funktionsweise eines ReAct-Agenten zu erklären, ist anhand einer einfachen Fallstudie. Unten sehen Sie ein Beispiel für die Antwort eines Agenten, der ein einfaches arithmetisches Problem löst.
Wir beginnen mit der Frage „Was ist 20 * 15“ und als Antwort beginnt der Agent, ‚laut zu denken‘.
Was ist 20 * 15
Gedanke: Dies ist eine einfache Multiplikationsaufgabe, die ich mit der Aktion „berechnen“ lösen kann.
Aktion: berechnen: 20 * 15
Beobachtung: 300
Antwort: Das Ergebnis von 20 * 15 ist 300.
Die Antwort des Agenten beginnt mit der Bezeichnung Gedanke, wo er darüber nachdenkt, was er tun sollte. In diesem Fall beschließt er, die Aktion berechnen zu verwenden, um das Problem zu lösen (berechnen ist ein Werkzeug, das wir dem Agenten zur Verfügung gestellt haben).
Die nächste Antwort wird mit der Bezeichnung Aktion eingeleitet, wo wir sehen, wie der Agent das Berechnungswerkzeug verwendet, um eine Antwort zu erhalten. Darauf folgt eine Beobachtung, die die Antwort des Werkzeugs ist.
Die abschließende Antwort ist das Ergebnis des Agenten, der aus der ursprünglichen Frage und der Ausgabe des Berechnungswerkzeugs eine Antwort erstellt.
Auf den ersten Blick geschieht hier nichts außergewöhnlich Ungewöhnliches – außer dass der Agent ein Werkzeug verwendet, das wir ihm zur Verfügung gestellt haben.
Und genau das ist grundlegend für einen ReAct-Agenten: Der Agent denkt nach und handelt dann unter Verwendung externer Werkzeuge.
Iteration
Es wird spannender, wenn der Agent entscheidet, dass er noch keine Schlussfolgerung gezogen hat und eine weitere Denk-/Handlungssequenz durchlaufen muss. Der Agent kann die Denk-/Handlungssequenz so lange wiederholen, bis er zu einer gültigen Schlussfolgerung kommt.
Das obige Beispiel ist trivial, zeigt Ihnen aber die grundlegende Abfolge der Ereignisse.
Wenn wir eine komplexere Anfrage ausprobieren, können wir die Iteration in der Praxis sehen.
Ich habe den Agenten Folgendes gefragt: „Was ist das Ergebnis der Multiplikation der Anzahl der Spieler in einer Basketballmannschaft mit der Anzahl der Spieler in einer Lacrosse-Mannschaft?„.
Um diese Frage zu beantworten, muss der Agent die Anzahl der Spieler in jeder Sportart herausfinden und sie miteinander multiplizieren. Er kann das Werkzeug berechnen für die Multiplikation verwenden, aber um Informationen über Basketball und Lacrosse zu erhalten, benötigt er ein weiteres Werkzeug – Wikipedia. Das Wikipedia-Werkzeug ermöglicht es dem Agenten, alles nachzuschlagen (offensichtlich aus Wikipedia) und ein Ergebnis zu erhalten.
Unten sehen wir, wie der Agent dreimal die Sequenz Gedanke-Aktion-Beobachtung durchläuft, bevor er zu einer zufriedenstellenden Antwort kommt.
Um diese Frage zu beantworten, muss ich herausfinden, wie viele Spieler in einer Basketball- und einer Lacrosse-Mannschaft sind und dann diese Zahlen multiplizieren. Beginnen wir mit Basketball.
Gedanke: Ich werde zuerst die Anzahl der Spieler in einer Basketballmannschaft nachschlagen.
Aktion: wikipedia: Basketball
Beobachtung: Basketball ist ein Mannschaftssport. Jedes Team besteht aus fünf Spielern auf dem Spielfeld, obwohl ein Team Ersatzspieler auf der Bank haben kann.
Gedanke: Jetzt, da ich weiß, dass eine Basketballmannschaft 5 Spieler auf dem Feld hat, muss ich herausfinden, wie viele Spieler in einer Lacrosse-Mannschaft sind.
Aktion: wikipedia: Lacrosse
Beobachtung: Lacrosse ist ein Mannschaftssport, der mit 10 Spielern pro Team auf dem Feld gespielt wird: drei Angreifer, drei Mittelfeldspieler, drei Verteidiger und ein Torwart. Die Anzahl kann jedoch in verschiedenen Versionen des Spiels variieren. Zum Beispiel wird Box-Lacrosse mit weniger Spielern gespielt.
Gedanke: Jetzt, da ich die Anzahl der Spieler für beide Sportarten habe, kann ich das Ergebnis berechnen.
Aktion: berechnen: 5 * 10
Beobachtung: 50
Antwort: Das Ergebnis der Multiplikation der Anzahl der Spieler in einer Basketballmannschaft (5) mit der Anzahl der Spieler in einer Lacrosse-Mannschaft (10) ist 50.
Wenn Sie die Ausgabe durchlesen, können Sie sehen, wie der Agent nachdenkt und die Werkzeuge konsultiert, bevor er ein Ergebnis liefert, und dabei sein Wissen aufbaut.
Sehr wenig Code
Überraschenderweise wird nur wenig Code benötigt, um einen ReAct-Agenten zu implementieren, da ein Großteil der Arbeit im Prompt erledigt wird.
Schauen wir uns das also zuerst an.
prompt = """
Sie laufen in einer Schleife von Gedanke, Aktion, Beobachtung, Antwort.
Am Ende der Schleife geben Sie eine Antwort aus.
Verwenden Sie Gedanke, um Ihre Gedanken über die Frage zu beschreiben, die Ihnen gestellt wurde.
Verwenden Sie Aktion, um eine der Ihnen zur Verfügung stehenden Aktionen auszuführen.
Beobachtung wird das Ergebnis der Ausführung dieser Aktionen sein.
Antwort wird das Ergebnis der Analyse der Beobachtung sein.
Ihre verfügbaren Aktionen sind:
berechnen:
z.B. berechnen: 4 * 7 / 3
Führt eine Berechnung durch und gibt die Zahl zurück - verwendet Python, also achten Sie darauf, bei Bedarf die Gleitkommasyntax zu verwenden.
wikipedia:
z.B. wikipedia: Django
Gibt eine Zusammenfassung aus der Suche in Wikipedia zurück.
Schauen Sie immer in Wikipedia nach, wenn Sie die Möglichkeit dazu haben.
Beispielsitzung:
Frage: Was ist die Hauptstadt von Frankreich?
Gedanke: Ich sollte Frankreich in Wikipedia nachschlagen.
Aktion: wikipedia: Frankreich
Sie sollten dann die entsprechende Aktion aufrufen und die Antwort aus dem Ergebnis bestimmen.
Dann geben Sie aus:
Antwort: Die Hauptstadt von Frankreich ist Paris
"""
Der erste Teil des Prompts erklärt dem LLM, wie es ein Problem angehen soll, indem es die gekennzeichneten Teile des Prozesses durchläuft, die wir zuvor gesehen haben.
Die Aktionen berechnen und wikipedia werden dann beschrieben, gefolgt von Beispielsitzungen.
Dieser Prompt ist ein einfaches Beispiel und definitiv nur zu Demonstrationszwecken gedacht. Offensichtlich ist der Prompt für diese spezielle Anwendung konzipiert: Die Beschreibung der Werkzeuge ist fest codiert, während Sie in einem Produktionssystem in der Lage sein möchten, diese programmatisch zu erweitern. (Wir werden auch gleich sehen, dass berechnen mit der Python-Funktion eval()
implementiert ist. Dies ist eine ziemlich fragwürdige Vorgehensweise, da sie sehr anfällig für Code-Injection-Angriffe ist.)
An dieser Stelle sollte ich die Arbeit von Simon Willison anerkennen, auf der ein Großteil dieses Codes basiert – siehe Eine einfache Python-Implementierung des ReAct-Musters für LLMs | Simon Willisons TILs – der Code ist Apache 2 lizenziert.
Die grundlegende Struktur des Codes und des Prompts stammt von Simon, aber ich habe sie modifiziert, um Anthropics Claude 3.5 Sonnet LLM zu verwenden und sowohl den Code als auch den Prompt ein wenig vereinfacht.
Claude 3.5
Claude Sonnet 3.5 ist eine kürzlich von Anthropic veröffentlichte Version, und in ihrer Ankündigung heißt es: „Claude 3.5 Sonnet setzt neue Maßstäbe in der Branche für Intelligenz, übertrifft Konkurrenzmodelle und Claude 3 Opus in einer Vielzahl von Bewertungen, mit der Geschwindigkeit und den Kosten unseres Mittelklasse-Modells, Claude 3 Sonnet“.
Um das in einen Kontext zu setzen: Anthropic hat drei Versionen ihres Claude LLM, vom günstigeren, schnelleren Haiku-Modell bis zum intelligenteren, aber teureren Opus. Sonnet ist das Zwischenmodell und zum Zeitpunkt des Schreibens das einzige mit einer Version 3.5.
Ich wollte mir Claude schon seit einiger Zeit ansehen, und dies schien eine gute Gelegenheit dafür zu sein.
Der Code beginnt mit den erforderlichen Importen:
import anthropic
import re
import httpx
Es ist nicht viel. Anthropic natürlich für das LLM, reguläre Ausdrücke und eine HTTP-Bibliothek für den Zugriff auf Wikipedia.
Um den Code auszuführen, benötigen Sie natürlich ein Konto bei Anthropic, und sie werden Ihnen die Nutzung in Rechnung stellen. Es ist jedoch ziemlich günstig: Claude 3.5 Sonnet ist, soweit ich weiß, günstiger als frühere Versionen und gleichzeitig leistungsfähiger – ich habe den Code hier (und noch einiges mehr) mehrmals ausgeführt und nur ein paar 10 Cent bezahlt.
Der Code beginnt mit der Erstellung eines Clients und definiert dann eine Python-Klasse, die einen Chatbot implementiert. Im Gegensatz zu OpenAI hält Claude den Systemprompt getrennt von denen des Benutzers oder Assistenten, und wir initialisieren ihn bei der Instanziierung von ChatBot
. Die Funktion __call__
kümmert sich um die Speicherung der Benutzernachrichten und der Antworten des Chatbots und ruft execute
auf, um den Agenten auszuführen.
client = anthropic.Anthropic(api_key="IHR API-SCHLÜSSEL HIER")
class ChatBot:
def __init__(self, system=""):
self.system = system
self.messages = []
def __call__(self, message):
self.messages.append({"role": "user", "content": message})
result = self.execute()
self.messages.append({"role": "assistant", "content": result})
return message
def execute(self):
message = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1000,
temperature=0,
system = self.system,
messages = self.messages
)
return message.content
Der nächste Schritt ist die Definition einer query()
-Funktion, die eine Instanz des Chatbots verwendet. Die Query-Funktion implementiert eine Schleife, die so lange läuft, bis es keine weiteren Aktionen mehr gibt (oder wir die maximale Anzahl von Iterationen erreicht haben). Die Funktion erkennt die Aktion und extrahiert den Namen der Aktion mit Hilfe eines regulären Ausdrucks. Die Aktionen werden aufgerufen und wenn es keine weiteren gibt, werden die Chatbot-Nachrichten zurückgegeben.
action_re = re.compile('^Action: (\w+): (.*)$')
def query(question, max_turns=5):
i = 0
bot = ChatBot(prompt)
next_prompt = question
while i < max_turns:
i += 1
result = bot(next_prompt)
print(result)
actions = [action_re.match(a) for a in result.split('\n') if action_re.match(a)]
if actions:
# Es gibt eine auszuführende Aktion
action, action_input = actions[0].groups()
if action not in known_actions:
raise Exception("Unbekannte Aktion: {}: {}".format(action, action_input))
print(" -- führe {} {} aus".format(action, action_input))
observation = known_actions[action](action_input)
print("Beobachtung:", observation)
next_prompt = "Beobachtung: {}".format(observation)
else:
return bot.messages
Jetzt müssen wir die Aktionsfunktionen (Werkzeuge) wikipedia
und eval
definieren und speichern Referenzen Entschuldigung für die Unterbrechung. Hier ist die Fortsetzung der Übersetzung:
darauf in einem Wörterbuch.
def wikipedia(q):
return httpx.get("https://de.wikipedia.org/w/api.php", params={
"action": "query",
"list": "search",
"srsearch": q,
"format": "json"
}).json()["query"]["search"][0]["snippet"]
def calculate(what):
return eval(what)
known_actions = {
"wikipedia": wikipedia,
"calculate": calculate
}
Als Nächstes folgt eine Hilfsfunktion, die die letzte vom Bot generierte Nachricht ausgibt.
def get_last_message():
for m in bot.messages[-1]['content'][0].text.split('\n'):
print(m)
Und nach all dem können wir den Agenten nun verwenden.
query("Was ist 20 * 15")
get_last_message()
Der obige Code führt zu dem allerersten Beispiel, das wir am Anfang des Artikels gesehen haben. Der folgende Code zeigt ein weiteres Beispiel dafür, wie der ReAct-Agent iterativ verschiedene Aktionen durchlaufen kann, um zu einer gültigen Schlussfolgerung zu gelangen.
Hier ist ein weiteres Beispiel, bei dem der Agent mehrmals ein Werkzeug aufrufen muss. In diesem Fall verwendet er nur Wikipedia, muss aber die Antworten intelligent analysieren, um zu einer Antwort zu kommen.
query("Welche Sprachen werden in Spanien gesprochen, die auch in Frankreich gesprochen werden?")
get_last_message()
Welche Sprachen werden in Spanien gesprochen, die auch in Frankreich gesprochen werden?
Gedanke: Um diese Frage genau zu beantworten, sollte ich Informationen über die in Spanien und Frankreich gesprochenen Sprachen nachschlagen. Ich beginne mit der Suche nach Informationen über die Sprachen in Spanien.
Aktion: wikipedia: Sprachen in Spanien
Beobachtung: Spanien hat mehrere Sprachen und Dialekte, die innerhalb seiner Grenzen gesprochen werden. Die offizielle Sprache Spaniens ist Spanisch (auch Kastilisch genannt), das von der großen Mehrheit der Spanier gesprochen wird. Es gibt jedoch mehrere kooffizielle Sprachen in verschiedenen autonomen Gemeinschaften:
Katalanisch: Wird in Katalonien, auf den Balearen und in Teilen der Valencianischen Gemeinschaft gesprochen (wo es als Valencianisch bekannt ist).
Galicisch: Wird in Galicien gesprochen.
Baskisch: Wird im Baskenland und in Teilen Navarras gesprochen.
Okzitanisch (Aranesisch): Wird im Aran-Tal in Katalonien gesprochen.
Andere anerkannte Sprachen sind Aragonesisch und Asturisch. Es gibt auch mehrere spanische Dialekte, die in verschiedenen Regionen gesprochen werden.
Gedanke: Jetzt, da ich Informationen über die Sprachen in Spanien habe, sollte ich die Sprachen in Frankreich überprüfen, um zu sehen, welche sich überschneiden.
Aktion: wikipedia: Sprachen in Frankreich
Beobachtung: Die offizielle Sprache Frankreichs ist Französisch, das die Erstsprache von 88% der Bevölkerung ist. Es gibt jedoch mehrere Regionalsprachen und Dialekte, die in Frankreich gesprochen werden:
Okzitanisch: Wird in Südfrankreich gesprochen.
Bretonisch: Eine keltische Sprache, die in der Bretagne gesprochen wird.
Elsässisch: Ein germanischer Dialekt, der im Elsass gesprochen wird.
Korsisch: Wird auf der Insel Korsika gesprochen.
Katalanisch: Wird im Département Pyrénées-Orientales gesprochen.
Baskisch: Wird im französischen Baskenland gesprochen.
Andere Regionalsprachen sind Frankoprovenzalisch, Lothringisch und verschiedene Öl-Sprachen.
Gedanke: Wenn ich die in Spanien und Frankreich gesprochenen Sprachen vergleiche, kann ich die sich überschneidenden Sprachen identifizieren.
Antwort: Die Sprachen, die sowohl in Spanien als auch in Frankreich gesprochen werden, sind:
Katalanisch: Wird in Teilen Spaniens (Katalonien, Balearen und Valencia) und im Département Pyrénées-Orientales in Frankreich gesprochen.
Baskisch: Wird im Baskenland und in Teilen Navarras in Spanien sowie im französischen Baskenland gesprochen.
Okzitanisch: In Spanien als Aranesisch im Aran-Tal Kataloniens bekannt und in Südfrankreich gesprochen.
Diese drei Sprachen werden in beiden Ländern anerkannt und gesprochen, obwohl ihr offizieller Status und die Anzahl der Sprecher zwischen Spanien und Frankreich variieren können.
Die obige Antwort zeigt den sinnvollen Prozess, den der Agent durchläuft, um zu einer geeigneten Antwort zu gelangen.
Ich habe auch komplexere Fragen ausprobiert, mit denen der Agent recht gut umzugehen scheint (weitere Beispiele finden Sie im herunterladbaren Code).
Fazit
Der hier vorgestellte Code veranschaulicht die Funktionsweise eines ReAct-Agenten. Er ist nicht robust und in keiner Weise für den Produktionseinsatz geeignet. Aber ich hoffe, dass Sie nun ein recht gutes Verständnis der Prinzipien von ReAct-Agenten und ihrer möglichen Implementierung haben.
Bitte probieren Sie den Code mit komplexeren Beispielen aus und fügen Sie vielleicht weitere Werkzeuge hinzu. Ich wäre sehr interessiert, von Ihren Experimenten zu hören.
Natürlich gibt es einfachere Lösungen mit Langchain und Llamaindex (zum Beispiel), und ich werde diese wahrscheinlich in einem zukünftigen Artikel betrachten. Ihre abstrahierten Implementierungen verbergen jedoch die inneren Abläufe des Agenten, die dieser Artikel und Simons Code hoffentlich verdeutlicht haben.
Alle Code-Beispiele sind in meinem Github-Repository verfügbar – Sie können sie gerne herunterladen.