Azure OpenAIサービスによる社内QAシステムの構築方法を紹介

はじめに

ISID AITCに所属しております、ファイサルです。本記事では、Azure OpenAIサービスを活用して、社内の質問応答(QA)システムを構築してみました。この記事では、構築手順や結果について簡潔にご紹介いたします。

背景と目的

最近話題になったChatGPTは、その優れた性能が注目を集めていますが、いくつかの制約も存在します。その中でも特に重要なのは、Hallucination(幻覚症状)という課題です。ChatGPTは、与えられた文脈や情報から推測を行い、存在しない情報を誤って生成することがあります。つまり、モデルが事実ではない情報を答えたり、根拠のない主張をしたりすることがあるのです。

社内QAシステムを構築する場合、Hallucinationの問題を最優先で解決することが重要です。そのためには、外部ツールを組み込んだQAシステムが求められます。例えば、検索エンジンや計算機などの外部ツールを活用することで、ChatGPTは情報を補完し、より正確な回答を提供することができるのです。

幸いなことに、この課題に対するいくつかの解決策が提案されています。Googleが提案したReAct(Reason and Act)、Metaが提案したToolformer、そしてOpenAIが実現したChatGPT プラグインなどが挙げられます。これらの手法を活用することで、Hallucinationの問題を効果的に解決することができます。

そして、最近注目されているLangChainのpythonライブラリを用いて、Out-of-the-boxで上記のReActフレームワークを実現することが簡単にできます。具体的には、LangChainの中でエージェントという概念があり、ReActの考え方が取り入れられています。

今回はReActを活用したLangChainエージェントを使用し、Azure Open AIサービスとAzure Cognitive Searchサービスを組み合わせてスマートなQAシステムを構築します。これにより、高度な質問応答が可能なシステムを実現し、社内の情報活用や問題解決に役立てることを目指しています。

LangChain

LangChainエージェント

LangChainは、Pythonライブラリであり、アプリケーション開発において大規模言語モデル(LLM)を簡単に利用できる強力なツールです。LangChainでは、LLMと外部システムを連携させるための概念としてChainが利用できます。Chainは、事前に定義されたフローに基づいて、LLMを活用したアプリケーションを容易に開発するための概念です。

一方で、フローが事前に決まっていない場合やフローの推論もLLMに任せたい場合には、エージェントという概念が利用されます。エージェントは、ユーザーの質問に対して回答するために必要なステップや使用する外部ツールを自律的に決定することができます。外部ツールには、検索エンジン、計算機、コードエグゼキュータが含まれます。

今回は、エージェントを活用して、必要な時だけ検索エンジンで関連する社内文書を検索し、その文章から情報を取り出してユーザーの質問に回答するQAシステムを実現します。

Azure OpenAIサービス

Azure OpenAIサービスは、Azureのクラウド上で提供される大規模言語モデルのサービスです。本家のOpenAIサービスとは違い、セキュリティとコンプライアンスの利点を持ちます。ユーザーが入力したデータは学習データとしてOpenAIに送らずにAzureネットワーク内で管理されます。そして、他のAzureサービスへの統合(ネットワーク周りの設定、IAM Roleによるアクセス制御など)は簡単にできるので、Microsoftとのパートナーシップを持つ弊社ではこの利点は重要です。

Azure OpenAIサービスで使用できるモデルは本家のOpenAIサービスが提供しているモデルとほとんど同じです。今回は、検索結果をLLMに落とし込む必要があるため、多くのトークン数に対応するGPT-4-32kモデルを使用します。

