Taste of Tech Topics

Taste of Tech Topics

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

Pineconeでハイブリッド検索/リランクは検索精度にどのように寄与するか?

はじめに

こんにちは、データ分析エンジニアの木介です。
秋も深まり、肌寒さを感じる季節となりましたが、皆様いかがお過ごしでしょうか。
今回はPineconeを使ったハイブリッド検索とリランクについて紹介していきます。

概要

今回は以下の形でPineconeをベクトルデータベースとして、ベクトル検索にリランクを合わせた検索とハイブリッド検索を行い、検索精度について検証を行っていきます。


1. Pineconeとは

Pineconeとは、RAGなどで活用されるマネージドなベクトルデータベースです。
従来のデータベースとは異なり、テキストや画像、音声などのデータを数値ベクトルとして保存し、これらのベクトル間の類似性を高速に検索することが可能です。
特にサーバレス版の料金形態として、データの読み込み/書き込み時とストレージの保存量に応じてのみかかるため、安価にサービスへの導入が可能です。
無料版もあるため、気軽にベクトルデータベースを試すことができます。

www.pinecone.io

2. ベクトル検索とは

ベクトル検索は、テキストや画像などのデータを数値の配列(ベクトル)に変換し、類似性を数学的に計算することで情報を効率的に検索する技術です。
従来のキーワードベースの検索とは異なり、データの意味や文脈を考慮した検索が可能となります。
そのセマンティックに検索できるという点から、RAG(Retrieval-Augmented Generation)でもよく使われている技術になります。

3. リランクとは

リランクとは図のように一度検索した結果を再度順位付けすることで、検索精度を高める手法です。
これにより、欲しい情報を上位の検索結果に持ってくることが可能となります。
Pineconeではリランクモデルを使用したものを利用することができます。

www.pinecone.io

4. ハイブリッド検索とは

ハイブリッド検索は、ベクトル検索と従来のキーワード検索を組み合わせた手法です。これにより、キーワードの一致度と意味的な類似性の両方を考慮した検索が可能となります。

Pineconeが提供しているハイブリッド検索では、ベクトル検索に使われる文章の意味的なの類似度を比較できる密ベクトルと、キーワードベースの類似度の比較が可能な疎ベクトルを用いて検索が行われます。
以下の図のように密ベクトルと、疎ベクトルを用いて、ベクトル検索とキーワード検索を組み合わせることで検索を行っています。

  1. 疎ベクトル:大半の数値が0のベクトルで、キーワード検索に使う
  2. 密ベクトル:大半の数値が0以外でEmbeddingモデルによるベクトル検索に使う

www.pinecone.io


Pineconeでのベクトル検索+リランクを用いた検索/ハイブリッド検索の検証

1. 検証環境

Pythonのライブラリは以下のバージョンを使用します。

名称 バージョン 説明
pinecone 5.3.1 PineconeのAPIを利用する
openai 1.51.2 OpenAIのAPIを利用する
Janome 0.5.0 日本語の形態解析ツール
rank_bm25 0.2.2 BM25を使ってキーワード検索のスコアを算出する

また、モデルとしては密ベクトルの作成にOpenAIのtext-embedding-3-smallを、リランクにはPineconeから利用できるbge-reranker-v2-m3を利用しました。

データセットしては宿のレビュー検索を想定した以下の簡単なレビュー文章を用意しました

レビューNo 文章
1 この宿の温泉は、まさに極上の癒しでした。源泉かけ流しの湯はとても柔らかく、肌がすべすべになる感じがしました。露天風呂からは美しい山々の景色が広がり、夜には満天の星空を眺めながらゆっくりと浸かることができました。何度でも訪れたくなる温泉です。
2 宿の温泉はとても気持ち良く、リラックスできました。内湯と露天風呂があり、それぞれ異なる趣があります。特に露天風呂から見える庭園の景色が素晴らしく、四季折々の美しさを楽しむことができます。お湯も適温で、長時間浸かっていても疲れませんでした。
3 温泉が自慢の宿とのことで期待していましたが、期待以上でした。天然温泉の源泉をそのまま使用しているので、お湯の質がとても良く、温まった後もポカポカが続きました。貸切風呂も予約して利用しましたが、プライベートな空間で贅沢なひとときを過ごすことができました。
4 温泉は広々としていて、開放感が抜群でした。露天風呂からは海が一望でき、波の音を聞きながらゆっくりと過ごすことができました。お湯も熱すぎず、じっくりと温まることができてとても満足です。また、日帰り利用も可能なので、気軽に訪れることができるのも嬉しいポイントです。
5 温泉は硫黄の香りが心地よく、いかにも温泉に来たという実感がありました。お湯の効能も高く、肌がツルツルになるのを感じました。複数の湯船があり、時間帯によっては貸切状態になることもあり、贅沢な気分を味わえました。また、湯上がりに提供される冷たいドリンクも嬉しいサービスでした。
6 宿の食事はまさに芸術品のようでした。地元の新鮮な食材をふんだんに使った会席料理は、見た目も美しく、一品一品が丁寧に作られているのが伝わってきました。特に旬の魚介を使った刺身は絶品で、これだけでもまた訪れたいと思いました。
7 夕食は地元の名物料理が盛りだくさんで、大満足でした。特に炭火焼きでいただいた和牛ステーキは口の中でとろける美味しさで、何度もおかわりしたくなるほどでした。朝食も種類が豊富で、地元の野菜を使ったサラダや、手作りの豆腐が美味しかったです。
8 夕食はコース料理で、どれも美味しかったのですが、特に印象に残ったのは地元で採れた野菜を使った前菜と、自家製のデザートです。食材の味を活かしたシンプルな調理法で、素材の良さが際立っていました。朝食もバランスが良く、特に焼きたてのパンが絶品でした。
9 宿の食事は期待以上でした。海の近くということもあり、新鮮な魚介類が豊富に使われていて、お刺身や煮魚がとても美味しかったです。夕食は量もたっぷりで、どの料理も心のこもった味付けでした。朝食の和食もとても美味しく、特に温泉卵が絶品でした。
10 夕食は地元の食材をふんだんに使った創作料理で、どの料理も工夫が感じられました。特に地元産の野菜とお肉を使ったグリル料理が絶品で、素材の味がしっかりと引き立っていました。朝食も手作りのジャムや焼き立てのパンなど、こだわりが感じられる内容で、大変満足しました。


上記のレビュー文章は1~5は温泉について言及していて、6~10は食事について言及しているレビューです。
このレビュー記事から「お肉の料理がおいしい宿」というクエリが入力される場合を考えます。

想定としては上記の中だとNo. 7とNo. 10の記事が取得できることを期待しています。
上記のデータセットについて以下の検索結果がそれぞれどうなるのかを比較していきます。

ベクトル検索 今回の検証のベースライン、文章の意味的な類似度から検索を行う
ベクトル検索+リランクを用いた検索 ベクトル検索結果にリランクモデルによる再順位付けを行う
ハイブリッド検索 文章の意味的な類似度とキーワードの一致度から検索を行う

2. 検索用のインデックス作成

では早速Pineconeでインデックスの作成をしていきます。
以下のPineconeのドキュメントを参考に進めていきます。

docs.pinecone.io

まず、Pineconeに保存するベクトルを算出します。

ベクトル検索でも使う密ベクトルとしてはOpenAIのtext-embedding-3-smallを利用して、以下の実装で算出を行いました。

from openai import OpenAI

def embedding_dense(sentence_list):
    """密ベクトルに変換

    Args:
        sentence_list (list): 密ベクトルに変換する文字列のリスト

    Returns:
        _type_: 密ベクトル
    """
    client = OpenAI(
    api_key=OPENAI_API_KEY
    )
    res = client.embeddings.create(
        model="text-embedding-3-small",
        input=sentence_list,
        dimensions=512
    )
    return  [d.embedding for d in res.data]

キーワード検索で使う疎ベクトルは、Pineconeで使っている自然言語処理モジュールのnltkが日本語に対応していないため、日本語の形態素解析Janomeを、BM25の算出にrank_bm2を用いていきます。
以下の実装で疎ベクトルの算出を行いました。

from rank_bm25 import BM25Okapi
from janome.tokenizer import Tokenizer

class BM25Vectorizer:
    def __init__(self):
        self.tokenizer = Tokenizer()
        self.bm25_model = None
        self.corpus_tokens = []

    def fit(self, corpus):
        """
        コーパスを受け取り、BM25モデルを初期化します。

        :param corpus: リスト形式の文書群
        """
        self.corpus_tokens = [list(self.tokenizer.tokenize(doc, wakati=True)) for doc in corpus]
        self.bm25_model = BM25Okapi(self.corpus_tokens)

    def encode_documents(self, text):
        """
        入力テキストをスパースベクトル形式に変換します。

        :param text: 文字列形式の入力テキスト
        :return: Pinecone互換のスパースベクトル(辞書形式)
        """
        if not self.bm25_model:
            raise ValueError("BM25モデルが初期化されていません。fitメソッドを先に呼び出してください。")

        # トークンに分割
        tokenized_text = list(self.tokenizer.tokenize(text, wakati=True))
        scores = self.bm25_model.get_scores(tokenized_text)
        # 非ゼロの値のインデックスとスコアを算出
        indices = np.nonzero(scores)[0].tolist()
        values = scores[indices].tolist()

        return {"indices": indices, "values": values}

def embedding_sparse(bm25_model,sentence_list):
    """疎ベクトルにEmbedding

    Args:
        bm25_model (BM25Vectorizer): BM25の処理を行う
        sentence_list (list): 疎ベクトルに変換する文字列のリスト

    Returns:
        _type_: 疎ベクトル
    """
    
    return [
        bm25_model.encode_documents(sentense)    
        for sentense in sentence_list
    ]

上記コードで算出した疎ベクトル、密ベクトルを以下のコードでPineconeに保存していきます。

from pinecone.grpc import PineconeGRPC as Pinecone

#hybrid-image-searchという名前のindexを作成
index_name = "hybrid-image-search"

pc = Pinecone(api_key=PINECONE_API_KEY)
# create the index
if index_name not in [ index_info["name"] for index_info in pc.list_indexes()]:
    pc.create_index(
        index_name,
        dimension=512,
        metric="dotproduct",
        spec=ServerlessSpec(
            cloud='aws', 
            region='us-east-1'
        ) 
    )

index = pc.Index(index_name)

# インデックスにデータセットを挿入
upsert_response = index.upsert(
  vectors=[
    {
      'id': f'{vector_index}',
      'values': dense_vec,
      'sparse_values': sparse_vec,
      "metadata":{"answer":answer}
      }
   for vector_index,(sparse_vec,dense_vec,answer) in enumerate(zip(
     sparse_vector,dense_vector,
     dataset_df.answer.to_numpy(),
     ))
  
   ],
  # metadata={"sentense":corpus[i]},
  namespace='test-hybrid'
)

これでベクトル検索およびハイブリッド検索で使うの密ベクトル、疎ベクトルの保存が完了しました。

3. ベクトル検索+リランクを用いた検索の利用方法

また、ベクトル検索+リランクを用いたについては、以下の実装で処理が可能です。

ベクトル検索で取得してきた検索結果10件に対してPineconeのリランクモデルで再順位付けを行っている形です。

def vector_rerank_query(query,dense_query):
    # 上位10件の検索結果をベクトル検索より取得
    query_response = index.query(
        top_k=10,
        vector=dense_query,
        namespace="test-hybrid",
        include_metadata=True,
    )
    respoinse_answers =[
        {"id": res["id"],"text":res["metadata"]["answer"]} for res in query_response["matches"]]

    # リランクによる再順位付け
    rerank_results=pc.inference.rerank(
            model="bge-reranker-v2-m3",
            query=query,
            documents=respoinse_answers,
            top_n=5,
            return_documents=True,
        )
 return response

