🤖

Google Cloud で生成 AI アプリケーションを作ろう!パート 7 : 複数サービスの組み合わせ技で実用的なアプリを作る

2023/08/18に公開

はじめに

パート 6 : LLM のみを用いたアプリ開発の限界とその対応方法」で説明したように、LLM を活用したアプリケーションを開発する際は、LLM だけで複雑な処理をこなすのではなく、他のサービスと連携したアーキテクチャーを考える点がポイントになります。

今回は、一例として、Google Cloud の Matching Engine による製品情報データベースの検索機能と LLM を組み合わせて、ユーザーの好みにあった製品情報を提供する検索システムを構築します。

Matching Engine による検索システム

従来の製品検索では、「ドローン 子供向け」などのキーワードを入力して検索することが多かったと思います。このようなシステムは、たとえば、Google Cloud の Matching Engine で構築できます。次のような仕組みになります。

はじめに、製品カタログに含まれるテキストデータ(製品の説明文など)を埋め込みベクトルに変換したものを Matching Engine に保存します。「埋め込みベクトル」というのは、テキストに含まれる「意味」を抽出してベクトル値に変換したものです。それぞれの製品の情報を個別にベクトル化して保存します。

そして、「ドローン 子供向け」などの検索キーワードを受け取ると、これも同様に「意味」を抽出した埋め込みベクトルに変換して、Matching Engine でこれと値が近い製品情報のベクトルを検索します。Matching Engine は、大量のベクトル値データに対して、値が近いベクトルを高速に検索します。

これにより、「ドローン 子供向け」と同じ意味を含んだ製品情報のベクトルが発見できます。このベクトルに対応する製品は、説明文に「ドローン 子供向け」という意味を含んだものになるので、この製品を検索結果として返すことができます。

このように、テキスト情報を埋め込みベクトルに変換することで、単純なキーワードマッチングではなく、キーワードの「意味」を利用した検索を実現します。

なお、細かい点ですが、ベクトル値に変換する前のテキストデータは、Matching Engine には保存されないので、この点はシステム設計時に考慮が必要です。たとえば、次のような実装をします。

Matching Engine は、それぞれのベクトル値にユニークな index_id を付与して、(index_id, ベクトル値) のペアが保存できます。そこで、これとは別に、inidex_id とテキストデータのペアをどこかに保存しておきます。Matching Engine は検索結果として index_id の値を返すので、これを元にして、別途、保存しておいたテキストデータを取り出します。

LLM と Matching Engine を組み合わせた検索システム

ここまで、キーワードで検索するシステムの仕組みを説明しました。一方、LLM を利用すれば、「サッカー好きの子供にあったプレゼントは何?」といった質問文でも検索できる気がします。

しかしながら、この質問を LLM に入力しても、LLM は学習時のデータに基づいて回答するので、学習データに含まれない製品を返すことは期待できません。あるいは、LLM は、事実に基づかない回答を生成する可能性もあるため、現実には存在しない製品の情報を返す恐れもあります。----- どうすればよいのでしょうか?

製品カタログのテキスト情報を LLM に入力するプロンプトに含めておき、このカタログ情報だけを用いて回答するように指示する方法も考えられますが、これはうまい方法とは言えません。LLM に入力するプロンプトには文字数の上限があるので、カタログの情報をすべて入力するのは困難です。また、プロンプトに大量の情報を入力すると、その分だけ、LLM を利用するコストも大きくなります。

そこで、LLM を先ほどの Matching Engine による検索システムと連携する方法を考えてみます。次のようなワークフローはどうでしょうか?

  • ステップ 1 :「サッカー好きの子供にあったプレゼントは何?」という質問から、Matching Engine に問い合わせるキーワードを LLM に考えさせる。

  • ステップ 2 : 得られたキーワードで Matching Engine に問い合わせて、製品情報を取得する。

  • ステップ 3 : 得られた製品情報から、説明文の要約や製品情報の URL など、必要な情報だけを LLM を使って取り出す。

  • ステップ 4 : 取り出した情報だけを用いて、最初の質問「サッカー好きの子供にあったプレゼントは何?」に回答するように、LLM に指示を出す。

