人間のあるべき姿の探索

思索・人形・エンジニアリング

Azure OpenAI Serviceを用いて簡易的なRAGを構築する

初めに

ChatGPTによるハルシネーションを防ぐために、RAG(Retrieval Augment Generation)という技術があります。これは、以下のように説明される仕組みです。

取得拡張生成 (RAG) は、データを提供する情報取得システムを追加することで、ChatGPT などの大規模言語モデル (LLM) の機能を拡張するアーキテクチャです。 情報取得システムを追加すると、LLM が応答を作成するときに使用するデータを制御できます。

learn.microsoft.com

特にMicrosoftのAzure OpenAI Serviceでは、データの取得先としてAzure Cognitive Searchという高速でインデックス付けされた様々な形式のファイルを全文検索できるサービスを用いて、特定の情報に基づいた結果を返す仕組みをサンプルとして公開しています。

しかし、ここでの問題として価格があります。エンタープライズ向けに社内文書を活用することを考えると、こういった高速で大量のデータを検索できるサービスが必要になるのですが、個人的にちょっとしたサービスを作りたい程度だと、オーバースペックになってしまいます。特に、こういったサービスはアクセスごとの従量課金ではなくRUベースで時間ごとの課金だったりとにかくお金がかかり、ビジネス価値を生んで収益が求められるのが常です。

そこで、

仕組みを理解する

こちらのサンプルが、Azure OpenAI ServiceとAzure Cognitive Searchを組み合わせてRAGを実現するためのコードです。基本的にはBackend APIのコードが各種サービスを呼び出して画面側に結果を返却しています。

github.com

実際に該当部分のコードを見ていきましょう。Backendに3つのアプローチが存在していますが、一番シンプルなRetrieveThenRead.pyを見ていきます。

まずは、Azure OpenAI Serviceに渡されるプロンプトのテンプレートを見ます。質問に回答してもらうこと、出典を必要とすること、そしてサンプルデータとして使用される水素ハイブリッド電車に関する質問回答のセットの例示が行われています。特にこういった例を与えるfew shotなプロンプトはプロンプトエンジニアリング術としてMicrosoftセミナーでも紹介されています。

    system_chat_template = \
"あなたは、鉄道技術に関する質問をサポートするインテリジェントアシスタントです。 " + \
"相手が「わたし」で質問しても、「あなた」を使って質問者を指してください。" + \
"以下の資料に記載されているデータのみを使って、質問に答えてください。" + \
"表形式の情報については、HTMLとして返してください。マークダウン形式は返さないでください。"  + \
"各出典元には、名前の後にコロンが続き、実際の情報が記載されています。回答で使用する各事実には、必ず出典元名を記載してください。" + \
"以下の資料で答えられない場合は、「わからない」と答えなさい。以下の例を使って答えてください。"

    #shots/sample conversation
    question = """
'水素ハイブリッド電車とはなんですか?'

Sources:
info1.txt: 水素をエネルギー源とする燃料電池は、高いエネルギー変換効率と環境負荷の少なさが特徴
info2.txt: 燃料電池自動車やバスの技術を鉄道車両の技術と融合・応用することにより、水素ハイブリッド電車を開発し、実証試験を始めた。
"""
    answer = "水素を燃料とする燃料電池は、高いエネルギー変換効率と環境負荷の少なさが特徴[info1.txt]です。この技術を鉄道車両に応用し、水素ハイブリッド電車を開発し、実証試験を始めました。[info2.txt] "

それでは、このプロンプトを使用した処理の流れを確認します。長いので中略を挟んでいます。グッとにらんでいると流れが掴めます。実はシンプルで、Cognitive Searchは検索クエリをSDK経由で投げると結果を返してくれるので、その結果をstringの列に変換した後に、OpenAI Serviceへのリクエストにデータとして含めています。OpenAI Serviceへのインプットは、先ほどのプロンプト、ユーザーの質問、Cognitive Searchから返却されたデータの3つを統合したものになります。要するに、このデータをCognitive Searchではなく他のデータ取得先に差し替えれば任意のデータを用いてRAGが実行できるわけですね。ソースコードを覗くことで、具体的な接続方法がわかってきました。

ちなみに、他のアプローチでは、Cognitive Searchに与えるクエリに直接ユーザーの質問文を入れるのではなくOpenAI Serviceで生成する方法や、ReActという仕組みを用いて十分な回答が生成できるまで逐次的に外部サービスへのデータ取得や回答文の再生成といった処理を実行する方法も存在します。

  async def run(self, q: str, overrides: dict[str, Any]) -> Any:
        # 中略
        if overrides.get("semantic_ranker") and has_text:
            r = await self.search_client.search(query_text,
                                          filter=filter,
                                          query_type=QueryType.SEMANTIC,
                                          query_language="en-us",
                                          query_speller="lexicon",
                                          semantic_configuration_name="default",
                                          top=top,
                                          query_caption="extractive|highlight-false" if use_semantic_captions else None,
                                          vector=query_vector,
                                          top_k=50 if query_vector else None,
                                          vector_fields="embedding" if query_vector else None)
        if use_semantic_captions:
            results = [doc[self.sourcepage_field] + ": " + nonewlines(" . ".join([c.text for c in doc['@search.captions']])) async for doc in r]
        # 中略
        message_builder = MessageBuilder(overrides.get("prompt_template") or self.system_chat_template, self.chatgpt_model)
        # 中略
        messages = message_builder.messages
        chat_completion = await openai.ChatCompletion.acreate(
            deployment_id=self.openai_deployment,
            model=self.chatgpt_model,
            messages=messages,
            temperature=overrides.get("temperature") or 0.3,
            max_tokens=1024,
            n=1)

        return {"data_points": results, "answer": chat_completion.choices[0].message.content, "thoughts": f"Question:<br>{query_text}<br><br>Prompt:<br>" + '\n\n'.join([str(message) for message in messages])}