4. ハイブリッド検索の利用方法

では次に挿入したデータセットに対してハイブリッド検索を行っていきます。

以下の実装でハイブリッド検索が可能です。

def hybrid_query(sparse_query,dense_query):
    response = index.query(
        top_k=5,
        vector=dense_query,
        namespace="test-hybrid",
        sparse_vector=sparse_query,
    )
    return response

検証結果

では検証の結果を以下に示します。

検索順位 ベクトル検索結果(レビューNo.) ベクトル検索+リランクによる検索結果(レビューNo.) ハイブリッド検索結果(レビューNo.)
1 6 6 10
2 9 9 7
3 8 10 5
4 10 7 9
5 2 8 3

レビューNo.10,7の以下の文章が今回のクエリである「お肉の料理がおいしい宿」に合致しているため、取得できることを期待しています。

レビューNo.7

夕食は地元の名物料理が盛りだくさんで、大満足でした。特に炭火焼きでいただいた和牛ステーキは口の中でとろける美味しさで、何度もおかわりしたくなるほどでした。朝食も種類が豊富で、地元の野菜を使ったサラダや、手作りの豆腐が美味しかったです。

レビューNo.10

夕食は地元の食材をふんだんに使った創作料理で、どの料理も工夫が感じられました。特に地元産の野菜とお肉を使ったグリル料理が絶品で、素材の味がしっかりと引き立っていました。朝食も手作りのジャムや焼き立てのパンなど、こだわりが感じられる内容で、大変満足しました。

今回の結果でいえば、ハイブリッド検索ではレビューNo.10,7のレビューを1番目、2番目に類似しているとして取得できていることが分かります。

ベクトル検索+リランクを用いた検索については、今回取得した5つの検索結果の中でレビューNo.10,7を取得することが出来ていました。
また、ベクトル検索のみと比較すると、リランクを用いた検索ではレビューNo.7をより類似度が高いものとして取得できているため、リランクを用いることで検索精度がベクトル検索よりも上がっていることが伺えます。

以上より、ハイブリッド検索が今回の結果では最も期待した検索結果を取得できており、ベクトル検索+リランクを用いた検索がそれに続く形になっていることが分かりました。
RAGによる回答精度の向上のために、Pinecone を使ったハイブリッド検索やリランクを取り入れるのは有効な一手となりそうです。

まとめ

今回はPineconeを使ったハイブリッド検索とリランクを用いた検査について検証を行いました。
結論としてハイブリッド検索、リランクを用いた検索の双方で通常のベクトル検索のみの結果と比較して期待した値が取得しやすい形となりました。
RAGでより高精度な情報の取得を求める場合に、まず取り入れたい技術となっていそうです。


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

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

MicrosoftのGraphRAG+Neo4jで簡単にKnowledge Graphを可視化する

こんにちは。データサイエンティストチームYAMALEXのSsk1029Takashiです。

LLMが広く使用されるようになってから、RAGに関する研究も増加し、RAGを拡張する手法の研究も多く出ました。
その中でもテキストをグラフデータとして扱いRAGの元データとする手法をGraphRAGと言います。
今回はその中の一つであるMicrosoftが提唱しているGraphRAGの手法を使っていきます。

www.microsoft.com

MicrosoftからはGraphRAGを使用できる実装もPythonライブラリとして発表されています。
ライブラリではグラフデータはあくまでRAGの元データとして使用していますが、せっかくテキストからグラフが生成できるのであれば、テキストマイニングしてみたくなるものです。
そこで、本記事ではこの手法で作成したグラフをグラフDBの一つであるNeo4jに投入して、可視化できないかを試してみます。
https://microsoft.github.io/graphrag/img/GraphRag-Figure1.jpg


本記事の目次は以下の通りになります。

この記事では、簡単に試してみることが目的のため以下の説明は割愛します。

  1. Neo4j自体の説明、セットアップ方法
  2. グラフクエリの基本的な文法

GraphRAGとは

一般的にはGraphとRAGを組み合わせた手法をGraphRAGと呼ぶことが多いですが、ここではMicrosoftが提案した手法をGraphRAGと呼びます。
GraphRAGでは入力された文章内でキーワード同士の関連をグラフ構造として保持して、そのデータをもとにRAGをします。

特徴としてGraphRAGではGlobal SearchとLocal Searchという2種類の検索方法を使用することができます。

  • Global Search:登録されたデータから全体的な大まかな概要を回答する
  • Local Search:特定の人物やイベントなど細かい情報をピンポイントで回答する

例えば、「2024年のLLMについて概要を教えてください」というのはGlobal Searchで、「GraphRAGで使用されている言語は?」というのはLocal Searchになります。

特に世の中にグラフ構造のデータを使ったRAGが多い中で、MicrosoftのGraphRAGに特有の概念としてCommunity Reportというものがあります。

GraphRAGでは、グラフデータを作成した後、いくつかグループにクラスタリングして、グループごとに要約を作成します。
このグループのことをグラフ構造ではCommunityと言うため、グループから作成した要約をCommunity Reportと言います。
Global SearchではこのCommunity Reportというものを利用して、回答することで大まかで網羅的な内容を回答できるようにしています。

GraphRAGでのグラフデータ作成

GraphRAGでテキストからグラフデータを作るときのステップは大まかに以下になります。

  1. 入力されたテキストをチャンクに分割する
  2. チャンクごとにEntity、RelationShipを説明付きでOpenAIモデルを使って抽出する
  3. Entity、Relationshipが複数回登場することもありえるので、OpenAIモデルを使って各Entity、Relationshipの説明を要約する
  4. グラフをクラスタリングして、グラフごとにCommunity Reportを作成する
  5. グラフデータとEntityのEmbeddingを作成する

EntityとRelationshipの抽出は以下のようなイメージで抽出されます。
入力

大規模言語モデル(だいきぼげんごモデル、英: large language model、LLM)は、多数のパラメータ(数千万から数十億)を持つ人工ニューラルネットワークで構成されるコンピュータ言語モデルで、膨大なラベルなしテキストを使用して自己教師あり学習または半教師あり学習(英語版)によって訓練が行われる。

抽出結果

("entity"\t大規模言語モデル\tlanguage model\t大規模言語モデル(LLM)は、数千万から数十億のパラメータを持つ人工ニューラルネットワークで構成され、膨大なラベルなしテキストを用いた自己教師あり学習または半教師あり学習で訓練される。)
{record_delimiter}
("entity"\tパラメータ\tparameter\tパラメータは、人工ニューラルネットワークを構成し、その性能を左右する要素である。)
{record_delimiter}
<中略>
("relationship"\t大規模言語モデル\tラベルなしテキスト\t大規模言語モデルはラベルなしテキストを使用して訓練される\t8)
{record_delimiter}
("relationship"\tラベルなしテキスト\t自己教師あり学習\tラベルなしテキストは自己教師あり学習の訓練データとして使用される\t7)
{record_delimiter}
<中略>

このようにentityとrelationshipという2種類の項目を同時にOpenAIのモデルを使って抽出して、それぞれの説明を付与します。
ここで抽出されたentityとrelationshipを使ってグラフデータを作成します。

この後のCommunityの作成やEmbeddingの作成については細かいアルゴリズムの話になるため本記事では割愛します。

GraphRAGで作成したデータをNeo4jに投入する

簡単にGraphRAGについても説明したので、実際にグラフデータを作成してNeo4jに投入してみましょう。
今回は以下の「大規模言語モデル」のWikipediaのページを元データとします。
ja.wikipedia.org

GraphRAG用のデータを作成する

GraphRAGのPythonライブラリでは作成したグラフ構造を表すデータは複数のparquetファイルで出力されます。
基本的な手順は以下のページに記載されているので、手順自体は簡易的に紹介します。
microsoft.github.io

まず、Wikipediaのページをコピーしてテキストファイルにしたうえで./blog_test/inputディレクトリに配置します。
そのうえで以下のコマンドを実行して、設定ファイルを生成します。

graphrag init --root ./blog_test

.envファイルとsettings.ymlファイルが生成されるため、OpenAIのAPI Keyなどの設定を埋めます。
その後、以下のコマンドを実行すことで、parquetファイルが/blob_test/outputディレクトリに生成されます。

graphrag index --root ./blog_test

実行結果として以下のparquetファイルが生成されるため、このファイルをもとにNeo4jに投入していきます。

create_base_documents.parquet
create_base_entity_graph.parquet
create_base_extracted_entities.parquet
create_base_text_units.parquet
create_final_communities.parquet
create_final_community_reports.parquet
create_final_documents.parquet
create_final_entities.parquet
create_final_nodes.parquet
create_final_relationships.parquet
create_final_text_units.parquet
create_summarized_entities.parquet

例えば、create_final_entities.parquetには抽出した結果のEntity情報が入っており、以下のようなデータフレームになっています。

作成したグラフデータをNeo4jに投入する

前章までで作成したparquetファイルをグラフデータとしてNeo4jに投入していきます。
今回は簡易的に実施するため、Neo4jのクラウドサービスである、AuraDBを利用しました。
https://neo4j.com/product/auradb/

Neo4jにデータ投入するための実装はGraphRAGのリポジトリの中に参考実装があるのでそちらを利用します。
全てのコードを説明すると長くなるため、主要な部分をピックアップして見ていきます。

まずは、ノードの種類を定義している部分になります。

# create constraints, idempotent operation

statements = """
create constraint chunk_id if not exists for (c:__Chunk__) require c.id is unique;
create constraint document_id if not exists for (d:__Document__) require d.id is unique;
create constraint entity_id if not exists for (c:__Community__) require c.community is unique;
create constraint entity_id if not exists for (e:__Entity__) require e.id is unique;
create constraint entity_title if not exists for (e:__Entity__) require e.name is unique;
create constraint entity_title if not exists for (e:__Covariate__) require e.title is unique;
create constraint related_id if not exists for ()-[rel:RELATED]->() require rel.id is unique;
""".split(";")

for statement in statements:
    if len((statement or "").strip()) > 0:
        print(statement)
        driver.execute_query(statement)

上記コードでは、以下の種類のデータをそれぞれノードとして投入しています。

ノードの種類 説明
__Entity__ 抽出したEntity
__Document__ 入力にしたファイル。今回は一つなのでノードも一つ
__Chunk__ Documentを分割して作成したChunk
__Community__ グラフをクラスタリングして作成したCommunity
__Covariate__ 関係性に追加した情報。デフォルトでは生成されないため、今回は使用しない

これらのノードは以下のリレーションによってそれぞれこの後のコードで定義づけされています。

リレーション 対応ノード 説明
HAS_ENTITY __Chunk__ → __Entity__ Chunk内に存在するEntityを示す
IN_COMMUNITY __Entity__ → __Community__ EntityがどのCommunity Reportに参照されたか
PART_OF __Chunk__ → __Document__ Chunkがどのファイルから作成されたか
RELATED __Entity__ → __Entity__ 関連のあるEntityであることを示す

例えば、HAS_ENTITYはのリレーションは以下のコードで作成されています。

entity_statement = """
MERGE (e:__Entity__ {id:value.id})
SET e += value {.human_readable_id, .description, name:replace(value.name,'"','')}
WITH e, value
CALL db.create.setNodeVectorProperty(e, "description_embedding", value.description_embedding)
CALL apoc.create.addLabels(e, case when coalesce(value.type,"") = "" then [] else [apoc.text.upperCamelCase(replace(value.type,'"',''))] end) yield node
UNWIND value.text_unit_ids AS text_unit
MATCH (c:__Chunk__ {id:text_unit})
MERGE (c)-[:HAS_ENTITY]->(e)
"""

batched_import(entity_statement, entity_df)

