‘製品およびエンジニアリングリーダーのための実践的なGenAI’

「製品とエンジニアリングリーダーのための実践的なGenAIガイド」

LLMベースの製品の内部をのぞいて、より良い製品の意思決定をしましょう

プロトタイプで作業する機械学習を搭載したアプリケーションのプロダクトオーナーに基づいてビングイメージクリエータが生成したイメージ

イントロダクション

普段運転する人にとって、車のフードは目に見えない部分で、中には綿でいっぱいでも気にしません。しかし、より良い車を作る責任を持つデザインチェーンのどこかにいる場合、異なるパーツがどのように連動して動作しているかを知ることは、より良い車を作るのに役立ちます。

同様に、製品オーナーやビジネスリーダー、または新しいLarge Language Model (LLM)パワード製品を作成する責任を持つエンジニア、または既存の製品にLLMや生成AIを導入する責任を持つ人物として、LLMパワード製品に必要な構成要素を理解することは、以下のような技術に関する戦略的な質問や戦術的な質問に対処するために役立ちます。

  1. うちのケースはLLMパワードの解決策に適していますか?おそらく従来の分析、教師あり機械学習、または他の手法の方が適していますか?
  2. LLMが適切な方法である場合、既製品(例:ChatGPT Enterprise)で現在または近い将来、うちのケースを対応できますか?従来の構築 vs 購入の判断です。
  3. LLMパワード製品の異なる構成要素は何ですか?これらの中で商品化したものと、構築やテストにさらに時間がかかる可能性のあるものは何ですか?
  4. ソリューションのパフォーマンスをどのように測定しますか?品質を向上させるための操作要素は何ですか?
  5. ケースに対してデータの品質は受け入れ可能ですか?データは正しく整理され、LLMに関連データが適切に渡されていますか?
  6. LLMの応答が常に事実と一致することに自信を持てますか。つまり、製品の生成応答時にわずかながら幻覚を見せることはありますか?

これらの質問は後で記事で説明されますが、実際のところはLLMパワードソリューションについて一層直感的な理解を得ることで、これらの質問に自分自身で答えるか、さらなる研究を行う上でより良い立場になることを目指しています。

前の記事では、LLMパワード製品の構築に関連する基礎的なコンセプトについて探求しました。しかし、ブログを読んだり、動画を見るだけでは運転を学ぶことはできません。実際にハンドルを握る必要があります。まあ、私たちが生きている時代のおかげで、数百万ドル費やして作成されたものを無料で利用できるツールが手に入りました。たった1時間で自分自身のLLMソリューションを作ることができます!それで、この記事ではちょうどそれを行ってみましょう。運転を学ぶよりもずっと簡単です 😝。

ウェブサイトと「チャット」するチャットボットを作成しましょう

目標:提供されたウェブサイトの情報に基づいて質問に答えるチャットボットを作成し、今日の人気のあるGenAIソリューションの構成要素に対する理解を深めましょう

ナレッジリポジトリ内の情報に基づいて質問に答えるチャットボットを作成します。このRetrieval Augmented Generation(RAG)と呼ばれるソリューションパターンは、企業での定番のソリューションパターンとなっています。RAGの人気の理由の1つは、LLMの独自の知識だけに頼るのではなく、外部情報を自動化でLLMにもたらすことができることです。実際の実装では、外部情報は組織のプロプライエタリな情報を保持したナレッジリポジトリから得られることがあります。これにより、製品はビジネスや製品、業務プロセスなどに関する質問に答えることができます。RAGはまた、LLMの「幻視」を減らします。つまり、生成される応答がLLMに提供された情報に基づいています。最近のトークによると、

「RAGは企業がLLMを使用するデフォルトの方法になるでしょう」- Dr. Waleed Kadous、AnyScaleのチーフサイエンティスト

ハンズオンの演習では、ユーザーがウェブサイトを入力し、私たちのソリューションがその知識リポジトリに「読み込む」ことを許可します。その後、ソリューションはウェブサイトの情報に基づいて質問に答えることができます。ウェブサイトはプレースホルダーです-現実では、PDF、Excel、別の製品や内部システムなど、任意のデータソースからテキストを消費するように調整することができます。このアプローチは画像などの他のメディアにも適用されますが、いくつかの異なるLLMが必要です。今のところ、私たちはウェブサイトのテキストに焦点を当てます。

この例では、このブログ用に作成されたサンプルの書籍リストのウェブページを使用します: Books I’d Pick Up — If There Were More Hours in the Day! 別のウェブサイトを使用しても構いません。

以下は私たちの結果のイメージです:

ウェブサイトの情報に基づいて質問にインテリジェントに答えるLLMパワードのチャットボット。 (著者によるイメージ)

以下は私たちの解決策の構築に取り組む手順です:

0. セットアップ – Google Colaboratory&OpenAI APIキーの取得1. 知識リポジトリの作成2. 質問に関連するコンテキストの検索3. LLMを使用して回答を生成する4.「チャット」機能の追加(オプション)5. 簡単なプリコーデッドUIの追加(オプション)

0.1. セットアップ – Google Colaboratory&OpenAI APIキーの取得

LLMソリューションを構築するには、コードを書いて実行する場所と、質問に対して応答を生成するLLMが必要です。コードの環境にはGoogle Colabを使用し、ChatGPTの背後にあるモデルをLLMとして使用します。

まず、Google Colabのセットアップから始めましょう。これはGoogleの無料サービスで、Pythonコードを簡単に読みやすい形式で実行できます-コンピュータに何かをインストールする必要はありません。後でColabノートブックを簡単に見つけるために、Google DriveにColabを追加すると便利です。

それを行うには、Google Drive(ブラウザを使用して)に移動します。>新規作成 >その他 >その他のアプリを接続 >Google Marketplaceで「Colaboratory」と検索> インストールします。

