Ein Mini-KI-Assistent von Grund auf mit Agenten, Tools und Fähigkeiten erstellen.
Ein kleines Python-Projekt, das eine Anfrage an Agenten weiterleitet, einen Rechner sowie ein Umrechnungstool aufruft, eine Zusammenfassungsfunktion ausführt und die neu erstellte Anfrage an ein LLM sendet.
Quellen: Agents, Tools, and Skills for a Working Mini AI Assistant
Fragen Sie einen Chatbot, wie das Wetter in Chicago letzte Woche war, und er wird raten. Geben Sie demselben Chatbot ein Werkzeug, das tatsächlich nachprüft, und er wird es wissen. Der Unterschied zwischen einem Modell, das ratet, und einem System, das weiß, wird durch Agenten, Werkzeuge und Fähigkeiten überbrückt – und das erklärt viel dazu, warum generative KI zu einem so oft diskutierten Bereich in der IT geworden ist.
Die Einstiegshürde ist niedriger, als es scheint. Wenn Sie eine ordentliche Google-Suche durchführen können, können Sie auch einen guten Prompt verfassen. Ein einzelner Prompt reicht jedoch nur bis zu einem gewissen Grad aus. Der interessante Teil beginnt, wenn Sie aufhören, vom Modell zu erwarten, dass es alles weiß, und ihm stattdessen Agenten, Werkzeuge und Fähigkeiten zur Verfügung stellen. Unten finden Sie eine Erklärung aller drei Konzepte, die Sie mit Python selbst ausprobieren können – basierend auf einem kleinen Beispiel für die Reiseplanung von Anfang bis Ende.
Definierte Werkzeuge, Fähigkeiten und Agenten
Tools stammen in der Regel von einer externen Quelle wie einem MCP (Model Context Protocol)-Server. Wenn Sie diesen Server aufrufen, erhalten Sie in der Regel eine Liste von Tools, die bei der Verarbeitung Ihrer Anfrage helfen. Nehmen wir an, Sie möchten Wetterinformationen in einer Anfrage enthalten. Ein Wetter-MCP-Server könnte Tools wie diese bereitstellen, wobei get_forecast die aktuelle Wettervorhersage zurückgibt. Wenn Ihre Anfrage nach der Veränderung des Wetters in Chicago im Laufe der letzten Woche fragt, würde ein solches Tool die richtigen Anweisungen hinzufügen, damit die Abfrage erfolgreich ist.
get_current_conditions
get_forecast
get_alerts
get_historical_weather
get_air_quality
get_marine_conditions
get_river_conditions
get_wildfire_info
search_location
check_service_status
Diese Liste ist nur ein Beispiel dafür, was ein Tools-Server anbieten könnte. Das praktische Beispiel weiter unten ersetzt diese durch zwei einfachere benutzerdefinierte Tools: eine Rechenuhr und einen Entfernungswandler.
Fähigkeiten ähneln eher Rezepten. Wenn Sie für Medium schreiben, könnte eine Ihrer Fähigkeiten ein Stilleitfaden sein. In vielen Datenengineering-Projekten handelt es sich dabei um Listen von Berechnungen oder vergleichbaren Werten, was sie zu etwas wie Suchtabellen macht. Wenn eine Anfrage nach den Preisen aller Flüge aus dem Gebiet um Chicago verlangt, könnte eine Fähigkeit ORD (O’Hare), MDW (Midway) oder CHI eintragen – die Codes für alle Flughäfen der Stadt. Hier sind Beispiele für Messfamilien, die eine Fähigkeit enthalten könnte:
Temperature = Celsius, Fahrenheit, Kelvin
Length / Distance = Inches, Feet, Yards, Miles, Millimeters, Centimeters, Meters, Kilometers
Mass / Weight = Ounces, Pounds, Stone, Grams, Kilograms, Metric Tons
Volume / Capacity = Fluid Ounces, Cups, Pints, Quarts, Gallons, Milliliters, Liters, Cubic Meters
Area = Square Feet, Square Meters, Acres, Hectares
Speed = Miles per Hour (mph), Kilometers per Hour (km/h), Knots, Meters per Second (m/s)
Time Zones = UTC/GMT offsets, Daylight Saving Time (DST) transitions, Unix timestamps to human-readable dates
Storage = Bytes, Kilobytes (KB), Megabytes (MB), Gigabytes (GB), Terabytes (TB)
Auch hier handelt es sich um eine allgemeine Darstellung dessen, was eine Abfrage-Tabelle für eine Fähigkeit enthalten könnte. Das Beispiel benötigt lediglich die Spalten für Entfernungen, um zwischen Meilen, Kilometern, Metern und Fuß umzurechnen.
Es sind die Agenten, die dafür sorgen, dass ein KI-Prozess so aussieht, als würde er logisch denken. In der Praxis durchläuft Ihre Anfrage mehrere Schleifen, wobei jede auf der vorherigen aufbaut. Die erste Anfrage gibt dem System genügend Richtlinien für den ersten Schritt. Danach entscheidet es, welcher nächste Schritt erforderlich ist, und setzt dies so lange fort, wie es Ihr Code zulässt. Stellen Sie sich ein Buch „Wähle dein eigenes Abenteuer“ vor: Jeder Durchlauf kann in eine andere Richtung abzweigen, um zu einer passenden Antwort zu gelangen.
Implementierung im Code
Die Architektur ist absichtlich einfach. Sie können den Code in einem Jupyter Notebook oder Google Colab schreiben – es kostet nichts und es ist einfach anzupassen. Die Schritt-für-Schritt-Anleitung behandelt zunächst die Codeabschnitte in umgekehrter Ausführungsreihenfolge, und der folgende Abschnitt zeigt, wie sich alles verhält, wenn eine echte Anfrage eingegeben wird.
Schritt 1: Imports
Die Bibliotheken requests, getpass und os holen den Schlüssel ab und rufen die Groq LLM auf. Die re-Bibliothek hilft dabei, Muster innerhalb der Anfrage zu finden, und jedes Tool sowie jede Fähigkeit setzt sie ein. Sie benötigen Python 3.10 oder neuer. Falls requests nicht installiert ist, führen Sie zuerst !pip install requests aus.
import re
import os
import getpass
import requests
Schritt 2: die Rechenmaschine
Ein reguläres Muster extrahiert jede Dollarsumme im Eingabetext und die Funktion addiert sie zusammen. Sie sucht einfach nach einem $ gefolgt von Ziffern und addiert die Zahlen. Wenn Sie mit britischen Pfund arbeiten, müssen Sie das Muster ändern.
def calculator_tool(prompt: str) -> str:
print(" [Tool 1: Calculator] scanning prompt for dollar amounts...")
dollar_amounts = re.findall(r'\$\s?(\d+(?:\.\d{1,2})?)', prompt)
if not dollar_amounts:
result = "no dollar amounts found"
print(f" [Tool 1: Calculator] {result}")
return result
values = [float(a) for a in dollar_amounts]
total = sum(values)
breakdown = " + ".join(f"${v:g}" for v in values)
result = f"{breakdown} = ${total:.2f}"
print(f" [Tool 1: Calculator] found {len(values)} amount(s) {values} -> {result}")
return result
Schritt 3: Ein Umrechnungstool für Einheiten
Dieses Tool wandelt gängige Längeneinheiten um. Zunächst normalisiert es die von dem Benutzer verwendete Einheit mithilfe des UNIT_ALIASES-Wörterbuchs und wandelt anschließend jede Einheit mit dem DISTANCE_TO_MILES-Wörterbuch in Meilen um, was die Gesamtsumme leicht berechnen lässt. Je nach Anwendungsfall können Sie auch Umrechnungen für Kilometer oder Schrittzähler hinzufügen.
UNIT_ALIASES = {
"kilometers": "km", "kilometer": "km", "km": "km",
"mile": "miles", "miles": "miles",
"m": "meters", "meter": "meters", "meters": "meters",
"ft": "feet", "foot": "feet", "feet": "feet",
}
DISTANCE_TO_MILES = {"km": 0.621371, "miles": 1.0, "meters": 0.000621371, "feet": 0.000189394}
def unit_converter_tool(prompt: str) -> str:
print(" [Tool 2: Unit Converter] scanning prompt for distance legs...")
legs = re.findall(r'(\d+(?:\.\d+)?)\s*(kilometers?|km|miles?|meters?|feet|ft)\b',
prompt, re.IGNORECASE)
if not legs:
result = "no distances found"
print(f" [Tool 2: Unit Converter] {result}")
return result
total_miles = 0.0
breakdown = []
for value, unit in legs:
value = float(value)
unit_norm = UNIT_ALIASES.get(unit.lower(), unit.lower())
total_miles += value * DISTANCE_TO_MILES.get(unit_norm, 1.0)
breakdown.append(f"{value:g} {unit_norm}")
result = f"{' + '.join(breakdown)} = {round(total_miles, 2)} miles total"
print(f" [Tool 2: Unit Converter] found {len(legs)} leg(s) -> {result}")
return result
Schritt 4: Eine Fähigkeit zum Zusammenfassen
Ein leichtgewichtiger Zusammenfasser. Er bewertet jeden Satz, indem er die in der stopwords-Menge aufgeführten Stop-Wörter entfernt und das Verhältnis der verbleibenden sinnvollen Wörter zur Gesamtzahl heranzieht, um eine Rangliste zu erstellen. Ziel ist es, Sätze wegzulassen, die nichts zum Eingabetext beitragen, und den unnötigen Tokenverbrauch zu reduzieren.
def summarizer_skill(text: str, max_sentences: int = 2) -> str:
print(" [Skill: Summarizer] scanning message for key sentence(s)...")
sentences = [s for s in re.split(r'(?<=[.!?])\s+', text.strip()) if s]
if len(sentences) <= max_sentences:
print(f" [Skill: Summarizer] only {len(sentences)} sentence(s) -- returning as-is")
return text.strip()
stopwords = {"the","a","an","is","are","was","were","in","on","at","to","of","and",
"or","for","it","this","that","i","you","he","she","they","we","really"}
words = re.findall(r'\b\w+\b', text.lower())
freq = {}
for w in words:
if w not in stopwords:
freq[w] = freq.get(w, 0) + 1
scored = []
for idx, sentence in enumerate(sentences):
s_words = re.findall(r'\b\w+\b', sentence.lower())
score = sum(freq.get(w, 0) for w in s_words)
scored.append((score, idx, sentence))
top = sorted(scored, key=lambda x: x[0], reverse=True)[:max_sentences]
top_in_order = sorted(top, key=lambda x: x[1])
summary = " ".join(s for _, _, s in top_in_order)
print(f" [Skill: Summarizer] kept {len(top_in_order)} of {len(sentences)} sentence(s) -> {summary}")
return summary
Schritt 5: Anbindung eines LLM
Hier kommuniziert der Code mit einem echten Modell. Groq wird verwendet, weil es eine kostenlose Version bietet, doch es ist eine eigene API-Schlüssel aus der Groq-Konsole erforderlich, damit es funktioniert.
GROQ_API_KEY = os.environ.get("GROQ_API_KEY") or getpass.getpass(
"Enter your free Groq API key (from https://console.groq.com/keys), "
"or press Enter to skip: "
)
GROQ_MODEL = "openai/gpt-oss-20b"
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"
def call_llm(augmented_prompt: str,
system_prompt: str = "You are a helpful, concise assistant.") -> str:
if not GROQ_API_KEY:
return ("[No LLM reply -- no Groq API key was provided. Get a free one at "
"https://console.groq.com/keys, then re-run the setup cell above.]\n"
f"Here is the augmented prompt that would have been sent:\n\"\"\"\n{augmented_prompt}\n\"\"\"")
try:
response = requests.post(
GROQ_ENDPOINT,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {GROQ_API_KEY}",
},
json={
"model": GROQ_MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": augmented_prompt},
],
"temperature": 0.7,
"max_tokens": 400,
},
timeout=30,
)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"].strip()
except requests.exceptions.RequestException as e:
return f"[LLM request failed -- {e}]"
except (KeyError, IndexError, ValueError):
return "[LLM returned an unexpected response format.]"
print("LLM configured." if GROQ_API_KEY else "No key entered -- running in fallback mode.")
Schritt 6: Die Agenten
Hier werden die Agenten konfiguriert. Sie fügen Kontext zu Ihrer Anfrage hinzu, und man sieht, wie sie Tools sowie Fähigkeiten nutzen, um genaue Daten zurückzugeben.
def show_step(step_num: int, label: str, content: str) -> None:
"""Small helper so every agent prints its pipeline the same, readable way."""
print(f"\n STEP {step_num} - {label}:")
for line in str(content).splitlines() or [""]:
print(f" {line}")
def trip_planner_agent(prompt: str) -> str:
"""Agent 1. Condition: 2+ dollar costs AND 2+ distances -- a multi-stop itinerary."""
print("[Router] -> Agent 1: Trip Planner Agent activated (detected an itinerary: multiple costs + multiple distances)")
show_step(1, "Original prompt", prompt)
cost_result = calculator_tool(prompt)
show_step(2, "Tool result (Calculator -- total cost)", cost_result)
distance_result = unit_converter_tool(prompt)
show_step(3, "Tool result (Unit Converter -- total distance)", distance_result)
augmented_prompt = (
f"The user asked: \"{prompt}\"\n\n"
f"A calculator tool already computed the total cost: {cost_result}\n"
f"A distance tool already computed the total distance traveled: {distance_result}\n\n"
"Using those two verified totals (don't redo either calculation yourself), give the "
"user a short, friendly trip summary that reports both totals clearly."
)
show_step(4, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)
reply = call_llm(augmented_prompt)
show_step(5, "Final response from Groq", reply)
return f"🧳 Agent 1: Trip Planner Agent:\n{reply}"
def text_agent(prompt: str) -> str:
"""Agent 2. Condition: default for any prompt, OR chained after Agent 1
when the itinerary prompt also has an extra narrative sentence."""
print("[Router] -> Agent 2: Text Analysis Agent activated")
show_step(1, "Original prompt", prompt)
summary = summarizer_skill(prompt)
show_step(2, "Skill result (Summarizer)", summary)
augmented_prompt = (
f"The user wrote: \"{prompt}\"\n\n"
f"Automatic summary of their message: {summary}\n\n"
"Write a short, thoughtful, natural-sounding reply to the user that responds "
"to what they actually said, informed by (but not just repeating) this summary."
)
show_step(3, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)
reply = call_llm(augmented_prompt)
show_step(4, "Final response from Groq", reply)
return f"📝 Agent 2: Text Analysis Agent:\n{reply}"
Schritt 7: Der Router
Der Router ist das „Gehirn“ der Abläufe. Er sucht nach bestimmten Eigenschaften in einer Anfrage und leitet sie an den oder die richtigen Agenten weiter. Ein Detail: has_extra_narrative ist eine Bedingung, die erst nachdem trip_planner_agent bereits zugeordnet wurde, überprüft wird. Der text_agent läuft entweder standardmäßig für jede Anfrage oder zusammen mit dem Reiseplaner, sobald diese Bedingung erfüllt ist.
def looks_like_itinerary(prompt: str) -> bool:
dollar_amounts = re.findall(r'\$\s?\d+(?:\.\d{1,2})?', prompt)
distance_legs = re.findall(r'\d+(?:\.\d+)?\s*(?:kilometers?|km|miles?|meters?|feet|ft)\b',
prompt, re.IGNORECASE)
return len(dollar_amounts) >= 2 and len(distance_legs) >= 2
def has_extra_narrative(prompt: str) -> bool:
sentences = [s for s in re.split(r'(?<=[.!?])\s+', prompt.strip()) if s]
extra = [
s for s in sentences
if "quot; not in s
and not re.search(r'\b(?:miles?|km|kilometers?|feet|ft)\b', s, re.IGNORECASE)
and not s.strip().endswith("?")
]
return len(extra) >= 1
def route_prompt(prompt: str) -> str:
if looks_like_itinerary(prompt):
reply = trip_planner_agent(prompt)
if has_extra_narrative(prompt):
print("[Router] -> also routing to Agent 2: Text Analysis Agent (extra narrative sentence detected)")
reply += "\n\n" + text_agent(prompt)
return reply
else:
return text_agent(prompt)
Schritt 8: Ausprobieren
Es ist Zeit, alles zu testen.
prompt = input("Ask me anything: ")
print("=" * 60)
print(f"PROMPT: {prompt}")
print("=" * 60)
final_answer = route_prompt(prompt)
print(f"\n >>> RETURNED: {final_answer}")
print("-" * 60 + "\n")
Eine Anfrage im System Schritt für Schritt verfolgen
Nehmen Sie diese Anfrage und verfolgen Sie jede Etappe:
"Ein Cubs-Spiel im Wrigley Field (Ticketpreis 55 Dollar) eröffnete den Tag. Danach folgte eine 5,5 Meilen lange Fahrt zum Art Institute of Chicago (Eintrittspreis 32 Dollar), weitere 1,5 Meilen zum Shedd Aquarium (Preis 20 Dollar) sowie noch 2,3 Meilen zu einem Abendessen im Stil des Deep-Dish-Pizzas bei Giordano’s in River North (Preis 30 Dollar). Wie viel kostete der Tag insgesamt und wie lang war die Strecke? Baseball, Kunst, Meeresleben und Pizza sorgten für einen vollen Tag – ein kurzer Überblick für das Reisetagebuch wäre schön."
Streckenplanung
Alles beginnt beim Prompt-Router. Wenn dieser etwas entdeckt, das einer Währung ähnelt, wie zum Beispiel ein $, zusammen mit einer Entfernung in Meilen oder Kilometern, leitet er den Prompt an den Reiseplaner, Agent 1, weiter. Anschließend wird has_extra_narrative überprüft, wobei dieser nur dann ausgeführt wird, wenn der Reiseplaner eine Übereinstimmung gefunden hat, und danach wird nach einem zusätzlichen Satz gesucht, der weder mit den Kosten noch mit der Entfernung zusammenhängt. Falls ein solcher Satz vorhanden ist, ruft der Router auch den Text-Agenten, Agent 2. Wenn weder ein Reiseplan-Muster noch ein zusätzlicher Satz vorliegen, kümmert sich der Text-Agent selbst um den Prompt. Hier sind beide Bedingungen erfüllt, sodass beide Agenten aktiv werden. Das Farbmarkieren des Prompts macht deutlich, welche Teile jeweils welche Agenten auslösen.
Der Reiseplaner ruft seine Tools auf
Der Reiseplaner-Agent ruft sowohl den Rechner als auch den Umrechner auf.
Der Rechner findet alle Beträge in Dollar und gibt einen einzigen String zurück:
$55 + $32 + $20 + $30 = $137.00
Der Umrechner ermittelt die Entfernungen und gibt folgendes zurück:
5,5 Meilen, 1,5 Meilen und 2,3 Meilen ergeben zusammen 9,3 Meilen
Die Anfrage wird neu erstellt
Sowohl die Ergebnisse als auch der ursprüngliche Text werden an den Reiseplaner zurückgesendet, der sie verwendet, um die Anfrage neu zu erstellen, indem er die berechneten Gesamtbeträge zum ursprünglichen Text hinzufügt. Diese neue Anfrage wird an Groq, das für die Demonstration verwendete große Sprachmodell, gesendet, welches möglicherweise wie folgt antworten könnte:
Was für ein großartiger Tag in Chicago.
- Ausgegeben: $137
- Entfernung: 9,3 Meilen
Bewahren Sie sich die Erinnerungen sowie das Deep-Dish auf.
Der Textagent und der Zusammenfasser
Nachdem der erste Agent fertig war, wurde has_extra_narrative wieder auf true gesetzt, wodurch der Textagent aktiviert wurde und die ursprüngliche Anfrage an die Zusammenfassungsfunktion weitergeleitet wurde. Da die Anfrage mehr Sätze enthielt als die von der Funktion vorgegebene Obergrenze von zwei Sätzen, behielt sie die beiden am besten bewerteten Sätze bei und ließ die mittlere Frage ganz aus. Ein Vergleich der ursprünglichen Anfrage mit der Ergebnisanfrage zeigt, dass die Frage nach Kosten und zurückgelegten Meilen fehlt und nur die erzählerischen Sätze übrig bleiben.
Diese ausschließlich aus Erzähltext bestehende Anfrage wird ein zweites Mal an Groq gesendet, und das Modell erstellt eine durchdachte Tagebucheintragung, ohne etwas neu berechnen zu müssen. Die Angaben zu Dollarbetrag und zurückgelegten Meilen in seiner Antwort stammen vom ersten Agenten früher im Prozess und nicht von diesem Schritt – genau das ist der Sinn dahinter, die Arbeit zwischen den Agenten aufzuteilen:
Ihr Tagesaufwand betrug insgesamt 137 Dollar und umfasste 9,3 Meilen.
Zusammenfassung des Tagesberichts:
- Spiel im Wrigley Field: 55 $
- Art Institute, 5,5 Meilen entfernt: 32 $
- Shedd Aquarium, 1,5 Meilen entfernt: 20 $
- Deep-dish-Pizza bei Giordano’s, 2,3 Meilen entfernt: 30 $
Basball, Kunst, Meereslebewesen und Chicago-Pizza – das macht einen vollgepackten Ausflug aus.
Warum dieses Muster nützlich ist
Dieses Beispiel zeigt, dass Agenten, Fähigkeiten und Tools es einem System ermöglichen, Ihren Prompt im Hintergrund neu aufzubauen, ohne dass Sie die zusätzlichen Schritte sehen. Ein Router und Agenten arbeiten zusammen, um herauszufinden, welche Tools oder Fähigkeiten benötigt werden, die relevanten Informationen zu sammeln, den Prompt entsprechend umzustrukturieren und das Ergebnis an das LLM weiterzuleiten. In echten Implementierungen würden mehrere Runden nacheinander durchgeführt, wodurch das System den Prompt jedes Mal erweitert und verfeinert, wenn es etwas Neues lernt.
Die hier aufgeführten Tools sind einfache Funktionen. In der Produktion würden sie meist auf einem von jemand anderem erstellten MCP-Server liegen, der zur Aufrufbereitschaft bereitsteht. Die Anleitungen würden sich ebenfalls etwas unterscheiden, da Entwickler diese in der Regel in einer Markdown-Datei statt in einer Python-Funktion wie der oben gezeigten schreiben.
Um anzufangen, benötigen Sie weder ein großes Team noch ein aufwändiges Framework. Ein Router zusammen mit zwei oder drei kleinen Tools sowie ein Modell, das auf diese vertrauen kann anstelle von Improvisationen, reichen aus, um loszulegen. Ersetzen Sie die Tools, Fähigkeiten oder Agenten durch Ihre eigenen und passen Sie alles an Ihre Bedürfnisse an, bevor Sie diejenigen für das Problem entwickeln, mit dem Sie konfrontiert sind.