製品カタログ全体を LLM に入力するのではなく、Matching Engine で発見した製品の情報だけを LLM に入力して回答させようというわけです。Matching Engine での検索(ステップ 2)に必要な前処理(ステップ 1 )と検索後の後処理(ステップ 3 )も LLM で行うところがポイントです。

LangChain による実装例

LLM と Matching Engine を組み合わせたアーキテクチャーができましたが、これを実際に構築するには、それなりの手間が掛かりそうです。----- こんな時に利用できるのが、LangChain です。

パート 6 : LLM のみを用いたアプリ開発の限界とその対応方法」で説明したように、LangChain は、LLM とその他の API サービスを連携させたデータ処理のパイプラインを実装するためのフレームワークで、まさに上図の処理をパイプラインとして構築できます。各ステップの処理については、実装済みのモジュールが用意されており、これらを組み合わせれば構築完了です。

具体的には、ステップ 2 とステップ 3 をまとめて実行する RetrievalQA モジュールと、RetrievalQA モジュールを組み込んで、ステップ 1 〜 ステップ 4 をまとめて実行する agents ライブラリーのモジュール群を使用します。

この後は、LangChain と Matching Engine を連携したこのシステムを構築する具体的な手順を説明します。

なお、このシステムを動かす際は、埋め込みベクトルの生成に必要な PaLM API のアクセス数に関する十分なクォータの割り当てが必要です。Vertex AI API を有効化した状態で、クラウドコンソールの「IAM」-「割り当て」から「textembedding」のキーワードでフィルタリングすると、下図の割り当てが確認できます。

これが 600 未満の場合は、公式ドキュメントを参考に、割り当ての上限の引き上げをリクエストしてください。

また、ネットワーク構成の関係上、「マネージドノートブック」の環境からは、Matching Engine にアクセスできません。そこで、次の手順で「ユーザー管理ノートブック」の環境を用意します。

Cloud Shell のコマンドターミナルからユーザー管理ノートブックを作成するために、クラウドコンソールの右上にある下図のボタンをクリックして、Cloud Shell のコマンドターミナルを開きます。

コマンドターミナルが開いたら、次のコマンドを実行して、環境変数 PROJECT_ID にプロジェクト ID をセットした上で、環境設定と必要な API の有効化を行います。[Project ID] の部分は実際に使用しているプロジェクト ID に置き換えてください。

PROJECT_ID=[Project ID]
gcloud config set project $PROJECT_ID
gcloud services enable \
  notebooks.googleapis.com \
  aiplatform.googleapis.com \
  servicenetworking.googleapis.com

続いて、次のコマンドでユーザー管理ノートブックを作成します。

gcloud notebooks instances create dev-instance \
  --vm-image-project=deeplearning-platform-release \
  --vm-image-family=common-cpu-notebooks-debian-11-py310 \
  --location us-central1-a

クラウドコンソールのナビゲーションメニューから[Vertex AI]-[ワークベンチ]を選択して、上部のタブで「ユーザー管理のノートブック」を選択すると、次のように「dev-instance」があります。起動が完了して、[JUPYTERLAB を開く]が表示されたら、これをクリックします。

ローンチャーが開くので、「Notebook」セクションの[Python 3]をクリックします。

新規のノートブックが開くので、次のコマンドを実行して、LangChain のライブラリーをインストールします。

!pip install langchain==0.0.260 --user

ライブラリーをインストールした直後は、一度、ノートブックのカーネルを再起動する必要があります。再起動ボタン(下図の回転矢印ボタン)をクリックして、カーネルを再起動します。