Colabを使用するには、新規作成 > その他 > Google Colaboratoryを選択します。これにより、Google Driveに新しいノートブックが作成され、後でそれに戻ることができます。

Google DriveでアクセスできるGoogle Colaboratory。 (著者によるイメージ)

次に、LLMにアクセスしましょう。いくつかのオープンソースおよびプロプライエタリなオプションがあります。オープンソースのLLMは無料ですが、強力なLLMには一般に強力なGPUが必要で、入力を処理し、応答を生成するためのGPUの通常の操作コストがかかります。この例では、代わりにChatGPTが使用するLLMを使用するためにOpenAIのサービスを使用します。そのためにはAPIキーが必要で、これはOpenAIにアクセスしようとしているユーザーを認識するためのユーザー名とパスワードのようなものです。この執筆時点では、OpenAIは新しいユーザーに$5のクレジットを提供しており、これはこのハンズオンチュートリアルに十分です。以下はAPIキーを取得する手順です。

OpenAIのプラットフォームのウェブサイトに移動します> 開始 > メールとパスワードでサインアップするか、GoogleアカウントまたはMicrosoftアカウントを使用します。確認するために電話番号も必要かもしれません。

ログインしたら、右上隅のプロファイルアイコンをクリックして、「APIキーを表示」>「新しいシークレットキーの作成」を選択します。キーは以下のようなものになります(情報提供のための偽のキーです)。後で使用するために保存してください。

sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64

さて、解決策の構築に移ります。

0.2. 解決策の構築のためのノートブックの準備

解決策を容易にするために、Colab環境にいくつかのパッケージをインストールする必要があります。以下のコードをColabのテキストボックス(「セル」と呼ばれるもの)に入力し、「Shift + Enter(もしくはReturn)」を押してください。または、セルの左側にある「再生」ボタンをクリックするか、ノートブックの上部にある「実行」メニューを使用することもできます。以降のコードを実行するためには、新しいコードセルを挿入するためにメニューを使用する必要があるかもしれません。

# OpenAIとtiktokenのパッケージをインストールして埋め込みモデルおよびチャット補完モデルを使用する!pip install openai tiktoken# langchainパッケージをインストールしてドキュメントの処理からLLMを使用した「チャット」までのほとんどの機能をサポートする!pip install langchain# ソリューションの質問に答えるために必要な「知識」を保存するために、インメモリベクトルデータベースパッケージであるChromaDBをインストールする!pip install chromadb# webページのコンテンツをより読みやすい形式に変換するためのHTML to textパッケージをインストールする!pip install html2text# ソリューションのための基本的なUIを作成するためにgradioをインストールする!pip install gradio

次に、インストールしたパッケージからコードを取り込んで、コード内でパッケージを使用できるようにします。新しいコードセルを使用して、「Shift + Enter」を再度押してください。その後の各コードブロックについても同様に続けてください。

# 異なる機能を有効にするために必要なパッケージをインポートするfrom langchain.document_loaders import AsyncHtmlLoader # ウェブサイトのコンテンツをドキュメントに読み込むためfrom langchain.text_splitter import MarkdownHeaderTextSplitter # ドキュメントを見出しによってより小さなチャンクに分割するため from langchain.document_transformers import Html2TextTransformer # HTMLをMarkdownテキストに変換するためfrom langchain.chat_models import ChatOpenAI # OpenAIのLLMを使用するためfrom langchain.prompts import PromptTemplate # 指示/プロンプトを作成するためfrom langchain.chains import RetrievalQA, ConversationalRetrievalChain # RAGのためfrom langchain.memory import ConversationTokenBufferMemory # チャット履歴を管理するためfrom langchain.embeddings.openai import OpenAIEmbeddings # テキストを数値表現に変換するためfrom langchain.vectorstores import Chroma # ベクトルデータベースとの対話的な操作に使用するimport pandas as pd, gradio as gr # データをテーブルとして表示し、UIを構築するためimport chromadb, json, textwrap # ベクトルデータベース、JSONをテキストに変換、整形して表示するためのユーティリティ関数from chromadb.utils import embedding_functions # Chromaが必要とする埋め込み関数のセットアップ

最後に、OpenAIのAPIキーを変数に追加します。このキーはパスワードのようなものであることに注意してください。共有しないでください。また、Colabのノートブックを共有する場合は、APIキーを削除してください。

# OpenAIのAPIキーを変数に追加する# キーをこのように変数に保存するのは悪い習慣です。環境変数にロードし、そこからロードする必要がありますが、この場合はクイックデモには問題ありませんOPENAI_API_KEY='sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # 偽のキー - ここにご自分の本物のキーを使用してください

さて、解決策の構築を開始する準備が整いました。次の手順のハイレベルな概要を以下に示します:

RAGソリューションを構築するための基本手順(著者によるイメージ)

コーディングの際には、このような解決策を構築するための人気のあるフレームワークであるLangChainを使用します。LangChainには、データソースへの接続からLLMへの情報の送受信までの各ステップを容易にするためのパッケージが用意されています。LlamaIndexは、LLMを活用したアプリケーションの構築を簡素化する別のオプションです。LangChain(またはLlamaIndex)の使用は厳密に必須ではありませんが、場合によっては高レベルの抽象化により、チームが裏側で何が起こっているかを見落とす可能性があるため、LangChainを使用しますが、頻繁に内部を確認します。

イノベーションのペースが非常に速いため、このコードで使用されるパッケージが更新され、一部の更新がコードが正常に動作しなくなる可能性があります。私はこのコードを最新の状態に保つつもりはありません。それでも、この記事はデモンストレーションとしての目的で作成され、コードは参照または出発点として使用でき、必要に応じて適応できるものとして考えてください。

1. 知識リポジトリの作成