Azure Cognitive Searchにおける検索インデックス作成フロー (source: https://learn.microsoft.com/en-us/azure/search/media/cognitive-search-working-with-skillsets/skillset-process-diagram-1.png)

Azure Cognitive Searchでは、大量のデータ(主に文書データ)を効率的にインデックス化し、高速で精度の高い検索を実現します。キーワード/全文検索、フィルタリング、ハイライト、類似性検索など、多様な検索要件に対応しています。さらに、Azure Cognitive Searchは、自然言語処理(NLP)の機能もSkillsetとして提供します。たとえば、キーワード抽出、エンティティ認識、感情分析などを活用して、テキストデータの解析や洞察の抽出を行うことができます。

最近では、プライベートプレビューとしてベクトル検索機能が提供されるらしいので、文書に限らずマルチモーダルも簡単に実現できます。

今回は、簡単に文書内容のみを格納してAzure Cognitive Searchをキーワード検索エンジンとして利用します。

アーキテクチャー

QAシステムアーキテクチャー

今回のQAシステムでは以下のサービスで構成されます。

  1. Azure Blob:ストレージ社内の文章データの保存する場所です。このBlobストレージをAzure Cognitive Searchのデータソースとして登録します。最近、Azure Cognitive SearchはSharepointとの統合にプレビューとしてサポートしていますが、安定していないため、今回はAzure Blob ストレージにします。

  2. Azure Cognitive Search:

    1. Indexer: データソースから文書の内容を抽出して検索インデックスに格納します。

    2. Search Index(検索インデックス): 文書データを効率的に検索する機能を提供します。

  3. Azure OpenAIサービス:今回はGPT-4-32k(3万2000トークン対応)のモデルを使用します。

  4. Langchain:

    1. エージェント: 必要なステップを推論します。

    2. Searchツール(検索): クエリに対してヒットする文書を返します。

    3. Lookupツール: Ctrl+Fのように、文書内部の中身を単語の位置を特定します。

手順

事前準備

事前準備として以下のサービスをAzureポータルから作成する必要があります。

  1. Azure ストレージアカウント + Blobコンテナー

    • 社内文書をこのBlobコンテナーに格納します。社内文書とはメール、報告書、マニュアル・手順書など、組織内で使用されるさまざまな形式の文書のことです。この場合、扱える社内文書のファイル形式は、.pdf、.docx、.txtの拡張子を持つファイルとなります。
  2. Azure Cognitive Searchのリソース

  3. Azure OpenAIサービスのリソース(gpt-4-32kのモデル)

データソース作成

Blobコンテナーへの接続するCognitive SearchのDatasourceを作成します。

response = requests.post(
    url=f"{base_url}/datasources?api-version=2020-06-30",
    headers={
        "Content-Type": "application/json",
        "api-key": "<SEARCH_API_KEY>"
    },
    json={
        "name": "blob-datasource",
        "type": "azureblob",
        # <省略>
    }
)

検索インデックスの作成

文書を格納するための検索インデックスを作成します。今回は、contentは日本語の内容を扱うので、Microsoftの日本語アナライザー(ja.microsoft)を使用します。Cognitive Searchでは、プリセットとして用意されている多言語のアナライザーが用意されています。

response = requests.post(
    url=f"{base_url}/indexes?api-version=2020-06-30",
    headers={
        "Content-Type": "application/json",
        "api-key": "<SEARCH_API_KEY>"
    },
    json={
        "name": "isid-index",
        "fields": [
            { "name": "ID", "type": "Edm.String", "key": True, "searchable": False },
            { "name": "content", "type": "Edm.String", "searchable": True, "filterable": False, "analyzer": "ja.microsoft" },
            # <省略>
        ]
    }
)

Indexerの作成

Blobにあるデータを抽出し、インデックスに格納するIndexerを作成します。

response = requests.post(
    url=f"{base_url}/indexers?api-version=2020-06-30",
    headers={
        "Content-Type": "application/json",
        "api-key": "<SEARCH_API_KEY>"
    },
    json={
        "name" : "isid-indexer",
        "dataSourceName" : "blob-datasource",
        "targetIndexName" : "isid-index",
        "parameters": {
            "batchSize": 10,
            "maxFailedItems": -1, # 無限
            "maxFailedItemsPerBatch": 10,
            "base64EncodeKeys": None,
            "configuration": {
                # <省略>
            }
        },
        # <省略>
    }
)

LangChainエージェントの実装

今回は、ReActのサンプルコードを参考にし、独自のDocstoreのクラスを定義します。Docstoreは、文書の検索をシミュレートするオブジェクトです。

Cognitive Searchのハイライト結果を検索結果サマリとして扱うために、Documentのメタデータに格納します。

from typing import Union
from langchain.docstore.base import Docstore
from langchain.docstore.document import Document

class IsidDocstore(Docstore):
    """ ISIDのドキュメントの検索API. """

    def search(self, search: str) -> Union[str, Document]:
        # Azure Cognitive Searchへ検索実行し、検索クエリがヒットした文書をlangchainのDocumentに変換する。
        # <省略>
        document.metadata["summary"] = result_doc["@search.highlights"]["content"][0]
        # <省略>

カスタムのDocstoreExplorerの実行

検索結果のサマリを作る処理を上書きします。今回は、上記に格納したメタデータからサマリーを取得して返す。

from langchain.agents.react.base import DocstoreExplorer

class CustomDocstoreExplorer(DocstoreExplorer):
    @property
    def _summary(self) -> str:
        return self.document.metadata["summary"]

エージェントの構築

上記のツールを組み合わせてエージェントを作成します。今回は、ZERO_SHOT_REACT_DESCRIPTIONを使用し、エージェントがツールの説明を元にアクションを決めることになります。

from langchain.agents import initialize_agent, Tool
from langchain.chat_models import AzureChatOpenAI
from langchain.agents import AgentType
import os

docstore = IsidDocstore(base_url=base_url, index_name="isid-index", api_key="<SEARCH_SERVICE_API_KEY>")
docstore_explorer = CustomDocstoreExplorer(docstore=docstore)
tools = [
    Tool(
        name="Search",
        func=docstore_explorer.search,
        description="useful for when you need to ask with search. it will return only document fist paragraph. if you need to further look into document, use lookup."
    ),
    Tool(
        name="Lookup",
        func=docstore_explorer.lookup,
        description="useful for when you need to ask with lookup a term inside search result document."
    )
]
llm = AzureChatOpenAI(
    temperature=0,
    model_name="<MODEL_NAME>",
    openai_api_base="<OPENAI_API_BASE>",
    openai_api_key="<OPENAI_API_KEY>",
    deployment_name="<DEPLOYMENT_NAME>",
    openai_api_version="<OPENAI_API_VERSION>"
)
react = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

このエージェントで使われているプロンプトを見ていきましょう。

print(react.agent.llm_chain.prompt.template)

# Answer the following questions as best you can. You have access to the following tools:

# Search: useful for when you need to ask with search. it will return only document fist paragraph. if you need to further look into document, use lookup.
# Lookup: useful for when you need to ask with lookup a term inside search result document.

# Use the following format:

# Question: the input question you must answer
# Thought: you should always think about what to do
# Action: the action to take, should be one of [Search, Lookup]
# Action Input: the input to the action
# Observation: the result of the action
# ... (this Thought/Action/Action Input/Observation can repeat N times)
# Thought: I now know the final answer
# Final Answer: the final answer to the original input question

# Begin!

# Question: {input}
# Thought:{agent_scratchpad}

実験結果

LLMが自ら適切なキーワードクエリを作成し、検索ツールを使用します。

検索結果に欲しい情報が得られない場合は、別のキーワードを作成します。トークン数上限に近く、それでも欲しい情報が得られない場合は、見つからないことをユーザーに伝えて謝ります。

上手くいった例 1

入力

育児休業は子供の何歳まで取れますか?

推論フロー

Question: 育児休業は子供の何歳まで取れますか?
Thought: I need to find information about the age limit for taking childcare leave in Japan.
Action: Search
Action Input: 育児休業 子供の年齢制限 日本
Observation: '勤務形態時間 ...<省略>...  時間外労働(〇〇時間/月、〇〇時間/年)の制限を申し出ることができる 当会社が事業の正常な運営に支障があると判断した場合は取得不可。'
Thought: The search result does not provide the information I need. I will try another search.
Action: Search
Action Input: 育児休業 子供の年齢 日本
Observation: '※法定上、育児休業は 1 日の取得でも可能ですが、原則として無給です。'
Thought: The search result still does not provide the information I need. I will try another search.
Action: Search
Action Input: 育児休業 子供の何歳まで 日本
Observation: 'また、原則としてお子様の 1 歳の誕生日の 前日まで(保育所に入所できない等の理由がある場合は 2 歳まで延長可)"育児休業※"が取得できます。'
Final Answer: 育児休業は、原則として子供が1歳の誕生日の前日まで取得できます。ただし、保育所に入所できない等の理由がある場合は、2歳まで延長することができます。

出力

育児休業は、原則として子供が1歳の誕生日の前日まで取得できます。ただし、保育所に入所できない等の理由がある場合は、2歳まで延長することができます。

上手くいった例 2

入力

住所変更の手続きは?

推論フロー

Question: 住所変更の手続きは?
Thought: まず、住所変更の手続きに関する情報を調べる必要がある。
Action: Search
Action Input: 住所変更の手続き
Observation: '各位 〇〇部  人事異動等を実施する際の手続きについて  首題の件、人事異動実施時の手続きは以下の通りです。'
Thought: この情報は住所変更の手続きに関するものではない。もう一度検索してみる。
Action: Search
Action Input: 住所変更手続き方法
Observation: '入会申込書は〇〇事業務部へ、各種変更届に関しては直接〇〇に連絡し手続きをしてください。'
Thought: この情報も住所変更の手続きに関するものではない。もう一度検索してみる。
Action: Search
Action Input: 住所変更の手続き方法
Observation: '入会申込書は〇〇事業務部へ、各種変更届に関しては直接〇〇に連絡し手続きをしてください。'
Thought: この情報も住所変更の手続きに関するものではない。もう一度検索してみる。
Action: Search
Action Input: 住所変更手続き
Observation: '各位 〇〇部  人事異動等を実施する際の手続きについて  首題の件、人事異動実施時の手続きは以下の通りです。'
Thought: 住所変更の手続きに関する情報が見つからない。別のキーワードで検索してみる。
Action: Search
Action Input: 住所変更方法
Observation: '①〇〇メニュー>〇〇申請>一般届出申請現住所・住民票住所の変更方法1 / 6 ページ②〇〇>現住所・住民票住所③現住所・住民票住所現住所・住民票住所を追加する場合2 / 6 ページ④現在住所・住民票住所例)転居、転勤に伴う転居等該当する場合は、「有」を選択してください。'
Final Answer: 住所変更の手続きは以下の通りです。①〇〇メニュー>〇〇申請>一般届出申請 ②〇〇>現住所・住民票住所 ③現住所・住民票住所>>現住所・住民票住所を追加する場合 ④現在住所・住民票住所。例)転居、転勤に伴う転居等。該当する場合は、「有」を選択してください。

