Geminis 1M-Fenster ersetzte meinen Vector Store, doch Latenz und Herkunftsnachweis legten Einspruch ein
Full-Context gegen RAG bei 500 Support-Fragen: Genauigkeit, Latenz und Kosten, warum die Herkunft einer Antwort zählt und welches hybride Setup die Vorteile beider Ansätze behielt.
Die Vektortabelle fliegt raus
Stellen Sie sich einen Support-Assistenten vor, den neuntausend interne Mitarbeiter nutzen. Jede seiner Antworten kann ein Mensch an einen Kunden weitergeben, deshalb sind falsche oder unerklärbare Ausgaben teuer.
Darunter liegt eine Retrieval-Schicht aus Chunker, Embedding-Job, Vector Store und ganz am Ende einem Reranker, der so oft nachjustiert wurde, dass niemand seine Werte mehr begründen kann.
user ─► embed ─► vector db ─► rerank ─► 4k ctx ─► model ─► answer
▲
chunker
▲
nightly reindex
Der Betrieb bedeutet vier Services und zwei Bereitschaftsalarme, dazu ein wiederkehrender Bug, bei dem ein PDF-Footer still und leise die Hälfte der Chunks kaputt macht. Der eigentliche Abfragepfad ist winzig:
q = embed(text)
hits = db.search(q, k=8)
ctx = "\n".join(h.doc for h in hits)
ans = llm(f"{ctx}\n\nQ: {text}")
Der ganze Ärger steckt in allem, was diese vier Zeilen umgibt.
Dann kommt eine verlockende Beobachtung. Gemini nimmt eine Million Tokens bei nahezu perfektem Recall an, und der gesamte Korpus umfasst nur 640.000 Tokens. Er passt hinein. Also löscht jemand an einem Dienstagabend eine Tabelle mit 2,3 Millionen Embeddings und ersetzt die Pipeline durch das hier:
docs = load_all()
ans = gem(f"{docs}\n\nQ: {text}")
Kein Chunker, kein Reindex-Job, kein Reranker mit einer Konstante, die niemand erklären kann. Es funktioniert, und etwa sechs Stunden lang fühlt es sich an, als hätte man bei einer Prüfung geschummelt und wäre davongekommen.
Was der nächste Morgen zeigte
Die Latenz wächst ungefähr proportional zur Eingabegröße. Das ist kein Geheimnis, aber man spürt es erst, wenn sich der Support beschwert. Für den Vergleich liefen 500 echte Fragen durch jedes Setup. Die Genauigkeit bewertete ein zweites Modell, das jede Antwort mit einer handgeschriebenen Referenz verglich, und sechzig dieser Bewertungen wurden von Hand nachgeprüft, um sicherzugehen, dass der Richter nicht zu nachsichtig war.
setup p50 p95 cost/query accuracy
-----------------------------------------------------------
RAG, 4k ctx 1.9s 3.4s $0.004 71%
full stuff, 640k 28.7s 41.2s $0.81 74%
cached, 640k 9.4s 15.1s $0.11 74%
Ein Median von 28 Sekunden ist eine lange Wartezeit für ein Chatfenster. Prompt-Caching drückt das und ist gute Praxis, hat aber Grenzen: eine Mindestgröße für den Prompt, eine Cache-Lebensdauer, für deren Warmhalten man zahlt, und eine Schwäche, die hier besonders wehtat. Der Korpus ändert sich etwa vierzigmal am Tag, jede Änderung invalidiert den Cache, und ein ganzer Nachmittag ging dafür drauf, den Neuaufbau von 640.000 Tokens zu bezahlen.
Die Frage, die wichtiger war
Die Genauigkeit stieg um drei Punkte, und das hätte das eigentliche Problem fast verdeckt. Eine Support-Leiterin stellte eine einfache Frage: Woher kommt diese Antwort?
Retrieval liefert eine konkrete Auskunft, etwa Chunk 4.112 auf Seite 9, zuletzt im März bearbeitet. Mit dem vollen Fenster gibt das Modell einen selbstsicheren Absatz zurück, der irgendwo aus 640.000 Tokens zusammengesetzt ist, und man kann ihr nichts vorzeigen. Nennen wir es den Herkunftstest. Es geht nicht darum, ob das Modell antworten kann, sondern ob man belegen kann, wo die Antwort stand. Das ist ein Produktproblem und kein Modellproblem, und zwar eines von der Sorte, die Karrieren beendet und nicht nur Sprints.
War nicht einfach der Reranker schlecht?
Sofort wird jemand einwenden: Den Reranker tunen, eine günstigere Stufe nehmen, cleverer chunken. Berechtigt, der Reranker war wirklich mittelmäßig. Aber auch ein gut eingestellter Reranker reicht acht Passagen weiter und hofft auf das Beste, und kein Tuning der Welt erzeugt ein Zitat aus einem Fenster, das alles auf einmal verschluckt hat.
Das Setup, das geblieben ist
Die Vektordatenbank kam zurück, der Reranker nicht.
ids = db.search(embed(q), k=40)
docs = fetch(ids)
ans = gem(f"{docs}\n\nQ: {q}", cache=c)
Retrieval findet die Antwort nicht mehr selbst. Seine Aufgabe ist jetzt, den Suchraum einzugrenzen, und das lange Fenster übernimmt das Lesen.
user ─► embed ─► vector db (k=40, no rerank)
│
▼
~90k ctx + cached policy docs
│
▼
model ─► answer + doc ids
Dieselben 500 Fragen liefen durch denselben Richter:
hybrid, 90k ctx 3.1s 5.2s $0.021 88%
Vierzig grob ausgewählte Chunks geben dem Modell Raum zum Schlussfolgern, wo ein Reranker die perfekten acht erraten müsste. Und jede Antwort trägt die IDs dessen, worauf sie sich stützt.
Große Fenster haben Retrieval nicht abgeschafft, sondern nur die Variante, bei der alles an Präzision hängt. Die alte Aufgabe war gnadenlos: die acht perfekten Chunks finden, weil das Budget nur acht zuließ. Diese Aufgabe ist erledigt, und niemand sollte ihr nachtrauern.