1.1. ドキュメントの識別と読み込み本のリストにアクセスし、その内容をColab環境に読み込みます。コンテンツは元々HTMLとしてロードされますが、それをより人間が読みやすい形式に変換します。

url = "https://ninadsohoni.github.io/booklist/" # ここには別のウェブサイトを自由に使用しても構いませんが、一部のコードは内容を正しく表示するために編集する必要があることに注意してください# URLからHTMLをロードし、より読みやすいテキスト形式に変換しますdocs = Html2TextTransformer().transform_documents(AsyncHtmlLoader(url).load())# さあ、もう一度簡単に見てみましょうprint("\n\n含まれているメタデータ:\n", textwrap.fill(json.dumps(docs[0].metadata), width=100), "\n\n")print("ページのコンテンツをロードしました:")print('...', textwrap.fill(docs[0].page_content[2500:3000], width=100, replace_whitespace=False), '...')

上記のコードをGoogle Colabで実行すると、以下のような結果が生成されます:

上記のコードの実行結果。ウェブサイトのコンテンツがColab環境にロードされます。 (著者による画像)

1.2. ドキュメントをより小さな抜粋に分割する知識リポジトリ(基本的には選択したデータベース)にブログの情報をロードする前にもう一つステップがあります。テキストをそのままデータベースにロードしてはいけません。まず、より小さなチャンクに分割する必要があります。その理由は次の通りです:

  1. テキストが長すぎると、「コンテキストサイズ」として知られるテキスト長の閾値を超えているため、LLMに送信することができません。
  2. 長いテキストには、広範かつゆるやかに関連する情報が含まれている場合があります。LLMが関連する部分を選び出すことに頼ることになるでしょうが、常に期待どおりに機能するわけではありません。より小さなチャンクでは、検索メカニズムを使用してLLMに送信するのに関連する情報だけを特定することができます(後で見るように)。
  3. LLMはテキストの始まりと終わりにより強い注意を払う傾向がありますので、長いチャンクは後半のコンテンツに十分な注意を払わない可能性があります(「中途で失われる」として知られています)。

各使用ケースに適したチャンクサイズは、コンテンツの特性、使用されるLLM、その他の要素によって異なります。解決策を最終化する前に、異なるチャンクサイズで実験し、応答品質を評価することが重要です。このデモンストレーションでは、リストの各ブックレビューが1つのチャンクになるようにコンテキストに意識した分割を使用しましょう。

# 現在、ウェブサイトのすべてのコンテンツをより小さなチャンクに分割します# 各ブックレビューごとに 1 つのチャンクが作成されます# ここでは使用される LangChain splitter は見出しからメタデータのセットを作成し、各チャンクのテキストと関連付けられますheaders_to_split_on = [ ("#", "見出し 1"), ("##", "見出し 2"),    ("###", "見出し 3"), ("####", "見出し 4"), ("#####", "見出し 5") ]splitter = MarkdownHeaderTextSplitter(headers_to_split_on = headers_to_split_on)chunks = splitter.split_text(docs[0].page_content)print(f"元のドキュメントから生成された{len(chunks)}個の小さなチャンク")# 1 つのチャンクを見てみましょうprint("\nサンプルのチャンクを見てみます:")print("含まれているメタデータ:\n", textwrap.fill(json.dumps(chunks[5].metadata), width=100), "\n\n")print("ページのコンテンツをロードしました:")print(textwrap.fill(chunks[5].page_content[:500], width=100, drop_whitespace=False), '...')
元のコンテンツを分割した結果の1つの文書チャンク (著者による画像)

注意してください、これまでに作成されたチャンクがまだ望ましいよりも長い場合、他のテキスト分割アルゴリズムを使用してさらに分割することができます。これらのアルゴリズムはLangChainまたはLlamaIndexを介して簡単に利用できます。たとえば、各書籍のレビューは必要に応じて段落に分割されることができます。

1.3. ナレッジリポジトリへの抜粋のロードテキストのチャンクは、ナレッジリポジトリにロードする準備ができています。これらはまず埋め込みモデルを介してテキストを意味を捉えた一連の数値に変換されます。その後、実際のテキストと数値表現(埋め込み)がベクトルデータベースにロードされます。埋め込みもLLMによって生成されますが、チャットLLMとは異なる種類です。埋め込みについてさらに詳しく読みたい場合は、前の記事を参照してください。

私たちは情報を保存するためにベクトルデータベースを使用します。これが私たちのナレッジリポジトリです。ベクトルデータベースは埋め込みの類似性による検索を可能にするために特別に作られたものです。データベースから何かを検索する場合、検索語句はまず埋め込みモデルを通して数値表現に変換され、その後問い合わせの埋め込みがデータベース内のすべての埋め込みと比較されます。質問の埋め込みに最も近いレコード(リスト上の各書籍に関するテキストチャンク)がしきい値をクリアする限り、検索結果として返されます。

# 各チャンクに対して(およびその後の質問に対して)埋め込みを取得します。OpenAIの埋め込みモデルを使用します。
openai_embedding_func = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)

# ベクトルDBを初期化し、コレクションを作成します。
persistent_chroma_client = chromadb.PersistentClient()
collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func)
cur_max_id = collection.count() # 既存のデータを上書きしないようにするため

# ベクトルDBのコレクションにデータを追加します。
collection.add(
    ids=[str(t) for t in range(cur_max_id+1, cur_max_id+len(chunks)+1)],
    documents=[t.page_content for t in chunks],
    metadatas=[None if len(t.metadata) == 0 else t.metadata for t in chunks]
)

print(f"{collection.count()} documents in vector DB") # ベクトルDBには25のドキュメントがあります

