Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

bolt.new はシーケンス図からどこまで自動生成できるか?

こんにちは、バックエンドエンジニアの前田です。
前回はbolt.newを用いて、画面イメージからどこまで画面を生成できるかを試してみました。
acro-engineer.hatenablog.com


画像を読み込み、ほぼイメージ通りに生成してくれることが分かったので、今回はシーケンス図を画像にしてbolt.newに渡し、どこまで生成してくれるかを試してみます。

1. 今回作成するアプリ

今回作成するアプリは、砂時計アプリです。
以下の画像のようなシーケンス図をdraw.ioやMermaid Graphical Editorなどを用いて作成し、作成した画像をbolt.newに渡して、砂時計アプリを生成してもらいます。
砂時計アプリの仕様としては、時間を1、3、5分から選択して設定し、スタート、一時停止、リセットボタンで操作できるようにします。
今回は、Mermaid Graphical Editorで、以下のようなシーケンス図を作成しました。

bolt.newに渡したシーケンス図

達成したいことは以下です。

  • シーケンス図に記載している機能を満たし、動くアプリを作成すること
  • メソッド名もシーケンス図に準拠すること

砂時計アプリは、言語はTypeScript、フレームワークはReactを用いて生成しています。
また、今回はフロントエンドのみで動作するWebアプリとしました。

2. アプリ作成

2.1. シーケンス図を指定してアプリを生成させる

シーケンス図の画像と以下のプロンプトを指定して生成させました。

砂時計アプリを作成してください。
以下の要件に従うこと。

# 要件
・画像のシーケンス図に準拠すること。
・メソッド名もシーケンス図に準拠すること。

以下のような画面が生成されました。

生成された砂時計アプリ

シーケンス図の指定のみで、画面の指定はしていませんが、使いやすい画面を生成してくれました。
シーケンス図で指定した設定時間の選択や、スタート、一時停止、リセットボタンの生成はできていました。

2.2. 生成結果の詳細

シーケンス図を指定して生成させた結果、ロジック部分は以下のようなコードが出力されていました。

import { TimerOptions } from '../types';

export class TimerLogic {
  private remaining_time: number = 0;
  private timer_id: number | null = null;
  private options: TimerOptions | null = null;

  getTimerOptions(): TimerOptions | null {
    return this.options;
  }

  getRemainingTime(): number {
    return this.remaining_time;
  }

  setTimer(time: number): void {
    this.remaining_time = time;
    this.saveToStorage();
  }

  start(): void {
    if (!this.timer_id) {
      this.timer_id = window.setInterval(() => {
        if (this.remaining_time > 0) {
          this.remaining_time--;
        } else {
          this.reset();
        }
      }, 1000);
    }
  }

  pause(): void {
    if (this.timer_id) {
      window.clearInterval(this.timer_id);
      this.timer_id = null;
    }
  }

  reset(): void {
    this.pause();
    this.remaining_time = 0;
    this.saveToStorage();
  }

  private saveToStorage(): void {
    localStorage.setItem('timer_options', JSON.stringify({
      remaining_time: this.remaining_time,
    }));
  }

  loadFromStorage(): void {
    const stored = localStorage.getItem('timer_options');
    if (stored) {
      const options = JSON.parse(stored);
      this.remaining_time = options.remaining_time;
    }
  }
}
シーケンス図で指定した内容 生成結果
クラス 想定通り出力されていた
メソッド名 3語以上のメソッド名は少し指定したものと異なっていた。getTimeOptionsとしていたメソッドがgetTimerOptionsとなっていた。シーケンス図で指定したものとは違っていたが、より適切な名称になっていた。
メソッドの内容 一部異なっていた。countTimeメソッドでカウントを更新するようにしたかったが、startメソッドに組み込まれていて、countTimeメソッドが存在しなかった
処理の流れ 基本的には想定通りだが、設定時間の保存がresetメソッド実行時のみに指定していたが、setTimerメソッド実行時にも設定時間を保存していた。処理自体は問題なかった

3. まとめ

今回は、bolt.newにシーケンス図を渡して、どこまで生成してくれるかを試してみました。
結果としては基本的な機能は実装してくれていて、動くことを確認できました。
メソッド名やコメントなど、長い文章を指定している場合、指定した内容とは、多少異なる内容が生成されることもあるようです
また、処理が一部想定と異なっている部分がありましたが、今回は、とても簡単なプロンプトを指定しただけだったので、実際には、より具体的な内容をプロンプトで指定すると良いと思われます。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com

bolt.new は画面イメージからどこまで自動生成できるか?

こんにちは、バックエンドエンジニアの前田です。
正月お餅を食べすぎたので、体重が跳ね上がっていないか心配です。

さて今回は、bolt.new に対して、画面イメージを元に、どこまで画面生成を簡単に行えるか、試してみました。
画面のレイアウトがあっても、イメージしづらい部分はありますが、実際に動く画面を素早く作れるならイメージもしやすいと思います。
bolt.newに関しては、以下を参考にしてください。
acro-engineer.hatenablog.com

1. 今回生成する画面について

図書館での本の一覧や登録削除、貸し借りを想定するWebアプリ「図書管理システム」のフロントエンド部分を生成させます。
具体的には、PowerPointなどのお絵描きツールで作成した以下の4枚の画面イメージ画像をbolt.newに渡し、同じレイアウトの画面を持つアプリを生成してもらうことを目標にします。

図書検索画面
新規追加ポップアップ
検索後画面
利用者一覧画面

達成したいことは、以下です。

  • ヘッダーやサイドメニュー、ボタンなどの基本的なレイアウトを再現すること
  • テキストボックス、プルダウン、チェックボックスなどを再現すること
  • 色を認識し、同じ色で画面を生成すること

また、今回は画面(フロントエンド)のみの生成で、言語はTypeScript、フレームワークはReactを用いて生成してみます。

2. 画面イメージを元に、画面を生成させる

一枚ずつ画面イメージを指定して、細かい機能の説明はプロンプトで指示し、生成させていきます。

2.1. 図書検索画面と新規追加ポップアップの生成

まずは、図書検索画面と新規追加機能を生成させるために「図書検索画面」、「新規追加ポップアップ」の画面イメージ画像と、以下のようなプロンプトを入力しました。

画像をもとに、図書管理システムを作成してください。
以下の要件を満たすようにしてください。

# 要件
・画像のような基本画面を作成すること。
・あとで機能追加するため、サイドメニューは画像に準拠すること。
・「新規作成」ボタンを押した際、画像のようなポップアップが出るようにすること。
・図書検索画面は基本的に、検索にしか使わない想定のため、検索する前は、本を一覧で出す必要はない。
・検索後の機能については、後で指定するため、現状は何も実装しなくて良い。

結果は以下のような画面になりました。