bathed_importの中では、与えられたDataFrameの各行に対して、entity_statementで定義されたクエリが実行されています。
クエリの中では、__Entity__のノードに対して、idやdescriptionなどのプロパティやベクトル情報を付与して、最終行のMERGE (c)-[:HAS_ENTITY]->(e)の部分でリレーションを付与しています。

リンク上のコードを実行して、Neo4jのコンソールから、例として以下のクエリを実行するとデータが入ったことが確認できます。

MATCH p=()-[]->() RETURN p LIMIT 25;

ここまででNeo4jにデータ投入はできたので、次の章でグラフの内容を見てみたいと思います。

グラフを可視化・分析してみる

Neo4jのコンソールでは、クエリを実行することで結果として取得できたグラフデータを可視化することができます。

まずは、Entity同士の関係性を見てみましょう。
以下のクエリを実行することで、Entity同士で関連があるものを取得します。

MATCH path = (:__Entity__)-[:RELATED]->(:__Entity__)
RETURN path LIMIT 200

ざっくり以下のようなグラフが可視化されます。

中心部のほうを詳しく見ると、以下のようになっています。

元々LLMに関するWikipediaページをデータにしているので、LLMが最も多くのEntityから関連されているのは想定通りです。
また、別のエリアを見てみるとLLMに関する重要単語が比較的多く関連付けされていることもわかります。

また、CommunityとEntityの関係性も見てみましょう。
以下のクエリでCommunityに対してどのEntityが属しているかを可視化できます。

MATCH (c:__Community__) WITH c LIMIT 2
MATCH path = (c)<-[:IN_COMMUNITY]-()-[:RELATED]-(:__Entity__)
RETURN path LIMIT 100

すると以下のように、Communityと属しているEntityがどのように関連しているかを可視化できます。
また、Communityの詳細な内容はノードのプロパティにしているため、選択すれば内容を確認できます。

トークン」や「大規模言語モデル」などの単語が表示しているCommunityから参照されていてなおかつ、Entityにも多く参照されており重要な単語になっていることがわかります。

このようにグラフとして目に見えるだけでもだいぶどのようなドキュメントかを理解できるようになりますね。
もちろんグラフデータなので、Local Searchのように特定の情報をクエリで取得することも可能ですので、RAGのバックエンドとしても利用できます。
なので、本記事で試したNeo4jもRAGアプリケーションのバックエンドや分析基盤として一つの選択肢になりそうです。

まとめ

本記事では、MicrosoftのGraphRAGで生成されるグラフデータをNeo4jに投入してグラフ表示しました。
GraphRAG自体はRAGするためのライブラリですが、テキストからグラフデータを作成できるという観点でドキュメントの概観を把握するという意味では有用そうです。
LLMとグラフの組み合わせは色々研究も広がっているため、注目領域になりますね。
それではまた。

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

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

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

www.wantedly.com

OpenAIのプロンプトジェネレーターで至高のプロンプトを生成する

こんにちはイワツカです。
食欲の秋ということでサツマイモやキノコが美味しい季節ですね。

さて今回は、生成AIを使おうと思ってもプロンプトの書き方がよく分からず、生成AIから思ったような回答を得られない...なんて方におススメのOpenAIのプロンプトジェネレーター機能を紹介します。

1. プロンプトジェネレーターとは

プロンプトジェネレーターとは、その名の通り、AIに対する指示文(プロンプト)を自動的に作成する機能です。
ユーザーが入力した簡単な指示やキーワードをもとに、AIが最適なプロンプトを生成します。
これにより、ユーザーは複雑なプロンプトを自分で考える手間を省き、プロンプトについて専門的な知識がなくてもAIから最適な応答を得ることができます。

特に、手間をかけずにクリエイティブなタスクをやらせたいときに便利です。

使い方

使い方は非常に簡単で、以下の3ステップで可能です。

  1. OpenAIのPlaygroundにアクセスする
    ※OpenAIのAPIへのログインとPlaygroundを利用できるようにするためのCredit balanceが必要です。
     ただし、プロンプトジェネレーターだけを使う分には現時点では無料のようです。
     入力画面に「Free beta」の表示があり、プロンプトジェネレーターを実行しただけでは、Creditは消費されませんでした。 platform.openai.com
  2. 画面上部の星のようなマークを押す
  3. 簡単な指示やキーワードを入力し、Createボタンを押す

Playgroundの画面。星マークを押す

プロンプトジェネレーター

また、既存のプロンプトを修正したい場合は、System instructionsに既存のプロンプトを入力し、プロンプトジェネレーターに追加の指示を書いてUpdateボタンを押せば、追加の指示の通りにプロンプトを修正してくれます。

既存のプロンプトを修正する場合

2. ユースケースごとのプロンプトと結果比較

プロンプトジェネレーターを使わない場合と使った場合の結果を比較してみました。
今回は、プロンプトジェネレーターで生成した内容を、通常のChatGPTのプロンプト画面に貼り付けて、実行していきます。

Pythonコードのリファクタリング

生成AIにPythonコードをリファクタリングさせる場合のプロンプトと結果を比較してみます。

プロンプトジェネレーターを使わない場合

プロンプトを工夫しない例として以下のプロンプトをChatGPTで利用しました。

以下のPythonコードをリファクタリングしてください。
---
def calculate_average(numbers):
    total = 0
    for num in numbers:
        total += num
    average = total / len(numbers)
    return average

結果がこちらです。

以下のようにリファクタリングすることで、コードを簡潔にし、Pythonicな書き方になります。

def calculate_average(numbers):
    return sum(numbers) / len(numbers)

## 主な改善点
1. 合計計算の簡略化: sum(numbers)を使用することで、ループを省略しました。
2. 可読性の向上: 不要な変数 total を削除し、関数が何をしているかが直感的にわかりやすくなりました。

この変更により、関数がより簡潔かつ効率的になります。

プロンプトジェネレーターを使う場合

まずはプロンプトジェネレーターに「Pythonコードをリファクタリングしてください。」と入力し、プロンプトを生成させます。
生成されたプロンプトがこちらです。手順や出力形式、具体例が追加されています。

与えられたPythonコードをリファクタリングして、読みやすさ、効率、または保守性を向上させてください。

# 手順
1. 与えられたPythonコードを分析して、その現在の機能、構造、および動作を理解します。
2. 最適化、単純化、または再作成によって恩恵を受ける部分を特定します。これには次の内容が含まれることがあります:
   - 冗長なコードの削除
   - 変数/関数名をより明確にするための改善
   - 複雑なループやロジックの簡略化
   - 大きな関数を小さく、よく定義された部分に分割する
   - ハードコーディングされた値を定数や設定変数に置き換える
   - エラーハンドリングの改善やよりPython的な構成の利用
3. リファクタリングプロセスを実行し、元の機能と同一の動作を保持します。
4. リファクタリングされたコードはドキュメントが整備され、読みやすさや効率が向上した部分にはコメントを付加してください。

# 出力形式
リファクタリングされたPythonコードをプレーンテキストで提供してください。変更点の大きい部分には、その変更がコードのどのように改善するかを明確にするコメントを含めてください。

# 例

**入力:**
python
def calc(a, b):
    result = 0
    for i in range(a):
        result += b
    return result


**リファクタリング後の出力:**
python
def calc(a, b):
    # 効率化のためループの代わりに乗算を使用
    return a * b


# 注意事項
- 同じ全体的な機能を維持してください。
- リファクタリングされたコードは簡潔で、Python的で、理解しやすいものであるべきです。
- 変数名と関数名にはスネークケース(snake_case)を使用するなど、ベストプラクティスに従ってください。
- 元のバージョンと一致することを確認するためにコードをテストしてください。

このプロンプトにリファクタリングしたいPythonコードを追加してChatGPTに生成させました。

生成したプロンプトをChatGPTで実行

結果がこちらです。
プロンプトジェネレーターを使わない場合と比較して、コメントが追加されていたり、入力が空リストの場合の処理が追加されています。

リファクタリングされたコードを以下に示します。変更点として、リスト内包表記と組み込み関数を活用してコードを簡潔にし、コメントを追加して改善点を明示しました。

def calculate_average(numbers):
    """
    Calculate the average of a list of numbers.
    
    Args:
        numbers (list of float): List of numbers to calculate the average for.
    
    Returns:
        float: The average value of the numbers.
    """
    # シンプルで効率的な合計計算と平均値算出
    return sum(numbers) / len(numbers) if numbers else 0  # 空リストの場合は0を返す

## 改善点の詳細
1. sum()関数の利用: sum()はPythonに組み込まれた効率的な方法でリストの合計を計算します。ループを手動で書く必要がなくなり、コードがシンプルになります。
2. 空リストのハンドリング: numbersが空のリストだった場合、ゼロ除算エラーが発生する可能性があるため、三項演算子を使ってデフォルト値(0)を返すようにしました。
3. コメントとドキュメンテーション: 関数にdocstringを追加し、コードの目的と引数/戻り値の説明を明確にしました。

リファクタリング後のコードは、元の機能を保持しつつ、よりPython的で簡潔かつ安全な実装になっています。

API設計

次にFastAPIを使ったToDoアプリの設計を比較してみます。

プロンプトジェネレーターを使わない場合

簡単なプロンプトとして以下をChatGPTに入力しました。

FastAPIを使ったToDOアプリの設計をしてください。

結果はこちらです。

以下は、FastAPIを使ったシンプルなToDoアプリの設計案です。この設計では、以下の機能を実現します:

## 機能一覧
1. タスクの作成: タスクを新規作成。
2. タスクの取得:
   - 全タスクのリスト取得。
   - 特定のタスク詳細を取得。
3. タスクの更新:
   - タイトルやステータスなどの変更。
4. タスクの削除:
   - 特定のタスクを削除。

## 使用技術
- フレームワーク: FastAPI
- データベース: SQLite (SQLAlchemy + Pydantic)
- 認証 (オプション): OAuth2 (JWTを使用)

## ディレクトリ構成
todo_app/
├── app/
│   ├── main.py          # エントリーポイント
│   ├── models.py        # データベースモデル
│   ├── schemas.py       # Pydanticスキーマ
│   ├── database.py      # データベース接続設定
│   ├── crud.py          # データ操作用ロジック
│   └── routers/
│       ├── todo.py      # タスク関連のエンドポイント
│       └── auth.py      # 認証関連 (オプション)
└── requirements.txt     # 必要なPythonパッケージ

## ファイルの内容
1. main.py
FastAPIのエントリーポイント。

from fastapi import FastAPI
from app.routers import todo

app = FastAPI(title="ToDo App")

# ルーター登録
app.include_router(todo.router)

@app.get("/")
def read_root():
    return {"message": "Welcome to the ToDo API!"}

(以下省略)

プロンプトジェネレーターを使う場合

まずはプロンプトジェネレーターに「FastAPIを使ったToDOアプリの設計をしてください。」と入力し、プロンプトを生成させます。
生成されたプロンプトがこちらです。要件やデータモデル、エンドポイントの定義が明確になっています。

FastAPIを使用してToDoアプリの設計を行ってください。データの処理、APIのインターフェース、エンドポイント、必要なリクエスト/レスポンス形式、ならびにデータモデルについて具体的に定義してください。

# 要件

以下の機能を備えたToDoアプリを設計してください:

1. ToDoリストの作成:新しいToDoタスクを追加できる。
2. ToDoリストの取得:現在のタスクのリストをすべて取得できる。
3. ToDoタスクの更新:指定されたタスクの情報(タイトル、ステータスなど)を更新できる。
4. ToDoタスクの削除:指定されたタスクを削除できる。

# データモデル

設計するためには、ToDoタスクに関して次の情報を考慮してください:

- ID: タスクの一意の識別子
- タイトル: タスクの内容を示す文字列
- 詳細(任意): タスクの詳細説明
- 期限(任意): タスクの締切日
- 完了状態: タスクの進行状況を示すフラグ(完了か未完了か)

# API エンドポイントの定義

必要なエンドポイントを含めて、FastAPIアプリの設計を行ってください。各エンドポイントには以下のものが含まれるべきです:

- メソッド(GET, POST, PUT, DELETE)
- エンドポイントURL(パス)
- 必要なリクエストのパラメータ(クエリパラメータ、パスパラメータ、リクエストボディなど)
- 期待されるレスポンスの形式

# Steps

1. **データモデル設計**: ToDoタスクに必要な属性(id, タイトル、状態など)をPythonの`pydantic`を使って定義。
2. **FastAPIサーバーのセットアップ**: 必要なエンドポイントと、それに関連する関数をFastAPIで構築。
3. **CRUD機能**: CRUD操作すべてに対応するエンドポイントを実装 (`create_to_do`, `read_to_do`, `update_to_do`, `delete_to_do`).
4. **エラーハンドリング**: 適切なステータスコードと共にエラーメッセージの設計。
5. **データの保存方法**: 一時的にメモリにデータを保持する方法を使用し、将来的にデータベースに移行可能である設計。

# Output Format

具体的なエンドポイントのURL、一つひとつのエンドポイントに関する説明、それぞれに対するリクエスト形式およびレスポンス形式、例外発生時のステータスコードなどを詳細に提供してください。出力は以下の形式で記述してください:

- **エンドポイントURL:**
  `/todos` (POST): 新規タスク追加
  - **リクエストボディ:**
    ```json
    {
      "title": "タスクのタイトル",
      "details": "タスクの詳細",
      "due_date": "YYYY-MM-DD",
      "completed": false
    }
    ```
  - **レスポンス:**
    ```json
    {
      "id": 1,
      "title": "タスクのタイトル",
      "details": "タスクの詳細",
      "due_date": "YYYY-MM-DD",
      "completed": false
    }
    ```

- **その他 CRUD 操作のエンドポイントについても同様に記述**。

# Examples 

**エンドポイントの作成例**:

1. 新しいToDoタスクを追加する:
   - **リクエスト**
     - メソッド: `POST`
     - パス: `/todos`
     - ボディ: { "title": "買い物", "details": "牛乳とパンを買う", "due_date": "2023-05-30", "completed": false }
   - **レスポンス**
     - ステータスコード: `200 OK`
     - ボディ: { "id": 1, "title": "買い物", "details": "牛乳とパンを買う", "due_date": "2023-05-30", "completed": false }

**注**:本設計はベースとして簡単なToDoアプリですので、スケーリングやデータ永続化の詳細についての検討が将来的には必要です。

次にこのプロンプトを使ってChatGPTに生成させた結果がこちらです。
プロンプトジェネレーターを使わない場合と比較して、DBや認証の有無について違いがあります。
また、何度か同じプロンプトを試した場合に、プロンプトジェネレーターを使った場合は安定して同じような結果が得られるのに対し、使わない場合はランダム性が高い出力になりました。

以下は、FastAPI を使用して設計したシンプルな ToDo アプリの仕様とコード例です。

## 設計概要
### データモデル
pydantic を使用して、以下のデータモデルを定義します。

- ID: 一意の識別子 (自動生成)
- タイトル: タスク名 (必須)
- 詳細: タスクの詳細説明 (任意)
- 期限: 締切日 (任意、ISOフォーマットの日付)
- 完了状態: 完了/未完了のフラグ (デフォルトは未完了)

## API エンドポイント
### エンドポイント一覧
1. ToDoリストの作成
   - メソッド: POST
   - パス: /todos
   - 説明: 新しい ToDo タスクを作成する。
2. ToDoリストの取得
   - メソッド: GET
   - パス: /todos
   - 説明: すべての ToDo タスクを取得する。
3. ToDoタスクの更新
   - メソッド: PUT
   - パス: /todos/{id}
   - 説明: 指定された ID の ToDo タスクを更新する。
4. ToDoタスクの削除
   - メソッド: DELETE
   - パス: /todos/{id}
   - 説明: 指定された ID の ToDo タスクを削除する。

## データモデル
以下は pydantic モデルの定義です。

from pydantic import BaseModel, Field
from typing import Optional
from datetime import date

class Todo(BaseModel):
    title: str
    details: Optional[str] = None
    due_date: Optional[date] = None
    completed: bool = Field(default=False)

class TodoResponse(Todo):
    id: int

## FastAPI アプリ
以下は main.py の内容です。

(以下省略)

ブログ作成

最後に、生成AIの比較記事をChatGPTで作成させた場合の違いについて見てみます。

プロンプトジェネレーターを使わない場合

「対話型生成AIの比較に関するブログを書いてください。」というプロンプトでChatGPTを実行した結果が以下です。

# 対話型生成AIの比較:どのAIが最適なのか?

近年、対話型生成AIは急速に進化し、私たちの日常生活やビジネスシーンにおいて重要な役割を果たすようになっています。この記事では、代表的な対話型生成AIを比較し、それぞれの特徴や用途について詳しく解説します。

---

## 比較対象のAI

以下の3つの対話型生成AIを比較します:

- **ChatGPT (OpenAI)**
- **Bard (Google)**
- **Claude (Anthropic)**

---

### 1. ChatGPT (OpenAI)

**特徴**  
ChatGPTはOpenAIが開発した対話型AIで、高度な自然言語処理(NLP)技術を活用しています。多様なタスクに対応可能で、ビジネスや教育、クリエイティブな作業に適しています。

**メリット**  
- 柔軟な会話能力  
- プラグインを活用した拡張機能(プレミアムプラン)  
- 多言語対応の精度が高い  

**デメリット**  
- 最新情報を取得するにはインターネット接続やプラグインが必要  
- 無料版は性能が制限される  

**主な用途**  
ビジネスドキュメントの作成、学習サポート、カスタマーサポート。

---

### 2. Bard (Google)

**特徴**  
BardはGoogleが開発した対話型AIで、検索エンジンとの強い統合が特徴です。最新情報の提供やデータ検索が得意です。

**メリット**  
- 最新のウェブ情報を即座に取得可能  
- Googleツール(Gmail, Docs, Sheetsなど)との連携  
- 簡潔な回答を得意とする  

**デメリット**  
- 一部の創造的なタスクでの応答が限定的  
- ユーザーのカスタマイズ性が低い  

**主な用途**  
ニュースの検索、プロジェクト管理、簡単な質問への迅速な回答。

---

### 3. Claude (Anthropic)

**特徴**  
Claudeは人間中心の設計哲学に基づき、安全性と信頼性を重視した対話型AIです。エラーや誤解の少ない、慎重な回答を提供します。

**メリット**  
- 高い安全性と倫理基準  
- 長文テキストの処理能力  
- 応答の透明性が高い  

**デメリット**  
- 創造性やユーモアがやや控えめ  
- 特定のタスクでの柔軟性が制限される  

**主な用途**  
契約書レビュー、倫理的な意思決定支援、教育コンテンツの作成。

---

## 比較表

| **特徴**       | **ChatGPT** | **Bard**        | **Claude**      |
|----------------|-------------|-----------------|-----------------|
| **柔軟性**     | 高い        | 中程度          | 中程度          |
| **最新情報**   | プラグインが必要 | 非常に高い       | 中程度          |
| **安全性**     | 高い        | 中程度          | 非常に高い       |
| **創造性**     | 高い        | 中程度          | 低い            |
| **用途の幅広さ** | 非常に広い   | 広い            | 狭い            |

---

## どのAIを選ぶべきか?

選ぶべきAIは、利用目的によります。

- **クリエイティブな作業や多様なタスクが必要な場合**:ChatGPT  
- **最新情報やGoogleツールとの連携を重視する場合**:Bard  
- **安全性や正確な回答を求める場合**:Claude  

---

## まとめ

対話型生成AIは日々進化しており、どのAIも独自の強みを持っています。どのAIを選ぶかは、あなたの目的や期待する成果次第です。今後も新たな機能や性能向上が期待されるため、常に最新の情報をキャッチアップし、最適なAIを選択しましょう。

プロンプトジェネレーターを使う場合

まずはプロンプトジェネレーターに「対話型生成AIの比較に関するブログを書いてください。」と入力してプロンプトを生成させた結果がこちらです。
ブログの構成や出力形式、例がプロンプトに含まれています。

対話型生成AIの比較に関するブログ記事を書いてください。

あなたのブログでは、異なる対話型生成AIツールについて、主な特長、メリット、デメリット、使い方の違いを取り上げ、読者にとってどのツールが最適かについてわかりやすく説明します。

# ブログ作成の手順
1. **対話型生成AIの概要**: 対話型生成AIとは何か、どのような目的で使用されるのかを簡単に説明してください。
2. **代表的なAIツールの紹介**: 対話型生成AIとして知られている代表的なツール(例: ChatGPT, Bing AI, Google Bardなど)について簡潔に紹介し、それぞれの開発企業とツールの違いを読者にわかりやすく説明してください。
3. **各ツールの特長と機能比較**:
   - 各ツールの特長をまとめましょう。特に、生成能力、対応するコミュニケーションスタイル、応答速度、価格、追加機能(例えば、情報の検索、API連携)などを考慮してください。
   - 比較表形式でも良いですし、個別に詳しく説明しても構いません。
4. **メリットとデメリットの分析**:
   - 各AIツールの長所と短所を具体的に解説してください。読者がどのような場面でどのAIを利用すべきかの参考になるようにすることを意識しましょう。
5. **使用する場面によって異なる推奨AI**:
   - 例えば、「ビジネスでの使用」、「教育目的」、「リサーチのための補助」別に、最適なツールを推薦してください。
6. **結論と推奨**:
   - 各ツールの使い勝手から最も適した利用シナリオを提示し、どのAIが読者にとって理想的であるかを導き出してください。

# 出力形式
- **全体の構成**: セクションごとに整理し、段落を使用して流れをわかりやすくしてください。
- **長さ**: 1000〜1500語のブログ。
- **語調**: 読み手に優しく、わかりやすく説明するトーンを使用してください。

# 例
**例1: 対話型生成AIの紹介部分**:
「ChatGPTはOpenAIによって開発された有名な対話型生成AIです。自然でフレンドリーな会話を通じて、幅広い質問に回答することができます。特にクリエイティブな文章生成に強みを持ち、マーケティングやコンテンツ作成などの場面で広く使用されています。」

**例2: 比較と推奨**:
「ビジネス用途の場合、精度と応答速度を重視するため、Bing AIが良い選択肢かもしれません。一方で、創造性や自由な会話を求めるのであれば、ChatGPTが最も使いやすいでしょう。」

# 注意事項
- 各ツールの最新の情報(リリース日、アップデート等)に基づいて記事を書いてください。
- 偏った評価は避け、公平かつ客観的な見解を述べることが重要です。

次にこのプロンプトを使ってブログを生成させた結果がこちらです。
プロンプトジェネレーターを使わない場合と比較して、どのような場面で使えるかが追加されていたり、タイトルやまとめの表現が読者に印象が残るような形に工夫されています。

# 対話型生成AIの徹底比較:自分に最適なツールを見つけよう

日常会話から専門的なリサーチまで、対話型生成AIは私たちの生活を一変させました。この記事では、ChatGPT、Bing AI、Google Bardといった主要な対話型生成AIツールを比較し、それぞれの特長、メリット、デメリット、適切な使用シナリオを解説します。どのツールがあなたに最適なのかを知るための参考にしてください。

---

## 対話型生成AIとは?