# オプション:データを少し見やすく表示するためのスクラップヘルパー関数を作成します。
def render_vectorDB_content(chromadb_collection):
    vectordb_data = pd.DataFrame(chromadb_collection.get(include=["embeddings", "metadatas", "documents"]))
    return pd.DataFrame({
        'IDs': [str(t) if len(str(t)) <= 10 else str(t)[:10] + '...'for t in vectordb_data.ids],
        'Embeddings': [str(t)[:27] + '...' for t in vectordb_data.embeddings],
        'Documents': [str(t) if len(str(t)) <= 300 else str(t)[:300] + '...' for t in vectordb_data.documents],
        'Metadatas': ['' if not t else json.dumps(t) if len(json.dumps(t))  <= 90 else '...' + json.dumps(t)[-90:] for t in vectordb_data.metadatas]
    })

# ヘルパー関数を使用してベクトルDB内のデータを確認しましょう。最初の4つのチャンクを見てみます。
render_vectorDB_content(collection)[:4]
A view of the first few text chunks loaded to the vector DB along with numerical representations (i.e., embeddings). (Image by the author)

2. 質問に関連するコンテキストを検索する

最終的には、解きたい質問と一緒に、ベクトルDBのナレッジコーパスから関連情報を取り出し、LLMに渡したいと考えています。質問「数冊の推理小説をおすすめしてもらえますか?」という質問でベクトルDB検索を試してみましょう。

# LangChainのChromaDBクライアントを使用して、以前に作成したChromaDBデータベースのインスタンスにリンクしています。
vectordb = Chroma(
    client=persistent_chroma_client,
    collection_name="my_rag_demo_collection",
    embedding_function=OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)
)

# ヘルパー関数を定義し、データを少し見やすく表示します。
def render_source_documents(source_documents):
    return pd.DataFrame({
        '#': range(1, len(source_documents) + 1),
        'Documents': [t.page_content if len(t.page_content) <= 300 else t.page_content[:300] + '...' for t in source_documents],
        'Metadatas': ['' if not t else '...' + json.dumps(t.metadata, indent=2)[-88:] for t in source_documents]
    })

# 質問をコンパイルします
question = "数冊の推理小説をおすすめしてもらえますか?"

# ベクトルDBに対して質問に基づいて検索を実行します
relevant_chunks = vectordb.similarity_search(question)

# 結果を表示します
print(f"Top {len(relevant_chunks)} search results")
render_source_documents(relevant_chunks)
「いくつかの推理小説をおすすめできますか?」という質問の上位検索結果(画像:著者)

デフォルトでは、上位4件の結果が表示されますが、別の数値を明示的に設定する場合を除いてはです。この例では、上位検索結果にはシャーロック・ホームズの小説が含まれており、「探偵」という言葉が直接使われています。2番目の結果(『ジャッカルの日』)には「探偵」という言葉はありませんが、「警察機関」と「陰謀を暴く」という言葉があり、「推理小説」と意味的に関連しています。3番目の結果(『アンダーカバーエコノミスト』)には「アンダーカバー」という言葉が使われていますが、経済に関する小説です。最後の結果は、単に小説/書籍と関連しているために表示されたと考えられます。「推理小説」という特定のカテゴリに関係なく、4件の結果が表示されるためです。

また、ベクトルDBを使用する必要は厳密にはありません。埋め込みをロードして他の形式のストレージで検索を容易にすることもできます。「通常の」リレーショナルデータベースやExcelなどが使用できますが、応用ロジック内での「類似度」計算(OpenAIの埋め込みを使用する場合はドット積)を自分で処理する必要があります。一方、ベクトルDBではその処理が自動で行われます。

メタデータによって特定の検索結果を事前にフィルタリングすることもできます。デモンストレーションのために、ジャンルによるフィルタリングを行いましょう。これは、私たちが書籍リストからロードしたメタデータの「ヘッダー2」の下にあります。

# 特定のメタデータフィルタに一致するレコードをフィルタリングしてアクセスしてみましょう。必要に応じて、これは事前フィルタに変換できるかもしれませんpd.DataFrame(vectordb.get(where = {'Header 2': 'Finance'}))
メタデータの事前フィルタリングに基づいて検索結果を表示し、主要な列のみを表示します(画像:著者)

LLMによって提供される興味深い機会は、LLM自体を使用してユーザーの質問を検討し、利用可能なメタデータを確認し、メタデータベースを事前にフィルタリングする必要性と可能性を評価し、実際にデータを事前にフィルタリングするためのクエリコードを作成することです。詳細については、LangChainの自己クエリリトリバーをご覧ください。

3. LLMを使用して回答を生成する

次に、LLMに対して「情報の一部と質問を提供します。提供された情報の一部を使用して質問に回答してください」というような指示を追加します。その後、これらの指示、ベクトルDBの検索結果、および質問をパケットにまとめてLLMに送信します。これらの手順は、以下のコードによって簡単に実現されます。

LangChainは、このコードの一部を抽象化する機会も提供していますので、以下のコードほど詳細に書く必要はありません。ただし、以下のコードでは指示がLLMに送信される方法を示している点に注意してください。この部分をカスタマイズすることもできます。この例では、デフォルトの指示を変更してLLMに短く保つように要求しています。デフォルトの指示が使用可能な場合は、質問テンプレート部分をスキップすることができ、LangChainは独自のパッケージからデフォルトのプロンプトを使用してLLMにリクエストを送信します。

# フリーバージョンのChatGPTの背後にある言語モデルを選択します:GPT-3.5-turbollm = ChatOpenAI(model_name = 'gpt-3.5-turbo', temperature = 0, openai_api_key = OPENAI_API_KEY)# プロンプトを作成します。これが実際にChatGPT LLMに送信されるもので、ベクトルデータベースからのコンテキストと質問がプロンプトに注入されます。template = """以下のコンテキストを使用して最後の質問に答えてください。回答が分からない場合は、回答するのに十分な情報がないと述べてください。回答をできるだけ簡潔に、最大5文に限定してください。{context}質問: {question}助けになる回答:"""QA_CHAIN_PROMPT = PromptTemplate.from_template(template)# 検索QAチェーンを定義し、質問を取得し、ベクトルDBから関連するコンテキストを取得し、それらを両方LLMに渡して回答を取得しますqa_chain = RetrievalQA.from_chain_type(llm,                                        retriever=vectordb.as_retriever(),                                       return_source_documents=True,                                       chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}                                       )

