ちょっと話題の記事

自然な対話で商品検索!OpenAI と全文検索エンジンで対話型ゆるふわ検索 AI アシスタントを作ってみた

ChatGPT(GPT-4)を対話のインタフェースに利用し、検索は自社で持つ商品 DB(OpenSearch)を組み合わせることで、店員さんと対話で商品を絞り込む体験を提供する商品検索AIアシスタントを作成したプロジェクトの結果報告です。
2023.06.12

こんにちは。CX 事業本部 Delivery 部のきんじょーです。

突然ですが皆さん、

ECサイトと実店舗での商品購入をどのように使い分けていますか?

私は欲しいものが決まっている場合、もっぱら Amazon を利用しています。
一方で、家具・家電や洋服など、詳しい店員さんに相談したい場合は実店舗に足を運ぶことが多いです。

両者の違いは何でしょうか?

自分の中に具体的な欲しいものが見えている場合、EC サイトでキーワードを入力して検索し、欲しい商品に辿り着くことができます。
自分の要求が曖昧な場合、キーワード検索では効率が悪く、店員さんとの対話の中で欲しいものを絞り込んでいく体験を望んで実店舗を選ぶ、というのが理由の 1 つです。
同じように感じられている方も多いのではないでしょうか?

クラスメソッドでは、OpenAI を用いたさまざまなサービスの技術検証に力を入れています。
その一環として、ChatGPT(GPT-4)を対話のインタフェースに利用し検索は自社で持つ商品 DB(OpenSearch) を組み合わせることで、店員さんと対話の中で商品を絞り込む体験を提供する AI アシスタントを実現できないかと、部門横断でデモ作成プロジェクトを進めていました。

その対話型ゆるふわ商品検索のデモが形になったので、このブログでご紹介します。

対話型ゆるふわ商品検索とは?

前述の通り、対話の中で商品を絞り込む体験を実現する、チャット形式の新しい検索方法です。
まずは検索のイメージをご覧ください。

ユーザーから曖昧な検索要求が自然言語で渡されると、AI アシスタントがそれを具体化する提案を行います。

アシスタントがベッドを提案してくれたので、セミダブルベッドと関連寝具をお願いしてみます。
すると、「ゆっくり眠れるように関連する寝具」というワードを、AI アシスタントが「掛け布団、枕、シーツ」という具体的なアイテムに変換して、商品情報を検索してくれました。

従来の EC サイトでは、ディレクトリ型に整理された商品情報を階層構造を辿って情報を見つけたり、キーワードで全文検索をかけて商品を見つける方式が一般的でした。
この方法では、ユーザーは自分の欲しい商品について「カテゴリ」や「キーワード」を知っている必要があります。

今回ご紹介した「対話形ゆるふわ検索」では、スタート時点でユーザーの頭の中に「初めての一人暮らしに必要なもの」という曖昧な要求しかありません。 その曖昧な要求をGPT-4 の自然言語処理能力と、学習した一般化されている知識を利用して絞り込むことで、まるで本当の店員と話しているような体験を実現しています。

どのように実現しているのか

ここからは検索の仕組みと、使用している技術について説明します。

検索の流れ

以下 3 つのステップで検索を行います。

  1. 検索キーワードの絞り込み
    • GPT-4 との対話を通じて、曖昧な要求を検索エンジンに投げられるキーワードへと落とし込みます
  2. 全文検索
    • OpenSearch にインデックスされた商品情報に対して、全文検索を実施します
  3. 応答調整
    • 返却された検索結果を元に AI アシスタントからの応答を調整し、ユーザーへ返却します

アーキテクチャ

フロントエンドは React を CloudFront で配信しています。 バックエンド API のアプリケーションレイヤーは App Runner を使用し、会話履歴の保存に DynamoDB、商品情報の検索に OpenSearch サービスを利用しています。

デモプロジェクトということもあり、当初は常時起動のコストがかからない API Gateway + Lambda の構成を採用していました。しかし、GPT-4 に言語モデルを切り替えると OpenAI からのレスポンスが API Gateway の強制タイムアウト(29 秒)より長くなるケースが多発し、App Runner へと移行しました。

