In Teil 1 Von dieser Tutorial -Serie haben wir vorgestellt KI -Agentenautonome Programme, die Aufgaben ausführen, Entscheidungen treffen und mit anderen kommunizieren.
In Teil 2 Von dieser Tutorial -Serie haben wir verstanden Iterationen und Ketten.
Ein einzelner Agent kann normalerweise mit einem Instrument effektiv arbeiten, kann jedoch weniger effektiv sein, wenn viele Instruments gleichzeitig verwendet werden. Eine Möglichkeit, komplizierte Aufgaben anzugehen Multi-Agent-System (MAS).
In einem MAS arbeiten mehrere Agenten zusammen, um gemeinsame Ziele zu erreichen, und stellen häufig Herausforderungen an, die für einen einzelnen Agenten zu schwierig sind, um allein zu handhaben. Es gibt zwei Hauptmöglichkeiten, wie sie interagieren können:
- Sequentieller Fluss – Die Agenten erledigen ihre Arbeit in einer bestimmten Reihenfolge nacheinander. Zum Beispiel beendet Agent 1 seine Aufgabe, und dann verwendet Agent 2 das Ergebnis, um seine Aufgabe zu erledigen. Dies ist nützlich, wenn Aufgaben voneinander abhängen und Schritt für Schritt erfolgen müssen.
- Hierarchischer Fluss – – Normalerweise verwaltet ein Agent auf höherer Ebene den gesamten Prozess und gibt Anweisungen für die niedrigeren Stufe, die sich auf bestimmte Aufgaben konzentrieren. Dies ist nützlich, wenn die endgültige Ausgabe einige Hin- und Her erfordert.
In diesem Tutorial werde ich zeigen, wie es geht Erstellen Sie von Grund auf verschiedene Arten von Multi-Agent-Systemenvon einfach bis fortgeschrittener. Ich werde einen nützlichen Python -Code präsentieren, der in ähnlichen Fällen leicht angewendet werden kann (einfach kopieren, einfügen, einfügen) und jede Codezeile mit Kommentaren durchgehen, damit Sie dieses Beispiel replizieren können (Hyperlink zum vollständigen Code am Ende des Artikels).
Aufstellen
Bitte beziehen Sie sich auf Teil 1 für die Einrichtung von Ollama und die Haupt -LLM.
import ollama
llm = "qwen2.5"
In diesem Beispiel werde ich das Modell bitten, Bilder zu verarbeiten, daher brauche ich auch a Imaginative and prescient LLM. Es handelt sich um eine spezielle Model eines großen Sprachmodells, das bei der Integration von NLP in CV zusätzlich zum Textual content visuelle Eingaben wie Bilder und Movies verstehen soll.
Microsoft’s Llava ist eine effiziente Wahl, da sie auch ohne GPU ausgeführt werden kann.
Nach Abschluss des Downloads können Sie zu Python übergehen und Code schreiben. Laden wir ein Bild, damit wir die Imaginative and prescient LLM ausprobieren können.
from matplotlib import picture as pltimg, pyplot as plt
image_file = "draghi.jpeg"
plt.imshow(pltimg.imread(image_file))
plt.present()
Um das Imaginative and prescient LLM zu testen, können Sie das Bild einfach als Eingabe übergeben:
import ollama
ollama.generate(mannequin="llava",
immediate="describe the picture",
photos=(image_file))("response")
Sequentiell Multi-Agent-System
Ich werde zwei Agenten bauen, die in einem arbeiten werden sequentieller Flusseiner nach dem anderen, wo der zweite die Ausgabe der ersten als Eingabe nimmt, genau wie eine Kette.
- Der erste Agent Muss ein vom Benutzer bereitgestellter Bild verarbeiten und eine verbale Beschreibung dessen zurückgeben, was es sieht.
- Der zweite Agent Suche im Web und versucht zu verstehen, wo und wann das Bild aufgenommen wurde, basierend auf der Beschreibung des ersten Agenten.
Beide Agenten müssen einen verwenden Werkzeug jede. Der erste Agent wird die Imaginative and prescient LLM als Werkzeug haben. Bitte denken Sie daran, dass mit OllamaUm ein Instrument zu verwenden, muss die Funktion in einem Wörterbuch beschrieben werden.
def process_image(path: str) -> str:
return ollama.generate(mannequin="llava", immediate="describe the picture", photos=(path))("response")
tool_process_image = {'sort':'perform', 'perform':{
'identify': 'process_image',
'description': 'Load a picture for a given path and describe what you see',
'parameters': {'sort': 'object',
'required': ('path'),
'properties': {
'path': {'sort':'str', 'description':'the trail of the picture'},
}}}}
Der zweite Agent sollte ein Net-Such-Instrument haben. In den vorherigen Artikeln dieser Tutorial -Serie habe ich gezeigt, wie man das nutzt Duckduckgo Paket für die Suche im Net. Diesmal können wir additionally ein neues Instrument verwenden: Wikipedia (pip set up wikipedia==1.4.0
). Sie können die ursprüngliche Bibliothek direkt verwenden oder die importieren Langchain Verpackung.
from langchain_community.instruments import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
def search_wikipedia(question:str) -> str:
return WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper()).run(question)
tool_search_wikipedia = {'sort':'perform', 'perform':{
'identify': 'search_wikipedia',
'description': 'Search on Wikipedia by spending some key phrases',
'parameters': {'sort': 'object',
'required': ('question'),
'properties': {
'question': {'sort':'str', 'description':'The enter have to be brief key phrases, not a protracted textual content'},
}}}}
## check
search_wikipedia(question="draghi")
Zunächst müssen Sie eine Aufforderung schreiben, um die Aufgabe jedes Agenten (detaillierter, desto besser) zu beschreiben, und dies ist die erste Nachricht im Chat -Verlauf mit dem LLM.
immediate = '''
You're a photographer that analyzes and describes photos in particulars.
'''
messages_1 = ({"function":"system", "content material":immediate})
Eine wichtige Entscheidung beim Erstellen eines MAS ist, ob die Agenten die Chat -Geschichte teilen sollten oder nicht. Der Verwaltung der Chat -Geschichte Hängt vom Design und den Zielen des Programs ab:
- Gemeinsamer Chat -Historie – Agenten haben Zugang zu einem gemeinsamen Gesprächsprotokoll, sodass sie sehen können, was andere Agenten in früheren Interaktionen gesagt oder getan haben. Dies kann die Zusammenarbeit und das Verständnis des Gesamtkontexts verbessern.
- Separate Chat -Historie – Agenten haben nur Zugang zu ihren eigenen Interaktionen und konzentrieren sich nur auf ihre eigene Kommunikation. Dieses Design wird normalerweise verwendet, wenn unabhängige Entscheidungen wichtig sind.
Ich empfehle, die Chats getrennt zu halten, es sei denn, es ist notwendig, etwas anderes zu tun. LLMs haben möglicherweise ein begrenztes Kontextfenster, daher ist es besser, die Geschichte so Lite wie möglich zu gestalten.
immediate = '''
You're a detective. You learn the picture description supplied by the photographer, and also you search Wikipedia to grasp when and the place the image was taken.
'''
messages_2 = ({"function":"system", "content material":immediate})
Zur Bequemlichkeit werde ich die in den vorherige Artikeln definierte Funktion verwenden, um die Antwort des Modells zu verarbeiten.
def use_tool(agent_res:dict, dic_tools:dict) -> dict:
## use instrument
if "tool_calls" in agent_res("message").keys():
for instrument in agent_res("message")("tool_calls"):
t_name, t_inputs = instrument("perform")("identify"), instrument("perform")("arguments")
if f := dic_tools.get(t_name):
### calling instrument
print('🔧 >', f"x1b(1;31m{t_name} -> Inputs: {t_inputs}x1b(0m")
### instrument output
t_output = f(**instrument("perform")("arguments"))
print(t_output)
### last res
res = t_output
else:
print('🤬 >', f"x1b(1;31m{t_name} -> NotFoundx1b(0m")
## do not use instrument
if agent_res('message')('content material') != '':
res = agent_res("message")("content material")
t_name, t_inputs = '', ''
return {'res':res, 'tool_used':t_name, 'inputs_used':t_inputs}
Wie wir bereits in früheren Tutorials getan haben, kann die Interaktion mit den Agenten mit einem begonnen werden Während der Schleife. Der Benutzer wird aufgefordert, ein Bild bereitzustellen, das der erste Agent verarbeitet.
dic_tools = {'process_image':process_image,
'search_wikipedia':search_wikipedia}
whereas True:
## person enter
attempt:
q = enter('📷 > give me the picture to research:')
besides EOFError:
break
if q == "stop":
break
if q.strip() == "":
proceed
messages_1.append( {"function":"person", "content material":q} )
plt.imshow(pltimg.imread(q))
plt.present()
## Agent 1
agent_res = ollama.chat(mannequin=llm,
instruments=(tool_process_image),
messages=messages_1)
dic_res = use_tool(agent_res, dic_tools)
res, tool_used, inputs_used = dic_res("res"), dic_res("tool_used"), dic_res("inputs_used")
print("👽📷 >", f"x1b(1;30m{res}x1b(0m")
messages_1.append( {"function":"assistant", "content material":res} )