さあ、再び探偵小説のおすすめを尋ねて、どんな回答が得られるか確認しましょう。

# 質問をして、質問応答チェーンを実行しましょう。question = "いくつかの探偵小説をおすすめしてもらえますか?"result = qa_chain({"query": question})# 結果を見てみましょうresult【 "result"】
解答からの探偵小説の推薦(著者によるイメージ)

前回の検索結果の4つすべてをモデルが確認したのか、または回答に記載されている2つの結果だけを得ているのか、確認しましょう。

# LLMによってコンテキストとして使用されたソースドキュメントを見てみましょう# 前に使用したヘルパー関数を使用して、表示される情報のサイズを制限します。render_source_documents(result[ "source_documents" ])
質問とともにLLMに渡されたコンテキストとしてのもの。 (著者によるイメージ)

LLMがまだ4つの検索結果全体にアクセスしており、最初の2冊だけが探偵小説であると結論付けたことがわかります。

ただし、LLMの応答は、指示とベクトルデータベースからの同じ情報を送信しても質問するたびに変わる可能性があることに注意してください。たとえば、ファンタジーの本の推薦を尋ねると、LLMは時には3冊の本を推薦したり、それ以上の本を推薦したりしますが、すべてが書籍リストからです。すべてのケースで、トップの推奨書籍は同じです。ばらつきを最小限にするために一貫性-創造性スペクトラムである「温度」パラメータを0に設定したにもかかわらず、これらの変動が発生したことに注意してください。

4. 「チャット」機能を追加(オプション)

ソリューションは現在、必要なコア機能を備えています-ウェブサイトからの情報を読み込んで、その情報に基づいて質問に答えることができます。ただし、現在のところ「会話形式」のユーザーエクスペリエンスは提供していません。ChatGPTのおかげで、「チャットインターフェース」が主流の設計になりました-私たちはこれが「自然な」方法で生成的AI、特にLLMとの対話を期待するようになりました 😅 。チャットインターフェースに移行するための最初のステップは、ソリューションに「メモリ」を追加することです。

ここで「メモリ」というのは、LLMが実際には会話を覚えているわけではないことに注意してください-それは各ターンごとに完全な会話履歴を示す必要があります。したがって、ユーザーがLLMにフォローアップの質問をする場合、ソリューションは元の質問、LLMの元の回答、およびフォローアップの質問をパッケージ化し、LLMに送信します。LLMは会話全体を読み取り、会話を続けるための意味のある応答を生成します。

私たちが構築しているような質問応答チャットボットでは、このアプローチをさらに拡張する必要があります。なぜなら、ユーザーのフォローアップの質問に応答するために、中間ステップでベクトルデータベースにアクセスして関連情報を取得する必要があるからです。「メモリ」が質問応答チャットボットでシミュレートされる方法は次のとおりです。

  1. 「チャット履歴」としてすべての質問と回答(変数に)を保持する
  2. ユーザーが質問をすると、チャット履歴と新しい質問をLLMに送信し、スタンドアロンの質問を生成するように依頼する
  3. この時点では、チャット履歴は不要になります。スタンドアロンの質問を使用して、ベクトルDBで新しい検索を実行します。
  4. 最終的な回答を取得するために、スタンドアロンの質問と検索結果を、指示とともにLLMに渡します。このステップは、前の段階「LLMを使用して回答を生成する」で実装したのと似ています。

簡単な変数でチャット履歴を追跡することもできますが、LangChainのメモリタイプの1つを使用します。使用する特定のメモリオブジェクトは、指定したサイズ制限に達したときに古いチャット履歴を自動的に切り捨てるという便利な機能を提供します。通常、選択したLLMが受け入れることができるテキストのサイズです。私たちの場合、LLMは少し4,000以上の「トークン」(単語の部分)を受け入れることができる必要があります。これはおおよそ3,000語またはWord文書の5ページくらいです。OpenAIは同じChatGPT LLMの16kバリアントを提供しており、入力を4倍に拡張できます。したがって、メモリサイズを設定する必要があります。

これらの手順を実現するコードは次のとおりです。再び、LangChainはより高いレベルの抽象化を提供し、コードはそれほど明示的である必要はありません。このバージョンは、まずチャット履歴を単一の独立した質問にまとめるための基になる指示をLMMに送信すること、そして次にベクトルDBの検索結果に基づいて生成された単独の質問に対する応答を生成することを公開するために使用されます。

