ChatOllama Notes | Mise en œuvre de RAG avancé pour la productivité et les bases de données de documents basées sur Redis

 

ChatOllama Il s'agit d'un chatbot open source basé sur les LLM. Pour une description détaillée de ChatOllama, cliquez sur le lien ci-dessous.

 

ChatOllama | Application locale RAG 100% basée sur Ollama

 

ChatOllama L'objectif initial était de fournir aux utilisateurs une application RAG native 100%.

 

Au fur et à mesure de son développement, de plus en plus d'utilisateurs ont fait part de leurs besoins. Aujourd'hui, `ChatOllama` prend en charge plusieurs modèles linguistiques, notamment :

 

Avec ChatOllama, les utilisateurs peuvent :

  • Gérer les modèles Ollama (retirer/supprimer)
  • Gestion de la base de connaissances (création/suppression)
  • Dialogue libre avec les LLM
  • Gestion de la base de connaissances personnelle
  • Communiquer avec les LLM par le biais d'une base de connaissances personnelle

 

Dans cet article, je vais voir comment réaliser un RAG avancé pour la production. J'ai appris les techniques de base et avancées de la RAG, mais il reste encore beaucoup de choses à faire pour mettre la RAG en production. Je partagerai le travail qui a été fait dans ChatOllama et les préparations qui ont été faites pour amener RAG en production.

 

ChatOllama 笔记 | 实现高级RAG的生产化和基于Redis的文档数据库

ChatOllama | RAG Constructed Runs

 

Dans la plateforme ChatOllama, nous utilisons la technologieLangChain.jspour manipuler le processus RAG. Dans la base de connaissances de ChatOllama, des extracteurs de documents parents sont utilisés en plus du RAG original. Nous allons nous plonger dans son architecture et dans les détails de sa mise en œuvre. Nous espérons que vous trouverez cela instructif.

 

 

Utilitaire de recherche de documents pour les parents

 

Pour que le récupérateur de documents parentaux fonctionne dans un produit réel, nous devons comprendre ses principes fondamentaux et sélectionner les éléments de stockage appropriés à des fins de production.

 

Principes fondamentaux de la recherche de documents par les parents

Des exigences contradictoires sont souvent rencontrées lorsqu'il s'agit de diviser des documents à des fins de recherche :

  • Vous pouvez souhaiter que les documents soient aussi petits que possible afin que leur contenu intégré puisse représenter le plus fidèlement possible leur signification. Si le contenu est trop long, les informations intégrées risquent de perdre leur signification.
  • Les documents doivent également être suffisamment longs pour que chaque paragraphe dispose d'un environnement complet en texte intégral.

 

Propulsé par LangChain.jsParentDocumentRetrieverCet équilibre est obtenu en divisant le document en morceaux. Chaque fois qu'il effectue une recherche, il extrait les morceaux et recherche ensuite le document parent correspondant à chaque morceau, ce qui lui permet d'obtenir un plus grand nombre de documents. En général, le document parent est lié aux morceaux par l'identifiant du document. Nous expliquerons plus tard comment cela fonctionne.

Il convient de noter qu'ici, leparent documentSe réfère à de petits morceaux de documents sources.

 

construire

Jetons un coup d'œil au diagramme d'architecture global du récupérateur de documents parentaux.

Chaque morceau sera traité et transformé en données vectorielles, qui seront ensuite stockées dans la base de données vectorielle. Le processus de récupération de ces morceaux sera le même que dans la configuration originale de RAG.

Le document parent (bloc-1, bloc-2, ...... bloc-m) est divisé en plusieurs parties. Il convient de noter que deux méthodes différentes de segmentation du texte sont utilisées ici : une plus grande pour les documents parents et une plus petite pour les blocs plus petits. Chaque document parent se voit attribuer un identifiant de document, qui est ensuite enregistré comme métadonnée dans le bloc correspondant. Cela garantit que chaque bloc peut trouver son document parent correspondant à l'aide de l'ID du document stocké dans les métadonnées.

Le processus de récupération des documents parents n'est pas le même. Au lieu d'une recherche de similarité, un identifiant de document est fourni pour trouver le document parent correspondant. Dans ce cas, un système de stockage clé-valeur suffit.

 

ChatOllama 笔记 | 实现高级RAG的生产化和基于Redis的文档数据库

Récupérateur de documents pour les parents

 

Espace de stockage

Deux types de données doivent être stockés :

  • Données vectorielles à petite échelle
  • Données sous forme de chaîne du document parent contenant l'ID du document

 

Toutes les principales bases de données vectorielles sont disponibles. Sur `ChatOllama`, j'ai choisi Chroma.

Pour les données des documents parents, j'ai choisi Redis, le système de stockage clé-valeur le plus populaire et le plus évolutif qui soit.

 

Ce qui manque à LangChain.js

LangChain.js propose plusieurs façons d'intégrer le stockage d'octets et de documents :

 

Stockage | 🦜️🔗 Langchain

Le stockage des données sous forme de paires clé-valeur est rapide et efficace, et constitue un outil puissant pour les applications LLM. Le référentiel de base...

