Singularity: AI Engineering für einen agentischen Astronomie-Assistenten
Wie Singularity mit LlamaIndex, Function Agents, lokalem RAG, Chainlit, Phoenix und Stellarium Astronomiefragen beantwortet und Beobachtungsabende plant.
Ein Astronomie-Chatbot, der eine Frage zum Mars beantwortet, ist schnell gebaut. Schwieriger wird es, wenn derselbe Assistent eine Folgefrage korrekt auflösen, seine Quellen belegen, aus Standort, Uhrzeit und Ausrüstung einen Beobachtungsplan berechnen, ungeeignetes Wetter erkennen und das ausgewählte Ziel anschliessend in Stellarium öffnen soll.
Genau dafür habe ich Singularity gebaut. Das Projekt entstand im Rahmen des CAS AI Engineering an der FFHS Zürich und ist inzwischen weit über einen einfachen RAG-Prototyp hinausgewachsen. Singularity verbindet einen LlamaIndex AgentWorkflow mit spezialisierten FunctionAgent-Instanzen, einer lokalen Qdrant-Wissensbasis, deterministischen Astronomie-Werkzeugen, Chainlit als Oberfläche, Phoenix für Tracing und einer lokalen Stellarium-Anbindung über MCP.
Der interessante Teil ist dabei nicht, dass ein LLM Texte über Astronomie formulieren kann. Entscheidend ist alles darum herum: Wie wird eine Anfrage geroutet? Welche Teile darf das Modell entscheiden und welche müssen berechnet werden? Wie bleibt der Zustand über mehrere Gesprächsschritte erhalten? Wie werden Quellen, Fehler und Tool-Aufrufe sichtbar? Und wie lässt sich prüfen, ob eine Änderung das System tatsächlich verbessert hat?
Dieser Artikel beschreibt den Stand vom Juli 2026. Singularity ist bewusst als lokales Single-User-System gebaut und noch kein produktionsreifer Mehrbenutzerdienst.
Die Aufgabe ist grösser als ein Chatfenster
Singularity deckt drei fachlich unterschiedliche Pfade ab.
Der Wissenspfad beantwortet Fragen zur Astronomie. Er durchsucht zuerst eine lokal gespeicherte Wissensbasis aus OpenStax Astronomy und kuratierten Wikipedia-Artikeln. Reichen die Treffer nicht aus, kann der Agent auf eine Websuche ausweichen. Antworten enthalten Quellenangaben, und bei expliziten Bildanfragen werden Bilder aus Wikimedia Commons und der NASA Image Library gesucht.
Der Planungspfad führt schrittweise durch die Vorbereitung eines Beobachtungsabends. Er benötigt einen Standort, einen Zeitpunkt, die verfügbare Ausrüstung und optionale Objektpräferenzen. Danach prüft er das Wetter, berechnet sichtbare Objekte und bewertet sie anhand von Höhe über dem Horizont, Helligkeit, Lichtverschmutzung und Ausrüstung.
Der Stellarium-Pfad setzt eine Planung oder eine direkte Objektanfrage in eine konkrete Aktion um. Singularity kann ein Objekt in einer lokal laufenden Stellarium-Instanz öffnen, vorher die Sichtbarkeit prüfen und anschliessend einen Screenshot der Ansicht in Chainlit anzeigen.
Diese drei Pfade teilen sich Gesprächskontext und Planungszustand, haben aber unterschiedliche Werkzeuge und unterschiedliche Regeln. Genau daraus ergab sich die Multi-Agent-Architektur.
Entwicklung in Schichten
Ich habe Singularity nicht als vollständiges Multi-Agent-System begonnen. Die Entwicklung erfolgte in mehreren klar getrennten Schritten.
- Technische Basis: Konfiguration, OpenRouter-Anbindung, lokale Embeddings über LM Studio, Qdrant, Phoenix und ein erster Smoke Test.
- Wissenspfad: Ingestion, lokales Retrieval, Query Rewriting, Web-Fallback, Bildsuche und Quellenverwaltung.
- Beobachtungsplanung: Standort- und Zeiterkennung, Wetterdaten, Ephemeriden, Lichtverschmutzung und ein deterministisches Scoring.
- Agenten und Oberfläche: Orchestrator, Spezialisten, gemeinsamer Workflow-Zustand, Streaming und sichtbare Tool-Schritte in Chainlit.
- Härtung und Erweiterung: native LlamaIndex-Handoffs, persistente Planungspräferenzen, Personas, Stellarium über MCP, Tracing und Evaluationsstrecken.
Diese Reihenfolge war wichtig. Erst nachdem die Werkzeuge einzeln funktionierten und testbar waren, wurden sie einem Agenten zur Verfügung gestellt. Dadurch blieb bei Fehlern erkennbar, ob das Problem im Modell, im Routing, im Tool oder in der Fachlogik lag.
Architekturüberblick
Vereinfacht sieht die Architektur so aus:
Benutzer
│
▼
Chainlit UI
│
▼
LlamaIndex AgentWorkflow
│
├── SingularityOrchestrator
│ ├── KnowledgeAgent
│ │ ├── Qdrant RAG
│ │ ├── Brave Search
│ │ ├── Wikimedia Commons
│ │ └── NASA Image Library
│ │
│ ├── PlanningAgent
│ │ ├── Nominatim / TimezoneFinder
│ │ ├── dateparser
│ │ ├── OpenWeatherMap
│ │ ├── Astropy
│ │ └── World Atlas of Artificial Sky Brightness
│ │
│ └── StellariumAgent
│ └── FastMCP Adapter
│ └── Stellarium Remote Control API
│
├── gemeinsamer Workflow-Zustand
├── Chat-Memory pro Chainlit-Session
└── Phoenix Tracing über OpenTelemetry/OpenInference
Alle LLM-Aufrufe laufen über OpenRouter. Das verwendete Modell ist pro Rolle in config.yaml konfigurierbar. Die Embeddings werden getrennt davon lokal über einen OpenAI-kompatiblen LM-Studio-Endpunkt erzeugt. Qdrant und Phoenix laufen als lokale Container.
Diese Aufteilung ist nicht nur eine Frage der Infrastruktur. Sie ermöglicht, Modelle auszutauschen, ohne Retrieval, Agenten oder Fachlogik umzubauen. Gleichzeitig bleibt das Embedding-Modell unabhängig vom Modell, das gerade Routing oder Antwortgenerierung übernimmt.
LlamaIndex FunctionAgent als ausführende Einheit
Die Agenten in Singularity sind LlamaIndex-FunctionAgent-Instanzen. Ein FunctionAgent erhält einen System Prompt, eine Beschreibung, ein LLM und eine Liste normaler Python-Funktionen, die als Tools aufrufbar sind.
LlamaIndex erzeugt aus Signaturen und Docstrings die Tool-Schemas für das Modell. Ein Tool ist damit kein frei formulierter Prompt-Baustein, sondern eine Funktion mit erwarteten Parametern und einem definierten Rückgabewert. Der Agent entscheidet, welches Tool benötigt wird und mit welchen Argumenten es aufgerufen werden soll. Die eigentliche Arbeit findet im Python-Code statt.
Der KnowledgeAgent wird beispielsweise mit vier Werkzeugen gebaut:
return FunctionAgent(
name="KnowledgeAgent",
description=(
"Answers astronomy knowledge questions using local RAG "
"and web fallback."
),
system_prompt=knowledge_prompt,
tools=[
search_local_knowledge,
search_multiple_topics,
search_web,
search_object_image,
],
llm=get_llm("knowledge_agent"),
)
Das klingt zunächst unspektakulär. In der Praxis hängt aber viel davon ab, ob das gewählte Modell natives Tool Calling wirklich zuverlässig beherrscht. Ein OpenAI-kompatibler Chat-Endpunkt allein reicht nicht. Manche Modelle erzeugen lediglich Textblöcke, die wie Tool-Aufrufe aussehen, statt einen tatsächlichen Function Call auszulösen. Andere rufen ein Tool korrekt auf, beenden danach aber die Delegationsschleife nicht sauber.
Deshalb werden die Modelle für Singularity nicht nur nach Preis, Kontextfenster oder Benchmark ausgewählt. Sie müssen in einem echten FunctionAgent-Lauf zeigen, dass sie Tools nativ aufrufen, strukturierte Argumente liefern und nach dem Tool-Ergebnis eine brauchbare Antwort erzeugen. Das ist eine der Stellen, an denen AI Engineering deutlich über einen erfolgreichen Prompt im Playground hinausgeht.
Warum mehrere Agenten und nicht ein Agent mit allen Tools?
Ein einzelner Agent hätte Zugriff auf Retrieval, Wetter, Standortauflösung, Ephemeriden, Bildsuche, Stellarium und Speicherfunktionen. Technisch wäre das möglich. Der System Prompt würde dadurch aber gleichzeitig Routing, Quellenregeln, Planungsdialog, Wetterentscheidungen, Objektauflösung, Bilddarstellung und Stellarium-Verhalten beschreiben müssen.
Das erhöht die Wahrscheinlichkeit, dass der Agent das falsche Werkzeug auswählt oder Regeln aus verschiedenen Domänen vermischt. Die Alternative ist eine Aufteilung nach Verantwortung.
Der SingularityOrchestrator ist der Root Agent. Er beantwortet selbst keine Astronomiefragen und besitzt keine eigenen fachlichen Tools. Seine Aufgabe ist ausschliesslich, eine Nachricht als Wissensfrage, Beobachtungsplanung, Stellarium-Anfrage oder fachfremde Anfrage einzuordnen und an den passenden Spezialisten zu übergeben.
Der KnowledgeAgent beantwortet Astronomiefragen. Er kennt nur die Werkzeuge für lokales Wissen, parallele Mehrthemensuche, Websuche und Bilder. Er ist ein terminaler Spezialist und delegiert nicht weiter.
Der PlanningAgent führt den Planungsdialog und ruft die Werkzeuge für Ort, Zeit, Wetter und Beobachtungsplan auf. Eine Wissensfrage während der Planung kann er an den KnowledgeAgent übergeben. Eine Bitte wie „Öffne das erste Ziel in Stellarium“ kann er an den StellariumAgent weiterreichen.
Der StellariumAgent kümmert sich um Objektauflösung, fehlenden Kontext, Sichtbarkeit und die lokale Stellarium-Instanz. Ändert sich die Anfrage wieder zu einer allgemeinen Wissensfrage oder einer neuen Planung, kann er zurück an den entsprechenden Spezialisten delegieren.
Die Agenten werden in einem gemeinsamen AgentWorkflow registriert:
return AgentWorkflow(
agents=[
orchestrator,
knowledge_agent,
planning_agent,
stellarium_agent,
],
root_agent=orchestrator.name,
initial_state=initial_state(...),
state_prompt=STATE_PROMPT,
)
Der wichtige Punkt ist das native handoff von LlamaIndex. Die Spezialisten sind Mitglieder desselben Workflows. LlamaIndex verwaltet Kontext, Zustand, Tool-Ausführung, Streaming und Tracing. Singularity muss keine eigenen Sub-Agent-Wrapper starten und keinen Zustand zwischen getrennten Workflows kopieren.
Eine frühere Ausbaustufe verwendete noch eigene Delegationswrapper. Das funktionierte grundsätzlich, führte aber zu unnötiger Komplexität bei Zustandsübergabe, Events und Traces. Der Wechsel auf native Handoffs war deshalb keine kosmetische Refaktorierung. Er hat die Architektur vereinfacht und mehrere Fehlerklassen entfernt.
Ein Detail, das erst im Mehrturnbetrieb auffällt
LlamaIndex merkt sich im Workflow-Kontext den aktuell aktiven Agenten. Das ist innerhalb eines laufenden Handoffs sinnvoll. Derselbe Kontext wird in Singularity aber über mehrere Chainlit-Nachrichten hinweg wiederverwendet, damit Planungsdaten und Gesprächskontext erhalten bleiben.
Ohne zusätzliche Behandlung würde die nächste Benutzernachricht deshalb direkt beim zuletzt aktiven Spezialisten landen. Eine Wissensfrage nach einer Planung könnte im PlanningAgent hängen bleiben, obwohl sie neu klassifiziert werden sollte.
Vor jedem neuen workflow.run() setzt Singularity daher nur den aktiven Agenten auf den Root Agent zurück. Der restliche Kontext bleibt erhalten. So wird jede neue Nachricht erneut durch den Orchestrator geroutet, ohne Planungszustand oder Chat-Memory zu verlieren.
Das ist ein kleines Implementierungsdetail, aber ein gutes Beispiel dafür, warum Agenten-Frameworks im Mehrturnbetrieb anders getestet werden müssen als mit einzelnen isolierten Prompts.
Zustand ist nicht dasselbe wie Memory
In Diskussionen über Agenten wird „Memory“ oft als ein einzelnes Feature behandelt. Singularity trennt drei verschiedene Arten von Kontext.
Die Chat-Memory enthält die jüngsten Nachrichten einer Chainlit-Session. Sie ist auf ein Tokenbudget begrenzt und wird nicht dauerhaft gespeichert.
Der Workflow-Zustand enthält strukturierte Daten, die für die aktuelle Aufgabe benötigt werden. Dazu gehören der aktive Pfad, die bereits erfassten Planungsparameter, der generierte Beobachtungsplan und das zuletzt besprochene Objekt.
{
"current_path": None,
"planning_data": {
"location": None,
"datetime_iso": None,
"duration_hours": 2.0,
"equipment": None,
"object_preferences": None,
"weather_checked": False,
"weather_ok": None,
"generated_plan": None,
},
"last_discussed_object": None,
"user_profile_memory": None,
}
Zusätzlich gibt es eine kleine persistente Profil-Memory für stabile Beobachtungspräferenzen. Sie kann den üblichen Standort, die typische Ausrüstung, bevorzugte Objektarten und eine Standarddauer speichern. Diese Daten werden beim Start einer Session geladen und als sichtbare Annahmen verwendet, falls der Benutzer in der aktuellen Planung keinen anderen Wert nennt.
Die aktuelle Session bleibt immer massgeblich. Eine gespeicherte Ausrüstung darf eine neue Angabe nicht überschreiben. Ausserdem speichert das Profil keine vollständigen Chats. Es ist eine kleine strukturierte Präferenzablage, kein vermeintlich allwissendes Langzeitgedächtnis.
Diese Trennung macht das Verhalten nachvollziehbarer und reduziert Überraschungen. Ausserdem kann jede Ebene getrennt getestet und später durch eine andere Persistenz ersetzt werden.
Lokales RAG ist mehr als Qdrant plus Embeddings
Die Wissensbasis besteht aus dem OpenStax-Lehrbuch Astronomy und einer kuratierten Liste von Wikipedia-Artikeln. Der Aufbau erfolgt in zwei getrennten Schritten.
Zuerst laden Quellenadapter die Inhalte und normalisieren sie in JSONL-Dokumente. Live-Zugriffe sind standardmässig deaktiviert und müssen explizit freigegeben werden. Rohdaten werden lokal gecacht, damit ein erneuter Lauf die Wissensbasis aus dem Cache reproduzieren kann, ohne alle Quellen erneut abzurufen.
Im zweiten Schritt werden die normalisierten Dokumente in Abschnitte zerlegt, lokal eingebettet und in Qdrant geschrieben. Die Point-IDs werden deterministisch aus Quelle, URL und Chunk-Index erzeugt. Dadurch führt ein erneutes Indexieren nicht bei jedem Lauf zu einer vollständig neuen Sammlung von Punkten.
Der aktuelle Aufbau verwendet ein lokal in LM Studio bereitgestelltes text-embedding-qwen3-embedding-8b mit 4096 Dimensionen. Die Inhalte der Wissensbasis sind überwiegend englisch, Anfragen können aber deutsch sein. Das mehrsprachige Embedding-Modell übernimmt die sprachübergreifende Zuordnung.
Query Rewriting für Folgefragen
Eine Anfrage wie „Wie warm ist es dort?“ ist für eine Vektorsuche unbrauchbar, wenn der vorherige Gesprächskontext nicht berücksichtigt wird. Singularity schreibt solche Folgefragen deshalb vor dem Retrieval in eine eigenständige Suchfrage um.
Aus einem vorherigen Gespräch über den Mars und der Frage „Wie warm ist es dort?“ wird beispielsweise „Wie warm ist es auf dem Mars?“. Dafür werden nur die letzten Nachrichten und die aktuelle Frage an einen kleinen Rewrite-Schritt übergeben. Ist die Frage bereits eigenständig, bleibt sie unverändert. Schlägt das Rewriting fehl, wird die ursprüngliche Frage verwendet.
Original und umgeschriebene Anfrage werden als Trace-Attribute an Phoenix übergeben. Dadurch lässt sich später prüfen, ob ein schlechter Retrieval-Treffer vom Embedding-Modell oder bereits von einer misslungenen Umschreibung verursacht wurde.
Einzelthema und Mehrthemensuche
Nicht jede Wissensfrage sollte auf dieselbe Weise gesucht werden. Eine einfache Frage zu einem einzelnen Objekt verwendet search_local_knowledge. Ein Vergleich zwischen Hubble und dem James-Webb-Weltraumteleskop wird dagegen in mehrere eigenständige Suchthemen zerlegt und parallel ausgeführt.
Das verhindert, dass eine lange Vergleichsfrage als einzelner Vektor in der Mitte zwischen mehreren Themen landet und zu keinem davon wirklich gute Treffer erhält. Die Resultate der Teilabfragen werden danach wieder vom KnowledgeAgent zusammengeführt.
Metadatenfilter mit bewusstem Fallback
Bei benannten Themen kann das Retrieval auf metadata.title filtern. Das erhöht die Präzision, wenn ein Dokumenttitel genau zum gesuchten Objekt passt. Die vorhandenen Quellen haben allerdings nicht überall dieselbe Titelgranularität. Informationen zu einem Planeten können beispielsweise nur als Abschnitt eines grösseren OpenStax-Kapitels vorliegen.
Liefert ein exakter Titelfilter keine Treffer, wiederholt Singularity die Suche einmal ohne Filter. Dieses Verhalten ist absichtlich implementiert und wird geloggt. Ein leerer gefilterter Treffer bedeutet nicht automatisch, dass die lokale Wissensbasis nichts zum Thema enthält.
Quellen werden im Tool erzeugt
Der Agent soll Quellen nicht nachträglich aus frei formuliertem Text rekonstruieren. Die Knowledge-Tools liefern deshalb neben dem Inhalt bereits eine deduplizierte Referenz-ID, einen Inline-Marker und eine formatierte Quellenangabe zurück.
Die Registry wird für jede neue Benutzernachricht zurückgesetzt. Mehrere Chunks derselben Quelle erhalten innerhalb einer Antwort dieselbe Referenznummer. Lokal gespeicherte Quellen werden zusätzlich als lokaler Stand gekennzeichnet, weil sich das Original seit der Ingestion geändert haben kann.
Damit wird Zitieren von einer höflichen Prompt-Bitte zu einem konkreten Datenvertrag zwischen Tool und Agent.
Beobachtungsplanung: Das LLM fragt, Python rechnet
Der PlanningAgent ist der Teil von Singularity, bei dem die Trennung zwischen Sprachmodell und Fachlogik am deutlichsten wird.
Das LLM führt das Gespräch. Es erkennt, welche Angaben bereits vorhanden sind, fragt fehlende Informationen einzeln ab und wählt die passenden Tools. Es berechnet aber weder Planetenpositionen noch Lichtverschmutzung oder Sichtbarkeit selbst.
Der Ablauf sieht vereinfacht so aus:
Standort erfassen
│
├── Koordinaten direkt übernehmen
└── Ortsname über Nominatim auflösen
└── Zeitzone über TimezoneFinder bestimmen
Zeitangabe parsen
└── deutsche und englische Angaben über dateparser
Ausrüstung klassifizieren
└── Auge, Fernglas, kleines oder grosses Teleskop
Wetter prüfen
└── Bewölkung, Niederschlag, Sichtweite und Vorhersagebereich
Beobachtungsplan berechnen
├── Ephemeriden mit Astropy
├── Lichtverschmutzung aus lokalem GeoTIFF
├── Objekte bewerten
└── Ergebnis sortieren und begrenzen
Ephemeriden statt Modellwissen
Astropy berechnet für den angegebenen Ort und Zeitraum die Positionen von Planeten, Mond und einem kompakten Katalog heller Deep-Sky-Objekte. Der Zeitraum wird in 15-Minuten-Schritten abgetastet. Für jedes Objekt wird der beste Zeitpunkt innerhalb des Fensters ermittelt.
Objekte, die während des gesamten Fensters unter dem Horizont bleiben, werden verworfen. Ein LLM könnte zwar eine plausibel klingende Liste erzeugen, hätte aber ohne Berechnung keine belastbare Aussage darüber, was an diesem Ort und zu dieser Zeit tatsächlich sichtbar ist.
Lichtverschmutzung aus realen Geodaten
Die Lichtverschmutzung wird aus dem World Atlas of Artificial Sky Brightness gelesen. Singularity greift dafür lokal auf ein GeoTIFF zu und ermittelt am angegebenen Standort einen ungefähren Bortle-Wert und eine daraus abgeleitete SQM-Schätzung.
Die grosse Datendatei liegt bewusst nicht im Repository. Sie wird lokal heruntergeladen und in der Konfiguration referenziert. Damit bleibt das Repository klein, während die Berechnung reproduzierbar bleibt.
Deterministisches Scoring
Die Kandidaten werden nicht vom LLM nach Gefühl sortiert. Das Scoring berücksichtigt die Grenzhelligkeit der Ausrüstung, eine Strafe für Lichtverschmutzung, die Schwierigkeit des Objekttyps, die Höhe über dem Horizont und die Präferenzen des Benutzers.
Vereinfacht basiert es auf folgendem Schema:
effective_limit = (
limiting_magnitude[equipment]
- light_pollution_penalty[bortle]
- object_type_difficulty[object_type]
)
score = 0.65 * magnitude_score + 0.35 * altitude_score
score *= preference_multiplier
Unter zehn Grad Höhe erhält ein Objekt keinen Score. Sehr lichtschwache Objekte werden verworfen, wenn sie deutlich ausserhalb der effektiven Grenzhelligkeit liegen. Präferierte Objekttypen werden höher gewichtet, andere aber nicht vollständig ausgeschlossen.
Das Modell erhält am Ende eine sortierte Liste mit strukturierten Daten: Objektname, Typ, Helligkeit, beste Beobachtungszeit, Höhe, Azimut, Score und eine kurze Einschätzung. Seine Aufgabe ist nur noch, daraus eine verständliche Antwort zu formulieren.
Diese Aufteilung ist für mich einer der wichtigsten Punkte des gesamten Projekts. Probabilistische Modelle eignen sich gut für Sprache, Absichtserkennung und flexible Dialoge. Berechenbare Fachlogik sollte dagegen berechnet werden.
Ein kompletter Planungsablauf
Eine Anfrage könnte beispielsweise lauten:
Plane mir morgen Abend in Kreuzlingen zwei Stunden mit einem 10-Zoll-Dobson. Ich möchte vor allem Galaxien sehen.
Der Orchestrator übergibt an den PlanningAgent. Dieser löst Kreuzlingen in Koordinaten und Zeitzone auf, interpretiert „morgen Abend“, klassifiziert den Dobson als grosses Teleskop und übernimmt die Präferenz für Galaxien. Danach prüft er die Wettervorhersage.
Ist die Bewölkung zu hoch oder Niederschlag angekündigt, erzeugt der Agent nicht einfach trotzdem eine optimistische Liste. Er weist auf die Bedingungen hin und fragt, ob der Plan dennoch berechnet werden soll. Erst nach Zustimmung wird die Zielberechnung fortgesetzt.
Eine anschliessende allgemeine Frage wie „Wie entstehen Spiralgalaxien?“ wird an den KnowledgeAgent übergeben, ohne den Plan zu verlieren. Eine objektspezifische Nachfrage wie „Was ist das Besondere am ersten Ziel?“ kann der PlanningAgent dagegen über sein Wissenswerkzeug beantworten. „Öffne es in Stellarium“ wechselt zum StellariumAgent, der Ziel, Ort und Zeitpunkt aus dem vorhandenen Plan übernimmt.
Gerade solche Unterbrechungen zeigen den Nutzen des gemeinsamen Workflow-Zustands. Ein mehrstufiger Assistent darf eine Anfrage nicht als Reihe voneinander unabhängiger Chat-Completions behandeln.
Chainlit als UI und Laufzeitgrenze
Chainlit ist in Singularity nicht nur ein hübsches Chatfenster. Die Oberfläche bildet die Laufzeitereignisse des AgentWorkflows ab.
Beim Start einer Session werden die Persona-Konfiguration geladen, Phoenix initialisiert, der lokale Stellarium-MCP-Server gestartet, gespeicherte Beobachtungspräferenzen eingelesen und der gemeinsame Workflow-Kontext angelegt.
Bei jeder Nachricht passiert anschliessend Folgendes:
- Die ausgewählte Persona wird aufgelöst.
- Die Quellen-Registry wird zurückgesetzt.
- Der aktive Agent wird auf den Orchestrator zurückgesetzt.
- Der Workflow wird mit bestehendem Kontext und Chat-Memory gestartet.
- LlamaIndex-Events werden direkt in Chainlit gestreamt.
- Erfolgreiche Planungswerte werden gegebenenfalls gespeichert.
Chainlit erhält dabei keine nachträglich analysierte Textantwort, sondern echte Workflow-Events. AgentStream liefert Text-Tokens, ToolCall eröffnet einen sichtbaren Schritt und ToolCallResult schliesst ihn mit dem Resultat ab.
Der Benutzer sieht dadurch nicht nur eine wartende Animation, sondern konkrete Schritte wie:
Routing to specialist...
Searching the knowledge base...
Checking weather...
Computing ephemerides and scoring targets...
Opening Stellarium target...
Diese Transparenz ist nicht nur optisch hilfreich. Wenn ein Ergebnis unerwartet ist, ist sofort sichtbar, ob überhaupt das richtige Tool aufgerufen wurde.
Bildresultate werden als Chainlit-Elemente mit Quelle, Beschreibung und Lizenz angezeigt. Stellarium-Screenshots werden als lokale Dateien eingebunden. Doppelte Markdown-Bilder oder rohe Bild-URLs werden aus dem finalen Text entfernt, damit dieselbe Ausgabe nicht mehrfach erscheint.
Auch Fehler sind Teil der UI-Architektur. Ist OpenRouter, LM Studio, Qdrant, das Wetter-API oder Stellarium nicht erreichbar, erhält der Benutzer eine verständliche Meldung. Wenn alle benötigten Tool-Aufrufe fehlschlagen, unterdrückt die Oberfläche eine eventuell dennoch erzeugte, nicht belegte Modellantwort und zeigt stattdessen die fehlgeschlagenen Werkzeuge an.
Personas ohne neue Agenten
Singularity bietet mehrere Antwortstile: den Standardstil sowie Varianten, die an Carl Sagan, Marvin, Data und H. P. Lovecraft angelehnt sind.
Die naheliegende, aber falsche Lösung wäre ein eigener PersonaAgent oder ein separater Workflow pro Stil. Eine Persona ist keine fachliche Zuständigkeit. Sie soll weder das Routing noch die Auswahl der Tools verändern.
Die Personas liegen deshalb als Daten in personas.yaml. Jede Konfiguration enthält eine ID, einen Anzeigenamen, eine Beschreibung, ein Icon und eine zusätzliche Stilvorgabe. Chainlit stellt sie als Mode-Auswahl im Eingabefeld dar.
Der Stil-Override wird nur an die Agenten angehängt, die längere Antworten formulieren: KnowledgeAgent und PlanningAgent. Der Orchestrator bleibt neutral, damit seine Routingentscheidung nicht durch eine Rolle beeinflusst wird. Auch Quellen, Zahlen, Warnungen, Fehler und Tool-Verhalten haben immer Vorrang vor dem Stil.
Die Prompt-Priorität ist damit explizit:
System- und Tool-Regeln
> fachliche Agentenregeln
> Quellen- und Formatregeln
> Persona-Stil
Das klingt selbstverständlich, verhindert aber eine typische Fehlentwicklung. Eine unterhaltsame Persona darf niemals entscheiden, dass schlechtes Wetter „dramatischer“ ist als die tatsächlichen Messwerte oder dass Quellen nicht zur Rolle passen.
Neue Personas können ohne Änderung am Routing-Code ergänzt werden. Das ist ein Beispiel dafür, wie eine saubere Trennung von Verhalten und Präsentation spätere Erweiterungen deutlich einfacher macht.
Stellarium über MCP anbinden
Die Stellarium-Integration verwendet das Model Context Protocol nicht als Selbstzweck, sondern als klare lokale Systemgrenze.
Der Aufrufweg besteht aus mehreren Schichten:
StellariumAgent
│
▼
agentenseitiger MCP-Client mit typisierten Tools
│
▼
lokaler FastMCP-Server
│
▼
Low-Level-Client für Stellarium Remote Control
│
▼
Stellarium
Der MCP-Server bleibt absichtlich dünn. Er führt keine Dialoge, fragt nichts beim Benutzer nach und entscheidet nicht, welches Objekt gemeint sein könnte. Diese Verantwortung liegt beim StellariumAgent.
Bei einem direkt eingegebenen Namen versucht Singularity zuerst eine lokale Normalisierung von Messier-, NGC- und IC-Bezeichnungen. Danach kann SIMBAD zur Auflösung verwendet werden. Erst wenn dort kein eindeutiges Resultat entsteht, wird die Stellarium-Suche als Fallback verwendet. Mehrere plausible Treffer werden nicht stillschweigend aufgelöst, sondern dem Benutzer zur Auswahl vorgelegt.
Vor dem Fokussieren setzt Singularity Standort und Zeitpunkt in Stellarium und prüft die Sichtbarkeit aus Sicht der Anwendung. Liegt das Objekt unter dem Horizont, wird es nicht als sinnvoller Beobachtungsblick geöffnet.
Nach einem erfolgreichen Fokus versucht der Low-Level-Client über den ScriptService einen Screenshot zu speichern. Chainlit zeigt diese Datei direkt im Gespräch an. Schlägt nur der Screenshot fehl, bleibt der eigentliche Stellarium-Aufruf trotzdem erfolgreich. Auch hier werden Teilerfolg und Fehler getrennt behandelt.
Ist Stellarium nicht gestartet oder das Remote-Control-Plugin deaktiviert, startet die restliche Anwendung weiterhin. Der StellariumAgent meldet die Integration bei Verwendung als nicht verfügbar. Eine optionale lokale Integration darf nicht den gesamten Chatbot unbrauchbar machen.
Observability: Agenten brauchen Traces
Bei klassischem Anwendungscode lässt sich ein Fehler oft mit Logs und einem Stacktrace eingrenzen. Bei einem Agenten kann die Antwort falsch sein, obwohl jeder einzelne Funktionsaufruf technisch erfolgreich war.
Mögliche Ursachen sind unter anderem:
- Der Orchestrator hat an den falschen Agenten übergeben.
- Eine Folgefrage wurde schlecht umgeschrieben.
- Ein Metadatenfilter war zu eng.
- Der Agent hat trotz guter Treffer die Websuche verwendet.
- Ein Tool war langsam und hat den Gesamtlauf dominiert.
- Das Modell hat ein Tool mehrfach oder mit unpassenden Argumenten aufgerufen.
Singularity verwendet deshalb Arize Phoenix zusammen mit OpenTelemetry und der OpenInference-Instrumentierung für LlamaIndex. Erfasst werden LLM-Aufrufe, Tool Calls, Retrievals, Agentenschritte und der gesamte Workflow als zusammenhängender Trace.
Zusätzliche Attribute machen die Traces filterbar. Dazu gehören beispielsweise die Chainlit-Session, die gewählte Persona sowie die ursprüngliche und die umgeschriebene Retrieval-Anfrage. Native Handoffs erscheinen als eigene Tool-Spans mit Zielagent und Begründung.
Damit wird aus „Der Agent hat sich merkwürdig verhalten“ eine untersuchbare Kette:
Benutzernachricht
-> Orchestrator
-> handoff an KnowledgeAgent
-> Query Rewrite
-> Qdrant Retrieval mit Titelfilter
-> ungefilterter Retry
-> Antwort mit Quellen
Tracing ist bei Singularity deshalb keine nachträgliche Betriebsfunktion. Es war bereits während der Entwicklung die wichtigste Oberfläche, um Routing, Retrieval und Tool-Nutzung zu verstehen.
Evaluation statt Bauchgefühl
Ein Agent wirkt nach einigen erfolgreichen manuellen Gesprächen schnell „gut“. Das sagt wenig darüber aus, ob Änderungen an Prompts, Modellen oder Retrieval-Parametern das Gesamtsystem verbessern.
Singularity trennt deshalb zwei Evaluationsarten.
Die RAG-Evaluation verwendet ein kuratiertes JSONL-Testset mit deutschen und englischen Fragen. Es enthält einfache Einzelthemen, komplexere Synthesefragen, Vergleiche, zusammengesetzte Fragen, erwartete Titelfilter und Fälle, bei denen ein Web-Fallback notwendig sein sollte.
Für das Retrieval werden klassische Metriken berechnet:
- Recall@k
- Precision@k
- Mean Reciprocal Rank
- NDCG@k
Zusätzlich wird geprüft, ob die erwartete Strategie verwendet wurde. Dazu gehören der Einsatz eines Titelfilters, die Zerlegung einer Frage in mehrere Themen und der Web-Fallback.
Der RAG-Runner kann als schneller Dry Run gestartet werden, um Datensätze, Metriken und Ergebnisdateien zu prüfen. Mit --live verwendet er die lokalen Retrieval-Tools und misst die tatsächlich zurückgegebenen Dokumenttitel.
Die End-to-End-Strecke enthält mehrstufige Gesprächsszenarien, darunter Planungen, unklare Orte, schlechtes Wetter, Wissensunterbrechungen und fachfremde Fragen. Der aktuelle Runner validiert diese Szenarien zunächst deterministisch gegen die Routinglogik. Eine vollständige Phoenix-basierte Auswertung mit LLM Judge ist als nächste Ausbaustufe vorgesehen, aber noch keine permanente CI-Schranke.
Diese Einschränkung ist bewusst sichtbar. Evaluation sollte nicht beeindruckender beschrieben werden, als sie implementiert ist. Gleichzeitig ist die Struktur vorhanden, um Retrieval, Routing und später die Qualität vollständiger Antworten über Git-Stände hinweg zu vergleichen.
Fehler, die das Design verbessert haben
Einige der wichtigsten Architekturentscheidungen entstanden nicht aus dem ersten Entwurf, sondern aus konkreten Problemen.
Tool Calling muss real getestet werden
Ein Modell kann in einer Dokumentation als Function-Calling-fähig gelten und sich im konkreten OpenRouter- und LlamaIndex-Setup trotzdem anders verhalten. Entscheidend ist der vollständige Ablauf aus Tool-Auswahl, Argumenten, Ergebnisverarbeitung und sauberem Abschluss.
Eigene Delegationswrapper waren unnötige Infrastruktur
Custom Wrapper für Spezialagenten machten Zustandskopien, Event-Weitergabe und Tracing komplizierter. Native Handoffs innerhalb eines gemeinsamen AgentWorkflow lösen genau dieses Problem bereits auf Framework-Ebene.
Der Kontextparameter gehört nicht ins Tool-Schema
Viele Tools erhalten intern einen LlamaIndex-Context. Dieser Parameter wird vom Framework injiziert und darf nicht als vom Modell zu lieferndes Argument erscheinen. Andernfalls versucht das LLM, einen nicht serialisierbaren Workflow-Kontext zu erfinden oder fragt nach einem fehlenden ctx.
Ein RAG-Treffer ist noch keine belegte Antwort
Retrieval, Quellenzuordnung und Antwortformatierung müssen als zusammengehörige Kette behandelt werden. Deshalb erzeugen die Tools stabile Referenzen, statt dem Modell nur Textblöcke zu geben und auf korrekte Zitate zu hoffen.
Zustand muss explizit bleiben
Ein Agent sollte nicht aus dem Chatverlauf erraten müssen, welcher Standort bereits aufgelöst wurde oder welcher Beobachtungsplan gerade gilt. Strukturierter Zustand ist leichter zu prüfen, zu persistieren und zwischen Spezialisten zu teilen.
Stil darf fachliches Verhalten nicht beeinflussen
Personas wurden erst dann sauber, als sie vollständig aus Routing und Tool-Nutzung herausgehalten wurden. Sie sind eine Präsentationsoption und keine neue Agentenrolle.
Was bewusst noch fehlt
Singularity ist ein umfangreicher Prototyp, aber kein fertiger öffentlicher Dienst.
Es gibt noch keine Benutzeranmeldung, keine Mandantentrennung und keine dauerhafte Chat-Historie. Die Profil-Memory ist für einen lokalen Benutzer ausgelegt und wird als JSON-Datei gespeichert. Rate Limits und Kostenbudgets sind noch nicht als zentrale Guardrails implementiert.
Der Deep-Sky-Katalog der Beobachtungsplanung ist bewusst kompakt. Er reicht aus, um Architektur und Scoring zu demonstrieren, ersetzt aber noch keinen vollständigen astronomischen Katalog. Auch Wettervorhersagen sind naturgemäss auf den vom Anbieter verfügbaren Zeitraum beschränkt.
Die Evaluation ist vorhanden, aber noch nicht vollständig automatisiert. Vor allem die End-to-End-Bewertung mit einem stärkeren Judge-Modell soll weiter ausgebaut werden.
Diese Grenzen gehören zur Architektur genauso wie die fertigen Funktionen. Ein AI-Engineering-Projekt wird nicht dadurch besser, dass jeder offene Punkt hinter einer beeindruckenden Demo verschwindet.
Singularity lokal starten
Nach der Veröffentlichung lässt sich das Projekt mit uv aufsetzen:
git clone https://github.com/dprinz/singularity.git
cd singularity
uv sync
cp .env.example .env
In .env werden die API-Keys für OpenRouter, Brave Search und OpenWeatherMap eingetragen. Qdrant und Phoenix starten über Docker Compose:
docker compose up -d
Für die Embeddings muss in LM Studio das in config.yaml konfigurierte Embedding-Modell über den OpenAI-kompatiblen Endpunkt bereitstehen. Danach startet die Anwendung mit:
uv run chainlit run app.py
Für die Beobachtungsplanung wird zusätzlich der World Atlas of Artificial Sky Brightness lokal benötigt. Die Download- und Ingestion-Schritte sind im README des Projekts dokumentiert.
Fazit
Singularity ist für mich vor allem ein AI-Engineering-Projekt, nicht nur ein Astronomie-Chatbot.
Das LLM übernimmt die Teile, für die es geeignet ist: Sprache verstehen, fehlende Angaben erkennen, Werkzeuge auswählen, zwischen Fachrollen wechseln und Ergebnisse verständlich erklären. Retrieval, Quellenverwaltung, Standortauflösung, Wetterprüfung, Ephemeriden, Lichtverschmutzung und Scoring liegen dagegen in klar definierten Werkzeugen.
LlamaIndex stellt mit FunctionAgent, AgentWorkflow und nativen Handoffs die agentische Laufzeit bereit. Chainlit macht Text und Tool-Ausführung sichtbar. Phoenix zeigt, was innerhalb eines Laufs tatsächlich passiert. Qdrant und lokale Embeddings bilden die Wissensschicht. Der MCP-Adapter verbindet den Agenten mit einer realen Desktop-Anwendung.
Das Ergebnis ist kein autonomes System, das beliebig viele Dinge irgendwie erledigen soll. Es ist ein begrenzter Assistent mit bekannten Zuständigkeiten, explizitem Zustand, nachvollziehbaren Werkzeugen und messbaren Teilstrecken.
Genau darin liegt für mich der Unterschied zwischen einer überzeugenden LLM-Demo und AI Engineering.
Der vollständige Quellcode ist auf GitHub verfügbar. Fachliche Rückmeldungen zur Architektur, zu LlamaIndex-Handoffs, Retrieval-Strategien oder zur Evaluation sind willkommen.