# チャット履歴を追跡するためのメモリオブジェクトを作成しましょう。ここでは「トークン」ベースのメモリが使用されており、チャット履歴の長さは選択したLLMに渡すことができるものに制限されます。一般的には、設定される最大トークン長はLLMに依存します。LLMの4Kバージョンを使用していると仮定すると、質問のプロンプトに少し余裕を持たせるためにトークンの最大値を3Kに設定します。# LLMパラメータは、選択したLLMのトークン化スキームをLangChainに知らせるためのものです。 memory = ConversationTokenBufferMemory(memory_key="chat_history", return_messages=True, input_key="question", output_key="answer", max_token_limit=3000, llm=llm)# LangChainには、ユーザーの最新の質問とその時点までの会話のコンテキストに基づいて単独の質問を生成するためのデフォルトのプロンプトが含まれていますが、デフォルトのプロンプトを追加の指示で拡張します。standalone_question_generator_template = """以下の会話と次のフォローアップ質問を考慮して、次のフォローアップ質問を元の言語で単独の質問に再表現してください。単独の質問を明示的に形成し、単独の質問を明確にするために必要な任意のコンテキストを含めてください。会話:{chat_history}フォローアップ質問: {question}単独の質問:"""updated_condense_question_prompt = PromptTemplate.from_template(standalone_question_generator_template)# 最終的な応答合成テンプレートを再構築しましょう(または、LangChainはデフォルトのプロンプトを使用しますが、多少異なる場合もあります)。final_response_synthesizer_template = """次のコンテキストの断片を使って質問の結末で回答してください。回答がわからない場合は、利用可能な情報では質問に回答できないと言ってください。回答をでっち上げないでください。回答はできるだけ簡潔にし、最大で5文に制限してください。{context}質問: {question}有益な回答:"""custom_final_prompt = PromptTemplate.from_template(final_response_synthesizer_template)qa = ConversationalRetrievalChain.from_llm(    llm=llm,     retriever=vectordb.as_retriever(),     memory=memory,    return_source_documents=True,    return_generated_question=True,    condense_question_prompt= updated_condense_question_prompt,    combine_docs_chain_kwargs={"prompt": custom_final_prompt})# 以前に検索QAチェーンに質問した質問を再度質問しましょうquery = "数冊の推理小説をおすすめしてもらえますか?"result = qa({"question": query})print(textwrap.fill(result['answer'], width=100))
解決策からの推理小説のおすすめ。単に「質問応答」の機能のみを使用した以前の応答と同じ応答、「メモリー」なし(著者による画像)

フォローアップ質問をして、解決策が「メモリー」を持ち、フォローアップ質問に対して会話形式で応答できるかどうかを確認しましょう:

query = "2冊目の本についてもっと教えてください"result = qa({"question": query})print(textwrap.fill(result['answer'], width=100))
「2冊目の本」についてのフォローアップ質問への応答。解決策は以前と同じ本に関するさらなる情報で応答します(著者による画像)

このセクションの最初に概説された4つのステップを解説するために内部で何が起こっているかを見てみましょう。まずは解決策がこれまでの会話をログに記録していることを確認するために、チャット履歴を見てみましょう:

# このポイントまでのチャット履歴を見てみましょうresult['chat_history']
二つ目の質問をした後のチャット履歴。この時点で応答も会話に含まれています。(作者による画像)

チャット履歴以外にトラッキングされている解決策について見てみましょう:

# LLMに基づいて生成された単体の質問を表示しましょうprint("次の質問はLLMによって生成された単体の質問です:")print(textwrap.fill(result['generated_question'], width=100 ))print("\nモデルが参照したソースドキュメントは以下の通りです:")display(render_source_documents(result['source_documents']))print(textwrap.fill(f"\n生成された回答:{result['answer']}", width=100, replace_whitespace=False) )
二つ目の質問をした後のチャット履歴以外の出力。(作者による画像)

この解決策では、最初に質問「2番目の本についてもう少し教えてください」を「『The Day of the Jackal』by Frederick Forsythについての追加情報は何ですか?」と変換するためにLLMを内部的に使用しています。この質問のもとで、解決策は関連情報を検索して最初にThe Day of the Jackalチャンクを取得することができます。ただし、他の本に関する無関係な検索結果も含まれていることに注意してください。

クイックオプションサイドバー:潜在的な問題について

潜在的な問題#1 — 単体質問生成の向上が必要: テストでは、チャットの解決策は常に良い単体の質問を生成することには成功しませんでした。質問生成のプロンプトを調整することで、改善されました。例えば、フォローアップ質問「2番目の本について教えてください」という質問では、生成されたフォローアップ質問は「2番目の本について教えてください」という意味のあるものではなく、ランダムな検索結果やそれによって見た目にランダムに生成されたLLMの応答につながることが多かったです。

潜在的な問題#2 — オリジナルの質問とフォローアップ質問での検索結果の変更: 特定の本の名前を含む2番目の質問が生成されたにもかかわらず、ベクトルDBの検索結果は他の本の結果も含まれていました。さらに重要なのは、これらの検索結果がオリジナルの質問のものとは異なるということです!この例では、この検索結果の変化は望ましかったです。質問が「推理小説のおすすめ」から特定の小説へ変わったためです。ただし、トピックを掘り下げるためにフォローアップ質問をするユーザーにとっては、質問の形式やLLMによって生成された単体の質問の変化によって、検索結果が異なるか、検索結果の順位付けが異なる可能性があります。これは望ましくない場合もあります。

この問題は、ベクトルDBからの幅広い初期検索を行い(例では4〜5件ではなく多数の結果を返す)、それらを再ランキングして最も関連性の高い結果を常にLLMに送ることで、自動的に緩和される可能性があります(Cohereの「再ランキング」を参照)。また、検索結果が変化したことをアプリケーションが自動的に認識することも比較的容易です。検索結果の変化の程度(順位や重複メトリックによって測定される)と質問の変化の程度(コサイン類似度などの距離メトリックによって測定される)のバランスが取れているかどうかに関して、いくつかのヒューリスティックを適用することが可能です。少なくとも、チャットのターンごとに検索結果に予期しない変動がある場合、エンドユーザーは警告され、使用ケースの重要度とエンドユーザーの訓練や洗練度に応じて、より詳しい検査のために関与させることができます。

この挙動を制御するための別のアイデアは、フォローアップ質問が再度ベクトルDBにアクセスする必要があるかどうか、以前に取得した結果で質問に意味のある回答ができるかどうかをLLMに判断させることです。一部の使用ケースでは2つの検索結果と回答を生成し、LLMによってそれらの回答の間の裁定を行うことが望ましい場合もあります。一部の使用ケースでは、リソースコントロールをユーザーに委ねることでコンテキストを管理します(使用ケースやユーザーの訓練や洗練度、その他の考慮事項に応じて)。また、フォローアップ質問で検索結果が変化することに対して寛容な使用ケースもあります。