本筋から少し外れますが、 FastAPI をLambda Web Adapterでコンテナ Lambda に乗せていたので、App Runner への移行がとても簡単でした。
Lambda に Web フレームワークを乗せるのは、アーティファクトサイズが増える事によるコールドスタート時間の増加や、IAM の最小権限原則に違反するため、かつてはアンチパターンでした。
しかし、

  • ローカル環境での開発・テストがしやすく、開発者体験が良い
  • API 本数が増えてもデプロイ時間が変わらない
  • API 本数が増えても CloudFormation の 1 スタックで定義できるリソースのクォータ(500 個)に引っかからない

といったメリットがあり、(まだ Java ランタイムのみですが) SnapStart 機能によりコールドスタート問題も解消しつつあるので、Web フレームワークを Lambda に乗せても良い時代が到来してきていると感じています。

実装時の検討ポイント

ベクトル検索 vs キーワード検索

LLM に独自の知識を埋め込む方法の 1 つに「インコンテキストラーニング」という手法があります。
この手法では、ユーザーの質問に関連する情報を、LLM に与えるプロンプトの中に加えることで LLM が持っていない情報を元に回答を生成できます。

この「ユーザーの質問に関連する情報」を取得する方法として、ベクトル検索があります。以下のようなフローです。

  1. 独自の知識をベクトル化し、事前にインデックスを作成しておく
  2. ユーザーからの質問をベクトル化し、インデックスに対してベクトル検索を行う
  3. ユーザーの質問に、意味的に関連する参考情報が得られる
  4. 参考情報を LLM に与えるプロンプトに付与して、LLM に回答を生成させる

プロジェクト開始当初は、このベクトル検索のアプローチを検討していました。
ユーザーの曖昧な要求を元に検索を行うことを考えると、キーワードでマッチするよりもベクトル DB に対して意味的に検索をかけた方が適切だと考えたからです。

しかし、以下の理由から今回はキーワードによる全文検索を用いることにしました。

  1. 曖昧な要求を先に明確にしないと、ユーザーが本当に欲しているものが見つからない
  2. 絞り込めたなら、絞り込んだキーワードをもとに、全文検索エンジンで検索が可能なはず
  3. EC サイトで扱うような商品在庫データをリアルタイムにベクトル DB に連携するのは現実的でない。結局検索エンジンへの問い合わせは必要になる
  4. ベクトル DB を使用するより、既存の EC サイトの仕組みに導入しやすい

LangChain は使わない

OpenAI をサービスに組み込む際、OpenAI が提供する SDK以外に、LangChainLlamaIndexというライブラリを使用する選択肢があります。
これらのライブラリを使用することで、インコンテキストラーニングや会話履歴の保持、プロンプトの管理など、自前で実装する処理を減らすことができます。

LangChain にはAgentという機能があり、ReActという推論(LLM 自身に何をすべきかを考えさせる)と行動を交互に繰り返させるフレームワークを実装しています。以下のようなフローです。

  1. LLM に対してツールを与える
    • Web 検索、Python の演算
  2. ユーザーの質問を解決するために、利用すべきツールを考えさせる
    • ユーザー「来週の東京の天気は?」
    • LLM「未来の情報は Web で検索する必要があるな?」
  3. LLM がツールを利用して外部の情報を取得する
    • Web 検索「東京 天気 来週」
    • 検索結果「月曜:雨, 火曜:雨, 水曜:曇り, 木曜: 晴れ, 金曜: 晴れ」
  4. 外部の情報をもとにユーザーの質問を解決する
    • LLM「来週の天気をお調べしました。週の前半は雨ですが、後半にかけて晴れ間が見えるでしょう」

2 と 3 は、ユーザーの質問が解決するまで繰り返します。
このように、LangChain のAgentを使用すると、ユーザーの質問を解決する自律型のエージェントを作成できます。

今回のデモでも、Agent に対して商品情報を検索するツールを与えて、自律型の AI アシスタントを構築しようと考えていました。