この後は、上図の + ボタンをクリックしてコード用のセルを追加しながら、コードを実行していきます。すでにコードを書き込んだノートブックが GitHub で公開されていますので、そちらも参考にしてください。

製品カタログデータの確認

このセクションの作業は、先ほど用意したノートブックから行います。はじめに、製品カタログデータの CSV ファイルをダウンロードします。

!curl -OL https://github.com/GoogleCloudPlatform/python-docs-samples/raw/main/cloud-sql/postgres/pgvector/data/retail_toy_dataset.csv

これは、Google Cloud の GitHub リポジトリで公開されているもので、おもちゃの小売店の商品リストです。製品名、製品の説明文、製品 URL などを含んだデータになっています。

次のコマンドで、LangChain が扱えるデータ形式に変換します。

from langchain.document_loaders import CSVLoader

loader = CSVLoader(
    file_path='./retail_toy_dataset.csv',
    csv_args={'delimiter': ','}
)

documents = loader.load()

変換後のデータ(先頭の 1 レコード)を表示してみます。

print(documents[0].page_content)

結果は次のようになります。それぞれのカラムの内容が改行で区切られた 1 つのテキストデータになっています。

product_id: 7e8697b5b7cdb5a40daf54caf1435cd5
crawl_timestamp: 2020-01-24 20:51:13 +0000
product_url: https://www.walmart.com/ip/Koplow-Games-Set-of-2-D12-12-Sided-Rock-Paper-Scissors-Game-Dice-White-with-Pink-Letters-13060/125007983
product_name: Koplow Games Set of 2 D12 12-Sided Rock, Paper, Scissors Game Dice - White with Pink Letters #13060
description: Rock, paper, scissors is a great way to resolve disputs and hard decisions. …(以下省略)
list_price: 3.56
sale_price: 3.56
brand: Koplow Games
item_number: 
gtin: 799443797461
package_size: 
category: Toys | Shop Toys by Price
postal_code: 
available: False

一般的なデータベースであれば、カラムごとにデータを分離するところですが、そういうわけではありません。LLM に入力する際は、最終的には、すべて 1 つのテキストデータとして取り扱うので、この方が都合がよいのでしょう。この後、このテキストデータを埋め込みベクトルに変換して、Matching Engine に保存していきます。

テキストデータを埋め込みベクトルに変換する際は、LangChain の embeddings ライブラリーを使用します。たとえば、次のコマンドを実行すると、最初のレコードをベクトルに変換した結果が得られます。

from langchain.embeddings import VertexAIEmbeddings

embeddings = VertexAIEmbeddings(model_name='textembedding-gecko')
initial_vector = embeddings.embed_documents([documents[0].page_content])

textembedding-gecko というオプションが目に入りますが、これは、PaLM API が提供するモデルの 1 つで、入力テキストの意味を LLM で解釈して、それをベクトル値に変換した結果を返します。

次のコマンドで、ベクトルの次元とデータの中身(先頭部分)を確認してみましょう。

len(initial_vector[0]), initial_vector[0][:5]

結果は次のようになります。768 次元のベクトルが得られたことがわかります。

(768,
 [-0.0023134888615459204,
  -0.03847910836338997,
  0.02027878724038601,
  0.015024062246084213,
  -0.014163974672555923])

この後の手順で必要になるため、このベクトルデータを JSON ファイルに変換して、Cloud Storage のバケットに保存しておきます。まず、次のコマンドで JSON ファイルを生成します。

import json
with open('initial_vector.json', 'w') as f:
    json.dump({'id': 'initial', 'embedding': initial_vector[0]}, f)

新しいセルを追加して、次のコマンドを実行します。これは、Cloud Storage のバケットを作成して、先ほどの JSON ファイルをアップロードします。

%%bash
PROJECT_ID=$(gcloud config get project)
BUCKET=$PROJECT_ID-embeddings
gsutil mb -l us-central1 gs://$BUCKET
gsutil cp initial_vector.json gs://$BUCKET