おそらくお分かりいただけるかと思いますが、基本的な解決策を作るのは比較的簡単ですが、それを完璧にするのは難しいです。ここで指摘されている問題はほんの一部です。さて、メインの演習に戻りましょう…

5. プリコードのUIを追加する

ついに、チャットボットの機能が完成しました。これで、ユーザーエクスペリエンスを向上させるための素敵なユーザーインタフェースを追加することができます。これは(多少)PythonのライブラリであるGradioとStreamlitのおかげで比較的簡単に実現できます。これらのライブラリは、Pythonで書かれた指示に基づいてフロントエンドのウィジェットを構築します。ここでは、Gradioを使用して簡単にユーザーインタフェースを作成します。

これまでのコードを実行できなかった場合や、同じ場所に到達するためのいくつかのバリエーションを示すために、以下の2つのコードブロックは自己完結型であり、完全なチャットボットを生成するために完全に新しいColabノートブックで実行できます。

# 初期設定 - 必要なソフトウェアのインストール!pip install openai tiktoken langchain chromadb html2text gradio   # ここから始める場合でまだ何もインストールしていない場合は、先頭の '#' を削除してコメント解除します。# ソリューションの異なる機能を有効にするために必要なパッケージのインポートfrom langchain.document_loaders import AsyncHtmlLoader # ウェブサイトのコンテンツをドキュメントにロードするためfrom langchain.text_splitter import MarkdownHeaderTextSplitter # ドキュメントを見出しで小さなチャンクに分割するため from langchain.document_transformers import Html2TextTransformer # HTMLをMarkdownテキストに変換するためfrom langchain.chat_models import ChatOpenAI # OpenAIのLLMを使用するためfrom langchain.prompts import PromptTemplate # プロンプトを作成するためfrom langchain.chains import RetrievalQA, ConversationalRetrievalChain # RAGのためfrom langchain.memory import ConversationTokenBufferMemory # チャット履歴を保持するためfrom langchain.embeddings.openai import OpenAIEmbeddings # テキストを数値表現に変換するためfrom langchain.vectorstores import Chroma # ベクトルデータベースとのやり取りに使用するためimport pandas as pd, gradio as gr # データをテーブルとして表示し、UIを構築するためimport chromadb, json, textwrap # ベクトルデータベース、jsonをテキストに変換し、表示を整形するためfrom chromadb.utils import embedding_functions # Chromaが必要にする埋め込み関数を設定# OpenAIのAPIキーを変数に追加# キーを変数に保存するのは良くない方法です。環境変数に保存し、そこから読み込むべきですが、このデモでは問題ありませんOPENAI_API_KEY='sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # フェイクキー - 実際のキーに置き換えてください

次のコードを実行してチャットボットのUIをレンダリングする前に、Colabを介してレンダリングされる場合、リンクを持つ人は3日間の間にアクセスできるようになります(リンクはColabノートブックのセル出力に提供されます)。原理的には、最後の行を`demo.launch(share=False)`に変更することでアプリを非公開にすることができますが、その場合、アプリが全く動作しなくなりました。代わりに、Colabで「デバッグ」モードで実行することをおすすめします。これにより、Colabセルが「実行」状態になり、停止するとチャットボットが終了されます。または、以下に示すコードを別のColabセルで実行してチャットボットを終了し、Colabに読み込まれたコンテンツを削除します。

# デモチャットボットを終了するために最後に実行されますdemo.close() # チャットセッションを終了し、共有デモを終了します。# チャットボットのために作成されたベクトルDBのコレクションを取得して削除するvectordb = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection", embedding_function=openai_embedding_func_for_langchain)vectordb.delete_collection()

以下は、チャットボットをアプリとして実行するためのコードです。このコードのほとんどは、この記事のこのポイントまでのコードを再利用しているため、馴染みがあるはずです。以下のコードと以前のコードとの間にいくつかの違いがあることに注意してください。それには、LangChainの「token」メモリオブジェクトを使用してメモリ管理を行わないことが含まれます。これは、会話がしばらく続くと履歴が長すぎて言語モデルのコンテキストに渡すことができなくなり、アプリを再起動する必要があることを意味します。

# OpenAIの埋め込み関数を初期化します。Chroma DBを介して関数を直接Chroma DBに渡す場合と、LangChainを使用してChroma DBと組み合わせて使用する場合で関数プロトコルが異なるため、2つありますopenai_embedding_func_for_langchain = OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)openai_embedding_func_for_chroma = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)# GPT 3.5 turboモデルを使用してLangChainチャットモデルオブジェクトを初期化するllm = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0, openai_api_key=OPENAI_API_KEY)# ベクトルDBを初期化し、コレクションを作成するpersistent_chroma_client = chromadb.PersistentClient()collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func_for_chroma)# ウェブサイトのコン

異なるURLを使用してアプリを試すことができます。言うまでもなく、これは製品レベルのアプリではなく、RAGベースのGenAIソリューションの構築ブロックを示すために作成されたものです。これは最高の早期プロトタイプであり、通常の製品に変換する場合は、大部分のソフトウェアエンジニアリング作業が必要になるでしょう。

はじめに提示されたFAQを再訪する

