Mein Agent verlor immer wieder gespeicherte Fakten
AIdaemon läuft komplett auf meiner eigenen Maschine, mit Open-Source-Modellen wie Gemma 4, ohne dass etwas eine Cloud-API aufruft. Es speichert Fakten über mich, während ich mit ihm spreche, und sucht sie dann heraus, wenn ich danach frage. Das Speichern funktionierte bereits. Das Finden war das, was mich immer wieder enttäuschte. Ich fragte nach etwas, von dem ich wusste, dass es gespeichert war, und bekam nichts Nützliches zurück oder zwei Fakten, die zum richtigen Thema passten, aber den Kern verfehlten.
Die Suche läuft über die Bedeutung. Jede Tatsache wird beim Speichern zu einem Vektor, meine Frage wird beim Abfragen zu einem Vektor, und es werden Fakten danach sortiert, wie nah die beiden liegen. Das Problem ist, dass nah nicht dasselbe ist wie richtig. Eine Frage zieht alles an, was ihr Thema teilt, sodass einige wortreiche Fakten über dasselbe Ding den kurzen, der die Antwort tatsächlich enthält, verdrängen. Bei einer Abfrage, die immer wieder fehlschlug, überprüfte ich, wo die Antwort lag. Ungefähr auf Rang 30. Die Suche las die ersten paar und brach lange ab, bevor sie dort ankam.
Warum die Kernaussage nicht ausreicht
Diese erste Suche verwendet einen Bi-Encoder, ein kleines Modell namens all-MiniLM-L6-v2. Es kodiert jede Tatsache für sich, wenn Sie sie speichern, und Ihre Abfrage für sich, wenn Sie suchen, und vergleicht dann die beiden. Es sieht sie nie nebeneinander. Das ist der ganze Grund, warum es schnell ist. Sie betten alles einmal ein und eine Suche ist nur billige Mathematik über den Speicher, kein Modellaufruf, wenn Sie fragen.
Das ist auch der Grund, warum die Reihenfolge falsch ist. Ein Bi-Encoder erfasst das allgemeine Thema und verpatzt die präzise Übereinstimmung. Fragen Sie nach einer Sache und es werden alle Dinge in der Nähe hochgezogen, während die gewünschte Tatsache absinkt. Es ist billig genug, um auf jeder Ihrer Fakten zu laufen, was genau der Grund ist, warum Sie darauf zurückgreifen, wenn alles auf Ihrer eigenen Hardware leben muss. Es ist einfach nicht sorgfältig genug, um sie gut zu sortieren. Die meiste Zeit ist das gut genug. Wenn ich eine spezifische Antwort brauche, zerfällt es.
Ein zweites Modell tatsächlich lesen lassen
Die Lösung ist ein Reranker. Es ist ein Cross-Encoder, der meine Frage und eine Kandidaten-Tatsache zusammen liest, im selben Durchgang, und bewertet, wie gut diese Tatsache die Frage beantwortet. Wo der Bi-Encoder zwei getrennt erstellte Zusammenfassungen verglich, liest dieser das Paar. Bessere Frage, bessere Antwort. Der Nachteil ist die Geschwindigkeit. Es ist ein Modellaufruf pro Tatsache, daher können Sie ihn nicht auf einen ganzen Speicher richten und warten.
Der Weg dorthin ist alt und langweilig und funktioniert. Zuerst abrufen, dann neu sortieren. Lassen Sie das billige Modell einen großen Haufen potenziell relevanter Fakten sammeln und verwenden Sie dann das teure nur für diesen kurzen Haufen.
In AIdaemon holt der Bi-Encoder die Top 50 Kandidaten mit einem lockeren Grenzwert von 0.22. Ich halte ihn lockerer als die 0.30, die ich für Speicher verwende, der von selbst in Prompts eingefügt wird, da Synonyme niedrig bewertet werden und ein enger Grenzwert die richtige Tatsache aussortieren würde, bevor der Reranker sie überhaupt sieht. Phase eins muss nicht richtig sein. Sie muss die Antwort nur irgendwo in den 50 landen. Dann liest der Cross-Encoder alle 50 gegen die Frage und ordnet sie neu an. Die Tatsache, die auf Rang 30 feststeckte, wird endlich nach ihren eigenen Bedingungen gelesen, springt nach oben und kommt zurück. Ich habe nichts daran geändert, wie Fakten gespeichert werden, nur wie sie beim Herausgehen geordnet werden.
// Phase 1: Bi-Encoder wirft ein weites Netz aus (Kosinus über alle aktiven Fakten)
let mut pool = cosine_rank(&query_vec, &facts, MIN_SCORE); // 0.22
pool.truncate(CANDIDATE_POOL); // 50
// Phase 2: Cross-Encoder liest jedes (Abfrage, Tatsache)-Paar neu und ordnet neu
let docs: Vec<String> = pool.iter().map(|c| c.text()).collect();
let ranked = reranker.rerank(&query, docs, false, None)?;
Der Reranker läuft auf derselben Maschine wie alles andere, über den fastembed-Crate als ONNX-Modell, ohne eine Rerank-API, die aufgerufen werden muss. Ich verwende Jina Reranker v2 Base Multilingual und habe mich bewusst für Mehrsprachigkeit entschieden, da meine Notizen an AIdaemon zwischen Englisch und Spanisch wechseln und ein rein englischer Reranker genau bei den Fakten stolpern würde, die ich am wichtigsten finde, um sie richtig zu machen.
Es günstig halten
Ein zweites Modell ist eine zweite Sache, die kaputt gehen kann, also bleibt es an einer kurzen Leine. Es wird erst geladen, wenn eine Suche es zum ersten Mal benötigt, da der Download nicht klein ist. Wenn das Laden fehlschlägt, fällt die Suche auf die einfache Kosinus-Reihenfolge zurück, die sie zuvor verwendet hat. Nichts geht kaputt, es wird nur weniger scharf. Und es läuft nur, wenn ich den Agenten explizit bitte, etwas nachzuschlagen. Der Speicher, der von selbst in jeden Prompt einfließt, nimmt immer noch den günstigen Weg. 50 Fakten durch einen Cross-Encoder laufen zu lassen, ist einmalig vernünftig, wenn ich eine Frage gestellt habe. Bei jeder Nachricht wäre es Verschwendung.
Abrufen und dann neu sortieren ist nichts Neues. Suchmaschinen verlassen sich seit Jahren darauf. Es passt einfach auch gut zum Agenten-Gedächtnis. Das erste Modell sammelt einen großen Haufen, das zweite liest diesen Haufen richtig, und diese zweite Lesung kostet fast nichts, fünfzig kurze Zeichenfolgen durch ein Modell. Das ist der Grund, warum AIdaemon mir jetzt die gespeicherte Tatsache zurückgibt, anstatt leer auszugehen, wenn ich frage.