しかし、実際に触ってみると、会話履歴保存やプロンプトの与え方、最大トークン数の調整などカスタマイズしたくなる部分が多く、カスタムエージェントを実装する必要がありました。 また、今回やりたいことは「キーワードを絞り込む」「全文検索をかける」というシンプルな要件で、LangChain の Agent を使用するのはややオーバーエンジニアリングに感じました。
一方で、LLM からの出力をプログラムで扱える形にパースする処理や、初期のプロンプトの与え方など実装の参考になる箇所はあり、それらのコードを参考にしつつ自前で実装する選択肢を取りました。

ChatGPT(GPT-4)の出力調整

プロンプトによる調整

ChatGPT(GPT-4)は与えられた自然言語を処理して、自然言語の回答を生成する生成 AI です。 サービスに組み込む上で、GPT-4 の出力をプログラムで理解できる形に処理する必要があります。

そのために、最初に以下のようなプロンプトを与えています。

背景

あなたは家具用品の EC ストアの AI チャットアシスタントです。ユーザーの曖昧な要求から商品を探す役割を果たします。

指示

  1. ユーザーからの検索要求が曖昧な場合、明確な情報を得るためにユーザーに追加質問をしてください。
  2. ユーザーの要求が明確になったら、それを元に最適な商品を思い浮かべ、その商品を表す検索キーワードを決定します。このキーワードを使って OpenSearch に対して商品検索を実行します。

出力形式

以下に示す JSON 形式に従って応答を作成してください。返却する出力は JSON 文字列でなければなりません。 その文字列は JSON.loads を使用して直接解析可能であるべきです。このフォーマットの遵守は重要です。出力形式は次のとおりです:

  {
      "message": "",
      "keywords": []
  }

それでは始めます。

ユーザーの質問: 家具探しを手伝ってくれますか?

AI チャットアシスタントの回答: { "message": "もちろんです!何をお探しですか?", "keywords": [] }

このプロンプトには、以下の工夫を加えています。

  1. 家具用品の EC ストアという設定を与えて、提案する領域をあらかじめ絞っている
  2. プロンプトの構成要素を明確に定義する
  3. 「してはいけない」より「何をすべきか」を指示する
  4. JSON の出力形式の例を最後に One-Shot で念押しする

これらのプロンプトエンジニアリングの具体的なテクニックについては、以下の記事にまとめています。興味がある方は併せてご確認ください。

トークン数節約のため、上記のプロンプトを英語化しましたが、出力が安定しなくなったため日本語のままとしています。
実は上記のプロンプトでも出力形式を無視することがあり、ユーザーの発話を GPT-4 に渡す前に、毎回AIアシスタントの応答は最初に指定した「出力形式」に従いJSONで応答してくださいというシステムメッセージを挟んでいます。

アプリケーション側での調整

以上で応答は JSON 形式になったのですが、JSON に加えて自然言語のメッセージを付与してくることがありました。 応答から JSON だけを抽出するため、以下のようなヘルパー関数を噛ませています。

def _extract_json(self, str_contains_json: str):
    """
    OPENAIの応答の揺れを吸収し、JSONのみ抽出する
    """

    json_str = re.search(r"{.*}", str_contains_json, re.DOTALL)

    if json_str is None:
        return None

    json_obj = json.loads(json_str.group())

    return json_obj

見えた課題

UI/UX 改善

GPT-4のレスポンスはgpt-3.5-turboに比べ低速で、アシスタントから応答が返るまで数十秒ユーザーを待たせることがあります。

今回使用している OpenAI API のCreate chat completion APIには、レスポンスをストリームで返すオプションがあります。
本番運用するサービスに導入するには、ChatGPT と同じくタイピングされる形式でレスポンスを返したり、ユーザー体験を向上させる工夫が必要です。

検索の精度向上

検索の精度向上には 2 つの課題があります。

  1. GPT-4 が提案するキーワードが、商品 DB に含まれていない可能性がある
  2. 実際の商品 DB には特定の商品名や用語などが含まれており、一般的なキーワード検索ではマッチしない可能性がある