やってみる

今回はAzure OpenAI Serviceの情報を返すMockを作成して、Azureについての質問を投げてみます。とりあえずシステムメッセージやfew shotの例は適宜Azureの質問と回答に書き換えます。

それでは、処理の流れを書いていきます。Mockを以下のように作ります。質問文にOpenAIって入ってたらOpenAI Serviceについての情報を返します。それ以外の場合はデータソース無しとして、システムプロンプトの方で何も返さないよう命令しています。

def mockApi(query):
    if "OpenAI" in query:
        return "OpenAI Service.txt:Azure OpenAI Service is a cloud-based service provided by Microsoft Azure that allows developers to easily integrate OpenAI's powerful language models into their applications. OpenAI is an artificial intelligence research laboratory that has developed state-of-the-art language models like GPT-3 (Generative Pre-trained Transformer 3)."
    else:
        return "data source is none."

few shotの例として与える質問回答は一つだけ作りました。Cognitive Searchの場合だとこうなります、ということで同じように他のAzureのサービスについても答えてもらいます。

question = """
'What Azure Cognitive Search is?'

Sources:
Cognitive Search.txt:Azure Cognitive Search, an AI-powered information retrieval platform, helps developers build rich search experiences and generative AI apps that combine large language models with enterprise data. Implement search functionality for any mobile or search application within your organization or as part of software as a service (SaaS) apps
"""
answer = "Azure Cognitive Search, an AI-powered information retrieval platform, helps developers build rich search experiences and generative AI apps that combine large language models with enterprise data. [Cognitive Search.txt]"

RAGの処理も簡易化して書きました。さっと動かす程度ならこれで動きます。最初にデータソース付きでシステムプロンプトを指定して、質問回答の例を与えてやります。最後にユーザーの質問を入れています。

def task(query: str):
    mockResult = mockApi(query)
    messages = [
        {"role": "system", "content": system_chat_template + mockResult},
        {"role": "user", "content":question},
        {"role": "assistant", "content":answer},
        {"role": "user", "content":query}
    ]
    response = openai.ChatCompletion.create(
        engine="gpt35",
        messages=messages,
        temperature=0,
    )
    print(response["choices"][0]["message"]["content"])    

それでは、Azure OpenAI Service及びAzure Storage Accountについてそれぞれ聞いてみます。結果は以下の通りで期待したものが返ってきました。Storage Accountのことはデータとして持ってないので答えられず、Open AI Serviceのことはデータを持っているので返してくれました。そのまま結果を返していますが、要約してほしい旨等を質問文に入れてやると要約して返してくれたりします。この辺りの返答のフォーマットはfew shotで与えるデータやシステムプロンプトで細かく制御していくことになります。

Input query:Please tell me about Azure Storage Account.
I can't answer it.

Input query:Please tell me about Azure OpenAI Service. Azure OpenAI Service is a cloud-based service provided by Microsoft Azure that allows developers to easily integrate OpenAI's powerful language models into their applications. OpenAI is an artificial intelligence research laboratory that has developed state-of-the-art language models like GPT-3 (Generative Pre-trained Transformer 3). With Azure OpenAI Service, developers can leverage these language models to enhance their applications with advanced natural language processing capabilities. This service enables developers to create applications that can understand and generate human-like text, making it useful for a wide range of use cases such as chatbots, content generation, language translation, and more. [OpenAI Service.txt]

終わりに

いかがでしたか?自分が持っているちょっとした情報を今回はMockを作成しましたが、あなたが持っているデータを好きに検索する処理を書けば、RAGを簡単に使用することができます。例えばPDF形式の書籍の情報が欲しければテキストに書き出してgrepするモジュールを書いたり、ちょっとしたデータならSQLのテーブルにデータを入れるのもよいでしょう*1

ポイントとしては、オーバースペックで高価なサービスを避けて自前でデータ取得部分を作ることにあります。処理の流れを見ると、実は天気を取得するAPIを呼ぶとかそういったことも同じことをしている訳ですが、自前のデータを使う面白いユースケースがたくさん出てくると嬉しいな…という気持ちで、こうった観点でRAGの話を書いてみました。データソースとして面白い例があれば僕も今度試してみようと思います。

自前データの話とは離れますが、以前NDC(日本十進分類法、書籍の分類に使用される)の番号をランダム生成してNDCのAPIから取得した種別を表示するちょっとしたサイトを作ったことがあります。そんな感じで、読みたい書籍を適当に投げたら検索をかけて、出てきた書籍について要約してするAIサービスとか作ったら面白そうですね、画面回り作ったりホスティングするの面倒なので誰か作ってくれると嬉しいです。

 

 

 

 

*1:LIKEで検索かけるとめちゃくちゃ遅そうですが…