対話型生成AIとは、自然言語処理(NLP)を活用して、人間と自然な会話ができるAIツールです。これらのAIは質問への回答、アイデアの提案、問題解決、クリエイティブな文章生成、そして学習支援など、幅広い用途で使用されています。ユーザーはこれらのツールを通じて、仕事の効率化や新しい知見の獲得を実現できます。

---

## 代表的なAIツールの紹介

### 1. ChatGPT
- **開発企業**: OpenAI  
- **概要**: ChatGPTは、OpenAIが開発した汎用性の高い対話型生成AIです。優れた自然言語生成能力を備え、創造的な文章作成や親しみやすい会話が得意です。

### 2. Bing AI
- **開発企業**: Microsoft  
- **概要**: Bing AIはMicrosoftが提供するツールで、Bing検索エンジンと統合されています。リアルタイムのウェブ検索機能を活用して、最新情報を迅速に提供します。

### 3. Google Bard
- **開発企業**: Google  
- **概要**: Google BardはGoogleの対話型生成AIで、Googleの膨大なデータベースを活用し、幅広い質問に対応可能。検索と知識提供をスムーズに行えます。

---

## 各ツールの特長と機能比較

| **特長**         | **ChatGPT**                         | **Bing AI**                          | **Google Bard**                      |
|------------------|-------------------------------------|--------------------------------------|--------------------------------------|
| **生成能力**     | 創造性が高く、文章の質が非常に高い         | 実用性が高く、事実に基づく回答に強い        | 情報の網羅性が高い                      |
| **応答速度**     | 高速                                | 高速                                 | 高速                                  |
| **最新情報の提供** | 最新情報には非対応                     | リアルタイムのウェブ検索が可能             | リアルタイムのデータに対応               |
| **価格**         | 無料プランあり、有料プラン(ChatGPT Plus)は追加機能提供 | 無料                                 | 無料                                  |
| **追加機能**     | API連携やプラグインが豊富               | Bing検索エンジンとの統合                 | Googleサービスとのシームレスな統合        |

---

## 各ツールのメリットとデメリット

### ChatGPT
**メリット**  
- 創造性が高く、マーケティングやコンテンツ作成に最適  
- 親しみやすい会話スタイルで初心者にも使いやすい  
- プラグインで機能拡張が可能  

**デメリット**  
- 最新の情報には対応していない  
- 情報の正確性に欠ける場合がある  

---

### Bing AI
**メリット**  
- 最新情報をリアルタイムで取得可能  
- 事実に基づいた実用的な回答が得意  
- 無料で使える  

**デメリット**  
- 創造的な文章生成や柔軟な会話には弱い  
- 一部の専門的な質問に対する対応が制限されることがある  

---

### Google Bard
**メリット**  
- Google検索と連動した膨大な知識にアクセス可能  
- 高度な質問に対応しやすい  
- 無料で利用でき、Googleサービスとの相性が良い  

**デメリット**  
- 応答の自然さや創造性ではChatGPTに劣る場合がある  
- 個別の詳細なリサーチには不向き  

---

## 使用する場面による推奨AI

### 1. ビジネスでの使用
- **おすすめツール**: Bing AI  
- **理由**: 最新情報の検索が必要なプレゼン資料やリサーチ、顧客対応のための正確な情報提供に最適。

### 2. 教育目的
- **おすすめツール**: Google Bard  
- **理由**: 知識の網羅性が高く、学術的な質問にも対応しやすい。Google ClassroomやGoogleドキュメントとの連携も便利。

### 3. リサーチのための補助
- **おすすめツール**: Bing AI  
- **理由**: リアルタイムで正確な情報を引き出せるため、最新のデータを必要とするリサーチに向いている。

### 4. クリエイティブな作業
- **おすすめツール**: ChatGPT  
- **理由**: 創造性に優れ、物語の作成、広告コピーの作成、ブログ記事のアイデア出しなどに最適。

---

## 結論と推奨

対話型生成AIは、それぞれ異なる特長を持っており、使用する目的や場面によって選ぶべきツールが異なります。

- **創造性を重視するなら**: ChatGPT  
- **正確で最新の情報を必要とするなら**: Bing AI  
- **幅広い知識とGoogleサービスの連携を活用したいなら**: Google Bard  

すべてのツールには無料プランがあるので、まずはそれぞれ試してみて、あなたの目的に最適なツールを見つけてください。これらのAIを活用することで、あなたの生活や仕事がさらに効率的で豊かになることでしょう!

3. まとめ

新しく登場した、OpenAIのプロンプトジェネレーターについて紹介しました。
プロンプトジェネレーターを使うことで、誰でも簡単に高品質なプロンプトを作成でき、生成AIから得られる出力の質が上がります。
実際の使い方としては、プロンプトジェネレーターで生成されたプロンプトをそのまま使うというよりは、プロンプトジェネレーターでまずはプロンプトを作成し、内容を修正するというのが良さそうです。
自分でイチからプロンプトを書くより遥かに効率化できるので、ぜひ使ってみてください。

参考までに、プロンプトジェネレーターで生成されるプロンプトをAPIで実現したい人は、OpenAIのドキュメントにプロンプト生成の記事があるので見てみてください。

platform.openai.com

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

Amazon Bedrock Knowledge Base は OpenSearch にどのようなインデックスを作成するのか?

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

寒くなってきたので、暖房をつけてしまいました。

今回は、Bedrock Knowledge Base のベクトルデータベースとして OpenSearch Serverless を使ったときに、 自動で作成される OpenSearch Serverless のインデックスには何が保存されるのか確認しました。

1. Bedrock Knowledge Base とは

Bedrock Knowledge BaseAmazon Bedrock が提供する簡単に RAG を実現するためのサービスです。

詳細はこちらの記事で紹介しています。

acro-engineer.hatenablog.com

2. OpenSearch Serverless とは

今回は Knowledge Base のベクトルデータベースとして OpenSearch Serverless を用います。

docs.aws.amazon.com

Bedrock KnowledgeBase はデフォルトのベクトルストアとして OpenSearch Serverless を用意しており、既存の OpenSearch Serverless のインデックスがない場合に、自動で作成してくれます。

OpenSearch Serverless を用いることで、Bedrock Knowledge Base の検索時にハイブリッド検索を利用できるようになります。

ハイブリッド検索では、ベクトル検索だけではなく、キーワード検索も行い正確な一致を補完するため、特にユーザーが特定のキーワードや地名などを検索したいときに優位性が見られます。

また、チャンキング戦略も柔軟に設定でき、セマンティックチャンキングや階層チャンキングを自前で実装することなく、設定変更だけで使用できます。

チャンクの設定は RAG を行う上で、精度に大きく影響する部分なので、簡単に変更できるのはうれしいです。

docs.aws.amazon.com

3. 中身を確認する

IPA が発行している DX 白書 を S3 に置き、それを取り込んだ結果を、デフォルトの固定チャンキング/セマンティックチャンキング/階層チャンキングを使った場合で比べてみます。

それぞれ下記手順で実施しました。

  1. ベクトルストアとして新規作成の OpenSearch Serverless を設定し、 Bedrock Knowledge Base を作成
  2. Bedrock Knowledge Base のデータソースに、対象の S3 バケットとチャンキング戦略を設定
  3. OpenSearch Serverless の DevTools から、インデックスに保存されたドキュメントを確認

※以下、出力内容を、見やすいように省略・整形を行っています。実際の内容とは異なりますが、ご了承ください。

まずは、作成されたコレクション・インデックスを確認してみます。

作成されたコレクション

インデックスは bedrock-knowledge-base-default-index という名前で作成されていました。

default-index 部分は作成した Bedrock Knowledge Base 名というわけではなく、固定値のようです。
同様にベクトルプロパティーdefault-vector 部分も Bedrock Knowledge Base 名に関わらず、固定値でこの値になります。

ベクトル検索のエンジンには FAISS を利用しているようです。
ディメンションに設定されている 1024 は作成時に設定した Titan Text Embeddings v2 の 次元数となっています。

また、複数の Bedrock Knowledge Base を作成した場合は、新しいコレクションを作成することが分かりました。

OpenSearch Serverless の課金単位の OCU は複数コレクション間で共有されるため、複数のコレクションが作成されることで必要以上に課金されてしまう、ということは無いので安心できます。

{
  "bedrock-knowledge-base-default-index": {
    "aliases": {},
    "mappings": {
      "dynamic_templates": [
        {
          "strings": {
            "match_mapping_type": "string",
            "mapping": {
              "fields": {
                "keyword": {
                  "ignore_above": 2147483647,
                  "type": "keyword"
                }
              },
              "type": "text"
            }
          }
        }
      ],
      "properties": {
        "AMAZON_BEDROCK_METADATA": {
          "type": "text",
          "index": false
        },
        "AMAZON_BEDROCK_TEXT_CHUNK": {
          "type": "text"
        },
        "bedrock-knowledge-base-default-vector": {
          "type": "knn_vector",
          "dimension": 1024,
          "method": {
            "engine": "faiss",
            "space_type": "l2",
            "name": "hnsw",
            "parameters": {}
          }
        }
      }
    },
    "settings": {
      "index": {
        "number_of_shards": "2",
        "provided_name": "bedrock-knowledge-base-default-index",
        "knn": "true",
        "creation_date": "1728124297900",
        "number_of_replicas": "0",
        "uuid": "4Us8XJIBvOt5ziw6InSg",
        "version": {
          "created": "136327827"
        }
      }
    }
  }
}

3.1. デフォルト(固定チャンキング)

デフォルト設定でデータソースを作成

ベクトルフィールドとして指定した bedrock-knowledge-base-default-vector に 1024 次元のベクトルが、テキストフィールドとして指定した AMAZON_BEDROCK_TEXT_CHUNK にチャンクの本文が保存されていました。
Bedrock Knowledge Base でハイブリッド検索を行う際は、これらのフィールドに対してクエリが発行されます。

また、元ドキュメントの URIx-amz-bedrock-kb-source-uri もしくは AMAZON_BEDROCK_METADATA.source から取得できます。

{
  "_index": "bedrock-knowledge-base-default-index",
  "_id": "1%3A0%3AHo6UGJIBPr4GtWZKo1_n",
  "_score": 1,
  "_source": {
    "bedrock-knowledge-base-default-vector": [
      -0.060821593,
      -0.019813094,
      -0.021656172,
      ......
    ],
    "x-amz-bedrock-kb-source-uri": "s3://bedockknowledgebaseindextest/DX白書2023.pdf",
    "AMAZON_BEDROCK_METADATA": "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
    "x-amz-bedrock-kb-data-source-id": "AZVXAGUKDE",
    "id": "9ea53e99-9de4-4845-a10a-d041e4845cac",
    "AMAZON_BEDROCK_TEXT_CHUNK": "独 立 行 政 法 人 情 報 処 理 推 進 機 構   進 み 始 め た「 デ ジ タ ル 」、進 ま な い「 ト ラ ン ス フ ォ ー メ ー シ ョ ン 」   ISBN978-4-905318-78-1 C0004¥3000E   定価 3,000 円(税別)......"
  }
}

3.2. セマンティックチャンキング

セマンティックチャンキングを指定して作成

チャンクの区切り位置が異なるだけで、保存されるフィールドなどは固定チャンキングの場合と同様でした。

{
  "_index": "bedrock-knowledge-base-default-index",
  "_id": "1%3A0%3A6iGuGJIBB-x3T8xG_KDI",
  "_score": 1,
  "_source": {
    "bedrock-knowledge-base-default-vector": [
      -0.023106523,
      0.06026018,
      0.03623754,
      ......
    ],
    "x-amz-bedrock-kb-source-uri": "s3://bedockknowledgebaseindextest/DX白書2023.pdf",
    "AMAZON_BEDROCK_METADATA": "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
    "x-amz-bedrock-kb-data-source-id": "2FC3G7Z7LK",
    "id": "4cc7f820-a014-47a0-baf5-7f6848d1427e",
    "AMAZON_BEDROCK_TEXT_CHUNK": "経済産業省がとりまとめた「DX推進指標」を用\nいて各企業が自己診断した結果を独立行政法人情報処理推進機構(IPA)が収集し、分析したレポート......"
  }
}

