<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>NDJSON |</title><link>https://aretascodes.dev/de/tags/ndjson/</link><atom:link href="https://aretascodes.dev/de/tags/ndjson/index.xml" rel="self" type="application/rss+xml"/><description>NDJSON</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>de-DE</language><lastBuildDate>Thu, 30 Apr 2026 00:00:00 +0000</lastBuildDate><image><url>https://aretascodes.dev/media/icon_hu_2ab4f4763b27c75b.png</url><title>NDJSON</title><link>https://aretascodes.dev/de/tags/ndjson/</link></image><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>