js.langchain.com

 

Support pour `IORedis` :

 

IORedis | 🦜️🔗 Langchain

Cet exemple montre comment utiliser l'intégration du référentiel de base RedisByteStore pour configurer le stockage de l'historique des chats.

js.langchain.com

 

La partie manquante du RedisByteStore est la fonctioncollectionMécanismes.

 

Lors du traitement de documents provenant de différentes bases de connaissances, chaque base de connaissances sera traitée dans unecollectionLes collections sont organisées sous forme de collections, et les documents d'une même bibliothèque sont convertis en données vectorielles et stockés dans une base de données vectorielles telle que Chroma dans l'une des bibliothèques de l'Union européenne.collectionDans l'assemblée.

 

Supposons que nous voulions supprimer une base de connaissances. Nous pouvons certainement supprimer une collection `collection` dans la base de données Chroma. Mais comment nettoyer le magasin de documents en fonction des dimensions de la base de connaissances ? Comme des composants tels que RedisByteStore ne supportent pas la fonctionnalité `collection`, j'ai dû l'implémenter moi-même.

 

ChatOllama RedisDocStore

Dans Redis, il n'y a pas de fonctionnalité intégrée appelée `collection`. Les développeurs implémentent généralement la fonctionnalité `collection` en utilisant des clés préfixes. La figure suivante montre comment les préfixes peuvent être utilisés pour identifier différentes collections :

 

ChatOllama 笔记 | 实现高级RAG的生产化和基于Redis的文档数据库

Représentations de clés Redis avec préfixes

 

Voyons maintenant comment il a été mis en œuvre pourRedisde la fonction de stockage des documents pour l'arrière-plan.

 

Message clé :

  • Chaque `RedisDocstore` est initialisé avec un paramètre `namespace`. (Le nommage des espaces de noms et des collections peut ne pas être standardisé pour le moment).
  • Les clés sont d'abord traitées dans l'espace de noms pour les opérations get et set.

 

import { Document } from "@langchain/core/documents".
import { BaseStoreInterface } from "@langchain/core/stores" ;
import { Redis } from "ioredis".
import { createRedisClient } from "@/server/store/redis" ;

export class RedisDocstore implements BaseStoreInterface
{
_namespace : string ;
Client : 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(keys).
});

stream.on('error', (err) => {
reject(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) {
lancer une nouvelle erreur (`ID ${recherche} introuvable.`) ;
} else {
const document = this.deserializeDocument(result) ;
retourner le document ;
}
}

/**
* : : Ajoute de nouveaux documents au magasin.
* @param texts Un objet dont les clés sont les ID des documents et les valeurs les documents eux-mêmes.
* @returns Void
*/
async add(texts : Record) : Promise {
for (const [key, value] of Object.entries(texts)) {
console.log(`Adding ${key} to the store : ${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(`Tried to add ids that already exist : ${overlapping}`) ;
}

for (const [key, value] 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) ;
retourner le document ;
}));
}

async mset(keyValuePairs : [string, Document][]) : Promise {
attend Promise.all(
keyValuePairs.map(([key, value]) => this.add({ [key] : value }))
);
}

async mdelete(_keys : string[]) : Promise {
lancer une nouvelle erreur ("Non implémenté.") ;
}

// eslint-disable-next-line require-yield
async *yieldKeys(_prefix? : string) : AsyncGenerator {
lancer une nouvelle erreur ("Non implémenté") ;
}

async deleteAll() : Promise {
return new Promise((resolve, reject) => {
laisser curseur = '0';

const scanCallback = (err, result) => {
if (err) {
reject(err) ;
retour ;
}

const [nextCursor, keys] = result ;

// Supprimer les touches correspondant au préfixe
keys.forEach((key) => {
this._client.del(key) ;
});

// Si le curseur est à '0', nous avons parcouru toutes les touches.
if (nextCursor === '0') {
resolve().
} else {
// Continuer balayage avec le curseur suivant
this._client.scan(nextCursor, 'MATCH', `${this._namespace}:*`, scanCallback) ;
}
};

// Lancer l'opération initiale de balayage (SCAN)
this._client.scan(cursor, 'MATCH', `${this._namespace}:*`, scanCallback) ;
});
}
}

 

Vous pouvez utiliser ce composant en toute transparence avec ParentDocumentRetriever :

retriever = new ParentDocumentRetriever({
vectorstore : chromaClient.
docstore : new RedisDocstore(collectionName),
parentSplitter : new RecursiveCharacterTextSplitter({
chunkOverlap : 200,
chunkSize : 1000,
}),
childSplitter : new RecursiveCharacterTextSplitter({
chunkOverlap : 50,
chunkSize : 200,
}),
enfantK : 20.
parentK : 5.
});

Nous disposons désormais d'une solution de stockage évolutive pour le RAG avancé, Parent Document Retriever, ainsi que pour `Chroma` et `Redis`.

© déclaration de droits d'auteur

Articles connexes

Pas de commentaires

Vous devez être connecté pour participer aux commentaires !
S'inscrire maintenant
aucun
Pas de commentaires...