<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>FastAPI |</title><link>https://aretascodes.dev/de/tags/fastapi/</link><atom:link href="https://aretascodes.dev/de/tags/fastapi/index.xml" rel="self" type="application/rss+xml"/><description>FastAPI</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>de-DE</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><image><url>https://aretascodes.dev/media/icon_hu_2ab4f4763b27c75b.png</url><title>FastAPI</title><link>https://aretascodes.dev/de/tags/fastapi/</link></image><item><title>CogniVault Backend erklärt, Teil 1 · Das Backend kennenlernen: Drei Prozesse, vier Schichten</title><link>https://aretascodes.dev/de/blog/backend-explained-meet-the-backend/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/backend-explained-meet-the-backend/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Alle Abkürzungen werden im Anhang am Ende der Seite vollständig erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Wenn Leute das erste Mal das CogniVault-Repository öffnen, höre ich meistens eine Variante der Frage: &lt;em&gt;&amp;ldquo;Wo fange ich überhaupt an?&amp;rdquo;&lt;/em&gt; Da gibt es einen RAG-Agenten, einen FAISS-Index, einen DBOS-Workflow, einen Ollama-Host — und wenn du gerade erst in die Tech-Welt einsteigst, ist jedes dieser Wörter wie eine verschlossene Tür.&lt;/p&gt;
&lt;p&gt;Diese Serie öffnet die Türen, eine nach der anderen. Kein RAG-Vorwissen vorausgesetzt, jede Abkürzung wird ausgeschrieben und jede Behauptung lässt sich im
nachprüfen. Falls du meine
schon gelesen hast, betrachte diese Serie als die Führung, die eigentlich hätte zuerst kommen sollen.&lt;/p&gt;
&lt;p&gt;Lass uns das mal aufzeichnen.&lt;/p&gt;
&lt;h2 id="die-ganze-app-besteht-aus-drei-prozessen"&gt;Die ganze App besteht aus drei Prozessen&lt;/h2&gt;
&lt;p&gt;Mit CogniVault kannst du mit deinen eigenen Dokumenten chatten und sie in Quizzes, Workshops, Karteikarten und Mindmaps verwandeln — und dabei verlässt absolut nichts jemals deinen Rechner. (Das &lt;em&gt;Warum&lt;/em&gt; hinter dieser Einschränkung ist eine eigene Geschichte:
.)&lt;/p&gt;
&lt;p&gt;Man könnte erwarten, dass so eine App ein Wildwuchs an Microservices ist. Aber es sind genau drei Prozesse:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prozess&lt;/th&gt;
&lt;th&gt;Was er macht&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Das Python-Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Eine FastAPI-App auf Port 8000 — sie serviert auch das kompilierte React-Frontend als statische Dateien&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ollama&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Der lokale Model-Server auf Port 11434, auf dem die KI-Modelle laufen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ein Docker-Container, der &lt;em&gt;nur&lt;/em&gt; für Workflow-Checkpoints genutzt wird — niemals für deine Dokumente&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Alles andere — deine Dateien, der Suchindex, dein Chatverlauf, deine Quiz-Ergebnisse — ist einfach eine Datei auf der Festplatte. Das ist keine Faulheit; das ist das Privatsphäre-Argument physisch umgesetzt. Du kannst jedes Byte, das die App speichert, mit einem Texteditor und einem SQLite-Browser öffnen.&lt;/p&gt;
&lt;h2 id="die-vier-schichten"&gt;Die vier Schichten&lt;/h2&gt;
&lt;p&gt;Bevor wir Technologien beim Namen nennen, hier das mentale Modell, das du für die ganze Serie im Kopf behalten solltest. Das Backend besteht von oben nach unten aus vier Schichten:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schicht 1 — die Web-Schicht.&lt;/strong&gt; Eine FastAPI-Anwendung nimmt jeden HTTP-Request entgegen und routet ihn an einen von sechs Routern: Chat (&lt;code&gt;/rag&lt;/code&gt;), Wissensmanagement (&lt;code&gt;/upload&lt;/code&gt;, &lt;code&gt;/ingest&lt;/code&gt;), Lerntools (&lt;code&gt;/api/study/*&lt;/code&gt;), Fortschritt (&lt;code&gt;/api/progress/*&lt;/code&gt;), Sprache (&lt;code&gt;/api/transcribe&lt;/code&gt;) und Chatverlauf (&lt;code&gt;/api/history&lt;/code&gt;). FastAPI (ein modernes Python-Web-Framework) generiert unter &lt;code&gt;/api/docs&lt;/code&gt; zudem automatisch eine interaktive API-Dokumentation, was der beste Weg ist, um das Backend zu erkunden, ohne eine Zeile Code lesen zu müssen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schicht 2 — die Intelligenz-Schicht.&lt;/strong&gt; Zwei KI-Modelle mit zwei verschiedenen Jobs. &lt;code&gt;gemma4:e4b&lt;/code&gt; &lt;em&gt;generiert&lt;/em&gt;: Chat-Antworten, Gedankengänge (Reasoning), Bildanalyse und Tool-Aufrufe. &lt;code&gt;embeddinggemma&lt;/code&gt; &lt;em&gt;erstellt Embeddings&lt;/em&gt;: Es verwandelt Text in Vektoren (Zahlenlisten, die Bedeutung einfangen), sodass ähnliche Ideen mathematisch gefunden werden können. Beide laufen innerhalb von Ollama — stell dir Ollama wie Docker vor, aber für KI-Modelle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schicht 3 — die Retrieval-Schicht.&lt;/strong&gt; Eine Suchmaschine über deine Dokumente, die &lt;em&gt;semantische&lt;/em&gt; Suche (finde Dinge mit gleicher Bedeutung) mit &lt;em&gt;Keyword&lt;/em&gt;-Suche (finde exakte Zeichenketten) kombiniert. Teil 3 dieser Serie dreht sich komplett um diese Schicht.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schicht 4 — die Persistenz-Schicht.&lt;/strong&gt; Vier Speichersysteme, jedes für einen speziellen Job ausgewählt: ein FAISS-Index plus einer JSON-Datei für durchsuchbares Wissen, SQLite für Lerndaten, PostgreSQL für Workflow-Checkpoints und einfache JSON-Dateien für den Chatverlauf.&lt;/p&gt;
&lt;h2 id="ein-diagramm-alle-wichtigen-teile"&gt;Ein Diagramm, alle wichtigen Teile&lt;/h2&gt;
&lt;div class="mermaid"&gt;flowchart TB
subgraph CLIENT["Browser"]
UI["React Frontend&lt;br/&gt;(kompiliert, serviert von FastAPI)"]
end
subgraph SERVER["FastAPI Backend — Port 8000"]
ROUTERS["6 Router&lt;br/&gt;rag · knowledge · study ·&lt;br/&gt;progress · audio · history"]
AGENT["RAG Agent&lt;br/&gt;(Strands SDK, 6 Tools)"]
VDB["VectorDB&lt;br/&gt;FAISS + BM25 + RRF"]
INGEST["Ingestion&lt;br/&gt;(DBOS dauerhafter Workflow)"]
GEN["Study Generatoren&lt;br/&gt;quiz · workshop · cards · mindmap"]
PROG["Fortschrittstracker&lt;br/&gt;+ 25 Achievements"]
end
subgraph OLLAMA["Ollama — Port 11434"]
GEMMA["gemma4:e4b&lt;br/&gt;chat · thinking · vision · tools"]
EMBED["embeddinggemma&lt;br/&gt;Text zu Vektoren"]
end
subgraph STORAGE["Lokaler Speicher"]
FAISSF["vector_store.faiss + .json"]
SQLITE["progress.db (SQLite)"]
PG["PostgreSQL&lt;br/&gt;nur Workflow-Status"]
DOCS["docs/ Ordner + chat_history.json"]
end
UI --&gt; ROUTERS
ROUTERS --&gt; AGENT --&gt; VDB
AGENT --&gt; GEMMA
VDB --&gt; EMBED
ROUTERS --&gt; INGEST --&gt; EMBED
INGEST --&gt; PG
INGEST --&gt; FAISSF
VDB --- FAISSF
ROUTERS --&gt; GEN --&gt; GEMMA
GEN --&gt; SQLITE
ROUTERS --&gt; PROG --&gt; SQLITE
ROUTERS --&gt; DOCS
&lt;/div&gt;
&lt;p&gt;Behalte dieses Bild im Hinterkopf — die Teile 2, 3 und 4 zoomen jeweils in einen Bereich davon hinein.&lt;/p&gt;
&lt;h2 id="der-tech-stack-und-warum-jedes-teil-seinen-platz-verdient-hat"&gt;Der Tech-Stack und warum jedes Teil seinen Platz verdient hat&lt;/h2&gt;
&lt;p&gt;Die komplette Liste der Abhängigkeiten (Dependencies) lebt in der &lt;code&gt;requirements.txt&lt;/code&gt;. Hier ist das, was wichtig ist, gruppiert nach Aufgabe:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requests bearbeiten.&lt;/strong&gt; FastAPI definiert die Endpoints und validiert jeden Request und jede Response mit Pydantic (einer Datenvalidierungs-Bibliothek — stell es dir wie einen strengen Zollbeamten für JSON vor). Uvicorn ist der ASGI-Server (Asynchronous Server Gateway Interface — der Python-Standard, der es einem Prozess erlaubt, viele gleichzeitige Requests zu jonglieren), der das Ganze am Ende ausführt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Denken.&lt;/strong&gt; Ollama hostet &lt;code&gt;gemma4:e4b&lt;/code&gt; — das &lt;code&gt;e4b&lt;/code&gt;-Tag steht für die Variante mit rund vier Milliarden effektiven Parametern, was ungefähr einem 9,6 GB Download entspricht — und &lt;code&gt;embeddinggemma&lt;/code&gt; (ca. 622 MB). Das Agentenverhalten wird mit dem Strands Agents SDK gebaut, welches das Modell in einen Loop (Schleife) verpackt, in dem es Tools aufrufen, die Ergebnisse lesen und erst danach antworten kann. (Wo ich Ollama in Relation zu Docker laufen lasse, ist eine bewusste Entscheidung mit eigener Hintergrundgeschichte:
.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dinge finden.&lt;/strong&gt; FAISS (Facebook AI Similarity Search — Metas Vektorsuch-Bibliothek) kümmert sich um semantische Lookups; &lt;code&gt;rank-bm25&lt;/code&gt; kümmert sich um Keyword-Lookups; eine Formel namens Reciprocal Rank Fusion vereint die beiden. Teil 3 packt all das genauer aus.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dokumente lesen.&lt;/strong&gt; &lt;code&gt;pypdf&lt;/code&gt; für PDFs, mit einem OCR-Fallback (Optical Character Recognition — verwandelt Bilder von Text in echten Text) für gescannte Seiten via &lt;code&gt;pymupdf&lt;/code&gt; und Tesseract. Word, PowerPoint und Excel bekommen jeweils ihren eigenen Extractor. &lt;code&gt;trafilatura&lt;/code&gt; zieht sauberen Artikeltext aus Webseiten.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keine Arbeit verlieren.&lt;/strong&gt; DBOS macht die Ingestion-Pipeline dauerhaft (durable) — jeder Schritt bekommt einen Checkpoint in PostgreSQL, sodass bei einem Absturz fortgesetzt statt von vorn begonnen wird. Teil 2 zeigt das in Aktion.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sich erinnern.&lt;/strong&gt; SQLite — eine vollwertige Datenbank-Engine, die in einer einzigen Datei namens &lt;code&gt;progress.db&lt;/code&gt; lebt — hält deine Lernsitzungen, Errungenschaften (Achievements), Quizzes, Workshops, Karteikartendecks und Mindmaps fest.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="anhang-abkürzungen-in-diesem-post"&gt;Anhang: Abkürzungen in diesem Post&lt;/h2&gt;
&lt;p&gt;Das Versprechen dieser Serie ist &amp;ldquo;keine unerklärten Abkürzungen&amp;rdquo;, also ist hier die Tabelle, von der ich wünschte, sie wäre in jedem technischen Tutorial dabei.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abkürzung&lt;/th&gt;
&lt;th&gt;Volle Form&lt;/th&gt;
&lt;th&gt;Bedeutung in einfachem Deutsch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Large Language Model&lt;/td&gt;
&lt;td&gt;Ein neuronales Netz, das mit massenhaft Text trainiert wurde und Sprache lesen und generieren kann&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Retrieval-Augmented Generation&lt;/td&gt;
&lt;td&gt;Hole erst relevante Passagen aus &lt;em&gt;deinen&lt;/em&gt; Dokumenten und lass das Modell daraus antworten — statt aus seinem Trainingsgedächtnis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Application Programming Interface&lt;/td&gt;
&lt;td&gt;Die Menge an URLs, die das Frontend aufruft, um mit dem Backend zu sprechen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ASGI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Asynchronous Server Gateway Interface&lt;/td&gt;
&lt;td&gt;Der Python-Standard, der es dem Server erlaubt, viele Anfragen gleichzeitig zu bearbeiten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;Das universelle Textformat für strukturierte Daten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NDJSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Newline-Delimited JSON&lt;/td&gt;
&lt;td&gt;Ein Stream, bei dem jede Zeile ein eigenes JSON-Objekt ist — ideal, um KI-Antworten Stück für Stück (Chunk für Chunk) zu streamen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Metas Bibliothek, um Vektoren zu speichern und die ähnlichsten schnell zu finden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BM25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best Match 25&lt;/td&gt;
&lt;td&gt;Eine klassische Keyword-Ranking-Formel — die 25. Ranking-Funktion, die im Okapi Information-Retrieval-System entwickelt wurde&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reciprocal Rank Fusion&lt;/td&gt;
&lt;td&gt;Eine Formel zum Zusammenführen mehrerer gerankter Ergebnislisten, die nur die Ränge benutzt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ANN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Approximate Nearest Neighbour&lt;/td&gt;
&lt;td&gt;Eine Geschwindigkeits-Abkürzung, die viele Vektordatenbanken nehmen. CogniVault nutzt stattdessen bewusst einen &lt;em&gt;exakten&lt;/em&gt; Index — präzise und für eine persönliche Bibliothek völlig schnell genug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database-Oriented Operating System (das Forschungsprojekt, aus dem es entstand)&lt;/td&gt;
&lt;td&gt;Eine Bibliothek, die Workflow-Schritte in einer Datenbank zwischenspeichert, sodass abgestürzte Jobs fortgesetzt werden können&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQL / SQLite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured Query Language / SQLite&lt;/td&gt;
&lt;td&gt;Die Sprache von relationalen Datenbanken / eine winzige Datenbank, die in einer Datei lebt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OCR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Character Recognition&lt;/td&gt;
&lt;td&gt;Verwandelt Bilder von Text (Scans) in maschinenlesbaren Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SHA-256&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secure Hash Algorithm, 256-bit&lt;/td&gt;
&lt;td&gt;Eine Fingerabdruck-Funktion — jede Datei wird auf einen eindeutigen Hash abgebildet, genutzt um geänderte Dateien zu erkennen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CORS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cross-Origin Resource Sharing&lt;/td&gt;
&lt;td&gt;Browser-Regeln, die kontrollieren, welche Websites die API aufrufen dürfen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-Side Request Forgery&lt;/td&gt;
&lt;td&gt;Ein Angriff, bei dem ein Server ausgetrickst wird, interne URLs abzurufen — der URL-Import Endpoint schützt davor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple-Choice Question&lt;/td&gt;
&lt;td&gt;Einer der beiden Quizfragen-Typen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Knowledge Base&lt;/td&gt;
&lt;td&gt;All deine eingelesenen, durchsuchbaren Dokumente&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;(Jede Behauptung in dieser Serie kann direkt im
überprüft werden — die relevante Datei wird immer genannt, wenn es wichtig ist, und die Repository-README skizziert die komplette Architektur.)&lt;/p&gt;
&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;
&lt;p&gt;Nimm die Abkürzungen weg, und CogniVault ist ein kleines System: ein Webserver, eine Modell-Laufzeitumgebung, eine Haltbarkeits-Datenbank (Durability Database) und eine Handvoll Dateien. Die Raffinesse liegt nicht in der Anzahl der Teile — sie liegt darin, wie ein paar gut gewählte Teile zusammenarbeiten. Von dieser Zusammenarbeit handeln die nächsten drei Teile.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Als Nächstes:&lt;/strong&gt;
— wie ein 1.000-seitiges gescanntes PDF zu etwas wird, das die KI in Sekunden durchsuchen kann, und warum die Pipeline einen Absturz bei Seite 800 überlebt.&lt;/p&gt;</description></item><item><title>Teil 8 · Eine lokale KI-App testen: 351 Tests, Null Infrastruktur</title><link>https://aretascodes.dev/de/blog/testing-local-ai-app/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/testing-local-ai-app/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Teil einer Serie über den Aufbau von
. Zuvor:
.
Alle Abkürzungen werden vollständig im Anhang am Ende der Seite erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CogniVault hat &lt;strong&gt;351 Tests verteilt auf 22 Dateien&lt;/strong&gt; (zum Zeitpunkt des Schreibens — die Suite wächst mit der App). Keiner davon benötigt Ollama. Keiner benötigt Postgres. Keiner braucht ein echtes PDF, ein Mikrofon oder eine Internetverbindung. Die gesamte Suite läuft in &lt;strong&gt;etwa drei Sekunden&lt;/strong&gt; auf meinem Laptop.&lt;/p&gt;
&lt;p&gt;Das liegt nicht daran, dass es nicht viel zu testen gäbe — die Oberfläche ist groß. Es liegt daran, dass die Test-Suite um ein einziges Prinzip herum aufgebaut ist: &lt;strong&gt;An den Rändern mocken, überall sonst echt.&lt;/strong&gt; In diesem Post geht es darum, was &amp;ldquo;der Rand&amp;rdquo; in einer lokalen KI-App bedeutet und wie man die Grenze so zieht, dass die Suite nützlich bleibt anstatt nur dekorativ zu sein.&lt;/p&gt;
&lt;h2 id="die-22-testdateien"&gt;Die 22 Testdateien&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Datei&lt;/th&gt;
&lt;th&gt;Was sie abdeckt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_api.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Die HTTP-Endpoints (Upload, Ingest, RAG, Historie, KB-Browsing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_tools.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Taschenrechner, Uhr, KB-Such-Tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_thinking.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zwei-Phasen-Stream, Thinking-Tokens, Session-Isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_chat_attachments.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-File-Attach, PDF/DOCX-Extraktion, Größenlimits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_chat_memory.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Budget für Session-Historie, Trimming, Restart-Rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_doc_scope_filter.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ContextVar-Isolation pro Request, Suchfilterung&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_doc_tools.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list_documents&lt;/code&gt;, &lt;code&gt;analyze_document&lt;/code&gt;, &lt;code&gt;compare_documents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_edit_regenerate.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Historie zurückspulen, trim_history_to_turns-Validierung&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_structure_chunking.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Markdown-Header-Splits, CSV-Zeilen-Batches, Dokumenttypen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_ocr_fallback.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OCR-Trigger-Schwellenwert, Graceful Degradation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_new_formats.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PPTX-, XLSX-, HTML-Extraktoren, Extension-Routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_docx_url.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DOCX-Ingestion und URL-Import (mit dem SSRF-Schutz)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_reingest.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SHA-256-Änderungserkennung, Idempotenz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_vector_db.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BM25, FAISS, RRF-Fusion, Hybrid-Suche&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_audio.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whisper-Transkriptions-Endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_progress.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sessions, tägliche Aggregation, Achievement-Kriterien&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_prompts.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Der Prompt-Template-Loader und benutzerdefinierte Overrides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_vault_stats.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Die Privacy Vault Audit-Zahlen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_quiz.py&lt;/code&gt; / &lt;code&gt;test_workshop.py&lt;/code&gt; / &lt;code&gt;test_flashcards.py&lt;/code&gt; / &lt;code&gt;test_mindmaps.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parsing pro Modus, Endpoints, Achievements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Alles, was isoliert getestet werden &lt;em&gt;kann&lt;/em&gt;, wird isoliert getestet. Alles, was durch die FastAPI-Schicht getestet werden muss, wird dort getestet, aber die &lt;em&gt;einzigen&lt;/em&gt; gemockten Dinge sind die Aufrufe, die die Prozessgrenze überschreiten.&lt;/p&gt;
&lt;h2 id="was-gemockt-wird-was-nicht"&gt;Was gemockt wird, was nicht&lt;/h2&gt;
&lt;p&gt;Die mit Abstand wichtigste Frage in so einem Projekt: Wo setzt man den Stub an?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ React frontend ] ←─ nicht im Scope für Backend-Tests
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ FastAPI handlers ] ←─ direkt mit TestClient getestet
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ services/ ] ←─ direkt getestet (vector_db, rag_agent, generators)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─► [ FAISS + BM25 ] ←─ echt, in-memory, schnell
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─► [ SQLite ] ←─ echt, gegen eine tmp_path-Datei
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─► [ DBOS ] ←─ gepatched (kein Start, kein Postgres)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─► [ Ollama ] ←─ gepatched am Import-Ort jedes Services
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─► [ Whisper ] ←─ als Stub (kein 145-MB-Modell-Laden)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als Faustregel gilt: &lt;strong&gt;Alles, was eine Prozess- oder Netzwerkgrenze überschreitet, wird gemockt. Alles In-Process läuft echt.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FAISS und BM25 sind echt, weil es Bibliotheken sind, die wir in den Testprozess einbinden. SQLite ist echt, weil es eine Datei ist. DBOS ist gepatched, weil beim Starten eine Postgres-Verbindung erwartet wird, und das ist Netzwerk. Ollama ist gepatched, weil es HTTP ist. Whisper ist als Stub ausgeführt, weil das Laden eines 145 MB großen Modells in einem Unit-Test ziemlich albern ist.&lt;/p&gt;
&lt;p&gt;Dieses Prinzip hält die Test-Suite schnell (kein I/O, den das OS nicht in Millisekunden verarbeiten kann) und aussagekräftig (die echten Code-Pfade durch Retrieval, Chunking, Parsing und Scope-Filterung werden ausgeführt).&lt;/p&gt;
&lt;h2 id="ollama-mocken"&gt;Ollama mocken&lt;/h2&gt;
&lt;p&gt;Die meisten CogniVault-Tests brauchen &lt;em&gt;irgendeinen&lt;/em&gt; Modell-Output, aber es ist ihnen egal, welches Modell ihn produziert hat. Jeder Service importiert das &lt;code&gt;ollama&lt;/code&gt;-Modul direkt, daher patchen die Tests diese Referenz &lt;strong&gt;direkt am Import-Ort des Services&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Real pattern from test_quiz.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;backend.services&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quiz_generator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_quiz_parses_questions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;message&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;questions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VALID_MCQ&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;})}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quiz_generator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ollama&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mock_ollama&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mock_ollama&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quiz_generator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_quiz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;difficulty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;beginner&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_questions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;mcq&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Eine Streaming-Variante füttert Chunk-Sequenzen anstelle einer einzelnen Antwort; dies wird für die RAG- und Thinking-Tests verwendet. Die wichtigste Eigenschaft: Ein &lt;code&gt;patch.object&lt;/code&gt; auf das Modul, das der Service tatsächlich benutzt. Keine tiefen Mock-Hierarchien, keine fragilen String-Pfade in Third-Party-Interna. Leicht in einem Code-Review zu lesen, leicht zu debuggen, wenn ein Test fehlschlägt.&lt;/p&gt;
&lt;h2 id="dbos-mocken"&gt;DBOS mocken&lt;/h2&gt;
&lt;p&gt;DBOS erwartet, dass sich &lt;code&gt;launch()&lt;/code&gt; mit Postgres verbindet. Die gemeinsam genutzte &lt;code&gt;client&lt;/code&gt;-Fixture in der &lt;code&gt;conftest.py&lt;/code&gt; patcht einfach die &lt;code&gt;dbos&lt;/code&gt;-Instanz, bevor die App ausgeführt wird:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Real pattern from conftest.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;A FastAPI TestClient with DBOS launch mocked out — no Postgres needed.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;backend.services.ingest.dbos&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mock_dbos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mock_dbos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MagicMock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;backend.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die dekorierten Workflow-Schritte werden weiterhin als gewöhnliche Python-Funktionen ausgeführt — wir verlieren die Durability-Semantik, aber die Tests prüfen ja nicht Durability, sondern die &lt;em&gt;Geschäftslogik innerhalb der Schritte&lt;/em&gt; (Hash-Erkennung, Extraktion, Chunking). Die Durability-Schicht hat ihre eigenen Tests weiter oben, in der eigenen Suite von DBOS.&lt;/p&gt;
&lt;p&gt;Es gibt noch eine zweite Isolationsschicht, die &lt;strong&gt;jeden&lt;/strong&gt; Test automatisch durchläuft: Eine Autouse-Fixture richtet den Docs-Ordner, den FAISS-Index und die Metadaten-Datei über Umgebungsvariablen auf einen &lt;code&gt;tmp_path&lt;/code&gt; pro Test ein, sodass kein Test jemals echte Daten auf der Festplatte berühren kann.&lt;/p&gt;
&lt;h2 id="echtes-sqlite-mit-einem-override"&gt;Echtes SQLite, mit einem Override&lt;/h2&gt;
&lt;p&gt;Progress-Tracking, Achievements, Quiz-Speicherung, Deck-CRUD — alles SQLite. Der Progress-Tracker bietet eine einzige Test-Nahtstelle: Einen Pfad-Override auf Modulebene.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Real pattern from test_quiz.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_isolate_progress_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress_tracker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_db_path_override&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;progress_test.db&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jeder Test bekommt eine frische Datenbankdatei; das Schema wird bei der ersten Nutzung automatisch erstellt. Kein Drama mit Connection-Pooling, kein durchgesickerter Status zwischen Tests, keine in-memory &lt;code&gt;:memory:&lt;/code&gt;-Gymnastik. Einfach eine Temp-Datei pro Test.&lt;/p&gt;
&lt;p&gt;Das ist die Art von Test, die Fehler aufdeckt, die ein Mock auf SQL-Ebene niemals sehen würde — ein fehlender Index, eine vermurkste Migration, ein Constraint, der nicht auslöst. SQLite ist auf jedem Rechner, den ich je besessen habe, so schnell, dass &amp;ldquo;die echte Datenbank nutzen&amp;rdquo; nicht mal ein Kompromiss ist.&lt;/p&gt;
&lt;h2 id="das-testclient-pattern"&gt;Das TestClient-Pattern&lt;/h2&gt;
&lt;p&gt;Für HTTP-Tests führt FastAPIs &lt;code&gt;TestClient&lt;/code&gt; die App in-process aus. Der Upload, die Validierung, das Chunking, das Vector-Store-Update, die Response-Serialisierung — jede Schicht läuft echt. Nur die Aufrufe, die den Prozess verlassen würden (der Ollama-Embedding-Aufruf in der Ingestion, der Modell-Aufruf in der Generierung), sind gepatched. Das ist genau die richtige Grenze: Der Test verifiziert die &lt;em&gt;Integration&lt;/em&gt; dieser Schichten, hängt aber nicht von einem externen Service ab.&lt;/p&gt;
&lt;p&gt;Die Streaming-Endpoint-Tests nutzen einen leicht anderen Stil — sie iterieren über den Response-Body und parsen jede &lt;strong&gt;NDJSON&lt;/strong&gt;-Zeile (ein JSON-Envelope pro Zeile, wie im
beschrieben) — aber das Prinzip ist identisch.&lt;/p&gt;
&lt;h2 id="lücken-in-der-abdeckung-die-ich-akzeptiere"&gt;Lücken in der Abdeckung, die ich akzeptiere&lt;/h2&gt;
&lt;p&gt;Drei Dinge, die die Test-Suite &lt;em&gt;nicht&lt;/em&gt; abdeckt:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Das Frontend.&lt;/strong&gt; Keine React-Tests in dieser Suite — das ist ein separates Anliegen. Die meisten Fehler zeigen sich ohnehin in API-Tests, da das Frontend ein Thin-Client über einer typisierten API ist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Die tatsächliche Ollama-Prompt-Qualität.&lt;/strong&gt; Ob &lt;code&gt;gemma4:e4b&lt;/code&gt; wirklich &lt;em&gt;nützliche&lt;/em&gt; Quizfragen generiert, ist nichts, was Tests beantworten können. Das ist Evaluierung, kein Testing. Es gehört in eine separate Testumgebung, in der ein echtes Modell läuft.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Race Conditions über DBOS-Workflow-Restarts hinweg.&lt;/strong&gt; Der Resume-Pfad wird auf Logikebene geprüft, aber der volle Zustandsraum von &amp;ldquo;Was passiert, wenn Postgres in genau diesem Moment weg ist&amp;rdquo; ist zu groß, um ihn komplett durchzuspielen.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Das sind bewusste Lücken. Die Test-Suite ist dazu da, Regressionen in meinem Code zu fangen; sie ist kein Ersatz für Evaluierung, Integrationstests oder gar echtes Chaos-Engineering.&lt;/p&gt;
&lt;h2 id="wofür-die-suite-eigentlich-da-ist"&gt;Wofür die Suite eigentlich da ist&lt;/h2&gt;
&lt;p&gt;Zwei Dinge, in dieser Reihenfolge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Vertrauen beim Refactoring.&lt;/strong&gt; Wenn ich die Agent-Loop rausreiße und eine neue einsetze, laufen die Tests dann immer noch grün durch? Wenn ja, haben sich die API-Verträge, die mir wichtig sind, nicht verschoben.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Absicherung für PR-Reviews.&lt;/strong&gt; Jeder PR lässt die Suite in der CI laufen. Ein grüner Durchlauf ist Voraussetzung für den Merge. Die Suite ist laut genug, dass eine echte Regression auch wirklich Lärm macht.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Beachte, wofür sie &lt;em&gt;nicht&lt;/em&gt; da ist: um zu beweisen, dass das Modell funktioniert. Das kann sie nicht. Tests können Verhalten festnageln, aber keine Qualität. Das ist ein anderer Muskel, und er gehört in eine andere Testumgebung.&lt;/p&gt;
&lt;h2 id="was-sich-zum-ausborgen-lohnt"&gt;Was sich zum Ausborgen lohnt&lt;/h2&gt;
&lt;p&gt;Wenn du eine lokale KI-App baust und deine Tests Ollama am Laufen haben müssen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Patche das &lt;code&gt;ollama&lt;/code&gt;-Modul am Import-Ort jedes Services mit &lt;code&gt;patch.object(service_module, &amp;quot;ollama&amp;quot;)&lt;/code&gt; — eine Nahtstelle pro Service, keine Shims nötig.&lt;/li&gt;
&lt;li&gt;Gib deiner DB-Schicht einen Pfad-Override und lass sie gegen eine &lt;code&gt;tmp_path&lt;/code&gt;-SQLite-Datei laufen.&lt;/li&gt;
&lt;li&gt;Nutze eine Autouse-Fixture, um jedes On-Disk-Artefakt (Docs-Ordner, Indexdateien) auf &lt;code&gt;tmp_path&lt;/code&gt; umzuleiten, damit kein Test jemals versehentlich echte Daten berührt.&lt;/li&gt;
&lt;li&gt;Ziehe für jeden externen Service (Modell, Audio, Workflow-Engine) die Naht an der Prozessgrenze. Teste alles darüber mit echtem Code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Das Ergebnis ist eine Suite, in der jeder Test in jeder Umgebung läuft, in Millisekunden fertig ist und die tatsächliche Integration jeder von dir geschriebenen Codezeile testet. 351 Tests in etwa drei Sekunden sind keine Optimierung, sondern ein Nebeneffekt davon, dass man nur an den Rändern mockt.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="anhang-abkürzungen-in-diesem-post"&gt;Anhang: Abkürzungen in diesem Post&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abkürzung&lt;/th&gt;
&lt;th&gt;Volle Form&lt;/th&gt;
&lt;th&gt;Bedeutung&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Continuous Integration&lt;/td&gt;
&lt;td&gt;Automatisches Ausführen der Test-Suite bei jedem Push/PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pull Request&lt;/td&gt;
&lt;td&gt;Eine vorgeschlagene Code-Änderung — wird nur gemerged, wenn die Suite grün ist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Application Programming Interface&lt;/td&gt;
&lt;td&gt;Die HTTP-Oberfläche, die der TestClient in-process testet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HyperText Transfer Protocol&lt;/td&gt;
&lt;td&gt;Das Protokoll, das die (in-process) Endpoint-Tests sprechen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Retrieval-Augmented Generation&lt;/td&gt;
&lt;td&gt;Die Retrieval-then-Answer-Pipeline, die getestet wird&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Knowledge Base&lt;/td&gt;
&lt;td&gt;Die indizierte Dokumentensammlung&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Echt in Tests — es ist eine In-Process-Bibliothek&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BM25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best Match 25&lt;/td&gt;
&lt;td&gt;Der Keyword-Index — auch echt in Tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reciprocal Rank Fusion&lt;/td&gt;
&lt;td&gt;Die Rank-Merging-Formel, die in &lt;code&gt;test_vector_db.py&lt;/code&gt; abgedeckt wird&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQLite / SQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(SQL = Structured Query Language)&lt;/td&gt;
&lt;td&gt;Die echte, dateibasierte Datenbank, gegen die jeder Progress-Test läuft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database-Oriented Operating System&lt;/td&gt;
&lt;td&gt;Die Durable-Workflow-Bibliothek — gepatched, sodass kein Postgres nötig ist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OCR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Character Recognition&lt;/td&gt;
&lt;td&gt;Der Fallback für eingescannte PDFs mit eigenen Trigger-Threshold-Tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-Side Request Forgery&lt;/td&gt;
&lt;td&gt;Die URL-Import-Angriffsklasse, die in &lt;code&gt;test_docx_url.py&lt;/code&gt; abgedeckt ist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NDJSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Newline-Delimited JSON&lt;/td&gt;
&lt;td&gt;Das Streaming-Format, das die Endpoint-Tests Zeile für Zeile parsen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SHA-256&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secure Hash Algorithm, 256-bit&lt;/td&gt;
&lt;td&gt;Der Content-Fingerprint hinter den Re-Ingest-Tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CRUD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create, Read, Update, Delete&lt;/td&gt;
&lt;td&gt;Die grundlegenden Speicheroperationen für Decks, Quizzes und Maps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF / DOCX / PPTX / XLSX / HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Portable Document Format / Word / PowerPoint / Excel / HyperText Markup Language&lt;/td&gt;
&lt;td&gt;Die Extraktor-Formate mit dedizierten Tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;Das war die Serie. Acht Posts über die Teile von
, auf die ich am stolzesten bin — und ein paar, die ich heute anders bauen würde. Wenn irgendetwas davon nützlich für dich war, der Code ist Open Source auf
zu finden und der
ist auf YouTube.&lt;/p&gt;
&lt;p&gt;Deine Daten. Deine Hardware. Deine KI. Dein Vault.&lt;/p&gt;</description></item><item><title>Teil 3 · Zwei-Phasen-Streaming: Zeigen, wie das Modell denkt, bevor es handelt</title><link>https://aretascodes.dev/de/blog/two-phase-streaming-strands-agents/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/two-phase-streaming-strands-agents/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Teil einer Serie über den Aufbau von
. Zuvor:
.
Alle Abkürzungen werden vollständig im Anhang am Ende der Seite erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Als ich Gemma 4 zum ersten Mal mit
in CogniVault verkabelt habe, fühlte sich der Chat langsam an. Nicht laggy — langsam auf eine Art, die schlimmer ist als laggy. Der User tippt eine Frage ein. Der Cursor sitzt da und macht nichts. Dann, irgendwann, fällt eine Antwort aus dem Nichts.&lt;/p&gt;
&lt;p&gt;Das Modell war nicht untätig. Es hat &lt;em&gt;nachgedacht&lt;/em&gt;. Gemma 4 hat einen Chain-of-Thought-Modus, der einen (manchmal langen) Gedankengang produziert, bevor die finale Antwort kommt. Bei einem einphasigen Agenten-Stream passiert dieses ganze Nachdenken &lt;em&gt;innerhalb der Agenten-Loop&lt;/em&gt; — still und heimlich — bevor irgendwelche Tool-Aufrufe laufen oder irgendwelche Tokens an die UI gesendet werden.&lt;/p&gt;
&lt;p&gt;Also habe ich den Aufruf in zwei Phasen unterteilt.&lt;/p&gt;
&lt;h2 id="die-struktur"&gt;Die Struktur&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;POST /rag
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── Phase 1 — Direkter Ollama-Aufruf, Thinking aktiviert
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ stream: {&amp;#34;type&amp;#34;:&amp;#34;thinking&amp;#34;,&amp;#34;data&amp;#34;:&amp;#34;...&amp;#34;} (Reasoning-Tokens)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Phase 2 — Strands Agent (Thinking deaktiviert)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; stream: {&amp;#34;type&amp;#34;:&amp;#34;metadata&amp;#34;,&amp;#34;data&amp;#34;:{...}} (Quellenangaben, sobald die Suche läuft)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; stream: {&amp;#34;type&amp;#34;:&amp;#34;text&amp;#34;,&amp;#34;data&amp;#34;:&amp;#34;...&amp;#34;} (Antwort-Tokens)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; stream: {&amp;#34;type&amp;#34;:&amp;#34;memory&amp;#34;,&amp;#34;data&amp;#34;:{...}} (End-of-Stream: Speicherverbrauch der Session)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der Endpoint streamt &lt;strong&gt;Newline-Delimited JSON&lt;/strong&gt; (NDJSON): Jede Zeile im Response-Body ist ein eigenständiger JSON-Umschlag mit einem &lt;code&gt;type&lt;/code&gt; und einem &lt;code&gt;data&lt;/code&gt;. Das Frontend entscheidet anhand des &lt;code&gt;type&lt;/code&gt; und rendert entsprechend: ein &lt;strong&gt;ausklappbares Reasoning-Panel&lt;/strong&gt; für die Thinking-Tokens, die Hauptnachrichten-Blase für die Text-Tokens und eine Sidebar-Card pro Quelle.&lt;/p&gt;
&lt;p&gt;Der User sieht das Modell &lt;em&gt;sofort&lt;/em&gt; anfangen zu denken. Die Latenz bis zum ersten Byte sinkt von &amp;ldquo;lang genug, um sich zu fragen, ob es abgestürzt ist&amp;rdquo; zu &amp;ldquo;sofort&amp;rdquo;. Die Gesamtzeit bis zur finalen Antwort ändert sich nicht. Aber die gefühlte Geschwindigkeit schon.&lt;/p&gt;
&lt;h2 id="phase-1--nur-nachdenken"&gt;Phase 1 — Nur Nachdenken&lt;/h2&gt;
&lt;p&gt;Phase 1 ist ein einzelner direkter Aufruf an Ollama mit aktiviertem Thinking. Er bekommt exakt das, was auch Phase 2 sehen wird — denselben System-Prompt, die aktuelle Frage und alle angehängten Bilder —, sodass die Argumentation die Realität widerspiegelt. Nur die &lt;em&gt;Reasoning&lt;/em&gt;-Tokens werden konsumiert; was auch immer an Antworttext Phase 1 zu produzieren beginnt, wird verworfen, weil wir nicht wollen, dass eine halbfertige Antwort mit der echten konkurriert.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Simplified from backend/services/rag_agent.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ollama_host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;system&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;images&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;thinking&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;thinking&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Phase 1 ist absichtlich &lt;strong&gt;Best-Effort&lt;/strong&gt;: Jeder Fehler hier wird einfach geschluckt und geloggt, und der Stream geht direkt über zu Phase 2. Ein kaputtes Reasoning-Panel sollte den User niemals seine Antwort kosten.&lt;/p&gt;
&lt;h2 id="phase-2--agent-mit-tools"&gt;Phase 2 — Agent mit Tools&lt;/h2&gt;
&lt;p&gt;Phase 2 baut einen &lt;strong&gt;frischen Strands &lt;code&gt;Agent&lt;/code&gt; pro Request&lt;/strong&gt; auf — kein geteilter veränderlicher Zustand zwischen gleichzeitigen Chats —, stellt die Konversationshistorie der Session darin wieder her und führt die Tool-Loop mit sechs registrierten Tools aus:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Zweck&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_knowledge_base(query)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hybride FAISS + BM25 Suche, Top-7, RRF Fusion. Scope-Filter-aware.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_documents()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bestandsaufnahme jeder indizierten Datei mit Typ und Chunk-Anzahl.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;analyze_document(filename)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Innerer Gemma-Aufruf → strukturierte Zusammenfassung (Themen, Entitäten, Fakten).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compare_documents(doc_a, doc_b, question)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Innerer Gemma-Aufruf, der dokumentübergreifend antwortet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calculator(expression)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sicherer AST-Evaluator — kein &lt;code&gt;eval()&lt;/code&gt;, kein beliebiger Code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;current_time()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zeitstempel für zeitbewusste Fragen.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Der Agent entscheidet, welche Tools er in welcher Reihenfolge aufruft. Es gibt keinen hart codierten Router; der System-Prompt erklärt, was verfügbar ist, und Strands kümmert sich um die Schleife. Für die meisten Dokumentenfragen ist der Weg: &lt;code&gt;search_knowledge_base&lt;/code&gt; → Antwort. Für Vergleiche: &lt;code&gt;compare_documents&lt;/code&gt; → Antwort. Für &amp;ldquo;Welche Dateien habe ich?&amp;rdquo;: &lt;code&gt;list_documents&lt;/code&gt; → Antwort. Für Begrüßungen und einfache Mathematik sagt der System-Prompt dem Agenten, dass er die Suche komplett überspringen darf. Das Modell wählt selbst.&lt;/p&gt;
&lt;p&gt;Zwei Details, deren Debugging Zeit gekostet hat, um sie richtig hinzubekommen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase 2 läuft mit explizit deaktiviertem Thinking.&lt;/strong&gt; Ohne dieses Flag kann Gemmas Standardverhalten &lt;code&gt;&amp;lt;think&amp;gt;…&amp;lt;/think&amp;gt;&lt;/code&gt;-Tags in die sichtbare Antwort durchsickern lassen, und alles vor dem schließenden Tag wird vom Markdown-Renderer verschluckt. Eine Modelloption — &lt;code&gt;options={&amp;quot;thinking&amp;quot;: False}&lt;/code&gt; — behob einen Bug mit &amp;ldquo;abgeschnittenen Antworten&amp;rdquo;, der viel unheimlicher aussah, als er tatsächlich war.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zitate werden vor dem ersten Antwort-Token rausgeschrieben.&lt;/strong&gt; Tools laufen, bevor die Text-Deltas ankommen. Bis das erste sichtbare Token gestreamt wird, ist also jede Quelle, die die Suche gefunden hat, bereits in der Sidebar. Der Accumulator ist ein Request-lokaler &lt;code&gt;ContextVar&lt;/code&gt;, an den das Such-Tool anhängt.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Simplified — the real loop reads Strands&amp;#39; raw event dicts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stream_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;event&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;contentBlockDelta&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;delta&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new_citations&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="c1"&gt;# drain the ContextVar accumulator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;metadata&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="warum-das-wichtiger-ist-als-es-klingt"&gt;Warum das wichtiger ist, als es klingt&lt;/h2&gt;
&lt;p&gt;Du könntest ähnliches Verhalten mit einem einzigen Agenten-Aufruf implementieren, der &lt;code&gt;thinking&lt;/code&gt;-Events mit &lt;code&gt;text&lt;/code&gt;-Events verschränkt. Die Gründe, warum ich es trotzdem aufgeteilt habe:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Das Thinking-Modell und das Tool-Modell können unterschiedlich sein.&lt;/strong&gt; Aktuell sind beide &lt;code&gt;gemma4:e4b&lt;/code&gt;, aber die Architektur erlaubt es mir, ein kleineres, schnelleres Modell für das Reasoning in Phase 1 auszutauschen und das große für die Tool-Nutzung in Phase 2 zu behalten. Das mache ich noch nicht — aber ich will die Option haben.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Phase 1 streamt immer sofort.&lt;/strong&gt; Eine reine Agenten-Loop fängt erst an, Tokens zu produzieren, nachdem das Modell entschieden hat, was es sagen will. Das Zwei-Phasen-Modell garantiert, dass der User fast sofort nach Drücken der Enter-Taste eine Aktivität sieht, unabhängig davon, wie komplex die Tool-Arbeit in Phase 2 wird.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fehler sind isoliert.&lt;/strong&gt; Wenn Phase 2 umfällt (Ollama Timeout, Tool Error), ist die Argumentation aus Phase 1 immer noch sichtbar — der User kann sehen, &lt;em&gt;was das Modell tun wollte&lt;/em&gt;, was den Fehler deutlich weniger frustrierend macht als ein blankes &amp;ldquo;irgendwas ist schiefgelaufen&amp;rdquo;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="contextvar-isolation-noch-einmal"&gt;ContextVar-Isolation, noch einmal&lt;/h2&gt;
&lt;p&gt;Der gleiche &lt;code&gt;ContextVar&lt;/code&gt;-Trick, der im
das Retrieval eingegrenzt hat, greift auch hier. Zu Beginn jedes &lt;code&gt;/rag&lt;/code&gt;-Streams setzt der Handler zwei Request-lokale Variablen: den &lt;strong&gt;Dokument-Scope-Filter&lt;/strong&gt; und den &lt;strong&gt;Zitier-Accumulator&lt;/strong&gt;. Die Tools des Agenten lesen und schreiben diese implizit. Die Konversationshistorie selbst lebt in einem Per-Session-Store, der durch Per-Session &lt;code&gt;asyncio&lt;/code&gt;-Locks geschützt ist. Zwei gleichzeitige Requests im selben Chat können sich also auch nicht gegenseitig korrumpieren.&lt;/p&gt;
&lt;p&gt;Getestet mit zwei offenen Browser-Tabs im selben Backend, mit Scope auf verschiedene Dokumentenkategorien, in denen gleichzeitig überlappende Queries gesendet wurden. Null Kreuzkontamination. Die Test-Suite deckt dies explizit in &lt;code&gt;test_thinking.py&lt;/code&gt; und &lt;code&gt;test_doc_scope_filter.py&lt;/code&gt; ab — schau dir den
für die ganze Geschichte an.&lt;/p&gt;
&lt;h2 id="die-frontend-seite-des-vertrags"&gt;Die Frontend-Seite des Vertrags&lt;/h2&gt;
&lt;p&gt;Ein Detail, das mich ins Straucheln gebracht hat: Das ist ein &lt;code&gt;POST&lt;/code&gt;-Endpoint, also scheidet die &lt;code&gt;EventSource&lt;/code&gt;-API des Browsers (die nur GET macht) aus. Das Frontend nutzt &lt;code&gt;fetch&lt;/code&gt; und liest den Response-Body inkrementell aus, splittet bei Newlines und parst jede Zeile als JSON:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Simplified from useRagStream.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/rag&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;body&lt;/span&gt;: &lt;span class="kt"&gt;JSON.stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;: &lt;span class="kt"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// keep the trailing partial line
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;thinking&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;appendThinking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;appendText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;metadata&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;addCitation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;updateMemoryMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das Reasoning-Panel startet &lt;strong&gt;zusammengeklappt&lt;/strong&gt;, mit einem kleinen pulsierenden Indikator, solange die Thinking-Tokens noch streamen — genug, um zu signalisieren &amp;ldquo;das Modell arbeitet&amp;rdquo;, ohne dem User gleich eine Wand aus Chain-of-Thought ins Gesicht zu drücken. Ein Klick klappt den vollen Text aus, während oder nach dem Stream.&lt;/p&gt;
&lt;h2 id="was-ich-mir-noch-mal-ansehen-würde"&gt;Was ich mir noch mal ansehen würde&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase 1 denkt auf eine volle Antwort hin, und wir werfen den Antwortteil weg.&lt;/strong&gt; Ein eigener &amp;ldquo;Plane dein Vorgehen, aber antworte noch nicht&amp;rdquo;-Prompt für Phase 1 würde den Argumentationsstrang kompakter und billiger machen. Heute teilt er sich den Haupt-System-Prompt — das ist simpler, aber die Argumentation kann abschweifen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noch kein Interrupt.&lt;/strong&gt; Sobald Phase 1 startet, läuft sie bis zum Ende durch. Wenn der User mitten im Stream eine Nachfrage tippt, lassen wir sie zu Ende laufen. Ein echter Cancel-Button würde bedeuten, ein Abort-Signal durch Ollamas HTTP-Client zu fädeln — machbar, aber noch nicht gemacht.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 1 denkt manchmal zu viel nach.&lt;/strong&gt; Begrüßungen und triviale Fragen produzieren immer noch einen Absatz an Begründung. Ein &amp;ldquo;Sollte ich nachdenken?&amp;quot;-Gate (wahrscheinlich ein winziger Classifier oder einfach eine Heuristik basierend auf der Query-Länge) würde Phase 1 in diesen Fällen komplett überspringen.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="takeaway"&gt;Takeaway&lt;/h2&gt;
&lt;p&gt;Streaming ist &lt;em&gt;nicht&lt;/em&gt; einfach nur eine Optimierung. Es ist ein UX-Primitiv. Zwei-Phasen-Streaming kauft dir eine Eigenschaft gratis ein: Der &lt;em&gt;sichtbare&lt;/em&gt; Teil der Interaktion startet, bevor der &lt;em&gt;langsame&lt;/em&gt; Teil beginnt. Der User darf dem Modell beim Denken zusehen, was — ehrlich gesagt — interessanter ist, als einem Spinner zuzuschauen.&lt;/p&gt;
&lt;p&gt;Wenn sich deine Agenten-App langsam anfühlt, obwohl die Antworten schnell kommen, schau dir an, &lt;em&gt;wann&lt;/em&gt; die Tokens anfangen zu fließen. Der Fix ist oft nicht ein schnelleres Modell.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="anhang-abkürzungen-in-diesem-post"&gt;Anhang: Abkürzungen in diesem Post&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abkürzung&lt;/th&gt;
&lt;th&gt;Volle Form&lt;/th&gt;
&lt;th&gt;Bedeutung&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NDJSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Newline-Delimited JSON&lt;/td&gt;
&lt;td&gt;Ein Stream, in dem jede Zeile ihr eigenes komplettes JSON-Objekt ist — das, was &lt;code&gt;/rag&lt;/code&gt; ausgibt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;Das universelle Textformat für strukturierte Daten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User Experience&lt;/td&gt;
&lt;td&gt;Wie sich das Produkt in der Nutzung anfühlt — der eigentliche Profiteur vom Zwei-Phasen-Streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User Interface&lt;/td&gt;
&lt;td&gt;Die sichtbare Oberfläche, in die der Stream rendert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Die dichte (dense) Hälfte des hybriden Retrievals (vorheriger Post)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BM25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best Match 25&lt;/td&gt;
&lt;td&gt;Die Keyword-Hälfte des hybriden Retrievals (vorheriger Post)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reciprocal Rank Fusion&lt;/td&gt;
&lt;td&gt;Die Rank-only-Formel, die die beiden Ergebnislisten zusammenführt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AST&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Abstract Syntax Tree&lt;/td&gt;
&lt;td&gt;Die geparste Form eines Ausdrucks — wie der Taschenrechner Mathe ohne &lt;code&gt;eval()&lt;/code&gt; berechnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HyperText Transfer Protocol&lt;/td&gt;
&lt;td&gt;Das Protokoll, das den Stream transportiert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-Sent Events&lt;/td&gt;
&lt;td&gt;Das eingebaute GET-only Streaming-Format des Browsers — hier &lt;em&gt;nicht&lt;/em&gt; nutzbar, weil &lt;code&gt;/rag&lt;/code&gt; ein POST ist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Application Programming Interface&lt;/td&gt;
&lt;td&gt;Die Grenze, die das Frontend aufruft&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Als Nächstes:&lt;/strong&gt;
— wie CogniVault bearbeitete PDFs neu einliest, ohne alles neu zu embedden, und ein &lt;code&gt;kill -9&lt;/code&gt; mitten in der Pipeline überlebt.&lt;/p&gt;</description></item></channel></rss>