出力

住所変更の手続きは以下の通りです。
①〇〇メニュー>〇〇申請>一般届出申請 
②〇〇>現住所・住民票住所 
③現住所・住民票住所>>現住所・住民票住所を追加する場合 
④現在住所・住民票住所。例)転居、転勤に伴う転居等。該当する場合は、「有」を選択してください。

上手く行った例 3

この例では、社内文書から情報が存在しないが、一般的な質問なのでLLMが直接回答できます。

入力

日本の首都はなんでしょう?

推論フロー

Question: 日本の首都はなんでしょう?
Thought: I need to search for the capital of Japan in Japanese.
Action: Search
Action Input: 日本の首都
Observation: 'A.主にその国外居住親族が海外留学中の者など日本国籍を有する者である場合...<省略>'
Thought: The search result did not provide the information I need. I will try again with a different search query.
Action: Search
Action Input: 日本の首都は何ですか
Observation: '(※旧住所の記載は不要です)'
Thought: The search result still did not provide the information I need. I will try again with a different search query.
Action: Search
Action Input: Japan capital
Observation: 'お問い合わせ先 受付時間 システムに関するお問い合わせ (日本語) xx-xxxx-xxxx xxxx@xxx.com (祝日除く) (祝日除く) 月~金 9:00~20:00 土・日 9:00~18:00 ご予約に関するお問い合わせ*(日本語) ご予約に関するお問い合わせ*(英語) xx-xx-xxx-xxx(中国) xxxx@xxx.com xx-xxxx-xxxx(ドイツ) 24 時間 ホテルへのリクエストに関するお問い合せ ご宿泊ホテル ※但しリクエストの確約はできかねますので予めご了承ください。'
Thought: The search result still did not provide the information I need. I will try again with a different search query.
Action: Search
Action Input: capital of Japan
Observation: 'お問い合わせ先 受付時間 システムに関するお問い合わせ (日本語) xx-xxxx-xxxx xxxx@xxx.com (祝日除く) (祝日除く) 月~金 9:00~20:00 土・日 9:00~18:00 ご予約に関するお問い合わせ*(日本語) ご予約に関するお問い合わせ*(英語) xx-xx-xxx-xxx(中国) xxxx@xxx.com xx-xxxx-xxxx(ドイツ) 24 時間 ホテルへのリクエストに関するお問い合せ ご宿泊ホテル ※但しリクエストの確約はできかねますので予めご了承ください。'
Thought: The search results are not providing the information I need. I will try one more time with a different search query.
Action: Search
Action Input: Japan's capital city
Observation: "Could not find |Japan's capital city|."
Thought:I am unable to find the information using the search function. However, I know the answer from general knowledge.
Final Answer: 東京 (Tokyo)