3.3. 階層チャンキング

階層チャンキングを指定して作成

階層チャンキングを指定した場合は、 AMAZON_BEDROCK_METADATA.parentText が追加されました。

検索するときは他のチャンキング戦略と同様に AMAZON_BEDROCK_TEXT_CHUNK フィールドを使用し、返却する文字列が AMAZON_BEDROCK_TEXT_CHUNK フィールドではなく AMAZON_BEDROCK_METADATA.parentText になるようです。

親チャンクの内容は複数回インデックスに含まれることになるため、固定チャンキング・セマンティックチャンキングよりも消費するデータ量が多くなりそうです。

{
  "_index": "bedrock-knowledge-base-default-index",
  "_id": "1%3A0%3AnSGYGJIBB-x3T8xGX5zG",
  "_score": 1,
  "_source": {
    "bedrock-knowledge-base-default-vector": [
      -0.060821593,
      -0.019813094,
      -0.021656172,
      ......
    ],
    "x-amz-bedrock-kb-source-uri": "s3://bedockknowledgebaseindextest/DX白書2023.pdf",
    "AMAZON_BEDROCK_METADATA": "{
      \"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\",
      \"parentText\":\"また、2022年9月には、経済産業省により「デジタルガバナンスコード」の改 訂版として時勢の変化に対応した「デジタルガバナンス・コード2.0」が公表され、企業によるDXの更な る促進が期待されています......\"
    }",
    "x-amz-bedrock-kb-data-source-id": "IYDKX62IHR",
    "id": "58564fd7-fc27-41c6-9e33-785e7e3ad0f5",
    "AMAZON_BEDROCK_TEXT_CHUNK": "独 立 行 政 法 人 情 報 処 理 推 進 機 構   進 み 始 め た「 デ ジ タ ル 」、進 ま な い「 ト ラ ン ス フ ォ ー メ ー シ ョ ン 」   ISBN978-4-905318-78-1 C0004¥3000E   定価 3,000 円(税別)......"
  }
}

階層チャンキングの仕組み

以上、各チャンク戦略において、インデックスに含まれる情報をまとめると下記の表のようになります。

No チャンク戦略 ベクトル テキスト ソースURI 親テキスト
1 固定(デフォルト)
2 セマンティック
3 階層
  • ベクトル: bedrock-knowledge-base-default-vector
  • テキスト: AMAZON_BEDROCK_TEXT_CHUNK
  • ソース URIAMAZON_BEDROCK_METADATA.source
  • 親テキスト: AMAZON_BEDROCK_METADATA.parentText

4. OpenSearch Serverless に直接クエリを投げる

Bedrock Knowledge Base に作ってもらったインデックスですが、当然、直接 API リクエストを投げることも可能です。

今回は『DX白書』に記載されているOded Galor氏の著書の内容を知りたいものとして、テキスト検索・ベクトル検索・ハイブリッド検索を順番に試してみます。

下記文章が検索でヒットさせたい内容です。

ブラウン大学経済学教授のOded Galorが書いた「格差の起源*2」によると、人類の進歩は多様性と共にあるが、日本人は同一性が高く内側に閉じているので、世界で最もこの輪の中に加われていない可能性が高い。」

4.1. テキスト検索

下記手順で検索を実行しました。

  1. リクエストを投げるためのユーザーをロールを作成する

    aoss:APIAccessAll の権限が必要です。

  2. OpenSearch Serverless のコレクションに紐づけられているデータアクセスコントロールポリシーを編集し、 プリンシパルとして、作成したロールを追加する

    プリンシパルを追加

  3. 作成したロールを assume し、認証情報を取得する

     session=$(aws sts assume-role \
       --role-arn ${role_arn} \
       --role-session-name "role_session"
     )
    
     aws_access_key_id=$(echo "${session}" | jq -r ".Credentials.AccessKeyId")
     aws_secret_access_key=$(echo "${session}" | jq -r ".Credentials.SecretAccessKey")
     aws_session_token=$(echo "${session}" | jq -r ".Credentials.SessionToken")
    
  4. AWS Signature V4 認証を使ってリクエストを投げる

     region=us-west-2
     service=aoss
    
     curl -v -k -X GET "${opensearch_serverless_endpoint}/bedrock-knowledge-base-default-index/_search?pretty" \
     -H "Content-Type: application/json" \
     -H "X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" \
     -H "X-Amz-Security-Token: ${aws_session_token}" \
     --aws-sigv4 "aws:amz:${region}:${service}" \
     --user "${aws_access_key_id}:${aws_secret_access_key}" \
     -d '
     {
       "_source": ["AMAZON_BEDROCK_TEXT_CHUNK", "AMAZON_BEDROCK_METADATA"],
       "query": {
         "match": {
           "AMAZON_BEDROCK_TEXT_CHUNK": " オデッド・ガロー 著書"
         }
       }
     }'
    

下記のようにレスポンスを受け取ることができました。

{
  "took" : 20,
  "timed_out" : false,
  "_shards" : {
    "total" : 0,
    "successful" : 0,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 575,
      "relation" : "eq"
    },
    "max_score" : 20.662754,
    "hits" : [
      {
        "_index" : "bedrock-knowledge-base-default-index",
        "_id" : "1%3A0%3AR1T795IBG2d6FmmHb-K4",
        "_score" : 20.662754,
        "_source" : {
          "AMAZON_BEDROCK_METADATA" : "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
          "AMAZON_BEDROCK_TEXT_CHUNK" : "* 2  オデッド・ガロー著、柴田裕之監訳、森内薫翻訳、『格差の起源......"
        }
      },
      {
        "_index" : "bedrock-knowledge-base-default-index",
        "_id" : "1%3A0%3ApVT795IBG2d6FmmHjuST",
        "_score" : 7.843234,
        "_source" : {
          "AMAZON_BEDROCK_METADATA" : "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
          "AMAZON_BEDROCK_TEXT_CHUNK" : "Agency, Japan.     ・本白書は著作権法上の保護を受けています......"
        }
      },
      ......
    ]
  }
}

引用情報は取得できましたが、内容が書かれた部分はヒットしませんでした。

該当部分はアルファベット表記のため、文字列が一致せず、検索に引っかからなかったものと思われます。

4.2. ベクトル検索

  1. 検索したいテキストを Titan Text Embeddings V2 ( Bedrock Knowledge Base 作成時に選択したモデル)でベクトル化し、ファイルに保存する

    vector.txt (「Oded Galor 著書」 をベクトル化したもの)

     [-0.09754358977079391, 0.004242392256855965, -0.024888699874281883, ......]
    
  2. 「4.1.」 と同様に認証情報を取得する

  3. ベクトルを使った検索を行う

     vector=$(cat vector.txt)
    
     curl -v -k -X GET "${opensearch_serverless_endpoint}/bedrock-knowledge-base-default-index/_search?pretty" \
     -H "Content-Type: application/json" \
     -H "X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" \
     -H "X-Amz-Security-Token: ${aws_session_token}" \
     --aws-sigv4 "aws:amz:${region}:${service}" \
     --user "${aws_access_key_id}:${aws_secret_access_key}" \
     -d '
     {
       "_source": ["AMAZON_BEDROCK_TEXT_CHUNK", "AMAZON_BEDROCK_METADATA"],
       "query": {
         "knn": {
           "bedrock-knowledge-base-default-vector": {
             "vector": '"$vector"',
             "k": 10
           }
         }
       }
     }'
    

下記のレスポンスを得られました。

{
  "took" : 20,
  "timed_out" : false,
  "_shards" : {
    "total" : 0,
    "successful" : 0,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 20,
      "relation" : "eq"
    },
    "max_score" : 0.37559095,
    "hits" : [
      {
        "_index" : "bedrock-knowledge-base-default-index",
        "_id" : "1%3A0%3AKkb795IB38U8vDmpYXU0",
        "_score" : 0.37559095,
        "_source" : {
          "AMAZON_BEDROCK_METADATA" : "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
          "AMAZON_BEDROCK_TEXT_CHUNK" : "また、売上規模が大きくなるほど、受発注や物流のプ ラットフォームサービスなど、......"
        }
      },
      {
        "_index" : "bedrock-knowledge-base-default-index",
        "_id" : "1%3A0%3A0kb795IB38U8vDmphXe1",
        "_score" : 0.37314636,
        "_source" : {
          "AMAZON_BEDROCK_METADATA" : "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
          "AMAZON_BEDROCK_TEXT_CHUNK" : "製品・サービスの開発     製造工程、製造設備     ロジスティクス・調達・物流......"
        }
      },
      ......
    ]
  }
}

ベクトル検索でも、欲しい文章はヒットしませんでした。

ベクトル検索では、人名や機種名などの記号的な内容は、検索でヒットさせるのが難しいです。

4.3. ハイブリッド検索

  1. 「4.2.」 と同様に検索したいベクトルをファイルに保存する

  2. 「4.1.」 と同様に認証情報を取得する

  3. ベクトルと文字列を使った検索を行う

     vector=$(cat vector.txt)
    
     curl -v -k -X GET "${opensearch_serverless_endpoint}/bedrock-knowledge-base-default-index/_search?pretty" \
     -H "Content-Type: application/json" \
     -H "X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" \
     -H "X-Amz-Security-Token: ${aws_session_token}" \
     --aws-sigv4 "aws:amz:${region}:${service}" \
     --user "${aws_access_key_id}:${aws_secret_access_key}" \
     -d '
     {
       "_source": ["AMAZON_BEDROCK_TEXT_CHUNK", "AMAZON_BEDROCK_METADATA"],
       "query": {
         "knn": {
           "bedrock-knowledge-base-default-vector": {
             "vector": '"$vector"',
             "k": 10,
             "filter": {
               "match": {
                 "AMAZON_BEDROCK_TEXT_CHUNK": "Oded Galor 著書"
               }
             }
           }
         }
       }
     }'
    

下記のようにレスポンスを得られました。

{
  "took" : 18,
  "timed_out" : false,
  "_shards" : {
    "total" : 0,
    "successful" : 0,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 20,
      "relation" : "eq"
    },
    "max_score" : 0.40019944,
    "hits" : [
      {
        "_index" : "bedrock-knowledge-base-default-index",
        "_id" : "1%3A0%3AR1T795IBG2d6FmmHb-K4",
        "_score" : 0.40019944,
        "_source" : {
          "AMAZON_BEDROCK_METADATA" : "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
          "AMAZON_BEDROCK_TEXT_CHUNK" : "* 2  オデッド・ガロー著、柴田裕之監訳、森内薫翻訳、『格差の起源......"
        }
      },
      {
        "_index" : "bedrock-knowledge-base-default-index",
        "_id" : "1%3A0%3ARFT795IBG2d6FmmHb-K4",
        "_score" : 0.36775848,
        "_source" : {
          "AMAZON_BEDROCK_METADATA" : "{\"source\":\"s3://bedockknowledgebaseindextest/DX白書2023.pdf\"}",
          "AMAZON_BEDROCK_TEXT_CHUNK" : "ブラウン大学経済学教授のOded Galorが書いた「格差の起源*2」によると、......"
        }
      },
      ......
    ]
  }
}

テキスト検索とベクトル検索を組み合わせたハイブリッド検索を行うことで、テキスト検索でヒットしていた引用部分に加え、著書の内容が書かれている部分もヒットするようになり、求めているドキュメントを取得することができました。