生成された図書検索画面
生成された新規追加ポップアップ

以下のことを確認できました。

  • ヘッダー、サイドメニュー、新規追加ボタンが存在する
  • テキストボックス、プルダウン、チェックリストが再現されている
  • 色が指定した通りの色になっている

いくつか修正したい内容があったため、以下のプロンプトで修正させました。

以下の点を修正してください。
1. ヘッダーを「図書管理システム」に変更すること。
 また、図書管理システムっぽいアイコンを追加すること。
2. 「サイドメニュー」文字は削除すること。
3. 「新規作成」のポップアップにキャンセルボタンを追加すること。

以下の画像のヘッダーの赤枠で囲っている部分の文字の修正とアイコンの追加、矢印で示した部分から「サイドメニュー」という文字の削除、新規追加ポップアップに赤枠で囲った部分にキャンセルボタンが追加されました。

修正後の新規追加ポップアップ

2.2. 検索後画面の生成

上記で、達成したいことは確認できましたが、もう少し難しいレイアウトも再現してくれるか試したいため、「検索後画面」の画面イメージ画像と、以下のようなプロンプトを指定し、検索後画面を生成させました。

本を検索した後は、画像のような検索された図書一覧画面に遷移するようにしてください。
要件は以下です。

# 要件
・検索ボタンを押すまで、登録された本を表示しない。
・検索画面に戻るボタンを作ること。

以下のように検索後画面が生成されました。

生成された検索後画面

再現しづらいレイアウトだと思いますが、同じように再現してくれました。

2.3. 利用者一覧画面の生成

最後に、「利用者一覧画面」の画面イメージ画像と以下のようなプロンプトを指定して、サイドメニューで切り替えられるようにします。

画像のような利用者一覧画面を作成してください。
要件は以下です。

# 要件
・サイドメニューの「利用者一覧」をクリックすると利用者一覧画面に遷移するようにすること。
・新規ユーザー追加ボタンを押すと、ユーザー名と電話番号を登録するポップアップを表示すること。

以下のように利用者一覧画面が生成され、サイドメニューで切り替わることも確認しました。

生成された利用者一覧画面
生成された新規ユーザー追加ポップアップ

3. まとめ

今回はbolt.newを用いて、画面イメージ画像から画面を生成させてみました。
画面イメージ画像の作成から、実際に画面を生成させるのに、合計1時間ほどで、これらの画面を生成させることができました。

画面イメージの内容 生成結果
基本的な画面レイアウト ヘッダー、サイドメニュー 画面イメージ通りに生成でき、プロンプトで指定して、ヘッダーのアイコンやサイドメニューの切り替えもできた
複雑な画面レイアウト 検索後画面で図書をカード型で表示する ステータスを表示するタグも含めて画面イメージ通りに生成できた
入力フォーム ポップアップ、テキストボックス、プルダウン、チェックボックス、ボタン 画面イメージ通りに生成できた
画面やボタンの配色 ヘッダーの青色や新規追加ボタンのオレンジ色 画面イメージ通りに生成できた
画面イメージ内のコメント チェックボックスと1冊ごとにIDを付与する チェックボックスはイメージ通りだが、1冊ごとにIDを付与することはできなかった

画像の中のコメントは難しい内容だと、あまり反映してくれないのかもしれません。
しかし、ほぼ画面イメージ通りに生成でき、検索や新規追加などの動作も確認できました。
今回は実際の画面を素早く作成することが目標だったため、バックエンド部分は実装していませんが、誰かに見せて説明したり、実装者のイメージを明確にしたりするには十分かと思います。
他にもbolt.newを使ってできることは多いので、どんどん利用して、効率的に開発を進めていきたいですね。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com

2024年アドベントカレンダー振り返り、今年は生成AIと検索が大反響

皆さんこんにちは
Acroquestのデータサイエンスチーム「YAMALEX」のチームリーダ、@tereka114です。
YAMALEXチームでは、コンペティションへの参加や自社製品開発、技術研究などに日々取り組んでいます。

今年も、アドベントカレンダーとして、9つの記事を本ブログで投稿しました。
本記事では、今年の記事の傾向や、私個人が特に面白いと感じた記事を紹介します。

本ブログの今年のアドベントカレンダー記事投稿傾向

今年は生成AI関係(主にBedrock)とElasticsearchの記事を執筆しました。
生成AI関係はここ数年、技術の進歩が活発になっており、今年のアドベントカレンダーでも多数の記事が投稿されていました。
そのため、アドベントカレンダーに限らず、当ブログにおいても、非常に執筆記事が増えました。

今年、本ブログで投稿した、アドベントカレンダー記事は以下の通りです。

日付 タイトル Advent Calendarのテーマ
12/11 ElasticsearchでLIKE検索のような部分一致検索を高速に実現する方法 - Taste of Tech Topics
12/13 Elastic CloudでObservabilityを簡単に始める 2024年版 - Taste of Tech Topics
12/16 Amazon Bedrock の新モデル Amazon Nova の精度を確認してみた - Taste of Tech Topics
12/17 Amazon Bedrock Knowledge Baseのクエリフィルター自動生成で検索の精度を向上させる - Taste of Tech Topics
12/17 Elasticsearchのハイブリッド検索を用いて高精度なRAGを簡単に実現する - Taste of Tech Topics
12/23 Amazon Bedrock Knowledge Base の構造化データ取得はどこまで複雑なクエリに対応できるか - Taste of Tech Topics
12/24 Difyの基本!ワークフローとノードのおさらい - Taste of Tech Topics
12/24 vLLMを利用したLLM推論高速化テクニック - Taste of Tech Topics
12/25 PDFドキュメントを画像のまま検索できるColQwen2でマルチモーダル検索を試す - Taste of Tech Topics

特に読んで欲しい記事3選

アドベントカレンダーに投稿した記事の中で私が特に読んで役立つと思った記事を3つ紹介します。

ElasticsearchでLIKE検索のような部分一致検索を高速に実現する方法

MySQLPostgreSQLなどのRDBではLIKE検索は可能ですが、LIKE検索の速度は非常に遅いです。
Elasticsearchは柔軟な検索エンジンであるため、LIKE検索をサポートしていますが、そのLIKE検索の性能を十分に引き出すには工夫が必要になります。
記事では、LIKE検索を実現するためのいくつかのパタンを紹介していますので詳細はぜひ、御覧ください。

acro-engineer.hatenablog.com

PDFドキュメントを画像のまま検索できるColQwen2でマルチモーダル検索を試す

PDFドキュメントに画像が含まれている場合、単純な抽出では正しく検索できません。
その場合、PDFのページを画像化するのが一つの手段です。