作成したチャットボットの文脈と知識を活用して、はじめに提示されたいくつかの質問についてより詳しく検討してみましょう。

  1. LLMを利用したソリューションは、私たちのユースケースに適していますか?おそらく、伝統的な分析、監視された機械学習、または他の手法の方が適していますか?LLMは言語関連のタスクの「理解」と指示に従うことに長けています。そのため、LLMの早期ユースケースは質問応答、要約、生成(この場合はテキスト)、より意味に基づいた検索の実現、感情分析、コーディングなどにあります。また、LLMは問題解決と推論の能力も持っています。例えば、LLMは回答キーを提供すれば、あるいは場合によっては提供しなくても、学生の課題の自動採点を行うことができます。一方、多数のデータポイントに基づく予測や分類、マーケティング最適化のためのマルチアームバンディット実験、レコメンダーシステム、強化学習システム(Roomba、Nestサーモスタット、電力消費や在庫レベルの最適化など)は、他のタイプの分析または機械学習の得意分野です... 少なくともそれは当面の間です。従来のMLモデルが情報をLLMに供給し、その逆の情報をLLMが供給するハイブリッドアプローチも、コアなビジネス問題に対する包括的な解決策として検討されるべきです。
  2. LLMが進むべき道であるとした場合、ユースケースは現在のまたは近い将来の製品(たとえばChatGPT Enterpriseなど)で対応できますか?従来の構築対買いの判断となります。OpenAI、AWS、その他が提供するサービスと製品は、より幅広く、優れ、おそらく安価になるでしょう。たとえば、ChatGPTはユーザーが分析するためにファイルをアップロードできるようにし、BingチャットやGoogleのバードを使用して外部のウェブサイトを指定して質問応答を行うことができ、AWS Kendraは企業の情報に意味ベースの検索をもたらし、Microsoft CopilotはLLMをWordやPowerpoint、Excelなどに組み込むことができます。企業が自分自身でオペレーティングシステムやデータベースを開発しないのと同じ理由で、現在および将来のオフシェルフ製品によって陳腐化する可能性のあるAIソリューションを開発する必要があるかどうかを考えるべきです。一方で、企業のユースケースが特定の場合、またはある意味で制約がある場合(例えば、感度により自社の機密データを任意のベンダーに送信できない場合や、規制のガイドラインにより送信できない場合)、ユースケースに対応するために会社内で生成型AI製品を開発する必要があるかもしれません。LLMの推論能力を使用する製品だが、タスクや生成物がベンデッドソリューションと異なる場合、インハウス開発を検討する必要があります。例えば、工場の床や製造プロセス、在庫レベルなどを監視するシステムは、特に適切なドメイン固有の製品提供がない場合、カスタム開発が必要になるかもしれません。また、アプリケーションが専門的なドメイン知識を必要とする場合、ドメイン固有のデータでファインチューニングされたLLMがOpenAIの汎用LLMよりも優れたパフォーマンスを発揮する可能性が高く、インハウス開発が検討されることがあります。
  3. LLMパワード製品の構築ブロックはどのようなものですか?これらのうち、どれが商品化済みで、どれがさらに時間をかけて構築とテストが必要ですか?私たちが構築したようなRAGソリューションの上位の構築ブロックは、データパイプライン、ベクトルデータベース、検索、生成、そしてもちろんLLMです。LLMやベクトルデータベースの選択肢はたくさんあります。データパイプライン、検索、生成のプロンプトエンジニアリングには、ユースケースに最適化するためにデータサイエンス的な実験が必要です。初期の解決策が完成した後、本番導入には多くの作業が必要になります。これは、デ

    結論

    もし11か月前にこれらすべてを知っていたなら、会社のCEOとのデモンストレーションが正当化されていたでしょう。おそらく、より広い観衆のためにTEDトークを行うことさえできたでしょう。今日、これは特に生成型AI製品の提供に関与している場合には、AIリテラシーベースラインの一部になっています。このエクササイズのおかげで、おそらく追いついていることを願っています! 👍

    いくつかのまとめとして。

    • このテクノロジーには深い約束があります。どれだけの他のテクノロジーがこの程度に「考える」ことができ、"推論エンジン"として使用できるでしょうか(ここでのDr. Andrew Ngの言葉によるものです)。
    • フロンティアモデル(現時点ではGPT-4)が引き続き進化する一方で、オープンソースモデルとそのドメイン固有およびタスク固有の微調整バリアントは、多くのタスクで競争力を持ち、多くの応用を見つけるでしょう。
    • 良いか悪いかに関わらず、この数百万ドル(数十億ドル?)を開発するためにかかった最先端のテクノロジーが無料で利用できます。フォームを記入して、Metaの能力のあるLlama2モデルを非常に許容度の高いライセンスでダウンロードすることができます。HuggingFaceのモデルハブには約30万の基準LLMまたはその微調整バリアントがあります。ハードウェアも一般化されています。
    • OpenAIモデルは今では「ツール」(関数、APIなど)を認識し、使用することができます。これにより、ソリューションが人間とデータベースだけでなく他のプログラムともインターフェースできるようになりました。LangChainやその他のパッケージは、既にLLMを「脳」として使用する自律エージェントの構築で、入力を受け入れ、アクションを決定し、そのアクションを繰り返し、エージェントが目標に達するまでこれらの手順を続けることを実証しています。当社の簡単なチャットボットでは、2つのLLM呼び出しからなる決定論的なシーケンスが使用されました- スタンドアロンの質問を生成し、検索結果を一貫した自然言語の応答に合成します。発展し続けるLLMへの何百もの呼び出しを用いたエージェントの自律性によって、何が可能になるかを想像してみてください!
    • これらの急速な進歩は、GenAIを中心とする莫大な勢いの結果であり、これは企業や日常生活を通じてデバイスに普及していくでしょう。初めはより単純な方法で、しかし後には伝統的なAIと組み合わせてテクノロジーの推論と意思決定能力を活用した、ますます洗練されたアプリケーションで。
    • 最後に、ChatGPTブーム以来、このテクノロジーの適用に関しては、現在のところプレイングフィールドはかなり均等ですので、今参加する絶好の機会です!もちろん、研究開発の側では状況が異なり、何年もの時間と何十億ドルを費やしたビッグテック企業がこのテクノロジーを開発しています。それでも、後により高度なソリューションを構築するためには、今始めるのに最適なタイミングです!

    追加リソース

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more