Das Problem entsteht nicht erst beim Parsen der Antwort.
Das Problem: Zwei Ausgabekanäle
Ein Kommandozeilenprozess liefert Antworten über stdout und Diagnosen über stderr. Wird nur die Antwort gelesen, kann sich der zweite Kanal füllen. Der Prozess wartet auf freien Platz, während die Anwendung auf seine Antwort wartet. Die Entwicklungsnotiz vom 5. Oktober dokumentiert eine Korrektur für diese Rückstau-Situation.
Die Lösung: Parallel lesen
Der Transport leert stderr parallel zu stdout. Für die Fehlersuche bleibt nur ein begrenzter Diagnoseausschnitt erhalten. Der Leser für native Ergebnisframes bekam eine größere Frame-Grenze; die Grenze für die gesamte Ausgabe bleibt bestehen. Endergebnisse werden verarbeitet, ohne auf das Ende eines noch laufenden CLI-Prozesses zu warten.
Der Nachweis: Blockade nachstellen
Ein Regressionstest schreibt zuerst 200 KB Diagnosedaten und danach einen 70-KB-Ergebnisframe. Laut Protokoll wird dieser Ablauf erfolgreich abgeschlossen. Das prüft genau die problematische Reihenfolge. Ein Timeout muss also nicht bedeuten, dass das Modell zu langsam denkt: Auch der Transport kann festhängen. Diese Prüfung belegt die Transportkorrektur, keine allgemeine Beschleunigung der Modelle.
Die gefährliche Reihenfolge im Wrapper
Das Problem entsteht nicht erst beim Parsen der Antwort. Der Wrapper startet einen Prozess mit zwei Pipes. Liest er zuerst stdout bis zum Ende, kann der Kindprozess zuvor große Diagnosen auf stderr schreiben. Sobald dessen Betriebssystempuffer voll ist, blockiert der Schreibvorgang. Der Prozess erreicht seine eigentliche Antwort nicht mehr. Der Wrapper wartet gleichzeitig auf das Ende von stdout. Das ist ein klassischer gegenseitiger Wartezustand. Python weist in seiner Subprocess-Dokumentation ausdrücklich auf diesen Fall hin. Größere Anfrage-Timeouts verschieben dabei nur die Fehlermeldung; sie lösen die Abhängigkeit nicht auf.
Parallel lesen, Ausgabe trotzdem begrenzen
In unserem Subscription-Transport starten die Leseaufgaben für stdout und stderr gleichzeitig. Eine dritte Aufgabe schreibt die Eingabe. Es reicht aber nicht, beide Kanäle vollständig in unbegrenzten Bytearrays zu sammeln. Auch ein fehlerhafter Prozess kann beliebig viel ausgeben. Deshalb braucht der Reader eine Gesamtgrenze und für Diagnosen einen begrenzten Restpuffer. Der sichtbare Code zeigt dieses Muster in einem reduzierten Beispiel. Für große Streaming-Ergebnisse verwenden wir getrennte Frame-Verarbeitung; communicate ist für kurze abgeschlossene Prozesse einfacher, puffert jedoch die gesamte gelesene Ausgabe im Speicher.
import asyncio
async def read_bounded(stream, max_bytes=1_000_000):
data = bytearray()
while chunk := await stream.read(8192):
data.extend(chunk)
if len(data) > max_bytes:
raise ValueError("Output limit exceeded")
return bytes(data)
async def capture(command):
proc = await asyncio.create_subprocess_exec(
*command, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
try:
return await asyncio.wait_for(asyncio.gather(
read_bounded(proc.stdout), read_bounded(proc.stderr)), 180)
finally:
if proc.returncode is None:
proc.kill()
await proc.wait()
# Vereinfachtes Transportmuster, nicht der gesamte Protokollparser.Ergebnisframe und Prozessende sind zwei Ereignisse
Ein CLI kann ein gültiges Endergebnis schreiben und anschließend für Aufräumarbeiten weiterlaufen. Wer das Ergebnis erst nach dem endgültigen Prozessende zustellt, verlängert die wahrgenommene Wartezeit. Umgekehrt darf eine beliebige Textzeile nicht als fertige Antwort gelten. Der Transport muss ein gültiges terminales Ergebnis im erwarteten Protokoll erkennen. Die größere Grenze für native Ergebnisframes ändert diese Bedingung nicht. Ein größerer erlaubter Frame ist außerdem keine Erlaubnis für unbegrenzte Gesamtausgabe.
Technische Referenz: Python: Subprocess-Pipes und Deadlocks.
Der Regressionstest reproduziert die Reihenfolge
Der dokumentierte Test erzeugt vor dem 70-KB-Ergebnisframe zunächst 200 KB auf stderr. Ein Reader, der nur stdout abarbeitet, gerät genau hier in Schwierigkeiten. Der parallel lesende Transport schließt diesen Ablauf erfolgreich ab. Ergänzend müssen Fehlerende, Zeitlimit und übergroße Ausgabe untersucht werden: Bleiben Tasks zurück? Wird der Kindprozess beendet? Werden Diagnoseinhalte ungefiltert gespeichert? Die praktische Konsequenz ist klar: Bei einer scheinbar langsamen Modellanfrage prüfen wir nicht nur das Modell, sondern den gesamten Weg vom Prozessstart bis zur Zustellung.
Weiterlesen: Agenten schneller machen, ohne die Qualitätskontrolle zu verlieren.