本記事では、ドキュメントをVLM(Vision Language Model)で埋め込むColPailに対して、複数言語対応モデル(日本語込)のQwen2-VLを組み込んだColQwen2を活用してPDFに対する画像検索を実施することで期待のページを検索できました。
詳しい内容はこちらの記事を御覧ください。
acro-engineer.hatenablog.com

vLLMを利用したLLM推論高速化テクニック

LLMをHuggingface(transformers)で推論すると現実的に難しい計算時間がかかることがあります。
しかし、本記事で紹介しているvLLMを利用すると、高速に推論できます。
また、vLLMは推論に関する便利な機能を多く実装されており、高速に推論するのみならず、CPUオフロードなどの巨大モデルを運用する機能やGPUメモリ節約のための量子化対応も備わっています。

細かいテクニックは、記事に掲載しましたので御覧ください。

acro-engineer.hatenablog.com

最後に

今年のアドベントカレンダーを紹介してきました。
非常に面白い記事が多く、生成AIをはじめとした検索などの関連技術が大きく伸びた1年に思えます。

来年も引き続き良い情報を提供していきたいと思います!

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com

PDFドキュメントを画像のまま検索できるColQwen2でマルチモーダル検索を試す

こんにちは。データサイエンスチームYAMALEXの@Ssk1029Takashiです。
完全に年末ですが、最後まで油断せずに年を越したいですね。

このブログは、LLM・LLM活用 Advent Calendar 2024の25日目になります。
qiita.com

今年は1年間RAGを扱う多くの事例に触れさせていただきましたが、どこでも難しいのが図や画像が使われているドキュメントですね。
ただのPDFであれば、pdfminerなどのライブラリを使用して文字を読み取り検索すればよいですが、画像が使われるとそう簡単にもいきません。
画像部分からはOCRを使って文字を取り出すなど、工夫が必要になります。

今回はドキュメントを画像にした状態で検索可能にするColQwen2というモデルを使って日本語ドキュメントの検索を試してみます。

ColQwen2とは

手法自体はColPaliというモデルがもとになっています。
ColPaliは画像も扱えるVMLとColBERTを組み合わせたモデルになります。
arxiv.org
ColBERTとは詳細は省きますが、BERTを使って効率的に精度の良い文章のEmbeddingを作成する手法です。
arxiv.org

ColPaliはEmbeddingを計算するモデルに、画像に対応したVision LLMを使用することで、LLMの画像解釈能力を生かしつつ、テキストと画像を同じベクトル空間に表現できます。
ColPaliの段階ではモデルとしてPaliGemmaが使用されていたので、日本語には対応していませんでしたが、ColQwen2ではQwen2-VLが使用されているため、多言語対応の一部として日本語でも使用できます。

https://huggingface.co/vidore/colpali

実験

今回は製造業を巡る現状と課題 今後の政策の方向性という経済産業省の一般公開されている資料を基に検索を試してみます。
流れとしては以下になります。

  1. pdfをページごとの画像に変換する
  2. 各画像をColQwen2を使ってベクトルに変換する
  3. テキストも同じくベクトルに変換して、類似度が高いページを検索する

また、基本的なサンプルコードは以下のページで紹介されているため本記事では部分的に重要なコードを紹介します。
huggingface.co

環境

今回は以下の環境で実行しています。

項目
OS Ubuntu24.04
Python 3.11.9
GPU RTX4090

GPUはRTX4090を使っていますが、推論だけであればVRAMは8GB程度あれば実行可能です

PDFドキュメントのEmbeddingを作成する

それでは実際に実装に入っていきます。
colpaliやcolqwen2を使うにはcolpali_engineというライブラリが便利です。
PDFの画像変換、ベクトル作成は以下のコードで実行できます。

# PDFを画像に変換
images = convert_from_path(pdf_path)

# プロセッサ作成
processor = ColQwen2Processor.from_pretrained("vidore/colqwen2-v0.1")

# モデルの読み込み
model = ColQwen2.from_pretrained(
    "vidore/colqwen2-v0.1", 
    torch_dtype=torch.bfloat16, 
    device_map="cuda"
)

# 2ページずつ読み込んで変換する
dataloader = DataLoader(
    images,
    batch_size=2, 
    shuffle=False, 
    collate_fn=lambda x: processor.process_images(x)
)

embedding = []
for batch_doc in tqdm(dataloader):
    with torch.no_grad(): 
        batch_doc = {k: v.to("cuda") for k, v in batch_doc.items()} 
        embeddings_doc = model(**batch_doc) 
    embedding.extend(list(torch.unbind(embeddings_doc))) 

# ベクトル情報を保存
torch.save(embedding, "embedding.pt")

この結果としてembedding.ptというファイルが生成されれば完了です。

検索を試してみる

それでは例として「国製造業の売上高は?」というクエリで検索を試してみます。
流れとしては、事前に作ったベクトルをロードした後、クエリテキストから作成したベクトルと最も類似度が高いページを計算しています。

query = "国製造業の売上高は?",

# モデルの読み込み
model = ColQwen2.from_pretrained(
    "vidore/colqwen2-v0.1", 
    torch_dtype=torch.bfloat16, 
    device_map="cuda"
)

# プロセッサの作成
processor = ColQwen2Processor.from_pretrained("vidore/colqwen2-v0.1")

# クエリのベクトルを作成
processed_query = processor.process_queries([query]).to(model.device)
with torch.no_grad():
    query_embedding = model(**processed_query)

# 事前に作成したPDFのベクトルをロード
embedding = torch.load("./embedding.pt", weights_only=True)

# クエリのベクトルとPDFのベクトルで類似度を計算
scores = processor.score_multi_vector(query_embedding, embedding)[0]

# トップ5のページを取得
scores_indices = scores.argsort().tolist()[-5:][::-1]
print([x + 1 for x in scores_indices])

上記を実行すると以下の結果を得られます。

[5, 7, 10, 6, 12]

トップはP5となっているので、P5の内容は以下のようになっています。

これを見ると売上高について言及しているページを取れているようです。
また、上記の場合は文字情報から類似度が高いページを選んでいましたが、それ以外の情報ではどうでしょうか?
例えば、「世界地図」というワードで検索してみましょう。
結果としては以下のページが出力されます。

[66, 65, 56, 116, 69]

P66は以下のようになっています。

「世界地図」というワードはページに含まれていませんが、世界地図が載っているページを取得できています。
つまり、Qwen2-VLモデルがページに世界地図が載っていることを認識できるため、ベクトルに反映されているようです。

このように、ColQwen2を使うことでドキュメントに載っているテキストやグラフの情報をもとに、複雑な処理をせずとも同じベクトル空間で検索可能になります。

まとめ

