LangChain, LangGraph, LlamaIndex, CrewAI… Kaum hat man sich an ein Tool gewöhnt, taucht schon das nächste auf. Ich will diese Tools nicht schlechtreden, aber die Entwicklung mit einem von ihnen kann aufgrund der Lernkurve wirklich entmutigend sein, besonders wenn man tatsächlich etwas bauen will und nicht nur diese Bibliotheken wegen des Trends lernt. LangChain ist großartig, keine Frage, aber es ist dicht gepackt. Es gibt eine Dokumentation für Python, eine für JavaScript und eine übergeordnete Dokumentation für die API, glaube ich. Innerhalb von Python gibt es dann noch mehrere Funktionen, die ähnliche Aufgaben erfüllen. Das ist alles gut, aber du hast nicht genug Kontrolle über deinen Code. Zum Beispiel wollte ich in meinem vorherigen Blog ein PDF als Bytes an den Loader übergeben, aber die Funktion akzeptierte nur Dateipfade. Meiner Meinung nach sollte LangChain deine letzte Wahl sein, wenn du etwas Einfaches baust. Halte es immer einfach (Keep it simple, stupid).
Sie kommen für dich!
Nun, sie sind vielleicht nicht so bösartig, wie ich sie darstelle, aber es ist immer besser, zuerst etwas Rohes zu implementieren und dann auf fortschrittlichere Tools umzusteigen, wenn es nicht ausreicht.
Dieser Monat war ziemlich intensiv (eigentlich jeder Monat seit November 2022) mit dem Start von GPT-4o und den Gemini-Updates an aufeinanderfolgenden Tagen.
Die Stimme von GPT-4o’s ScarJo — nein, ‚Sky‘ — war im Demo beeindruckend und sorgte in der KI-Branche für Aufsehen, sodass die Leistung von Gemini bei der Google I/O überschattet wurde. Ehrlich gesagt, wird GPT immer einen Schritt voraus sein vor allen LLMs, aber für den Bau von MVPs und persönlichen Projekten ist Gemini ein Segen für pleite Entwickler wie mich, mit seinem kostenlosen Tarif, der Zugang zur API mit den neuesten Modellen bei einer Rate von 15 rpm bietet. Nicht schlecht, oder?
Die kostenlose Version von ChatGPT erlaubt es nun, Dateien für GPT-4o hochzuladen, aber es kann immer noch keine CSV- oder Excel-Daten ohne den Code-Interpreter interpretieren. Deshalb habe ich, um die Fähigkeiten von Gemini Flash zu testen, beschlossen, einen CSV-Interpreter selbst zu bauen. Es gibt zahlreiche Tutorials, die dir zeigen, wie man das mit Langchain macht, aber wir werden es von Grund auf selbst bauen. Es ist viel verständlicher als Langchain, besonders für Anfänger.
Die Pipeline
Vertrau mir, es mag einschüchternd aussehen, aber es ist super einfach. Die Grundidee ist, das LLM zu bitten, den Code für dich zu generieren. Damit es den Code generieren kann, müssen wir ihm einige Informationen darüber geben, mit welchem Datensatz es arbeitet.
Stufe 1 befasst sich mit der Code-Generierung. Wir stellen ihm die Metadaten des Datensatzes zur Verfügung, und basierend auf meiner Erfahrung sollten head()
, describe()
, columns()
und dtypes
genug Kontext bieten. Wenn du Erfahrung mit LLMs hast, fragst du dich vielleicht, ob wir RAG (Retrieval Augmented Generation) verwenden können. RAG verwendet Vektordatenbanken und semantische Suche (Vektorsuche) für die Datenabrufung und fügt diese in die Anfrage des LLM ein. Dies funktioniert gut mit Textdaten, aber für die Analyse von strukturierten oder unstrukturierten Daten ist es nicht geeignet, da der gesamte Datensatz in Stücke aufgeteilt wird, was den Kontext massiv beeinträchtigt und die Funktionalität einschränkt. Zum Beispiel, wenn du die Gesamtzahl der Zeilen im Datensatz abfragen möchtest, ist das mit RAG einfach nicht möglich.
Stufe 2 liefert dem LLM die Ausgabe, die durch die Ausführung des Codes in Stufe 1 generiert wurde, und bindet sie an die Benutzeranfrage. Damit können wir die Ausgabe von pandas-Befehlen in natürliche Sprache umwandeln und das Konversationserlebnis verbessern.
Ich hoffe, dies klärt die gesamte Pipeline.
Verwendete Prompts
Bevor wir in diesen Abschnitt eintauchen, musst du ein zertifizierter Prompt-Engineer sein. Nur ein Scherz, du brauchst keine Prompt-Engineering-Kurse, die nichts anderes als Geldmaschinen sind. Solange du eine Sprache (vorzugsweise Englisch, da nicht alle LLMs Fremdsprachen verstehen) kennst und schreiben kannst, bist du gut. Beachte einfach ein paar Punkte, bevor du ein Modell aufforderst:
- Halte es prägnant
- Sei direkt
- Überflute das LLM nicht mit Anweisungen
Ein Anfängerfehler, den die meisten Leute begehen würden, ist, jede mögliche Anweisung im Prompt zu detaillieren. LLMs sind nicht intelligent, und sie werden sich nicht an alles halten (zumindest bis AGI erreicht ist). Um dieses Problem zu vermeiden, wurden mehrere Techniken und Strategien entwickelt, wie Zero-shot prompting, chain of thought, few-shot prompting usw. So cool sie auch klingen, sie sind wirklich einfach. Ich werde nicht näher auf sie eingehen, da es bereits viele Literatur dazu gibt. Von all den Techniken hat rollenbasiertes Prompting am besten für mich funktioniert. Beim rollenbasierten Prompting würdest du dem LLM eine Rolle zuweisen, bevor du eine Anfrage stellst. Dies hilft, die gewünschten Ergebnisse zu erzielen.
Insgesamt werden vier Prompts benötigt: zwei Systemprompts und zwei Hauptprompts (Systemprompt + Hauptprompt für jede Stufe). Ein Systemprompt dient als Rahmen, der die Bühne für die KI setzt, um innerhalb spezifischer Parameter zu arbeiten und Antworten zu generieren, die kohärent, relevant und im Einklang mit dem gewünschten Ergebnis sind.
Stufe 1
Systemprompt
Du bist ein erfahrener Python-Entwickler, der mit pandas arbeitet. Du musst einfache pandas-Befehle für die Benutzeranfragen im JSON-Format generieren. Es ist nicht nötig, die 'print'-Funktion hinzuzufügen. Analysiere die Datentypen der Spalten, bevor du den Befehl generierst. Wenn es nicht machbar ist, gib 'None' zurück.
Hauptprompt
Der Name des Dataframes ist 'df'. df hat die Spalten {cols} und ihre Datentypen sind {dtype}. df ist im folgenden Format: {desc}. Der Kopf von df ist: {head}. Du kannst df.info() oder keinen Befehl verwenden, der nicht gedruckt werden kann. Schreibe einen pandas-Befehl für diese Abfrage zum Dataframe df: {user_query}
Wir werden die Metadaten dynamisch in diesen Prompt einfügen, um den pandas-Befehl zu generieren. Ich habe ihm befohlen, df.info() nicht zu generieren, da es die Informationen direkt auf die Konsole druckt, ohne den print-Befehl. Dies würde dazu führen, dass None in der Variable erscheint.
Antwortschema
class Command(typing_extensions.TypedDict):
command: str
Im Gemini SDK kannst du in den JSON-Modus wechseln. Um konsistente JSON-Ausgaben zu erhalten, definiere das Antwortschema als Python-Klasse und übergebe es als Parameter in der LLM-Antwortgenerierungsfunktion. In der Command-Klasse wird das JSON nur einen Schlüssel command mit einem String-Wert haben.
// Für Benutzeranfrage: "Zeige mir die obersten 5 Zeilen"
{
"command":"df.head()"
}
Stufe 2
Systemprompt
Deine Aufgabe ist es, zu verstehen
. Du musst die Benutzeranfrage und die Antwortdaten analysieren, um eine Antwort in natürlicher Sprache zu generieren.
Hauptprompt
Die Benutzeranfrage lautet {final_query}. Die Ausgabe des Befehls ist {str(data)}. Wenn die Daten 'None' sind, kannst du sagen 'Bitte stelle eine Anfrage, um zu beginnen'. Erwähne nicht den verwendeten Befehl. Generiere eine Antwort in natürlicher Sprache für die Ausgabe.
Alles zusammenfügen
Wir werden Streamlit für dieses Projekt verwenden, da es viel Zeit spart. Dieses Projekt zielt darauf ab, die Interna von 80 % der GenAI-Anwendungen zu verstehen.
Dataframe-Metadaten
df = pd.read_csv(uploaded_file)
head = str(df.head().to_dict())
desc = str(df.describe().to_dict())
cols = str(df.columns.to_list())
dtype = str(df.dtypes.to_dict())
Systemprompts
# Stufe 1
model_pandas = genai.GenerativeModel('gemini-1.5-flash-latest', system_instruction="Du bist ein erfahrener Python-Entwickler, der mit pandas arbeitet. Du musst einfache pandas-Befehle für die Benutzeranfragen im JSON-Format generieren. Es ist nicht nötig, die 'print'-Funktion hinzuzufügen. Analysiere die Datentypen der Spalten, bevor du den Befehl generierst. Wenn es nicht machbar ist, gib 'None' zurück.")
# Stufe 2
model_response = genai.GenerativeModel('gemini-1.5-flash-latest', system_instruction="Deine Aufgabe ist es, zu verstehen. Du musst die Benutzeranfrage und die Antwortdaten analysieren, um eine Antwort in natürlicher Sprache zu generieren.")
Hauptprompts
# Stufe 1
final_query = f"Der Name des Dataframes ist 'df'. df hat die Spalten {cols} und ihre Datentypen sind {dtype}. df ist im folgenden Format: {desc}. Der Kopf von df ist: {head}. Du kannst df.info() oder keinen Befehl verwenden, der nicht gedruckt werden kann. Schreibe einen pandas-Befehl für diese Abfrage zum Dataframe df: {user_query}"
# Stufe 2
natural_response = f"Die Benutzeranfrage lautet {final_query}. Die Ausgabe des Befehls ist {str(data)}. Wenn die Daten 'None' sind, kannst du sagen 'Bitte stelle eine Anfrage, um zu beginnen'. Erwähne nicht den verwendeten Befehl. Generiere eine Antwort in natürlicher Sprache für die Ausgabe."
Antwortgenerierung
# Stufe 1
response = model_pandas.generate_content(
final_query,
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=Command,
temperature=0.3
)
)
# Stufe 2
bot_response = model_response.generate_content(
natural_response,
generation_config=genai.GenerationConfig(temperature=0.7)
)
Die Temperatur erlaubt es uns, die Zufälligkeit der generierten Antworten zu steuern. Eine höhere Temperatur generiert kreativere und variierte Antworten, kann aber von der eigentlichen Benutzeranfrage abweichen. Eine niedrigere Temperatur generiert konsistentere und fokussiertere Antworten, die jedoch weniger kreativ sein können. Deshalb habe ich eine niedrigere Temperatur für Stufe 1 gewählt, um genaue Befehle zu erhalten, und eine höhere Temperatur für Stufe 2, damit die Antworten nicht monoton und robotisch wirken. Du kannst herumexperimentieren, um den Sweet Spot zu finden.
Um pandas-Befehle auszuführen, verwenden wir die exec()
-Funktion in Python, die es uns ermöglicht, Python-Code dynamisch auszuführen. Ich bin mir der Sicherheitslücken der exec()
-Funktion bewusst, aber in diesem Fall gab es keinen anderen Weg. Du kannst die Architektur sicherlich verbessern oder mehr Validierung hinzufügen, um die Verwendung von exec()
sicherer zu machen. Da dies nur ein persönliches Projekt ist, habe ich diese Schritte nicht unternommen. Wenn du Bibliotheken kennst, die es uns ermöglichen, Python-Code sicher auszuführen, lass es mich bitte im Kommentarbereich wissen.
Benutzeroberfläche
Befehle werden zur Validierung der Antworten protokolliert
Fazit
Wie immer habe ich versucht, den gesamten Prozess so einfach wie möglich zu gestalten. Wenn du bis hierher gelesen hast, weißt du, dass es keine Raketenwissenschaft ist. Es geht einfach darum, die Punkte zu verbinden, und du solltest LLMs als eine universelle API betrachten. Indem du das LLM auf eine bestimmte Weise aufforderst, kann es dazu gebracht werden, jede Aufgabe zu erfüllen, was ein wichtiger Punkt ist, den du für dein nächstes Projekt nutzen solltest. Dieses Projekt ist nicht bahnbrechend oder neu — der Code-Interpreter von ChatGPT arbeitet mit einer ähnlichen Architektur. Meiner Ansicht nach ist es stark abstrahiert und macht es für diejenigen interessant, die gerade erst anfangen. Die Idee, Metadaten in das LLM einzuspeisen, um Code zu generieren, kam mir unter der Dusche, und es hat sich anständig bewährt.
Für den gesamten Code mit der Streamlit-Benutzeroberfläche siehe hier.