出力

東京 (Tokyo)

上手くいかなかった例

入力

総合社員の次にどのレベルがある?

推論フロー

Question: 総合社員の次にどのレベルがある?
Thought: まず、総合社員とは何かを理解する必要があります。そして、それに関連するレベルや階層を調べる必要があります。
Action: Search
Action Input: 総合社員とは
Observation: '社員各位 〇〇本部 〇〇部  『〇〇保険』募集のご案内 標記の保険につきまして、本年も下記の通り、一斉募集をいたします。'
Thought: この検索結果は総合社員に関する情報ではないため、再度検索してみます。
Action: Search
Action Input: 総合社員 レベル 階層
Observation: '「レベル1:十分注意してください。」'
Thought: この検索結果も総合社員に関する情報ではないため、再度検索してみます。
Action: Search
Action Input: 総合社員 階級
Observation: '社員各位 〇〇本部 〇〇部  『〇〇保険』募集のご案内 標記の保険につきまして、本年も下記の通り、一斉募集をいたします。'
Thought: この検索結果も総合社員に関する情報ではないため、再度検索してみます。
Action: Search
Action Input: 企業の階級制度
Observation: '〇〇部  対象者は、男女を問わず、次の全ての条件を満たす者とする。'
Thought: この検索結果も総合社員に関する情報ではないため、再度検索してみます。
Action: Search
Action Input: 企業の役職 レベル
Observation: '「レベル1:十分注意してください。」'
Thought: この検索結果も総合社員に関する情報ではないため、再度検索してみます。
Action: Search
Action Input: 企業の役職一覧
Observation: '企業・組織様とのご契約内容によってえらべるサービスは異なります。'
Final Answer: 総合社員の次のレベルに関する情報は見つかりませんでした。具体的な企業名や業界を特定して検索する必要があります。

出力

総合社員の次のレベルに関する情報は見つかりませんでした。具体的な企業名や業界を特定して検索する必要があります。

まとめ

本記事では、Cognitive SearchとLangChainエージェントを組み合わせることで、Azure OpenAIサービスによる社内QAシステムを構築する方法を紹介しました。

今回は、単純なキーワード検索を使用したため、検索結果が誤っていた場合はエージェントが正しい回答を提供することができませんでした。しかし、「日本の首都」のような一般情報の場合は検索結果に正解が存在しなくてもエージェントが回答できるというのはLLMの素敵なところだと思います。

今後は、検索の精度を向上するために、ベクトル検索の導入やデータ前処理の改善などが必要と考えられます。検索の精度が向上できれば、このようなQAシステムを社員の皆様に使っていただけるのではないでしょうか。

ここまでお読みいただきありがとうございました。

執筆
AITC AI製品開発グループ
ファイサル ハディプトラ