Der erste Agent verwendete das Imaginative and prescient LLM -Instrument und den anerkannten Textual content im Bild. Jetzt wird die Beschreibung an den zweiten Agenten übergeben, der einige Schlüsselwörter zum Durchsuchen extrahiert Wikipedia.
## Agent 2
messages_2.append( {"function":"system", "content material":"-Image: "+res} )
agent_res = ollama.chat(mannequin=llm,
instruments=(tool_search_wikipedia),
messages=messages_2)
dic_res = use_tool(agent_res, dic_tools)
res, tool_used, inputs_used = dic_res("res"), dic_res("tool_used"), dic_res("inputs_used")
Der zweite Agent verwendete das Instrument und extrahierte Informationen aus dem Net, basierend auf der Beschreibung des ersten Agenten. Jetzt kann es alles verarbeiten und eine endgültige Antwort geben.
if tool_used == "search_wikipedia":
messages_2.append( {"function":"system", "content material":"-Wikipedia: "+res} )
agent_res = ollama.chat(mannequin=llm, instruments=(), messages=messages_2)
dic_res = use_tool(agent_res, dic_tools)
res, tool_used, inputs_used = dic_res("res"), dic_res("tool_used"), dic_res("inputs_used")
else:
messages_2.append( {"function":"assistant", "content material":res} )
print("👽📖 >", f"x1b(1;30m{res}x1b(0m")
Das ist buchstäblich perfekt! Gehen wir zum nächsten Beispiel über.
Hierarchisch Multi-Agent-System
Stellen Sie sich vor, eine Gruppe von Agenten zu haben, die mit a arbeiten hierarchischer Fluss, Genau wie ein menschliches Group mit unterschiedlichen Rollen, um eine reibungslose Zusammenarbeit und eine effiziente Problemlösung zu gewährleisten. An der Spitze überwacht ein Supervisor die Gesamtstrategie, spricht mit dem Kunden (dem Benutzer), trifft hochrangige Entscheidungen und führt das Group zum Ziel. In der Zwischenzeit kümmern sich andere Teammitglieder mit operativen Aufgaben. Genau wie Menschen können Agenten zusammenarbeiten und Aufgaben angemessen delegieren.
Ich werde ein technisches Group von 3 Agenten mit dem Ziel erstellen, eine SQL -Datenbank nach Anfrage eines Benutzers abzufragen. Sie müssen in einem hierarchischen Fluss arbeiten:
- Der Hauptagent spricht mit dem Benutzer und versteht die Anfrage. Dann entscheidet es, welches Teammitglied für die Aufgabe am besten geeignet ist.
- Der Junior Agent Hat die Aufgabe, die DB zu erkunden und SQL -Abfragen zu erstellen.
- Der leitende Agent Überprüfen Sie den SQL -Code, korrigieren Sie ihn gegebenenfalls und führen Sie ihn aus.
LLMs wissen, wie man codiert, indem sie einem großen Korpus von Code- und natürlicher Sprachtext ausgesetzt sind, in dem sie Muster, Syntax und Semantik von Programmiersprachen lernen. Das Modell lernt die Beziehungen zwischen verschiedenen Teilen des Codes, indem es das nächste Token in einer Sequenz vorhergesagt. Kurz gesagt, LLMs können SQL -Code generieren, können ihn jedoch nicht ausführen, Agenten können.
Zunächst werde ich eine Datenbank erstellen und eine Verbindung dazu herstellen, dann werde ich eine Reihe von vorbereiten Werkzeuge an Führen Sie den SQL -Code aus.
## Learn dataset
import pandas as pd
dtf = pd.read_csv('http://bit.ly/kaggletrain')
dtf.head(3)
## Create dbimport sqlite3
dtf.to_sql(index=False, identify="titanic",
con=sqlite3.join("database.db"),
if_exists="change")
## Join db
from langchain_community.utilities.sql_database import SQLDatabase
db = SQLDatabase.from_uri("sqlite:///database.db")
Beginnen wir mit dem Junior Agent. LLMs benötigen keine Instruments, um SQL -Code zu generieren, aber der Agent kennt die Tabellennamen und die Struktur nicht. Daher müssen wir Instruments zur Untersuchung der Datenbank bereitstellen.
from langchain_community.instruments.sql_database.instrument import ListSQLDatabaseTool
def get_tables() -> str:
return ListSQLDatabaseTool(db=db).invoke("")
tool_get_tables = {'sort':'perform', 'perform':{
'identify': 'get_tables',
'description': 'Returns the identify of the tables within the database.',
'parameters': {'sort': 'object',
'required': (),
'properties': {}
}}}
## check
get_tables()
Dadurch werden die verfügbaren Tabellen in der DB angezeigt und die Spalten in einer Tabelle drucken.
from langchain_community.instruments.sql_database.instrument import InfoSQLDatabaseTool
def get_schema(tables: str) -> str:
instrument = InfoSQLDatabaseTool(db=db)
return instrument.invoke(tables)
tool_get_schema = {'sort':'perform', 'perform':{
'identify': 'get_schema',
'description': 'Returns the identify of the columns within the desk.',
'parameters': {'sort': 'object',
'required': ('tables'),
'properties': {'tables': {'sort':'str', 'description':'desk identify. Instance Enter: table1, table2, table3'}}
}}}
## check
get_schema(tables='titanic')
Da dieser Agent mehr als ein Instrument verwenden muss, das möglicherweise fehlschlägt, werde ich eine solide Eingabeaufforderung schreiben, die der Struktur des vorherigen Artikels folgt.
prompt_junior = '''
(GOAL) You're a information engineer who builds environment friendly SQL queries to get information from the database.
(RETURN) You could return a last SQL question primarily based on person's directions.
(WARNINGS) Use your instruments solely as soon as.
(CONTEXT) As a way to generate the right SQL question, you should know the identify of the desk and the schema.
First ALWAYS use the instrument 'get_tables' to seek out the identify of the desk.
Then, you MUST use the instrument 'get_schema' to get the columns within the desk.
Lastly, primarily based on the knowledge you bought, generate an SQL question to reply person query.
'''
Umzug in die Senior Agent. Die Codeüberprüfung erfordert keinen bestimmten Trick, Sie können einfach die LLM verwenden.
def sql_check(sql: str) -> str:
p = f'''Double test if the SQL question is right: {sql}. You MUST simply SQL code with out feedback'''
res = ollama.generate(mannequin=llm, immediate=p)("response")
return res.change('sql','').change('```','').change('n',' ').strip()
tool_sql_check = {'sort':'perform', 'perform':{
'identify': 'sql_check',
'description': 'Earlier than executing a question, all the time evaluation the SQL question and proper the code if essential',
'parameters': {'sort': 'object',
'required': ('sql'),
'properties': {'sql': {'sort':'str', 'description':'SQL code'}}
}}}
## check
sql_check(sql='SELECT * FROM titanic TOP 3')
Das Ausführen von Code in der Datenbank ist eine andere Geschichte: LLMs können das nicht alleine tun.
from langchain_community.instruments.sql_database.instrument import QuerySQLDataBaseTool
def sql_exec(sql: str) -> str:
return QuerySQLDataBaseTool(db=db).invoke(sql)
tool_sql_exec = {'sort':'perform', 'perform':{
'identify': 'sql_exec',
'description': 'Execute a SQL question',
'parameters': {'sort': 'object',
'required': ('sql'),
'properties': {'sql': {'sort':'str', 'description':'SQL code'}}
}}}
## check
sql_exec(sql='SELECT * FROM titanic LIMIT 3')
Und natürlich eine gute Aufforderung.
prompt_senior = '''(GOAL) You're a senior information engineer who evaluations and execute the SQL queries written by others.
(RETURN) You could return information from the database.
(WARNINGS) Use your instruments solely as soon as.
(CONTEXT) ALWAYS test the SQL code earlier than executing on the database.First ALWAYS use the instrument 'sql_check' to evaluation the question. The output of this instrument is the proper SQL question.You MUST use ONLY the proper SQL question whenever you use the instrument 'sql_exec'.'''
Schließlich werden wir das schaffen Lead Agent. Es hat den wichtigsten Job: andere Agenten aufzurufen und ihnen zu sagen, was zu tun ist. Es gibt viele Möglichkeiten, dies zu erreichen, aber ich finde, dass ein einfaches Instrument das genaueste erstellt.
def invoke_agent(agent:str, directions:str) -> str:
return agent+" - "+directions if agent in ('junior','senior') else f"Agent '{agent}' Not Discovered"
tool_invoke_agent = {'sort':'perform', 'perform':{
'identify': 'invoke_agent',
'description': 'Invoke one other Agent to give you the results you want.',
'parameters': {'sort': 'object',
'required': ('agent', 'directions'),
'properties': {
'agent': {'sort':'str', 'description':'the Agent identify, one in all "junior" or "senior".'},
'directions': {'sort':'str', 'description':'detailed directions for the Agent.'}
}
}}}
## check
invoke_agent(agent="intern", directions="construct a question")
Beschreiben Sie in der Aufforderung, welche Artwork von Verhalten Sie erwarten. Versuchen Sie, so detailliert wie möglich zu sein, denn hierarchische Multi-Agent-Systeme können sehr verwirrend werden.
prompt_lead = '''
(GOAL) You're a tech lead.
You've a crew with one junior information engineer known as 'junior', and one senior information engineer known as 'senior'.
(RETURN) You could return information from the database primarily based on person's requests.
(WARNINGS) You're the just one that talks to the person and will get the requests from the person.
The 'junior' information engineer solely builds queries.
The 'senior' information engineer checks the queries and execute them.
(CONTEXT) First ALWAYS ask the customers what they need.
Then, you MUST use the instrument 'invoke_agent' to move the directions to the 'junior' for constructing the question.
Lastly, you MUST use the instrument 'invoke_agent' to move the directions to the 'senior' for retrieving the information from the database.
'''
Ich werde den Chat -Verlauf getrennt halten, damit jeder Agent nur einen bestimmten Teil des gesamten Prozesses kennt.
dic_tools = {'get_tables':get_tables,
'get_schema':get_schema,
'sql_exec':sql_exec,
'sql_check':sql_check,
'Invoke_agent':invoke_agent}
messages_junior = ({"function":"system", "content material":prompt_junior})
messages_senior = ({"function":"system", "content material":prompt_senior})
messages_lead = ({"function":"system", "content material":prompt_lead})
Alles ist bereit zu Starten Sie den Workflow. Nachdem der Benutzer mit dem Chat begonnen hat, ist der erste, der geantwortet hat, der Führer, der die einzige ist, die direkt mit dem Menschen interagiert.
whereas True:
## person enter
q = enter('🙂 >')
if q == "stop":
break
messages_lead.append( {"function":"person", "content material":q} )
## Lead Agent
agent_res = ollama.chat(mannequin=llm, messages=messages_lead, instruments=(tool_invoke_agent))
dic_res = use_tool(agent_res, dic_tools)
res, tool_used, inputs_used = dic_res("res"), dic_res("tool_used"), dic_res("inputs_used")
agent_invoked = res.break up("-")(0).strip() if len(res.break up("-")) > 1 else ''
directions = res.break up("-")(1).strip() if len(res.break up("-")) > 1 else ''
###-->CODE TO INVOKE OTHER AGENTS HERE<--###
## Lead Agent last response print("👩💼 >", f"x1b(1;30m{res}x1b(0m") messages_lead.append( {"function":"assistant", "content material":res} )
Der Lead -Agent entschied sich, den Junior -Agenten aufzurufen, der ihm eine Anweisung basierte, basierend auf der Interaktion mit dem Benutzer. Jetzt soll der Junior Agent an der Abfrage arbeiten.
## Invoke Junior Agent
if agent_invoked == "junior":
print("😎 >", f"x1b(1;32mReceived directions: {directions}x1b(0m")
messages_junior.append( {"function":"person", "content material":directions} )
### use the instruments
available_tools = {"get_tables":tool_get_tables, "get_schema":tool_get_schema}
context = ''
whereas available_tools:
agent_res = ollama.chat(mannequin=llm, messages=messages_junior,
instruments=(v for v in available_tools.values()))
dic_res = use_tool(agent_res, dic_tools)
res, tool_used, inputs_used = dic_res("res"), dic_res("tool_used"), dic_res("inputs_used")
if tool_used:
available_tools.pop(tool_used)
context = context + f"nTool used: {tool_used}. Output: {res}" #->add instrument utilization context
messages_junior.append( {"function":"person", "content material":context} )
### response
agent_res = ollama.chat(mannequin=llm, messages=messages_junior)
dic_res = use_tool(agent_res, dic_tools)
res = dic_res("res")
print("😎 >", f"x1b(1;32m{res}x1b(0m")
messages_junior.append( {"function":"assistant", "content material":res} )
Der Junior Agent aktivierte alle seine Instruments, um die Datenbank zu untersuchen, und sammelte die erforderlichen Informationen, um einen SQL -Code zu generieren. Jetzt muss es sich an die Führung melden.
## replace Lead Agent
context = "Junior already wrote this question: "+res+ "nNow invoke the Senior to evaluation and execute the code."
print("👩💼 >", f"x1b(1;30m{context}x1b(0m")
messages_lead.append( {"function":"person", "content material":context} )
agent_res = ollama.chat(mannequin=llm, messages=messages_lead, instruments=(tool_invoke_agent))
dic_res = use_tool(agent_res, dic_tools)
res, tool_used, inputs_used = dic_res("res"), dic_res("tool_used"), dic_res("inputs_used")
agent_invoked = res.break up("-")(0).strip() if len(res.break up("-")) > 1 else ''
directions = res.break up("-")(1).strip() if len(res.break up("-")) > 1 else ''
Der Lead -Agent erhielt die Ausgabe vom Junior und forderte den Senior Agent auf, die SQL -Abfrage zu überprüfen und auszuführen.
## Invoke Senior Agent
if agent_invoked == "senior":
print("🧓 >", f"x1b(1;34mReceived directions: {directions}x1b(0m")
messages_senior.append( {"function":"person", "content material":directions} )
### use the instruments
available_tools = {"sql_check":tool_sql_check, "sql_exec":tool_sql_exec}
context = ''
whereas available_tools:
agent_res = ollama.chat(mannequin=llm, messages=messages_senior,
instruments=(v for v in available_tools.values()))
dic_res = use_tool(agent_res, dic_tools)
res, tool_used, inputs_used = dic_res("res"), dic_res("tool_used"), dic_res("inputs_used")
if tool_used:
available_tools.pop(tool_used)
context = context + f"nTool used: {tool_used}. Output: {res}" #->add instrument utilization context
messages_senior.append( {"function":"person", "content material":context} )
### response
print("🧓 >", f"x1b(1;34m{res}x1b(0m")
messages_senior.append( {"function":"assistant", "content material":res} )
Der Senior Agent hat die Abfrage auf der DB ausgeführt und eine Antwort erhalten. Schließlich kann es sich an die Führung melden, die dem Benutzer die endgültige Antwort gibt.
### replace Lead Agent
context = "Senior agent returned this output: "+res
print("👩💼 >", f"x1b(1;30m{context}x1b(0m")
messages_lead.append( {"function":"person", "content material":context} )
Abschluss
Dieser Artikel hat die grundlegenden Schritte zum Erstellen von Multi-Agent-Systemen von Grund auf nur mit nur mit Ollama. Mit diesen Bausteinen sind Sie bereits ausgestattet, um Ihre eigenen MAS für verschiedene Anwendungsfälle zu entwickeln.
Bleib dran für Teil 4wo wir tiefer in fortgeschrittenere Beispiele eintauchen.
Voller Code für diesen Artikel: Github
Ich hoffe es hat dir gefallen! Wenden Sie sich an mich, um mich für Fragen und Suggestions zu kontaktieren oder einfach Ihre interessanten Projekte zu teilen.
👉 Lassen Sie uns eine Verbindung herstellen 👈
Alle Bilder, sofern nicht anders angegeben, stammen vom Autor