ここで作成したバケットは、[Project ID]-embeddings というバケット名になります。([Project ID] の部分は、実際に使用している Project ID が自動で入ります。)

Matching Engine の初期設定

ここでは、Matching Engine の初期設定を行います。このセクションの作業は、先に開いた Cloud Shell のコマンドターミナルで行います。ノートブックから実行するわけではないので、注意してください。

はじめに、次のコマンドで、Matching Engine にアクセスするためのネットワーク設定を行います。コマンドの実行が完了するまで、1 分程度かかります。

gcloud compute addresses create metchingengine \
  --global --prefix-length=16 --network=default \
  --purpose=VPC_PEERING

gcloud services vpc-peerings connect \
  --service=servicenetworking.googleapis.com \
  --network=default --ranges=metchingengine

続いて、index-metadata.json というファイル名で、次の内容の設定ファイルをカレントディレクトリに作成します。最初の [Project ID] の部分は、実際に使用しているプロジェクトの Project ID に置き換えてください。

{
  "contentsDeltaUri": "gs://[Project ID]-embeddings",
  "config": {
    "dimensions": 768,
    "approximateNeighborsCount": 30,
    "distanceMeasureType": "COSINE_DISTANCE",
    "shardSize": "SHARD_SIZE_SMALL",
    "algorithm_config": {
      "treeAhConfig": {
        "leafNodeEmbeddingCount": 5000,
        "leafNodesToSearchPercent": 3
      }
    }
  }
}

contentsDeltaUri は、Matching Enginge がデータ保存に使用する Cloud Storage のバケットの指定で、先ほどノートブックから作成したバケットを指定しています。このバケットには、ベクトルデータのサンプルが入っている必要があります。ベクトルデータの JSON ファイルをアップロードしておいたのはこのためです。

dimensions は、保存するベクトルの次元を指定します。先ほどのサンプルでみたように、textembedding-gecko は 768 次元のベクトルを生成するので、同じ値を指定しています。

このファイルを用いて、Matching Engine の検索インデックスと Matching Engine にアクセスするためのエンドポイントを用意します。

gcloud ai indexes create \
  --metadata-file=./index-metadata.json \
  --display-name=retail-data \
  --region=us-central1

PROJECT_NUMBER=$(gcloud projects list \
  --filter="PROJECT_ID:'$(gcloud config get project)'" \
  --format='value(PROJECT_NUMBER)')

gcloud ai index-endpoints create \
  --display-name=retail-endpoint \
  --network=projects/$PROJECT_NUMBER/global/networks/default \
  --region=us-central1

これらのコマンドを実行すると、バックエンドでインデックスの初期化が開始します。クラウドコンソールのナビゲーションメニューから[Vertex AI]-[マッチング エンジン]を選択すると、インデックスの状態が確認できます。インデックスの初期化が完了すると、下図のように「準備完了」と表示されます。

インデックスの初期化が完了したら、次のコマンドを実行して、インデックスをエンドポイントにデプロイして紐づけます。

INDEX_ID=$(gcloud ai indexes list \
  --region=us-central1 --format="value(name)" \
  --filter="displayName=retail-data" \
  | rev | cut -d "/" -f 1 | rev)

INDEX_ENDPOINT_ID=$(gcloud ai index-endpoints list --region=us-central1 \
  --format="value(name)" \
  --filter="displayName=retail-endpoint" \
  | rev | cut -d "/" -f 1 | rev)

gcloud ai index-endpoints deploy-index $INDEX_ENDPOINT_ID \
  --deployed-index-id=deployed_retail_index \
  --display-name=deployed-retail-index \
  --index=$INDEX_ID \
  --region=us-central1

インデックスのデプロイには 15 分程度掛かります。クラウドコンソールの画面で、[インデックス エンドポイント]のタブを選択して、エンドポイント名「retail-endpoint」をクリックします。