今回はColQwen2を使ってPDFドキュメントを画像として検索する手法を試しました。
複雑なデータ前処理なく載っているテキストや図表の情報をまとめて検索できるのはかなり便利そうです。
テキストと画像を同じベクトル空間で検索する手法はCLIPなどもあるのでそれらとの比較してみるのも面白そうです。
マルチモーダル検索は今後も注目の領域ですね。
それではまた。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com

vLLMを利用したLLM推論高速化テクニック

皆さんこんにちは
Acroquestのデータサイエンスチーム「YAMALEX」のチームリーダ、@tereka114です。
YAMALEXチームでは、コンペティションへの参加や自社製品開発、技術研究などに日々取り組んでいます。

大規模言語モデル(通称:LLM)は近年、非常に注目される技術となりました。
ただ、7Bや13Bといった巨大モデルのパラメータは推論時間も長時間で計算時間の面からも運用が非常に難しいです。
しかし、vLLMを使えば、高速化できます。本記事では、推論をどこまで高速化できるのかを検討したいと思います。

※本記事はLLM・LLM活用のAdvent Calendar 24日目の記事です。
qiita.com

vLLMとは?

vLLMは大規模言語モデル(LLM)の推論とサービングを高速かつ効率的に行うためのライブラリです。
計算コストの高いTransformerで利用されるAttentionについて、Paged Attentionと呼ばれるアルゴリズムで計算することにより、高速化を実現しています。

それ以外のvLLMの良いポイントとして、
1. 有名どころのモデルはカスタム実装不要で計算が可能
vLLMは、Huggingfaceの主要モデルの多くについて、推論をサポートしているため、簡単に扱えることです。
主要なモデルであれば、モデル特有の部分の実装は不要です。
また、量子化手法であるGPTQ, AWQに加え、最新版であればBitsandBytesにも対応しており、、巨大モデルを推論できる可能性が高いです。

2. リソース管理が容易
複数のGPUが存在していた場合もライブラリ側がコントロールするため、マシンのリソースを殆ど意識することなく、使える点が便利です。
また、CPUへのオフロード機能が備わっているため、GPUリソースで足りない場合にもvLLMを使って高速化できる見込みがあります。

github.com

vLLMによる高速化実践

vLLMを利用しない場合

まずは、Huggingfaceを利用した実装を使います。
今回は質疑応答データセットであるJSQuADテストデータ、4482件のデータに対して推論を実行します。

huggingface.co

モデルは多言語で学習され、日本語に対応しているQwen2.5の7Bを利用します。

huggingface.co

from datasets import load_dataset
from tqdm.auto import tqdm
from transformers import AutoTokenizer, AutoModelForCausalLM
 
dataset = load_dataset("shunk031/JGLUE", name="JSQuAD")
model_name = "Qwen/Qwen2.5-7B-Instruct"
df = dataset["validation"].to_pandas()
df["assistant_content"] = df.apply(lambda x: "\nQuestion:" + x["question"] + "\nContext: " + x["context"] + "\nOutput:", axis=1)

tokenizer = AutoTokenizer.from_pretrained(model_name)
prompts = [tokenizer.apply_chat_template([{"role": "system", "content": "Contextを参考にして、問題に回答してください。"}, 
                                          {"role":"user", "content": content}], tokenize=False) for content in df["assistant_content"].to_list()]
 
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    device_map="auto",
)
 
inputs = tokenizer(prompts, return_tensors="pt", padding=True)
 
generated_tokens = []
for text in tqdm(texts):
    inputs = tokenizer(text, return_tensors="pt", padding=False)
    generated_tokens.append(model.generate(
        input_ids=input_id.cuda().reshape(1, -1),
        attention_mask=attention_mask.cuda().reshape(1, -1),
        max_new_tokens=100
    ))

24h経過しても終了しませんでしたが、進捗具合から、おおよそ、92h必要になるようです。
これを基準にvLLMを使って高速化します。

vLLMを利用する

vLLMを利用する場合、次のとおりです。
vLLM特有の部分は非常にシンプルで、基本、LLMクラスで初期化して、generateで生成します。

from datasets import load_dataset
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer, AutoModelForCausalLM

dataset = load_dataset("shunk031/JGLUE", name="JSQuAD")
model_name = "Qwen/Qwen2.5-7B-Instruct"
df = dataset["validation"].to_pandas()
df["assistant_content"] = df.apply(lambda x: "\nQuestion:" + x["question"] + "\nContext: " + x["context"] + "\nOutput:", axis=1)

tokenizer = AutoTokenizer.from_pretrained(model_name)
prompts = [tokenizer.apply_chat_template([{"role": "system", "content": "Contextを参考にして、問題に回答してください。"}, 
                                          {"role":"user", "content": content}], tokenize=False) for content in df["assistant_content"].to_list()]
 
llm = LLM(
    model=model_name,
    gpu_memory_utilization=0.95,
    dtype='half',
    enforce_eager=True
)
 
sampling_params = SamplingParams(
        temperature=0.0,
        top_p=0.9,
        max_tokens=1000
    )
outputs = llm.generate(prompts, sampling_params, use_tqdm=True)


この実装で、281秒です。前回の92時間といったスケールとは大きく異なり、現実的に計算できる時間になってますね。

vLLMに加えてAWQを利用して量子化する

vLLMはAWQやGPTQをはじめとしたLLMの量子化テクニックを使った推論が可能です。
AWQ/GPTQを利用することで、GPUメモリの消費量を大きく削減できるため、限られたリソースの中で、巨大なモデルを高速に推論できるメリットがあります。
AWQでのvLLMの推論を利用するには次のとおりです。事前にAWQに量子化すると読み込みのみの時間になるため、おすすめします。

huggingface.co

llm = LLM(
    model=model_name,
    quantization="awq",
    gpu_memory_utilization=0.95,
    dtype='half',
    enforce_eager=True,
)

こちらは、360秒です。vLLMをAWQなしで実施する場合と比較して、遅くはなりました。
しかし、GPUメモリが削減されることもあり、巨大モデルを有効に利用できます。

Auto Prefix Caching

「以下の質問に回答してください。」など、先頭に必ず含まれるプロンプトが存在する場合、そのプロンプト分の計算結果を使い回すことで計算時間を削減することが可能です。
この機能を実現しているのがAuto Prefix Cachingです。
例えば、特定のタスクに関してバッチで解く場合、同じプロンプトになる箇所が含まれるため、計算量の削減が期待できます。

実装も非常に簡単で、LLMの初期化のオプションを追加するのみです。

llm = LLM(
    model=model_name,
    gpu_memory_utilization=0.95,
    dtype='half',
    enforce_eager=True,
    enable_prefix_caching=True
)

今回はOne-Shot Sampleを想定して、先頭に問題と答えのサンプルを載せて精度向上を試みます。
この先頭のサンプルが毎回の推論で同じになるので、この部分の高速化を本オプションで期待できます。