文字列での検索と意味的な検索の良いとこ取りができています。

まとめ

Bedrock Knowledge Base が OpenSearch Serverless のインデックスにどのような情報を保存しているのかを確認しました。

特に階層チャンキングがどのように実現しているのかはあまり気にしていなかったのですが、データ量とのトレードオフがあることが分かりました。

メリット・デメリットを考えながら、適切なチャンキング戦略を選択したいですね。

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

Difyを安全にバージョンアップできるようにする

はじめに

こんにちは。10月も半ばを過ぎ、秋らしい空気が広がっていますね。
紅葉が見頃になるのが待ち遠しいです。
AWSエンジニアの小林です。

さて今回は、生成AIアプリの開発プラットフォームとして注目を集めている「 Dify」を扱っていきます。
Difyには、Dify自体が提供するSaaSとは別に、Dockerを利用してセルフホストできる Community Edition があります。
本記事では、Community EditionでDify構築を行っていきます。
docker-compose.yamlを使用してアプリケーションをデプロイする際、デフォルトでデータベースもコンテナとして構築し、管理しています。
これは試験的な開発にはとても便利ですが、アップデートの運用などを考えるとコンテナの停止や削除によるデータ損失リスクなど課題が出てきます。
そこで、安全にDifyをアップデートするためにデータベースを自前で用意してDifyに組み込んでみたいと思います。

Difyの概要

DifyはLLMを使用した生成AIアプリをノーコードで簡単に作成できるオープンソースのプラットフォームです。
dify.ai

Difyについては以下の記事で詳細が説明されているので、参考にしてもらえればと思います。
acro-engineer.hatenablog.com

自前で用意したDBをDifyに組み込む

Dify用データベースを構築する

DifyではデータベースとしてPostgreSQLを使用します。
そこで今回は、Amazon RDS for PostgreSQLでデータベースを用意していきます。
構築するアーキテクチャは次の通りです。

パブリックサブネットおよびプライベートサブネット、Difyサーバー用EC2インスタンスは既に構築済みとして進めていきます。
RDS > データベースの作成ページを開きます。
「エンジンのオプション」で「PostgreSQL」を選択します。

「設定」>認証情報の設定で指定する、「マスターユーザー名」、「パスワード」はDifyの設定ファイルに使用するので控えておきます。
また、「パスワードを自動生成」の場合、パスワードは「データベースの作成」を押下後のダイアログからしか確認できないため注意してください。

パスワード確認箇所

「接続」で、「コンピューティングリソース」としてDifyサーバー用のEC2インスタンスを選択します。
これによって、EC2インスタンスがRDSに接続するためのセキュリティグループ設定が自動で構築されます。

続いて、構築したRDS PostGreSQLに、Dify用のデータベースを作成します。
Difyサーバー用EC2インスタンスSSH接続し、以下コマンドでPostgreSQLをインストールします。

$ sudo dnf install -y postgresql15-server

構築したRDSのPostgreSQLに接続します。

$ psql -h {RDSのエンドポイント} -p {RDSのport} -U postgres

PostgreSQLにDifyアプリケーション用のデータベースをdifyという名前で作成します。

postgres=> CREATE DATABASE dify;

以上で、自前のPostgreSQLデータベースの構築は完了です。

Dify構築用ソースを修正する

Difyサーバー用EC2インスタンスSSH接続し、以下コマンドでDify構築に必要なソフトウェアをインストールします。

$ sudo yum update -y
$ sudo yum install -y git
$ sudo yum install -y docker
$ DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
$ mkdir -p $DOCKER_CONFIG/cli-plugins
$ curl -SL https://github.com/docker/compose/releases/download/v2.4.1/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
$ sudo chmod +x  $DOCKER_CONFIG/cli-plugins/docker-compose
$ sudo systemctl start docker
$ sudo systemctl enable docker
$ sudo chmod 666 /var/run/docker.sock

以下コマンドで、Difyの公式リポジトリからソースコードを取得します。

$ git clone https://github.com/langgenius/dify.git

今回は後々Difyのアップデートを実施するので、古い0.8.3バージョンを使用します。

$ git checkout -b main-0.8.3 refs/tags/0.8.3

既存のdify/docker/docker-compose.yamlファイルを修正し、データベースをコンテナで立ち上げないようにします。
次の2点を修正します。

  1. dbサービスをコメントアウト
  2. apiサービスとworkerサービスで、depends_onとして参照されているdbサービスをコメントアウト
  # The postgres database.
  # db:
  #   image: postgres:15-alpine
  #   restart: always
  #   environment:
  #     PGUSER: ${PGUSER:-postgres}
  #     POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456}
  #     POSTGRES_DB: ${POSTGRES_DB:-dify}
  #     PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata}
  #   command: >
  #     postgres -c 'max_connections=${POSTGRES_MAX_CONNECTIONS:-100}'
  #              -c 'shared_buffers=${POSTGRES_SHARED_BUFFERS:-128MB}'
  #              -c 'work_mem=${POSTGRES_WORK_MEM:-4MB}'
  #              -c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
  #              -c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
  #   volumes:
  #     - ./volumes/db/data:/var/lib/postgresql/data
  #   healthcheck:
  #     test: [ "CMD", "pg_isready" ]
  #     interval: 1s
  #     timeout: 3s
  #     retries: 30
  # API service
  api:
    image: langgenius/dify-api:0.8.3
    restart: always
    environment:
      # Use the shared environment variables.
      <<: *shared-api-worker-env
      # Startup mode, 'api' starts the API server.
      MODE: api
    depends_on:
      # - db
      - redis
    volumes:
      # Mount the storage directory to the container, for storing user files.
      - ./volumes/app/storage:/app/api/storage
    networks:
      - ssrf_proxy_network
      - default
  # worker service
  # The Celery worker for processing the queue.
  worker:
    image: langgenius/dify-api:0.8.3
    restart: always
    environment:
      # Use the shared environment variables.
      <<: *shared-api-worker-env
      # Startup mode, 'worker' starts the Celery worker for processing the queue.
      MODE: worker
    depends_on:
      # - db
      - redis
    volumes:
      # Mount the storage directory to the container, for storing user files.
      - ./volumes/app/storage:/app/api/storage
    networks:
      - ssrf_proxy_network
      - default

次に、以下コマンドで環境変数用ファイル.envを用意し、.envで定義されているデータベースへの接続情報を修正します。

$ cp .env.example .env
# ------------------------------
# Database Configuration
# The database uses PostgreSQL. Please use the public schema.
# It is consistent with the configuration in the 'db' service below.
# ------------------------------

DB_USERNAME={RDSのマスターユーザー名}
DB_PASSWORD={RDSのパスワード}
DB_HOST={RDSのエンドポイント}
DB_PORT=5432
DB_DATABASE={RDSのPostgreSQLに作成したデータベース名}

Difyを構築する

それではDifyアプリケーションを構築します。

$ docker compose up -d

アプリケーションが起動することが確認できました。

アプリケーション用のPostgreSQLテーブル等のスキーマは、自前のデータベースであっても自分で定義する必要なくDify構築時に生成されます。
以下コマンドで作成されたテーブル一覧が確認できます。

$ psql -h {RDSのエンドポイント} -p {RDSのport} -U postgres
postgres=> /connect {RDSのPostgreSQLに作成したデータベース名}
{RDSのPostgreSQLに作成したデータベース名}=> \dt

簡単なチャットアプリケーションとして、「chat application」を作成しておきます。
また、Difyのバージョンが0.8.3であることも確認しておきます。

安全にDifyをアップデートする

次にDifyのバージョンをアップデートしていきます。
まずは、Docker コンテナを停止します。

$ docker compose down

アップデート用に0.9.1バージョンを使用します。

$ git checkout -b main-0.9.1 refs/tags/0.9.1

0.8.3を構築したとき同様の修正を、docker-compose.yaml,.envファイルに適用します。
その後、アプリケーションを起動します。

$ docker compose up -d

無事、Difyが0.9.1にアップデートできていることが確認できました!
0.8.3で作成した「chat application」もそのまま残っています。

まとめ

今回はDifyのデータベースを自前で用意して安全なアップデートを試してみました。
アップデートなどの運用面で、アプリケーションとデータベースを分けて管理するメリットは大きいと思います。
Dockerコンテナで立ち上げるデフォルト設定から、大きな変更なく自前のデータベースに繋げられるのは良いですね。
今回はDBスキーマ自体の変更はなかったので、Difyのアプリ本体のアップデートだけで簡単にアップデートできました。
DBスキーマ自体に変更があり、自前のデータベースへのスキーマ移行が必要となる場合はDifyのアップデート情報を確認してください。

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

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

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

www.wantedly.com

Amazon Bedrock をTeamsとノーコードで連携する

はじめに

10月に入り、やっと秋らしい感じになってきました。
データ分析エンジニアの木介です。
先日、AWS Chatbotの新機能を利用して、BedrockがTeamsやSlackと簡単に連携できるようになったと発表がありました。
今回は、その内容を確認して、BedrockとTeamsを連携する方法について、説明していきます。

aws.amazon.com

概要

Amazon Bedrockとは?

Amazon Bedrock は、AWSが提供するマネージドサービスで、開発者が複数の大規模言語モデル(LLM)を簡単に利用できるプラットフォームです。
Bedrockを使用することで、アプリケーションにAI機能を組み込むために高度なAIやMLの知識を持っていなくても、自然言語処理、テキスト生成、質問応答、翻訳、要約といった高度なAIタスクを実現することができます。

docs.aws.amazon.com

今回のゴール

以下の形でTeamsにAmazon Bedrockで作成したAIエージェントを用いたチャットボットをAWS Chatbotを用いてノーコードで組み込むことが今回の目的になります。

Opening Slide

TeamsへのBedrockを使ったアプリの導入

構築環境

Teamsは、「一般法人向けプラン」と「家庭向けプラン」がありますが、今回は前者の「一般法人向けプラン」の内容を利用します。

www.microsoft.com

以下が利用するAWS サービスです

  • Agents for Amazon Bedrock
    • 生成AIを用いたAIエージェントを作成するために利用
  • AWS Chatbot
    • AWSサービスをTeamsに配信するために利用

また今回はus-east-1でのリージョンで構築を行っていきますが、AWS ChatbotのAmazon Bedrockとの連携はすべてのリージョンでサポートされているとのことです。
料金についてはTeamsにAIエージェントを配信するAWS Chatbotは無料で利用可能ですが、配信されるサービス(今回ではAgents for Amazon Bedrock)には別途費用が必要となります。

aws.amazon.com

aws.amazon.com

1. Agents for Amazon BedrockでAIエージェントを作成する

ではまずAmazon Bedrockを使ってチャットボットで回答をするAIエージェントを作成していきます。
AWSマネジメントコンソールのAmazon Bedrockより「エージェントを作成を選択」します。
以下の画面が出てくるので、任意のエージェント名を入力し作成を選択します。

エージェントの設定については以下の部分のみを修正し、他はデフォルトで設定をしました。

  • モデル:Claude 3 Sonnet
  • ユーザ入力:Enabled
    • 質問に対して情報不足な時に追加情報を求める
  • Memory: Enabled
    • 会話内容を記憶する


上記設定後、保存して終了→準備でテストが可能になります。
作成したAIエージェントのテストが右側のチャット欄から行えます。

作成したAIエージェントで回答が出来ていることが分かります。

2. AWS Chatbotの設定

次にAWS Chatbotでチャットボットの設定を行っていきます。
まずBedrockと連携したいTeamsアカウントからリンクを取得します。
チャネルより右クリックで「チャネルへのリンクを取得」でリンクをコピーします。
AWS Chatbotの設定で必要ですのでメモしておいてください。