次のように、デプロイされたインデックスの状態が表示されるので、「準備完了」になるまでしばらく待ちます。

時々、クラウドコンソールのブラウザ画面をリロードして、インデックスの状態表示を更新してください。

(参考)インデックスとエンドポイントの削除方法

本記事で使用するコードでは、インデックスとエンドポイントは、それぞれの名前(retail-data、および、retail-index)で識別して処理を行います。そのため、同じ名前のインデックスやエンドポイントが複数あると、正しく動作しない場合があります。誤って、同じ名前のインデックスやエンドポイントを複数作成した場合は、次のコマンドで不要なものを削除してください

  1. 削除対象のインデックスとエンドポイントの ID をクラウドコンソールの画面で確認します。

  1. 削除したいエンドポイントにインデックスがデプロイされている時は、次のコマンドでデプロイを解除します。(deployed_retail_index の部分は、クラウドコンソールでデプロイされたインデックスの状態を表示した際に、「デプロイされたインデックス」の部分に表示される名前を指定します。)
gcloud ai index-endpoints undeploy-index [エンドポイントの ID] \
  --deployed-index-id=deployed_retail_index \
  --region=us-central1

デプロイの解除には 15 分程度掛かります。クラウドコンソールの画面で、[インデックス エンドポイント]のタブを選択して、削除対象のエンドポイントをクリックします。次のように表示されれば、デプロイの解除は完了しています。

  1. 次のコマンドで、エンドポイントの ID を指定して、エンドポイントを削除します。
gcloud ai index-endpoints delete [エンドポイントの ID] --region=us-central1
  1. 次のコマンドで、インデックスの ID を指定して、インデックスを削除します。
gcloud ai indexes delete [インデックスの ID] --region=us-central1

Matching Engine へのデータ保存

Matching Engine の初期設定が完了したので、次は、Matching Engine に製品カタログのデータを保存します。ここからの作業は、「製品カタログデータの確認」で使用したノートブックに戻って、ノートブック上で実行していきます。

新しいセルを追加して、次の一連のコマンドをまとめて実行します。

from langchain.vectorstores import MatchingEngine

PROJECT_ID = !gcloud config get project

INDEX_ID = !gcloud ai indexes list \
  --region=us-central1 --format="value(name)" \
  --filter="displayName=retail-data" 2>/dev/null \
  | rev | cut -d "/" -f 1 | rev

INDEX_ENDPOINT_ID = !gcloud ai index-endpoints list --region=us-central1 \
  --format="value(name)" \
  --filter="displayName=retail-endpoint" 2>/dev/null \
  | rev | cut -d "/" -f 1 | rev

PROJECT_ID = PROJECT_ID[0]
INDEX_ID = INDEX_ID[0]
INDEX_ENDPOINT_ID = INDEX_ENDPOINT_ID[0]

vector_store = MatchingEngine.from_components(
    embedding=VertexAIEmbeddings(model_name='textembedding-gecko'),
    project_id=PROJECT_ID,
    region='us-central1',
    gcs_bucket_name='gs://{}-embeddings'.format(PROJECT_ID),
    index_id=INDEX_ID,
    endpoint_id=INDEX_ENDPOINT_ID
)

ここでは、LangChain の vectorstores ライブラリーが提供する MatchingEngine モジュールを用いて、先ほど用意した Matching Engine にアクセスするためのクライアントオブジェクトを取得しています。

このクライアントを利用すると、先に変数 documents に用意しておいた製品カタログデータをまとめて保存することができます。次のコマンドを実行します。

vector_store.add_documents(documents)

以前のセクション「Matching Engine による検索システム」では、Matching Engine にデータを保存する際は、index_id とテキストデータのペアを別途保存する必要があることを注意しました。上記のコマンドは、テキストデータのベクトル化や、index_id とテキストデータのペアの保存といった付随する処理をまとめて行います。ベクトル化には、embedding オプションで指定したモジュール(今の場合は、textembedding-gecko をバックエンドとしたモジュール)を使用して、index_id とテキストデータのペアは、gcs_bucket_name オプションで指定した Cloud Storage のバケットに保存します。

