ChatOllama Es ist ein quelloffener Chatbot, der auf LLMs basiert. Für eine detaillierte Beschreibung von ChatOllama klicken Sie auf den untenstehenden Link.
ChatOllama | Ollama-basierte 100% Lokale RAG-Anwendung
ChatOllama Das ursprüngliche Ziel war es, den Nutzern eine native RAG-Anwendung 100% zur Verfügung zu stellen.
Im Laufe der Zeit haben sich immer mehr Nutzer mit wertvollen Anforderungen gemeldet. Jetzt unterstützt "ChatOllama" mehrere Sprachmodelle, darunter:
- Ollama Unterstützte Modelle
- OpenAI
- Azure OpenAI
- Anthropisch
Mit ChatOllama können Benutzer:
- Verwalten von Ollama-Modellen (ziehen/löschen)
- Verwalten der Wissensbasis (Erstellen/Löschen)
- Freier Dialog mit LLMs
- Verwaltung der persönlichen Wissensbasis
- Kommunikation mit LLMs durch eine persönliche Wissensdatenbank
In diesem Artikel werde ich mich damit beschäftigen, wie man fortgeschrittene RAG für die Produktion erreichen kann. Ich habe die grundlegenden und fortgeschrittenen Techniken von RAG erlernt, aber es gibt immer noch eine Menge Dinge, um die man sich kümmern muss, um RAG in die Produktion zu bringen. Ich werde die Arbeit, die in ChatOllama geleistet wurde, und die Vorbereitungen, die getroffen wurden, um RAG in die Produktion zu bringen, teilen.
In der ChatOllama-Plattform verwenden wir dieLangChain.jsum den RAG-Prozess zu manipulieren. In der Wissensdatenbank von ChatOllama werden über die ursprüngliche RAG hinausgehende übergeordnete Dokumentenabfragen verwendet. Lassen Sie uns einen tiefen Einblick in die Architektur und die Details der Implementierung nehmen. Wir hoffen, Sie finden es erhellend.
Parent Document Retriever Dienstprogramm
Damit der Parent Document Retriever in einem realen Produkt funktionieren kann, müssen wir seine Grundprinzipien verstehen und die richtigen Speicherelemente für die Produktion auswählen.
Grundprinzipien von Parent Document Retriever
Bei der Aufteilung von Dokumenten für Abrufzwecke treten häufig widersprüchliche Anforderungen auf:
- Sie möchten vielleicht, dass die Dokumente so klein wie möglich sind, damit der eingebettete Inhalt die Bedeutung möglichst genau wiedergeben kann. Wenn der Inhalt zu lang ist, können die eingebetteten Informationen ihre Bedeutung verlieren.
- Außerdem sollten die Dokumente lang genug sein, um sicherzustellen, dass jeder Absatz eine vollständige Volltextumgebung hat.
Angetrieben von LangChain.jsParentDocumentRetriever
Dieses Gleichgewicht wird durch die Aufteilung des Dokuments in Teile (Chunks) erreicht. Bei jedem Abruf werden die Chunks extrahiert und dann das jedem Chunk entsprechende übergeordnete Dokument gesucht, wodurch eine größere Auswahl an Dokumenten zurückgegeben wird. Im Allgemeinen ist das übergeordnete Dokument mit den Chunks über die Dokument-ID verknüpft. Wie das funktioniert, werden wir später noch genauer erklären.
Beachten Sie, dass hier dieübergeordnetes Dokument
Bezieht sich auf kleine Teile von Quelldokumenten.
bauen
Werfen wir einen Blick auf das allgemeine Architekturdiagramm des übergeordneten Dokumentenabrufs.
Jedes Teilstück wird verarbeitet und in Vektordaten umgewandelt, die dann in der Vektordatenbank gespeichert werden. Das Abrufen dieser Chunks erfolgt wie bei der ursprünglichen RAG-Einrichtung.
Das übergeordnete Dokument (Block-1, Block-2, ...... block-m) wird in kleinere Teile aufgeteilt. Es ist erwähnenswert, dass hier zwei verschiedene Textsegmentierungsmethoden verwendet werden: eine größere für übergeordnete Dokumente und eine kleinere für kleinere Blöcke. Jedem übergeordneten Dokument wird eine Dokument-ID zugewiesen, und diese ID wird dann als Metadaten in dem entsprechenden Chunk gespeichert. Dadurch wird sichergestellt, dass jeder Chunk sein entsprechendes übergeordnetes Dokument anhand der in den Metadaten gespeicherten Dokument-ID finden kann.
Der Prozess der Suche nach übergeordneten Dokumenten ist nicht derselbe wie hier. Anstelle einer Ähnlichkeitssuche wird eine Dokument-ID angegeben, um das entsprechende übergeordnete Dokument zu finden. In diesem Fall ist ein Key-Value-Speichersystem für die Aufgabe ausreichend.
Bereich Lagerung
Es gibt zwei Arten von Daten, die gespeichert werden müssen:
- Kleinräumige Vektordaten
- String-Daten des übergeordneten Dokuments, die dessen Dokument-ID enthalten
Alle wichtigen Vektordatenbanken sind verfügbar. Von "ChatOllama" habe ich Chroma gewählt.
Für die Daten der übergeordneten Dokumente habe ich mich für Redis entschieden, das beliebteste und am besten skalierbare System zur Speicherung von Schlüsselwerten.
Was in LangChain.js noch fehlt
LangChain.js bietet eine Reihe von Möglichkeiten zur Integration von Byte- und Dokumentenspeicherung:
Unterstützung für "IORedis":
Der fehlende Teil des RedisByteStore ist dieSammlung
Mechanismen.
Bei der Verarbeitung von Dokumenten aus verschiedenen Wissensbasen wird jede Wissensbasis in einerSammlung
Die Sammlungen werden in Form von Sammlungen organisiert, und die Dokumente in derselben Bibliothek werden in Vektordaten umgewandelt und in einer Vektordatenbank wie Chroma in einer derSammlung
In der Versammlung.
Angenommen, wir wollen eine Wissensbasis löschen. Wir können natürlich eine Sammlung in der Chroma-Datenbank löschen. Aber wie bereinigen wir den Dokumentenspeicher entsprechend den Dimensionen der Wissensdatenbank? Da Komponenten wie RedisByteStore keine `collection`-Funktionalität unterstützen, musste ich sie selbst implementieren.
ChatOllama RedisDocStore
In Redis gibt es keine eingebaute Funktion namens "collection". Entwickler implementieren normalerweise die Funktion "Sammlung" durch die Verwendung von Präfixschlüsseln. Die folgende Abbildung zeigt, wie Präfixe verwendet werden können, um verschiedene Sammlungen zu identifizieren:
Schauen wir uns nun an, wie es umgesetzt wurde, umRedisder Dokumentenablagefunktion für den Hintergrund.
Schlüsselbotschaft:
- Jeder `RedisDocstore` wird mit einem `Namespace` Parameter initialisiert. (Die Benennung von Namespaces und Sammlungen ist im Moment nicht standardisiert).
- Die Schlüssel werden zunächst für Get- und Set-Operationen im "Namensraum" verarbeitet.
import { Document } from "@langchain/core/documents" ;
import { BaseStoreInterface } from "@langchain/core/stores" ;
importiere { Redis } von "ioredis".
import { createRedisClient } from "@/server/store/redis" ;export class RedisDocstore implements BaseStoreInterface
{
_Namensraum: Zeichenkette;
Klient: Redis.constructor(namespace: string) {
this._namespace = namespace;
this._client = createRedisClient();
}serializeDocument(doc: Document): string {
return JSON.stringify(doc);
}deserializeDocument(jsonString: string): Document {
const obj = JSON.parse(jsonString);
return new Document(obj);
}getNamespacedKey(key: string): string {
return `${this._namespace}:${key}`;
}getKeys(): Promise {
return new Promise((resolve, reject) => {
const stream = this._client.scanStream({ match: this._namespace + '*' });const keys: string[] = [];
stream.on('data', (resultKeys) => {
keys.push(... .resultKeys);
});stream.on('end', () => {
resolve(Schlüssel).
});stream.on('error', (err) => {
zurückweisen(err);
});
});
}addText(key: string, value: string) {
this._client.set(this.getNamespacedKey(key), value);
}async search(search: string): Promise {
const result = await this._client.get(this.getNamespacedKey(search));
if (!result) {
throw new Error(`ID ${Suche} nicht gefunden.`);
} sonst {
const document = this.deserializeDocument(result);
Dokument zurückgeben;
}
}/**
* :: Fügt dem Speicher neue Dokumente hinzu.
* @param texts Ein Objekt, bei dem die Schlüssel Dokument-IDs und die Werte die Dokumente selbst sind.
* @returns Void
*/
async add(texts: Record): Promise {
for (const [Schlüssel, Wert] of Object.entries(texts)) {
console.log(`Hinzufügen von ${Schlüssel} zum Speicher: ${this.serializeDocument(value)}`);
}const keys = [... .await this.getKeys()];
const overlapping = Object.keys(texts).filter((x) => keys.includes(x));if (overlapping.length > 0) {
throw new Error(`Versucht, bereits vorhandene IDs hinzuzufügen: ${überlappend}`);
}for (const [Schlüssel, Wert] of Object.entries(texts)) {
this.addText(key, this.serialiseDocument(value)); this.
}
}async mget(keys: string[]): Promise {
return Promise.all(keys.map((key) => {
const document = this.search(key);
Dokument zurückgeben;
}));
}async mset(keyValuePairs: [string, Document][]): Promise {
await Promise.all(
keyValuePairs.map(([Schlüssel, Wert]) => this.add({ [Schlüssel]: Wert }))
);
}async mdelete(_keys: string[]): Promise {
throw new Error("Nicht implementiert.");
}// eslint-disable-next-line require-yield
async *yieldKeys(_prefix?: string): AsyncGenerator {
throw new Error("Nicht implementiert");
}async deleteAll(): Promise {
return new Promise((resolve, reject) => {
lassen Sie Cursor = '0';const scanCallback = (err, result) => {
if (err) {
zurückweisen(err);
Rückkehr;
}const [nextCursor, keys] = result;
// Schlüssel löschen, die dem Präfix entsprechen
keys.forEach((key) => {
this._client.del(key);
});// Wenn der Cursor '0' ist, haben wir alle Tasten durchlaufen
if (nextCursor === '0') {
resolve().
} sonst {
// Fortsetzen des Scannens mit dem nächsten Cursor
this._client.scan(nextCursor, 'MATCH', `${this._namespace}:*`, scanCallback);
}
};// Start des ersten SCAN-Vorgangs
this._client.scan(cursor, 'MATCH', `${this._namespace}:*`, scanCallback);
});
}
}
Sie können diese Komponente nahtlos mit ParentDocumentRetriever verwenden:
retriever = new ParentDocumentRetriever({
vectorstore: chromaClient.
docstore: new RedisDocstore(collectionName),
parentSplitter: new RecursiveCharacterTextSplitter({
chunkOverlap: 200,
chunkSize: 1000,
}),
childSplitter: new RecursiveCharacterTextSplitter({
chunkOverlap: 50,
chunkSize: 200,
}),
kindK: 20.
parentK: 5.
});
Wir haben jetzt eine skalierbare Speicherlösung für fortgeschrittene RAG, Parent Document Retriever, zusammen mit `Chroma` und `Redis`.