AWSマネジメントコンソールのAWS Chatbotの画面よりTeamsを選択して「クライアントを設定」を選択します。

以下の画面で先ほど取得したチャネルへのリンクを入力し、設定を選択。

以下のアクセス許可について承諾が必要となります。

次にチャネルの設定を行っていきます。
以下から「新しいチャネルを設定」を選択します。

Bedrockでのチャネルの設定については以下の形で行っていきます。
画面の「チャネルのURL」項目についてはチャットボットを導入したいTeamsのチャネルのリンクを入力してください。

またアクセス許可のロール設定については「チャネルIAMロール」としました。
これにより、指定したチャネル内のメンバー全員に同じ権限を付与できます。
こちらで作成したロールに後ほどBedrockを使うための権限を付与します。

先ほど作成したロールにBedrockへのアクセス権限を付与していきます。
ロールを選択して、許可を追加から「インラインポリシーを作成」を選択。
以下の形で先ほど作成したエージェントのARNとエイリアスIDを入力します。


{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "AllowInvokeBedrockAgent",
			"Effect": "Allow",
			"Action": "bedrock:InvokeAgent",
			"Resource": [
				"arn:aws:bedrock:us-east-1:111222333444:agent-alias/AGENTARN/ALIASID"
			]
		}
	]
}

以上でAWS Chatbotの設定が完了しました。

3. Teamsの設定

それでは最後にTeams側の設定を行っていきます。
アプリの検索よりAWSを検索し、以下のアプリをチャネルに追加します。
これでTeamsでBedrockを使ったチャットボットが利用できます。


使い方

では早速作成したチャットボットをTeamsで使ってみましょう。
先ほどTeamsに導入したawsアプリでTeams上でAWS CLIの機能が利用可能となります。

docs.aws.amazon.com

まず動作確認として@aws connectorを入力してみましょう。

上記の形で応答があれば成功です。

次に以下のコマンドで利用するエージェントを指定します。
AGENT-IDとALIAS-IDに作成したエージェントのIDを指定します。

@aws connector add ai-chatbot arn:aws:bedrock:us-east-1:111111111111:agent/AGENT-ID ALIAS-ID

以下のレスポンスがあればOKです。

会話については以下のコマンドで行うことができます。

@aws ask ai-chatbot “会話内容”

以下が実際に行った内容です。

手順1で作成したエージェントの設定どおりに、不明点に対して聞き返したり、会話を記憶出来ていることが分かります。

以上でBedrockをTeamsで利用するまでの一通りの流れを紹介しました。
Agents for Amazon Bedrockと連携できるのでKnowledge Basesも扱うことができ、色々なことに応用が出来そうです。

まとめ

今回はノーコードでAmazon BedrockとTeamsを連携する方法を解説しました。
Amazon Bedrock Knowledge Basesを利用することで社内ドキュメントとの連携も可能となり、様々な場面で活用できると感じましたので、皆さんも是非試してみてください。


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

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

AWS Lambda(Python)でAmazon Bedrockの出力をレスポンスストリーミング対応してみた

こんにちはイワツカです。
今年の夏は、特に猛暑日が続いていたので、例年にも増して素麺を食べてました。

さて今回は、AWS Lambda(Python)でLambda Web Adapterを用いてレスポンスストリーミングする方法を試してみたので紹介します。

1. 概要

1.1 レスポンスストリーミングとは?

レスポンスストリーミングとは、HTTPリクエストに対してサーバーがレスポンスを一度にまとめて送るのではなく、データを分割して順次クライアントに送信する手法のことです。
AWS Lambdaでは、Lambda関数URLを設定してLambdaからのレスポンスをレスポンスストリーミングにすることができます。
レスポンスストリーミングにすることで、従来に比べてユーザーにレスポンスが届くまでの時間を軽減できるので、Webアプリケーションにおけるユーザー体験を向上させることができます。
用途としては、リアルタイムなチャットのやり取りや、処理の進行状況などを表示するのに便利です。

ただ、現在レスポンスストリーミングに直接対応しているのは、ランタイムがNode.jsの場合のみです。 docs.aws.amazon.com

Lambdaで使用するランタイムがPythonの場合は、Lambda Web Adapterを使用することでレスポンスストリーミングを実現可能です。

1.2 Lambda Web Adapterとは?

Lambda Web Adapterとは、ウェブアプリケーションAWS Lambda上で実行するためのツールです。
Lambda Web Adapterを使用するとFastAPIなどのフレームワークを利用できるようになります。
github.com

そしてFastAPIには、StreamingResponseという、レスポンスストリーミングを実現するための機能があります。
fastapi.tiangolo.com

そのため、Lambda Web AdapterとFastAPIを組み合わせることによって、Pythonで実装をするLambdaでも、レスポンスストリーミングを実現できるようになります。

2. アプリ作成

この記事では、Lambda Web Adapterを使ってFastAPIをLambda上で動かし、Amazon Bedrockを使ったLLM(大規模言語モデル)とのチャットアプリをレスポンスストリーミングで作成します。
構成や実装はAWSの公式の例を参考に実施しました。 github.com

全体像はこちらです。StreamlitというPythonフレームワークで画面を作成し、LLMとのチャットをできるようにしています。

作成するアプリの全体像

2.1 実行環境

本アプリを動かすには、FastAPIを動かすためのコンテナをビルドするためのDocker環境とStreamlitを動かすためのPython環境が必要です。
Python環境は、Python 3.12.4、Streamlit 1.38.0を使用しました。

2.2 ディレクトリ構成

ディレクトリ構成は、AWS公式の例に載っているファイルに加えて、Streamlitのスクリプトのみを追加しています。
Dockerfile、requirements.txt、template.yamlは本アプリでも同じ内容でそのまま利用しました。

./
  |--app/
  |  |--Dockerfile
  |  |--main.py
  |  |--requirements.txt
  |
  |--streamlit_app.py
  |--template.yaml

2.3 FastAPIの実装

FastAPIの実装(main.py)では、APIのレスポンスをStreamingResponseにすることがポイントです。
また、StreamingResponseに渡すbedrock_chat_stream()の処理では、画面から受け取ったユーザーのメッセージに対して、Bedrockからの応答をreturnでなくyieldで都度、返しているところも重要です。
基本的な実装は公式の例に載っているので、比較的簡単に実装することができました。

import boto3
import json
import os
import uvicorn
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional


# FastAPIのインスタンスを作成
app = FastAPI()


# チャットメッセージのモデル
class ChatMessage(BaseModel):
    message: Optional[str] = None


# APIエンドポイントを定義
@app.post("/api/chat")
def api_chat(chat_message: ChatMessage):
    if chat_message.message is None or chat_message.message.strip() == "":
        return {"error": "Message cannot be empty"}

    return StreamingResponse(bedrock_chat_stream(chat_message.message), media_type="text/event-stream")


# Bedrockクライアントを初期化
bedrock = boto3.client('bedrock-runtime')


async def bedrock_chat_stream(user_message: str):
    """ユーザーのメッセージを受け取り、LLMからの返答をストリーミング形式で返す"""
    instruction = f"ユーザーメッセージに対する会話を続けてください: {user_message}"

    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 1024,
        "messages": [
            {
                "role": "user",
                "content": instruction,
            }
        ],
    })

    # Bedrockからレスポンスストリーミングを取得
    response = bedrock.invoke_model_with_response_stream(
        modelId='anthropic.claude-3-haiku-20240307-v1:0',
        body=body
    )

    # ストリームが存在する場合、逐次データを処理
    stream = response.get('body')
    if stream:
        for event in stream:
            chunk = event.get('chunk')
            if chunk:
                message = json.loads(chunk.get("bytes").decode())
                if message['type'] == "content_block_delta":
                    yield message['delta']['text'] or ""
                elif message['type'] == "message_stop":
                    yield "\n"


if __name__ == "__main__":
    # Uvicornを使用してサーバーを起動
    uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8080")))

2.4 Streamlitの実装

Streamlitの公式ページに載っているLLMとのチャット画面を参考に実装しました。 docs.streamlit.io

Streamlitでストリーミング出力させる場合はst.write_stream()を利用します。
本アプリでは、LLMからの応答をrequestライブラリを通して取得していて、st.write_stream()が対応していない型になっているため、対応させるためにラッパー関数(stream_wrapper())を追加しています。

import requests
import streamlit as st


# FastAPIのエンドポイントURL
API_URL = "<Lambda関数URL>"

# 画面にタイトルを追加
st.title("AI Chat with Bedrock")

# チャット履歴のsession_stateを初期化
if "messages" not in st.session_state:
    st.session_state.messages = []

# チャット履歴を表示
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])


def stream_wrapper(response):
    """レスポンスをStreamlitで互換性のある形式に変換する"""
    for chunk in response.iter_content(chunk_size=128):
        if chunk:
            yield chunk.decode('utf-8')


# チャット入力
if prompt := st.chat_input("What would you like to discuss?"):
    with st.chat_message("user"):
        st.markdown(prompt)
    # チャット履歴にユーザー入力を追加
    st.session_state.messages.append({"role": "user", "content": prompt})

    # APIにリクエストを送信し、レスポンスストリーミングを受け取る
    with st.chat_message("assistant"):
        response = requests.post(API_URL, json={"message": prompt}, stream=True)
        response_text = st.write_stream(stream_wrapper(response))

    # チャット履歴にAPIのレスポンスを追加
    st.session_state.messages.append({"role": "assistant", "content": response_text})

3. アプリを動かして見る

本アプリは以下の2ステップで動かせます。
1. チャットアプリをデプロイ
2. Streamlitを起動する

3.1 チャットアプリをデプロイ

AWS公式の例に載っている手順でFastAPIを動かすLambdaのビルドとデプロイを行います。
そして、デプロイ後にLambda関数URLが分かります。

sam build --use-container
sam deploy --guided

以下は利用したtemplate.yamlです。AWS公式の例に載っているtemplate.yamlと同じ内容です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Streaming Chat with FastAPI on AWS Lambda

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 300

Resources:
  FastAPIFunction:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
      MemorySize: 512
      Environment:
        Variables:
          AWS_LWA_INVOKE_MODE: RESPONSE_STREAM
      FunctionUrlConfig:
        AuthType: NONE
        InvokeMode: RESPONSE_STREAM
      Policies:
      - Statement:
        - Sid: BedrockInvokePolicy
          Effect: Allow
          Action:
          - bedrock:InvokeModelWithResponseStream
          Resource: '*'
      Tracing: Active
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./app
      DockerTag: v1

Outputs:
  FastAPIFunctionUrl:
    Description: "Function URL for FastAPI function"
    Value: !GetAtt FastAPIFunctionUrl.FunctionUrl
  FastAPIFunction:
    Description: "FastAPI Lambda Function ARN"
    Value: !GetAtt FastAPIFunction.Arn

3.2 Streamlitを起動する

上記のデプロイで得られたLambda関数URLをstreamlit_app.pyのAPI_URLに代入します。
その後、streamlit_app.pyがあるディレクトリで以下のコマンドを実行します。
streamlit run streamlit_app.py

3.3 チャットを試してみる

それでは実際にチャットを試してみます。
LLMからの応答が逐次表示されているのが分かります。
※なお、本アプリはレスポンスストリーミングを試すために作成したので、会話履歴はLLMに入力しておらず単発の会話のみ可能です。

レスポンスストリーミングのチャット画面

4. まとめ

今回はLambda Web Adapterを使ってLambda関数URLの出力をレスポンスストリーミングにする方法を試してみました。
レスポンスストリーミングがAWS Lambdaでも可能になるので、チャットアプリの実現等、Webアプリケーションの幅が広がりますね。
AWSの公式のサンプルやドキュメントが充実していて、比較的簡単に実装することができるので、ぜひ試してみてください。

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