Fine-tuning

どちらの課題に対しても、GPT-4 を特定の商品データで Fine-tuning する方法が考えられます。 このアプローチについては、GPT-4 を Fine-tuning して商品分類させる興味深い記事があるので、詳しく知りたい方は以下の記事を参照ください。

データの整備

もう 1 つ必須なのは、商品 DB 側のデータの整備です。 最適な検索結果を返すためには検索しやすい形に OpenSearch 側のインデックスを整える必要があります。

ベクトル DB の活用

曖昧な要求の明確化は同じように GPT-4 を用いて行い、検索する先にベクトル DB を追加することで、より要望にマッチした検索ができるようになる可能性があります。 一方で、バックエンドで LLM や DB とのラウンドトリップ数が増加するため、レスポンス速度の問題も対応していく必要があります。

品質・性能評価方法

このサービス自体の品質・性能評価方法についての課題です。
ひととおり動くデモを作成するのがゴールなので、検索精度の向上や本格的なテストは行なっていません。

本番運用するサービス開発を考えた際、継続的に開発するのであればテストを自動化したり、品質を図る指標を持っておく必要があります。 しかし、対話のインタフェース部分は GPT-4 の応答に委ねているため、ランダム性が高く(パラメーターで調整は可能ですが)、決まりきったインプットとアウトプットの検証の自動化は困難です。

この課題については OpenAI が提供するモデルの性能評価フレームワーク「OpenAI Evals」を試してみたいと思います。
このフレームワークにはLLM の応答を LLM に検証させる面白いアプローチがあるので、ランダム性の高いアウトプットの評価を評価可能です。 モデルのアップデートに追従したり、プロンプト修正時の影響を測ったりできることを期待しています。

価値のある検証ができました

ChatGPT(GPT-4)に独自の知識を埋め込むことを考えた場合、まずは参考情報をベクトル化するアプローチが必要だと考えていました。
しかし、全文検索エンジンと組み合わせるだけでも、対話型で商品の絞り込み検索を行う仕組みを作ることができたことが、この技術検証の大きな成果でした。
事業会社が ChatGPT を組み込んだサービス開発を考える際、全文検索エンジンであれば既存の仕組みとの連携もしやすく、PoC の第 1 歩として検討しても良いでしょう。

もう 1 つの発見は、ChatGPT との共同開発の素晴らしさです。

  • プロジェクトの立ち上げ
    • ディレクトリ、レイヤー設計について指示を出して、ChatGPTから出力されたコマンドを叩いていき、プロジェクトのベースラインを構築していきました
  • 設計
    • 処理の流れを自然言語で伝えてシーケンス図を出力してもらい、ChatGPT と共通認識になるドキュメントを作成します
  • 実装
    • 共通認識がとれたところで、設計をもとにコードを出力してもらい必要に応じて手直ししました
  • テスト
    • 今回は書きませんでしたが、テスト対象のコードを渡すだけでかなり精度の高いテストコードを出力してくれます
  • デバッグ
    • エラーが発生した際には、エラー内容をそのまま ChatGPT に投げるだけで的確なアドバイスを返してくれることもあります

今回 FastAPI をはじめて使用しましたが、上記のサポートがあったおかげで、それほど公式ドキュメントを読み込むこともなくバックエンドの API が構築できました。
ChatGPT を変数・関数の命名といった壁打ち相手に使用している方は多いかと思いますが、ぜひがっつり ChatGPT と組んで開発をしてみてください。驚くべき生産性を得ることができます。

最後に、一緒にメインでプロジェクトを進めて下さった、長浜さんTanner さん中村さん北川さん、そしてPrismatix事業部の皆さん、この場を借りてお礼を申し上げます。
部門横断のプロジェクトはとても楽しかったです。またどこかで一緒に開発ができると嬉しいです!

この記事が少しでも誰かの役に立つと幸いです。
以上。CX 事業本部 Delivery 部のきんじょーでした。