保存が完了したら、動作確認のために検索を実行してみます。

result = vector_store.similarity_search('soccer')
print(result[0].page_content)

ここでは、「soccer」というキーワードにマッチする製品情報を取得しています。複数の候補が返却されるので、最初の 1 つを表示しています。Matching Engine が返却した index_id から、対応するテキストデータを取得するところまでが自動化されており、次のような結果が得られます。

product_id: 39cee39aee26c1f821836ec38efe4666
crawl_timestamp: 2020-03-10 11:54:48 +0000
product_url: https://www.walmart.com/ip/Sport-Squad-Flux-Magnetic-Reversible-Soccer-Hockey-Tabletop-Multi-Sport-Game-Set-2ct-Soccer-Foosballs-2ct-Hockey-Pucks/554346135
product_name: Sport Squad Flux Magnetic Reversible Soccer & Hockey Tabletop Multi Sport Game Set, 2ct Soccer Foosballs, 2ct Hockey Pucks
description: The Sport Squad Flux is the latest innovation to …(以下省略)
list_price: 24.95
sale_price: 24.95
brand: Sport Squad
item_number: 576287150.0
gtin: 712265225578
package_size: 
category: Toys | Shop Toys by Price
postal_code: 
available: True

検索パイプラインの構築

これで、Matching Engine による検索システムができたので、後は、「LLM と Matching Engine を組み合わせた検索システム」で説明した一連のパイプラインを LangChain で実装するだけです。

はじめに、RetrievalQA モジュールを用いて、先に説明した、ステップ 2 とステップ 3 の部分を実行するパーツを用意します。

from langchain.llms import VertexAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

prompt_template = """The string enclosed in the backquotes (```) is product information.
From that information, please extract product_name field, product_url field, and description filed.
Your output should be in the following format.

Name: the product_name field
URL: the product_url field
Summary: short summary of the description field less than 100 words

Here's the product information: ```{context}```
"""

toy_search_llm = VertexAI(temperature=0.2, max_output_tokens=1024)
retriever = vector_store.as_retriever(search_kwargs={'include_metadata': True})
prompt = PromptTemplate(input_variables=['context'], template=prompt_template)

toy_search = RetrievalQA.from_llm(llm=toy_search_llm, prompt=prompt, retriever=retriever)

変数 prompt_template で指定したプロンプトは、ステップ 3 で検索結果を要約する際に使用します。「sports」というキーワードで検索した結果を見てみましょう。

print(toy_search.run('sports'))

結果は次のようになります。確かに、プロンプトで指定した情報(製品名、製品 URL、製品説明のサマリー)が出力されています。

Name: Sport Squad Flux Magnetic Reversible Soccer & Hockey Tabletop Multi Sport Game Set, 2ct Soccer Foosballs, 2ct Hockey Pucks
URL: https://www.walmart.com/ip/Sport-Squad-Flux-Magnetic-Reversible-Soccer-Hockey-Tabletop-Multi-Sport-Game-Set-2ct-Soccer-Foosballs-2ct-Hockey-Pucks/554346135
Summary: The Sport Squad Flux is the latest innovation to combination gaming! Play soccer or reverse the playfield and play hockey!

次に、ここで用意した toy_search を検索エンジンとして、検索のパイプライン全体を実行するエージェントを定義します。

from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType

description = 'useful for when you need to answer questions about toys. \
Input should be a comma-separated words, do not input a fully formed question.'

tools = [
    Tool(name='Retail Toy QA System',
         func=toy_search.run,
         description=description),
]

agent_llm = VertexAI(temperature=0.4, max_output_tokens=1024)
agent = initialize_agent(
    tools, llm=agent_llm, 
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True
)