HEAD_PROMPT = "\n\nQuestion: 新たに造られた語のことを新語または何という?\nContext:造語 [SEP] 造語(ぞうご)は、新たに語(単語)を造ることや、既存の語を組み合わせて新たな意味の語を造ること、また、そうして造られた語である。新たに造られた語については、新語または新造語とも呼ばれる。\nOutput: 新造語"
 
prompts = [tokenizer.apply_chat_template([{"role": "system", "content": "Contextを参考にして、問題に回答してください。" + HEAD_PROMPT}, 
                                          {"role":"user", "content": content}], tokenize=False) for content in df["assistant_content"].to_list()]

データのサンプル

<|im_start|>system\nContextを参考にして、問題に回答してください。\n例\nQuestion: 新たに造られた語のことを新語または何という?\nContext:造語 [SEP] 造語(ぞうご)は、新たに語(単語)を造ることや、既存の語を組み合わせて新たな意味の語を造ること、また、そうして造られた語である。新たに造られた語については、新語または新造語とも呼ばれる。\nOutput: 新造語<|im_end|>\n<|im_start|>user\n\nQuestion:日本で梅雨がないのは北海道とどこか。\nContext: 梅雨 [SEP] 梅雨(つゆ、ばいう)は、北海道と小笠原諸島を除く日本、朝鮮半島南部、中国の南部から長江流域にかけての沿海部、および台湾など、東アジアの広範囲においてみられる特有の気象現象で、5月から7月にかけて来る曇りや雨の多い期間のこと。雨季の一種である。\nOutput:<|im_end|>\n

同じ条件でenable_prefix_cachingのTrue/Falseで次のような性能変化があります。
296s→189s

その他

CPU Offload

vLLMはGPUメモリに乗り切らない場合、CPUのオフロード機能を利用した推論が可能です。
例えば、この機能により、AWQを利用しても載らない24GB程度のGPUメモリでQwen2.5-72Bなどの大きなモデルの推論ができるので役立ちます。
実装は次のとおり、パラメータの設定を変更するのみで対応ができます。ただし、全てをGPUに乗せて推論するほどの性能は出ません。試しにQwen2.5の7Bで実施すると1200秒(約4-5倍)の計算時間が必要となりました。

llm = LLM(
    model=model_name,
    quantization="awq",
    gpu_memory_utilization=0.95,
    dtype='half',
    enforce_eager=True,
    cpu_offload_gb=8,
    swap_space=1,
)

最後に

本記事ではvLLMの高速化実践のテクニックをご紹介しました。
推論に関する有用な機能が様々使えますので、vLLMを有効に活用して快適なLLMライフを送りましょう。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com

Difyの基本!ワークフローとノードのおさらい

こんにちは、技術イベントに参加してきてアウトプット欲が高まっているハヤトです。
この記事は Dify Advent Calendar 2024 のシリーズ2、24日目の投稿です。

最近も精力的にアップデートが重ねられているDifyですが、
その中心的な機能であるワークフローとノードについて改めて基本的な内容を確認していきたいと思います。

実際に動作するワークフローを構築しながら説明するので、
Dify初心者の方も是非ご一読ください。
※本記事はDifyバージョン0.13.1時点のものです。

Difyについて

DifyはオープンソースのLLMアプリ開発プラットフォームです。
dify.ai

Difyの環境構築と基本的な使い方については以下の記事を参考にしてみてください。
acro-engineer.hatenablog.com

Difyの「ワークフロー」とは

Difyの「ワークフロー」は、小さな処理のステップ(ノード)を組み合わせることで一連のタスクを自動化することができる機能です。
以下のイメージのように、視覚的にノードを配置してつなぎ合わせることでワークフローを構築することができます。

非常に柔軟な機能なので様々なユースケースに適用できますが、例えば以下のようなことが実現できます。

  • カスタマーサービス:よくある質問への自動応答を作成し、サポートチームの負担を軽減する。
  • コンテンツ生成:与えられたアウトラインやテーマをもとに、ブログ記事や製品説明、マーケティング資料などを作成する。
  • データ分析とレポート:大規模なデータセットを分析し、レポートや要約を生成する。

Difyワークフローの種類と使い分け

Difyアプリを最初から作成する際に、タイプ選択の中に「チャットフロー(Chatflow)」と「ワークフロー」があります。
これらがDifyで利用可能なワークフローで、それぞれの差分は以下のようになっています。

チャットフロー ワークフロー
適用シナリオ カスタマーサービス、セマンティック検索、その他の対話型アプリケーションの構築において、複数のステップを含む論理を必要とする対話型シナリオに適している。 自動化とバッチ処理のシナリオに対応し、高品質な翻訳、データ分析、コンテンツ生成、メール自動化などのアプリケーションに適している。
固有のノード プロセス終了時にのみ選択可能な終了ノードがある。 プロセス中間ステップでも使用できる回答ノードがあり、テキストのストリーミング出力が可能。
使用する変数 開始時に実行変数を定義し、プロセス中で複雑な処理ロジックを実現するために使用する。 sys.query、sys.files、sys.conversation_id、sys.user_idなどの内蔵変数が使用され、ユーザーとの対話履歴を保持するメモリ機能も使用可能。

まとめると、チャットフローは対話型のシナリオ向け、ワークフローは自動化やバッチ処理のシナリオ向けで、
利用可能なノードもそれに応じたものになっているという形ですね。

参考:
docs.dify.ai
docs.dify.ai

ノードについて

ワークフローで利用可能な全ノードの概要は以下の通りです。

Icon ノード名 英名 説明
開始 Start ワークフローの初期パラメータを定義する。
終了 End ワークフローの最終出力内容を定義する。
返信 Answer チャットフロー内の回答内容を定義する。
大規模言語モデル LLM 大規模言語モデルを呼び出して質問に答えたり、自然言語を処理したりする。
ナレッジ検索 Knowledge Retrieval 質問に関連するテキストをナレッジベースから検索し、後続のLLMノードで使用する。
質問分類器 Question Classifier 分類の説明を定義することで、LLMがユーザーの入力に基づいて適切な分類を選択できるようにする。
条件分岐 IF/ELSE if/elseの条件に基づいてワークフローを分岐する。
コード実行 Code Python / NodeJSコードを実行してワークフロー内でデータ変換などのカスタムロジックを実行する。
テンプレート Template Jinja2のPythonテンプレート言語を利用して、データ変換やテキスト処理を柔軟に行う。
変数集約 Variable Aggregator 複数の分岐の変数を1つの変数に集約して、下流ノードで参照およびアクセスできるようにする。
パラメーター抽出器 Parameter Extractor LLMにより構造化パラメーターを推論し、後続のツール呼び出しやHTTPリクエストに使用する。
イテレーション Iteration リストオブジェクトに対して複数回のステップを実行し、すべての結果を出力する。
HTTPリクエス HTTP Request HTTPプロトコルを介してサーバーリクエストを送信し、外部サービスとの連携を実現する。
-
ツール Tools Dify内蔵ツール、カスタムツール、サブワークフローなどをワークフロー内で呼び出す。
変数代入 Variable Assigner 変数(会話変数など)に指定した値を代入する。
リスト操作 List Operator 配列変数からフィルタやソート条件に基づいて要素を抽出する。
テキスト抽出ツール Document Extractor アップロードした文書ファイルの情報を解析して読み取り、テキストに変換してLLMで使用する。

