ChatOllama Ele é um chatbot de código aberto baseado em LLMs. Para obter uma descrição detalhada do ChatOllama, clique no link abaixo.
ChatOllama | Aplicativo RAG local 100% baseado em Ollama
ChatOllama O objetivo inicial era fornecer aos usuários um aplicativo RAG nativo do 100%.
À medida que cresceu, mais e mais usuários apresentaram requisitos valiosos. Agora, o `ChatOllama` é compatível com vários modelos de idiomas, incluindo:
Com o ChatOllama, os usuários podem:
- Gerenciar modelos Ollama (extrair/excluir)
- Gerenciar a base de conhecimento (criar/excluir)
- Diálogo livre com LLMs
- Gerenciar a base de conhecimento pessoal
- Comunicação com LLMs por meio de uma base de conhecimento pessoal
Neste artigo, verei como obter o RAG avançado para produção. Aprendi as técnicas básicas e avançadas do RAG, mas ainda há muitas coisas que precisam ser resolvidas para colocar o RAG em produção. Compartilharei o trabalho que foi feito no ChatOllama e os preparativos que foram feitos para colocar o RAG em produção.
Na plataforma ChatOllama, utilizamos oLangChain.jspara manipular o processo RAG. Na base de conhecimento do ChatOllama, são usados recuperadores de documentos principais além do RAG original. Vamos nos aprofundar em sua arquitetura e nos detalhes de implementação. Esperamos que você ache isso esclarecedor.
Utilitário de recuperação de documentos dos pais
Para que o recuperador de documentos originais funcione em um produto real, precisamos entender seus princípios fundamentais e selecionar os elementos de armazenamento corretos para fins de produção.
Princípios básicos dos recuperadores de documentos dos pais
Requisitos conflitantes são frequentemente encontrados ao dividir documentos para necessidades de recuperação:
- Talvez você queira que os documentos sejam tão pequenos quanto possível para que o conteúdo incorporado possa representar o significado com mais precisão. Se o conteúdo for muito longo, as informações incorporadas poderão perder o significado.
- Você também quer documentos que sejam longos o suficiente para garantir que cada parágrafo tenha um ambiente de texto completo.
Desenvolvido por LangChain.jsParentDocumentRetriever
Esse equilíbrio é obtido dividindo o documento em partes. A cada recuperação, ele extrai os blocos e, em seguida, procura o documento pai correspondente a cada bloco, retornando um intervalo maior de documentos. Em geral, o documento pai é vinculado aos blocos pelo ID do documento. Explicaremos mais sobre como isso funciona mais tarde.
Observe que aqui odocumento pai
Refere-se a pequenas partes de documentos de origem.
construir
Vamos dar uma olhada no diagrama geral da arquitetura do recuperador de documentos pai.
Cada bloco será processado e transformado em dados vetoriais, que serão armazenados no banco de dados de vetores. O processo de recuperação desses blocos será como foi feito na configuração original do RAG.
O documento pai (block-1, block-2, ...... block-m) é dividido em partes menores. É importante observar que dois métodos diferentes de segmentação de texto são usados aqui: um maior para documentos pai e um menor para blocos menores. Cada documento pai recebe um ID de documento, e esse ID é registrado como metadados em seu bloco correspondente. Isso garante que cada bloco possa encontrar o documento pai correspondente usando o ID do documento armazenado nos metadados.
O processo de recuperação de documentos pai não é o mesmo. Em vez de uma pesquisa de similaridade, é fornecido um ID de documento para encontrar o documento pai correspondente. Nesse caso, um sistema de armazenamento de valor-chave é suficiente para o trabalho.
Seção de armazenamento
Há dois tipos de dados que precisam ser armazenados:
- Dados vetoriais em pequena escala
- Dados de cadeia de caracteres do documento pai contendo seu ID de documento
Todos os principais bancos de dados vetoriais estão disponíveis. Do `ChatOllama`, escolhi o Chroma.
Para os dados do documento principal, escolhi o Redis, o sistema de armazenamento de valores-chave mais popular e altamente escalável disponível.
O que está faltando no LangChain.js
O LangChain.js oferece várias maneiras de integrar o armazenamento de bytes e documentos:
Suporte para `IORedis`:
A parte que falta no RedisByteStore é ocoleção
Mecanismos.
Ao processar documentos de diferentes bases de conhecimento, cada base de conhecimento será processada em umcoleção
As coleções são organizadas na forma de coleções, e os documentos da mesma biblioteca são convertidos em dados vetoriais e armazenados em um banco de dados vetorial, como o Chroma, em um doscoleção
Na assembleia.
Suponha que queiramos excluir uma base de conhecimento. Certamente podemos excluir uma coleção `collection` no banco de dados Chroma. Mas como limpar o armazenamento de documentos de acordo com as dimensões da base de conhecimento? Como componentes como o RedisByteStore não oferecem suporte à funcionalidade `collection`, tive que implementá-la eu mesmo.
ChatOllama RedisDocStore
No Redis, não existe um recurso embutido chamado `collection`. Os desenvolvedores geralmente implementam a funcionalidade `collection` usando chaves de prefixo. A figura a seguir mostra como os prefixos podem ser usados para identificar diferentes coleções:
Agora vamos ver como isso foi implementado paraRedisda função de armazenamento de documentos para o plano de fundo.
Mensagem principal:
- Cada `RedisDocstore` é inicializado com um parâmetro `namespace`. (A nomeação de namespaces e coleções pode não estar padronizada no momento).
- As chaves são processadas primeiro no `namespace` para as operações get e set.
importar { Document } de "@langchain/core/documents".
importar { BaseStoreInterface } de "@langchain/core/stores" ;
importar { Redis } de "ioredis".
importar { createRedisClient } de "@/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));
Se (!result) {
lançar novo Error(`ID ${search} não encontrado.`);
} else {
const document = this.deserializeDocument(result);
devolver documento;
}
}/**
* :: Adiciona novos documentos ao repositório.
* @param texts Um objeto em que as chaves são IDs de documentos e os valores são os próprios documentos.
* @retorna Void
*/
async add(texts: Record): Promise {
for (const [key, value] of Object.entries(texts)) {
console.log(`Adicionando ${key} ao armazenamento: ${this.serializeDocument(value)}`);
}const keys = [... .await this.getKeys()];
const overlapping = Object.keys(texts).filter((x) => keys.includes(x));Se (overlapping.length > 0) {
lançar novo Error(`Tentou adicionar ids que já existem: ${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);
devolver documento;
}));
}async mset(keyValuePairs: [string, Document][]): Promise {
aguardar Promise.all(
keyValuePairs.map(([key, value]) => this.add({ [key]: value }))
);
}async mdelete(_keys: string[]): Promise {
lançar novo erro ("Não implementado.");
}// eslint-disable-next-line require-yield
async *yieldKeys(_prefix?: string): AsyncGenerator {
lançar um novo erro ("Não implementado");
}async deleteAll(): Promise {
return new Promise((resolve, reject) => {
deixar cursor = '0';const scanCallback = (err, result) => {
se (err) {
reject(err);
retorno;
}const [nextCursor, keys] = result;
// Excluir chaves que correspondam ao prefixo
keys.forEach((key) => {
this._client.del(key);
});// Se o cursor for '0', teremos iterado por todas as teclas
Se (nextCursor === '0') {
resolve().
} else {
// Continuar a varredura com o próximo cursor
this._client.scan(nextCursor, 'MATCH', `${this._namespace}:*`, scanCallback);
}
};// Iniciar a operação SCAN inicial
this._client.scan(cursor, 'MATCH', `${this._namespace}:*`, scanCallback);
});
}
}
Você pode usar esse componente sem problemas com o ParentDocumentRetriever:
retriever = new ParentDocumentRetriever({
vectorstore: chromaClient.
docstore: new RedisDocstore(collectionName),
parentSplitter: new RecursiveCharacterTextSplitter({
chunkOverlap: 200,
chunkSize: 1000,
}),
childSplitter: new RecursiveCharacterTextSplitter({
chunkOverlap: 50,
chunkSize: 200,
}),
criançaK: 20.
parentK: 5.
});
Agora temos uma solução de armazenamento dimensionável para o RAG avançado, o Parent Document Retriever, juntamente com o `Chroma` e o `Redis`.