上記の変数 tools には、検索に使用するツールの情報を与えます。description の情報を用いて、LLM は検索エンジンを使用する方法を自分で判断します。この例では、検索文字列は、コンマ区切りの単語列が必要なことを LLM に伝えています。また、エージェントが使用する LLM は、変数 agent_llm として定義しています。検索するごとにバリエーションのある結果が得られるよう、温度パラメーターを少し大きめの 0.4 に設定しています。

このエージェントに適切な質問を投げると、必要に応じて検索エンジンを使用しながら、回答を作成してくれます。ここでは、次のようなプロンプトのテンプレートを使って質問を投げることにします。

prompt_template = """You are a recommendation system that introduces the product that best matches the user's request.
The string enclosed in the backquotes (```) is the user's request. You must output only the product name and the url in the format as shown below.

Name: PRODUCT_NAME
URL: PRODUCT_URL

Here's the request: ```{query}```
"""

ここでは、商品名と商品 URL を回答するように依頼しています。これを用いて、実際に質問を投げてみましょう。

query = 'Can you suggest a birthday present for my 9-year-old daughter who loves sports?'
result = agent.run(prompt_template.format(query=query))

ここでは、「スポーツ好きな 9 才の娘への誕生日プレゼント」を提案するように依頼しています。実行ごとに異なる結果が得られますが、今回は次のような結果になりました。

先ほどエージェントを定義する際に、オプション verbose=True を指定していたので、回答を得るまでの LLM の「思考過程」が表示されています。「sports, 9-year-old, girl」というキーワードで検索エンジンを利用して、商品「Bell Ollie Multisport Helmet, Youth 8+ ( 54-58 cm), Matte Black」を発見しています。

ここでは、1 回の検索で回答が得られましたが、検索結果が適切でないと判断した場合は、キーワードを変更するなどの工夫をして、検索を繰り返す場合もあります。先に、一直線のパイプラインではなく、LLM がインテリジェントに判断するとの注意書きがありましたが、これがその意味になります。何度か検索を繰り返して、エージェントの「思考パターン」を観察するとよいでしょう。

ただし、今回構築したシステムは、まだ改善の余地があります。さまざまなパターンの質問文で検索を行うと、検索エンジンから適切な情報が取得できずに、Matching Engine に保存したデータを使わずに、LLM が学習時に記憶した商品の情報を返すことがあります。このような現象を避けるには、RetrievalQA モジュールやエージェントに与えるプロンプトを工夫したり、温度パラメーターを適切に調整するなどの方法が考えられます。

もしくは、エージェントが返した結果をチェックして、オリジナルの製品カタログに含まれていなければ再検索を行う処理のコードを追加する方法もあります。LLM のチューニングだけに頼るのでなく、必要な際は、通常の手続き型のコードと連携する方法を考えるとよいでしょう。

まとめ

今回は、LLM と Matching Engine を連携した製品検索システムを構築しました。簡単のためにノートブック上で検索エージェントを実行しましたが、これを Web アプリケーションに組み込んで実装するのもそれほど難しくはないでしょう。

今回のユースケースは、LLM を活用した検索システムとしては定番のもので、LangChain の既存のモジュールで簡単に実装できましたが、本当に重要なのは、LLM とその他のサービスをどのように連携させるかというアーキテクチャーの部分です。それぞれのユースケースに応じて、どのようなサービスを利用して、どの部分の処理に LLM を利用するかという新しいアーキテクチャーを考えていくことが、こらからのアプリケーション開発者に求められるスキルになるでしょう。

実用レベルの生成 AI が登場してからまだ日も浅いので、今はまだ、さまざまなアーキテクチャーを試して、時には失敗もしながら、適切なアーキテクチャーの知見を貯めていくことが大切です。Google Cloud を活用して、オリジナルアーキテクチャーのアプリケーション開発にチャレンジしてください。

Google Cloud Japan

Discussion