docs.dify.ai

ここでは、LLMを活用するノードについて実際に動作するワークフローを構築しながら詳細を確認していきます。

ナレッジ検索

ナレッジベースからユーザーの質問に関連する内容を検索し、それを後続のLLMノードのコンテキストとして使用することができます。
一般的なシナリオとして、外部データ/ナレッジに基づくAI質問応答システム(RAG)を構築するために使用します。

ナレッジ検索を使用したシンプルなアプリケーションの例を示します。

※翻訳が揺れていますが、ナレッジ検索のブロック名は「知識取得」になっています。

このアプリでは、IPAが公開しているデジタルスキル標準のPDFをナレッジとして使用し、それに基づいて回答を行います。

作成の手順を見ていきましょう。

ナレッジの作成

ナレッジ検索を使用するには、まずナレッジを作成する必要があります。
Difyの「ナレッジ」ページで「ナレッジ作成」を選択し、データソースの選択から「テキストファイルからインポート」でPDFをアップロードします。
今回は簡易的にナレッジ検索を試すため、インデックスモードは「経済的」を選択し、他は変更せずに「保存して処理」を実行します。

※ナレッジベースの詳細を知りたい方は以下を参照してみてください。
docs.dify.ai

作成したナレッジが使用可能になったら、ワークフローを構成していきましょう。

ナレッジ検索を使用するワークフローの構成

アプリの「最初から作成」でチャットフローを作成します。

LLMノードの手前にナレッジ検索(知識取得)のブロックを追加し、先ほど作成したナレッジを使用するように設定します。

次に、後続のLLMノードで、取得したナレッジをコンテキストとして使用するように設定します。

SYSTEMプロンプトとして以下のような内容を与えることで、ナレッジに基づいた回答のみを行うアプリを構築できます。

