チャットオラマ LLMをベースにしたオープンソースのチャットボットです。ChatOllamaの詳細については、以下のリンクをクリックしてください。
ChatOllama|Ollamaベースの100%ローカルRAGアプリケーション
チャットオラマ 当初の目標は、100%のネイティブRAGアプリケーションをユーザーに提供することでした。
その成長とともに、より多くのユーザーから貴重な要望が寄せられるようになりました。現在、`ChatOllama`は以下のような複数の言語モデルをサポートしています:
ChatOllamaで、ユーザーは次のことができます:
- Ollamaモデルの管理(プル/削除)
- ナレッジベースの管理(作成/削除)
- LLMとの自由な対話
- 個人の知識ベースの管理
- 個人的な知識ベースを通じてLLMとコミュニケーションを図る
今回は、本番用の高度なRAGを実現する方法を見ていこうと思う。RAGの基本的なテクニックと高度なテクニックを学びましたが、RAGをプロダクションに導入するためには、まだまだ気をつけなければならないことがたくさんあります。ChatOllamaで行われてきた作業と、RAGを本番に持ち込むために行われてきた準備を共有します。
ChatOllamaプラットフォームではLangChain.jsを使ってRAGプロセスを操作します。ChatOllamaの知識ベースでは、オリジナルのRAGを超えた親ドキュメント検索が使用されています。そのアーキテクチャと実装の詳細に深く潜ってみましょう。私たちは、あなたがそれを啓発見つけることを願っています。
親ドキュメント検索ユーティリティ
親ドキュメント・リトリーバーを実際の製品で機能させるためには、その核となる原理を理解し、生産目的に適したストレージ・エレメントを選択する必要がある。
親ドキュメント検索者の基本原則
検索が必要な文書を分割する場合、しばしば相反する要件が発生する:
- 埋め込まれたコンテンツがその意味を最も正確に表すことができるように、ドキュメントはできるだけ小さくすることを望むかもしれません。コンテンツが長すぎると、埋め込まれた情報が意味を失ってしまうかもしれません。
- また、各段落に完全なフルテキスト環境が確保できるよう、十分な長さの文書も必要だ。
Powered by LangChain.js親ドキュメントレトリバー
このバランスは、文書をチャンクに分割することで達成される。検索のたびに、チャンクを抽出し、各チャンクに対応する親ドキュメントを検索し、より広い範囲のドキュメントを返します。一般的に、親文書はdoc idによってチャンクにリンクされています。この仕組みについては後で詳しく説明します。
ここでは親ドキュメント
ソース文書の小片を指す。
ビルド
親ドキュメント・リトリーバの全体的なアーキテクチャ図を見てみよう。
各チャンクは処理され、ベクトルデータに変換され、ベクトルデータベースに格納される。 これらのチャンクを取り出すプロセスは、オリジナルのRAGセットアップで行われたものと同じである。
親文書(block-1, block-2, ....block-m) が小さな部分に分割される。 ここでは2つの異なるテキスト分割方法が使用されている。親文書には大きな分割方法、小さなブロックには小さな分割方法である。各親文書には文書IDが与えられ、このIDは対応するチャンクのメタデータとして記録される。 これにより、各チャンクは、メタデータに格納された文書IDを使って、対応する親文書を見つけることができる。
親文書を検索するプロセスはこれと同じではない。類似性検索の代わりに、文書IDが提供され、対応する親文書を検索する。この場合、キー・バリュー・ストレージ・システムで十分である。
保管セクション
保存すべきデータには2種類ある:
- 小規模ベクトルデータ
- 文書IDを含む親文書の文字列データ
主要なベクターデータベースはすべて利用できる。ChatOllama`のうち、私はChromaを選んだ。
親ドキュメント・データには、最もポピュラーでスケーラビリティの高いキー・バリュー・ストレージ・システムであるRedisを選んだ。
LangChain.jsに欠けているもの
LangChain.jsは、バイトストレージとドキュメントストレージを統合する方法を数多く提供している:
データをキーと値のペアとして格納することは、高速かつ効率的であり、LLMアプリケーションの強力なツールである。ベースリポジトリ...
IORedis`のサポート:
この例では、RedisByteStore ベースリポジトリの統合を使用して、チャット履歴用のストレージを設定する方法を示します。
RedisByteStoreに欠けている部分はコレクション
メカニズム
異なる知識ベースからのドキュメントを処理する場合、各知識ベースはコレクション
コレクションはコレクションの形で整理され、同じライブラリ内のドキュメントはベクターデータに変換され、Chromaのようなベクターデータベースのいずれかに保存される。コレクション
集会で
知識ベースを削除したいとします。確かにChromaデータベースの`コレクション`コレクションを削除することはできます。しかし、ナレッジベースのディメンションに従ってドキュメントストアをクリーンアップするにはどうすればいいでしょうか?RedisByteStoreのようなコンポーネントは`コレクション`機能をサポートしていないので、自分で実装する必要がありました。
チャットオラマRedisDocStore
Redis には `collection` と呼ばれる組み込みの機能はない。開発者は通常、プレフィックスキーを使用して `collection` 機能を実装します。次の図は、異なるコレクションを識別するためにプレフィックスを使用する方法を示しています:
では、どのように実装されたのかを見てみよう。レディス背景の文書保存機能の
キーメッセージ
- 各 `RedisDocstore` は `namespace` パラメータで初期化される。(名前空間とコレクションの命名は現時点では標準化されていないかもしれない)。
- キーは、get操作とset操作の両方で、最初に`名前空間`が処理される。
import { Document } from "@langchain/core/documents" ;
import { BaseStoreInterface } from "@langchain/core/stores" ;
import { Redis } from "ioredis".
インポート { createRedisClient } from "@/server/store/redis" ;export class RedisDocstore implements BaseStoreInterface.
{
名前空間:文字列;
_client: Redis.コンストラクタ(namespace: string) {.
this._namespace = namespace;
this._client = createRedisClient();
}serializeDocument(doc: Document): string {.
JSON.stringify(doc)を返す;
}deserializeDocument(jsonString: string): ドキュメント {.
const obj = JSON.parse(jsonString);
return new Document(obj);
}getNamespacedKey(key: string): string {.
${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));
もし(!result) {。
throw new Error(`ID ${search} not found.`);
} else {
const document = this.deserializeDocument(result);
ドキュメントを返す;
}
}/**
* :: ストアに新しい文書を追加する。
* param text ドキュメントIDをキーとし、ドキュメントそのものを値とするオブジェクト。
* Void を返す
*/
async add(texts: Record): Promise {.
for (Object.entries(texts)のconst [key, value]) {。
console.log(`ストアに${key}を追加:${this.serializeDocument(value)}`);
}const keys = [....awaitこの.getKeys()];
const overlapping = Object.keys(texts).filter((x) => keys.includes(x));if (overlapping.length > 0) {.
throw new Error(`Trying to add id that already exist: ${overlapping}`);
}for (Object.entries(texts)のconst [key, value]) {。
this.addText(key, this.serialiseDocument(value)); this.
}
}async mget(keys: string[]): Promise {.
return Promise.all(keys.map((key) => {)
const document = this.search(key);
ドキュメントを返す;
}));
}async mset(keyValuePairs: [string, Document][]): Promise {.
await Promise.all()
keyValuePairs.map(([key, value]) => this.add({ [key]: value }))
);
}async mdelete(_keys: string[]): Promise {.
throw new Error("実装されていません。");
}// eslint-next-lineを無効にする require-yield
async *yieldKeys(_prefix?: string): AsyncGenerator {.
throw new Error("実装されていません");
}非同期deleteAll(): Promise { {.
return new Promise((resolve, reject) => {.
レット カーソル = '0';const scanCallback = (err, result) => {.
もし (err) {
reject(err);
を返す;
}const [nextCursor, keys] = result;
// プレフィックスに一致するキーを削除する
キー.forEach((キー) => {)
this._client.del(key);
});// カーソルが'0'の場合、すべてのキーを反復処理したことになる。
if (nextCursor === '0') {.
resolve()。
} else {
// 次のカーソルでスキャンを続ける
this._client.scan(nextCursor, 'MATCH', `${this._namespace}:*`, scanCallback);
}
};// 最初のSCAN操作を開始する
this._client.scan(cursor, 'MATCH', `${this._namespace}:*`, scanCallback);
});
}
}
このコンポーネントは ParentDocumentRetriever とシームレスに使用できます:
retriever = 新しいParentDocumentRetriever({)
vectorstore: chromaClient.
docstore: 新しいRedisDocstore(collectionName)、
parentSplitter: new RecursiveCharacterTextSplitter({)
chunkOverlap: 200、
チャンクサイズ:1000
}),
childSplitter: new RecursiveCharacterTextSplitter({)
チャンクオーバーラップ:50
チャンクサイズ:200
}),
childK:20歳。
親K:5.
});
私たちは現在、`Chroma`と`Redis`とともに、高度なRAG、Parent Document Retrieverのためのスケーラブルなストレージソリューションを手に入れました。