{{#context#}}
---
ユーザーからの質問に上記のコンテキストに基づいて回答してください。
コンテキストに含まれない情報については回答しないでください。。

プレビューから質問をしてみると、期待通りナレッジに基づいた回答をしてくれました。

質問分類器

LLMを使用して、与えられた分類の定義に対してユーザーの入力がどれに属するものかを判定し、処理を分岐することができます。
例えばカスタマーサービスのアプリを構築することを考えると、
ユーザーの質問が「製品について」なのか「アフターサービスについて」なのかを分類し、
それぞれに適したナレッジ検索を行うことで回答の精度を上げることができます。

先ほど作成したナレッジ検索の例を拡張して、以下のようなアプリを構築してみます。

  1. デジタルスキル標準に関する質問にはナレッジに基づいて回答する。
  2. それ以外のITに関する質問にはLLMを使って回答する。
  3. IT関連以外の質問には「IT以外の質問についてはお答え出来ません。」と回答する。


質問分類器の設定

開始ブロックの直接的に質問分類器を追加して、以下のようにクラスを設定します。

クラス1:「デジタルスキル標準に関する質問」には先ほど作成したナレッジ検索のワークフローを接続します。
クラス2:「IT全般に関する質問」には新しくLLMブロックを追加して接続します。
クラス3:「その他の質問」には固定の回答を返す回答ブロックを接続します。

これで、質問の分類に応じた回答が得られるようになりました。

パラメーター抽出器

Difyで用意されているツールやサードパーティAPIを実行する際には、適切な入力パラメータが必要となります。
パラメーター抽出器では、ユーザーが入力した自然言語から構造化パラメーターを推論し、それらの呼び出しに使うことが出来ます。

先ほどのワークフローを更に拡張してみましょう。
論文に関する質問から著者名を抽出し、Dify組み込みのArxiv論文検索ツールを使って該当する論文を回答するようにしてみます。

パラメーター抽出器の設定

質問分類器のクラスに「論文に関する質問」を追加し、そこにパラメーター抽出器のブロックを作成して接続します。

ブロックの設定で「パラメータを抽出」から変数名"author"として論文の著者名を抽出するように設定します。

Arxiv論文検索ツールの設定

Arxiv論文検索ツールを追加してパラメーター抽出器に接続します。
入力変数には、先ほど作成した変数"author"を設定します。
後ろの回答ブロックでは、Arxiv論文検索ツールの出力"text"をそのまま表示するようにします。

これで、質問からArxiv論文検索ツールの使用に必要なパラメータを抽出して検索結果を得られるようになりました。

まとめ

今回はDifyのワークフローとノードの基本について、実際にワークフローを作成しながら確認していきました。
特に、LLMを使用して処理を行うノードはうまく活用することでより回答の精度を上げたり、
外部サービスと連携して機能を拡張することが可能になります。ぜひ皆さんも活用してみてください。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com

Amazon Bedrock Knowledge Base の構造化データ取得はどこまで複雑なクエリに対応できるか

こんにちは、機械学習チーム YAMALEX の駿です。
YAMALEX は Acroquest 社内で発足した、会社の未来の技術を創る、機械学習がメインテーマのデータサイエンスチームです。
(詳細はリンク先をご覧ください。)

この記事は Amazon Bedrock Advent Calendar 2024 23日目の投稿です。

Amazon Bedrock Knowledge BaseのデータベースとしてRedshiftを指定して、構造化データを検索し、その結果を元に回答を生成することができるようになりました。 現在のところ、データベースとしてはRedshiftのみに対応しています。
Redshiftであれば、Serverless版、クラスタ版いずれも選択が可能です。

この記事ではRedshiftにサンプルとして用意されているTICKITデータセットを用いて、複雑なテーブルに対してどのようなSQLが生成できるのか、生成されたSQLを調整したい場合に何ができるか、を確認します。

今回試した構成

1. はじめに

1.1. Amazon Bedrock Knowledge Baseとは

Amazon Bedrock Knowledge Base (以下、Knowledge Base)はAmazonが提供する、RAGを簡単に構築するためのサービスです。

概要についてはこちらの記事で説明しているので、RAGやKnowledge Baseになじみのない方はまずはこちらをご一読ください。

acro-engineer.hatenablog.com

1.2. 従来のRAGの欠点

Knowledge Baseではデータベースとしてベクトルデータベースを使うことが多いです。

ベクトル検索は文章の意味を捉えるのが得意なため、それらしい内容をいくつかベクトルデータベースから取得し、回答を生成する、ということが上手くできます。

その反面、最も多い事象は何か、条件に一致するレコードが何件あるか、などデータの件数や正確な数値を扱うような情報を把握する必要がある質問には答えることができない、という欠点もあります。

しかし、今回の構造化データ検索を行うと、Redshift上のデータに対して、ユーザーの質問からSQLを作成し実行することで、上に書いたような質問にも適切に回答できるようになります。

2. [新機能]構造化データ検索とは

先日のre:Invent2024で発表された新機能として、Bedrock Knowledge BaseのデータベースとしてRedshiftを使用し、構造化データから取得した情報を元にRAGを行うことが可能になりました。

Bedrock Knowledge Baseの構造化データ検索は下記の流れで動きます。

  1. テーブル、データがあらかじめ投入されたRedshiftデータベースを用意する
  2. Bedrock Knowledge BaseにRedshift上のデータベースのテーブル、カラム情報を取り込む
  3. ユーザーが質問をする
  4. Bedrockが、ユーザーの質問に答えるために必要な情報をRedshiftのデータベースから取得するためのSQL文を生成する
  5. RedshiftでSQLを実行し、結果を取得する
  6. 取得した結果を元にユーザーの質問に回答する

従来のRAGと変わったのは、「4.」「5.」の部分で、SQL文で検索することで、「上位5件」や「合計件数」など、ベクトル検索では取得できない情報を取得できるようになりました。

3. 実施

今回は実際にどんなSQLが生成できるのか、テーブル/カラムの説明を追加することで、どれくらい精度を上げられるのか、を確認します。

下記手順で検証します。

  1. Redshiftデータベース準備
  2. Bedrock Knowledge Base作成
  3. SQL生成
  4. 説明を追加
  5. SQL再生成

    → より良いSQLが生成できていることを確認する

■使用した環境

No 項目
1 データベース Redshift
2 データ TICKIT データベース(※1)

※1 TICKITデータベースはRedshiftがサンプルとして用意しているデータベースで、スポーツやコンサートのチケットの売り上げ情報などを検索することができます。

扱う質問について

今回はKnowledge BaseのコンソールからSQLを生成させるために下記の質問を用意しました。

いずれもRedshiftでTICKITのデータをロードしたときにサンプルのクエリとして用意されていたSQLのようなものが生成されることを期待してます。

①「日付ごとの売り上げ上位5件を教えて」

■想定する参考SQL

SELECT d.caldate, sum(s.pricepaid) as sumpricepaid
FROM tickit.sales as s
JOIN tickit.date as d on s.dateid = d.dateid
GROUP BY d.caldate
ORDER BY sumpricepaid DESC
LIMIT 5;

■想定する検索結果

No caldate sumpricepaid
1 2008-04-14 407440
2 2008-09-10 402603
3 2008-07-27 400729

②「最も多くのチケットを購入した人は誰?」

■想定する参考SQL

SELECT firstname, lastname, total_quantity 
FROM   (SELECT buyerid, sum(qtysold) total_quantity
    FROM  tickit.sales
    GROUP BY buyerid
    ORDER BY total_quantity desc limit 10) Q, tickit.users
WHERE Q.buyerid = userid
ORDER BY Q.total_quantity desc;

■想定する検索結果

No firstname lastname total_quantity
1 Jerry Nichols 67
2 Armando Lopez 64
3 Kameko Bowman 64

3.1. Redshiftデータベース準備

こちらの手順に従って、サンプルデータをロードします。

docs.aws.amazon.com

次のようなノートブックが開いたら、準備完了です。

サンプルのSQLクエリが用意されている

3.2. Bedrock Knowledge Base作成

上記、Redshiftのデータベースを参照するKnowledge Baseを作成します。

  1. Bedrockコンソールのナレッジベースを開き「ナレッジベースを作成」>「Knowledge Base with structured data store」を押下する
  2. 「ナレッジベースの詳細を入力」画面でナレッジベース名など設定を行い「次へ」を押下する。

    後ほど必要になるため、ナレッジベースに紐づけるIAMサービスロール名をメモしておく。

  3. 「Configure query engine」画面で下記を設定して「次へ」を押下する。

    No 項目
    1 Query engine details.Connection options Redshift serverless
    2 Query engine details.Workgroup default-workgroup
    3 Authentication IAM Role
    4 Default storage metadata.Options Amazon Redshift databases
    5 Default storage metadata.Database sample_data_dev
    6 Query configurations 何も設定しない
  4. 設定を確認して、Knowledge Baseを作成する

  5. Redshiftのノートブック上で下記コマンドを実行し、Knowledge Baseに紐づけたIAMサービスロールがRedshiftにアクセスできるようにする

     CREATE USER "IAMR:{ロール名}" WITH PASSWORD DISABLE;
    
  6. Knowledge Baseのコンソールから「同期」を実行する

3.3. SQL生成

Bedrock Knowledge Baseのコンソールから、実際にSQLを生成した結果を以下に示します。

①「日付ごとの売り上げ上位5件を教えて」

SQLが生成された

■生成されたSQL

SELECT d.caldate AS "Date", SUM(s.qtysold * s.pricepaid) AS "Total Sales"
FROM tickit.sales s
JOIN tickit.date d ON s.dateid = d.dateid
GROUP BY d.caldate
ORDER BY "Total Sales" DESC
LIMIT 5;

■検索結果

No date total sales
1 2008-09-10 1036333
2 2008-04-14 1028582
3 2008-07-14 1019392

■問題点

  1. pricepaid は チケット単価 x チケット枚数 の値のため、改めて qtysold をかけてはいけない

上記の問題があるため、想定する検索結果とは全く異なる結果となりました。
チケット単価 x チケット枚数 ^ 2 を計算してしまっています。

②「最も多くのチケットを購入した人は誰?」

SQLが生成された

■生成されたSQL

SELECT u.firstname, u.lastname, SUM(s.qtysold) AS "Total Tickets Sold" 
FROM tickit.sales s
JOIN tickit.users u ON s.buyerid = u.userid 
GROUP BY u.firstname, u.lastname
ORDER BY "Total Tickets Sold" DESC 
LIMIT 1;

■検索結果(上位5件に編集して実行)

No firstname lastname total tickets sold
1 Charity Gamble 69
2 Jerry Nichols 67
3 Josephine Robbins 67

■問題点

  1. firstname + lastname の組は重複が存在するため、これだけでGROUP BYしてはいけない。

こちらは重複する可能性がある姓名でGROUP BYをしているため、異なる都市(BoiseとOverland Park)にそれぞれ存在するCharity Gamble が上位に来てしまいました。

続いて、この問題を解決するためにどのような設定が有効かを確認していきます。

3.4. SQL生成の精度を向上する

Bedrock Knowledge BaseではRedshiftのテーブル、カラムに対して、説明を追加する機能があります。

また、検索には使わないテーブル、カラムの指定、サンプルクエリを設定する機能もあり、 これらを組み合わせることで、SQL生成の精度を向上させることが可能です。

今回は、その中でも下記2点を確認しました。

  1. サンプルクエリを追加して上記問題を解決できるか
  2. カラム説明を追加して上記問題を解決できるか

3.4.1. サンプルクエリを追加

まずはTICKITのサンプルとして用意されていたクエリをBedrock Knowledge Baseの Curated queries に指定して、SQL生成の精度が上がるかを確認しました。

サンプルで用意されていたSQLクエリを例として追加

  1. Curated queries にサンプルSQLを設定して、「送信」を押下
  2. Query engine を「同期」

    Redshiftには変更を加えていないため、不要だと思うが、念のため

  3. 「3.3.」と同様にSQL生成を行う

■生成されたSQL

①「日付ごとの売り上げ上位5件を教えて」

SELECT d.caldate AS "Date", SUM(s.qtysold * s.pricepaid) AS "Total Sales"
FROM tickit.sales s
JOIN tickit.date d ON s.dateid = d.dateid
GROUP BY d.caldate
ORDER BY "Total Sales" DESC
LIMIT 5;

→ 変わりませんでした。。。

②「最も多くのチケットを購入した人は誰?」

SELECT u.firstname, u.lastname, SUM(s.qtysold) AS "Total Tickets Sold" 
FROM tickit.sales s
JOIN tickit.users u ON s.buyerid = u.userid 
GROUP BY u.firstname, u.lastname
ORDER BY "Total Tickets Sold" DESC 
LIMIT 1;

→ 変わりませんでした。。。

3.4.2. カラム説明を追加

誤った使い方をされていたカラムについて説明を追加

  1. Descriptions にテーブル名とカラム名、その説明を入力して、「送信」を押下
  2. Query engine を「同期」

    Redshiftには変更を加えていないため、不要だと思うが、念のため

  3. 「3.3.」と同様にSQL生成を行う

No Table name Column name Descriptions
1 sample_data_dev.tickit.sales pricepaid the total revenue. pricepaid / qtysold = price of a ticket. do not multiply pricepaid by qtysold when calculating total revenue.
2 sample_data_dev.tickit.users firstname name columns cannot be used to identify a user. there are users with same first and last names
3 sample_data_dev.tickit.users lastname name columns cannot be used to identify a user. there are users with same first and last names

■生成されたSQL

①「日付ごとの売り上げ上位5件を教えて」

SELECT d.caldate AS "Date", SUM(s.pricepaid) AS "Total Sales"
FROM tickit.sales s
JOIN tickit.date d ON s.dateid = d.dateid
GROUP BY d.caldate
ORDER BY "Total Sales" DESC
LIMIT 5;

→ ○ 売り上げの計算方法が訂正された

②「最も多くのチケットを購入した人は誰?」

SELECT u.firstname, u.lastname, SUM(s.qtysold) AS "Total Tickets Sold" 
FROM tickit.sales s
JOIN tickit.users u ON s.buyerid = u.userid 
GROUP BY u.firstname, u.lastname
ORDER BY "Total Tickets Sold" DESC 
LIMIT 1;

→ 変わりませんでした。。。

No 質問 期待するSQL 追加設定 生成されたSQL 結果 説明
1 日付ごとの売り上げ上位5件を教えて SELECT d.caldate, sum(s.pricepaid) as sumpricepaid
FROM tickit.sales as s
JOIN tickit.date as d on s.dateid = d.dateid
GROUP BY d.caldate
ORDER BY sumpricepaid DESC
LIMIT 5;
なし SELECT d.caldate AS "Date", SUM(s.qtysold * s.pricepaid) AS "Total Sales"
FROM tickit.sales s
JOIN tickit.date d ON s.dateid = d.dateid
GROUP BY d.caldate
ORDER BY "Total Sales" DESC
LIMIT 5;
× 売り上げの計算方法が間違っている
2 あり SELECT d.caldate AS "Date", SUM(s.pricepaid) AS "Total Sales"
FROM tickit.sales s
JOIN tickit.date d ON s.dateid = d.dateid
GROUP BY d.caldate
ORDER BY "Total Sales" DESC
LIMIT 5;
売り上げの計算方法を修正することができた
3 最も多くのチケットを購入した人は誰? SELECT firstname, lastname, total_quantity
FROM (SELECT buyerid, sum(qtysold) total_quantity
  FROM tickit.sales
  GROUP BY buyerid
  ORDER BY total_quantity desc limit 10) Q, tickit.users
WHERE Q.buyerid = userid
ORDER BY Q.total_quantity desc;
なし SELECT u.firstname, u.lastname, SUM(s.qtysold) AS "Total Tickets Sold"
FROM tickit.sales s
JOIN tickit.users u ON s.buyerid = u.userid
GROUP BY u.firstname, u.lastname
ORDER BY "Total Tickets Sold" DESC
LIMIT 1;
× 集計時に「姓名」で判定してしまっている
4 あり SELECT u.firstname, u.lastname, SUM(s.qtysold) AS "Total Tickets Sold"
FROM tickit.sales s
JOIN tickit.users u ON s.buyerid = u.userid
GROUP BY u.firstname, u.lastname
ORDER BY "Total Tickets Sold" DESC
LIMIT 1;
× 集計時の問題を是正できなかった

テーブルの中に類似の概念を持つカラム(イベントIDとイベント名など)が複数存在する場合に、うまく使い分けることができていないと言えると思います。

反対に必要な情報が複数テーブルにまたがっている場合などは適切にJOINなど使って情報を集計することができており、そこの精度は高く、再現性が高い、と感じました。
また、計算方法が間違っている部分に関してもカラムの説明を追加することで、是正できることが分かりました。

まとめ

今回は、Bedrock Knowledge Baseに追加されたRedshiftと連携する機能を使って、どこまでできるのかを確認しました。

簡単なテーブル・簡単な質問であれば、SQLを生成して正しい情報で回答を生成できますが、 複雑なテーブル・難しい質問をされた場合に、SQLの生成が上手くいかない(正しくないクエリを生成してしまう)ことが分かりました。

正しいSQLが生成できないときのカスタマイズとして、

  1. テーブル・カラムの説明を追加する
  2. サンプルのクエリを追加する

などの機能があり、これらを使って出力を調整できることが分かりました。

この機能を活用して、従来のRAGでは回答できなかったユーザーの質問に適切に回答できるシステムを構築したいです。

Acroquest Technologyでは、キャリア採用を行っています。
  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com