Retrieval Augmented Generationを改良する2つの方法

2023.06.09

はじめに

新規事業統括部の山本です。

OpenAI APIをはじめとしたAIの言語モデル(Large Language Model:以下、LLM)を使用して、チャットボットを構築するケースが増えています。通常、LLMが学習したときのデータに含まれている内容以外に関する質問には回答ができません。そのため、例えば社内の文章ファイルに関する質問に回答するチャットボットを作成しようとしても、質問に対してわからないという回答や異なる知識に基づいた回答が(当然ながら)得られてしまいます。

この問題を解決する方法として、Retrieval Augmented Generation(以下、RAG)という手法がよく使用されます。RAGでは、

  • ユーザからの質問に回答するために必要そうな内容が書かれた文章を検索する

  • その文章をLLMへの入力(プロンプト)にプラスして渡す

というフローにすることで、LLMが学習していない内容であっても回答を考えさせることができます。

このページでは、RAGをさらに使いやすくする以下の2つの方法を紹介します。この方法はMicrosoft Azureのサンプル(https://github.com/Azure-Samples/azure-search-openai-demo)で使用されいる方法です。コードなどはリポジトリの中を併せてご覧いただくと、よりわかりやすいかと思います。

通常の方法と問題点

RAGを作成する際、ベーシックに実装すると処理は以下のようになるかと思います。

  • 準備時
    • 文章をベクトルに変換する
    • 各文章のベクトルをまとめてデータベースを作成する
  • 使用時
    • ユーザが質問文を入力する
    • 質問文をそのままクエリとしてベクトルに変換する
    • データベースを検索し、内容が近そうな文章を取り出す
    • 取り出した文章と質問を合わせて、LLMに入力し回答を得る

この場合、以下の問題が起きる可能性があります

  • 問題1:余計な単語を挟んだり、文章で使われているものと異なる単語を質問文で使うと精度が下がる可能性がある。質問文をより的確にするために、ユーザは文章の内容をある程度知っている必要がある。(ベクトル化の精度が良ければ問題にならない場合もあります)
  • 問題2:前後の質問文を考慮できず、一問一答になってしまう。ユーザは質問文を毎回考え直さなければならず、また、前の会話の内容を指す言葉(「その」など)は反映されない。
  • 問題3:1回の検索では解決できない質問に対応できない。例えば、別の箇所を参照するように書かれていて段階的に参照しなければいけない内容や、複数の文章を比較してまとめる、といった複数回の検索が必要な質問には回答できないため、対応できる質問の種類が限られてしまう。

(補足)

Azureサンプル内の以下のクラス(RetrieveThenReadApproach)が、使用時のコードに該当します。

https://github.com/Azure-Samples/azure-search-openai-demo/blob/main/app/backend/approaches/retrievethenread.py

※ このコードでは、文章を検索する際にsearch_clientを利用しているので、ベクトルへの変換は行っていません。

改良する方法

上記の問題を解決するために利用できる方法として、Azure-Samplesとaws-samplesのリポジトリで実装されている以下の2つ(Chat・Ask)がありましたので、その内容を記載します。

https://github.com/Azure-Samples/azure-search-openai-demo/ より引用

Chat:クエリを考えさせる

この方法は、文章を検索する際のクエリにユーザの質問文をそのままは使用せず、会話の履歴からLLMに考えさせます。準備時は変わりませんが、使用時に太字の処理を追加・変更します。

  • 使用時
    • ユーザが質問文を入力する
    • 質問文と会話の履歴をもとに、LLMにクエリを考えさせる
    • クエリとしてベクトルに変換する
    • データベースを検索し、内容が近そうな文章を取り出す
    • 取り出した文章と質問を合わせて、LLMに入力し回答を得る
    • 会話の履歴を保存する

リポジトリ内のコードとしては、以下のページに実装されているクラス(ChatReadRetrieveReadApproach)が該当します。query_prompt_templateがLLMにクエリさせるためのプロンプトです。Chat Historyに会話の履歴を、Questionにユーザからの質問文を入力できるようになっています。また、文章のテーマ(ヘルスケアと規則に関する文章であること)と、いくつかの制約をもとに、クエリを考えるよう指示しています。

https://github.com/Azure-Samples/azure-search-openai-demo/blob/b4f392274310744a5bc9786d220060f44f8ffdc7/app/backend/approaches/chatreadretrieveread.py

query_prompt_template = """
Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in a knowledge base **about employee healthcare plans and the employee handbook**.
**Generate a search query based on the conversation and the new question**. 
Do not include cited source filenames and document names e.g info.txt or doc.pdf in the search query terms.
Do not include any text inside [] or <<>> in the search query terms.
If the question is not in English, translate the question to English before generating the search query.

**Chat History:
{chat_history}**

**Question:
{question}**

Search query:
"""

この方法を利用することで、会話の履歴をもとに、文章のテーマに沿ったクエリを考えることができるため、問題1と問題2を解決できます。(プロンプトやLLMの精度に依存するので、実際には何回かトライアンドエラーで試してみる必要はありそうです)

Ask:Agentを利用する

この方法では、検索機能をツールとして与えたAgentを利用します。使用時の処理を以下のように変更します。

  • 使用時
    • ユーザが質問文を入力する
    • Agentを作成する(検索機能をツールとして与える)
    • 質問をAgentにわたす
    • Agentが必要な処理を考えながら、ツールを実行して回答を得る

リポジトリ内のコードとしては、以下のページに実装されている2つのクラス(ReadRetrieveReadApproach・ReadDecomposeAsk)が該当します。

ReadRetrieveReadApproachは、(筆者が)よく見るAgentの使用方法で実装されています。以下のようなプロンプトとツールをもとにAgentを作成し、質問文を渡して実行しています。

template_prefix = \
"You are an intelligent assistant helping Contoso Inc employees with their healthcare plan questions and employee handbook questions. " \
"Answer the question using only the data provided in the information sources below. " \
"For tabular information return it as an html table. Do not return markdown format. " \
"Each source has a name followed by colon and the actual data, quote the source name for each piece of data you use in the response. " \
"For example, if the question is \"What color is the sky?\" and one of the information sources says \"info123: the sky is blue whenever it's not cloudy\", then answer with \"The sky is blue [info123]\" " \
"It's important to strictly follow the format where the name of the source is in square brackets at the end of the sentence, and only up to the prefix before the colon (\":\"). " \
"If there are multiple sources, cite each one in their own square brackets. For example, use \"[info343][ref-76]\" and not \"[info343,ref-76]\". " \
"Never quote tool names as sources." \
"If you cannot answer using the sources below, say that you don't know. " \
"\n\nYou can access to the following tools:"

template_suffix = """
Begin!

Question: {input}

Thought: {agent_scratchpad}"""

acs_tool = Tool(name="CognitiveSearch", 
                    func=lambda q: self.retrieve(q, overrides), 
                    description=self.CognitiveSearchToolDescription,
                    callbacks=cb_manager)
employee_tool = EmployeeInfoTool("Employee1", callbacks=cb_manager)

ReadDecomposeAskでは同じくAgentが使われていますが、より質問を分解しながら進めるように指示するために、プロンプト(PREFIX)にその旨が書かれており、また、思考の例(EXAMPLES)も質問の内容を分解する様子が書かれています。

**EXAMPLES** = [
    """Question: What is the elevation range for the area that the eastern sector of the
Colorado orogeny extends into?
Thought: **I need to search Colorado orogeny, find the area that the eastern sector
of the Colorado orogeny extends into, then find the elevation range of the
area.**
Action: Search[Colorado orogeny]
Observation: <info1.pdf> The Colorado orogeny was an episode of mountain building (an orogeny) in
Colorado and surrounding areas.
Thought: It does not mention the eastern sector. **So I need to look up eastern
sector.**
Action: Lookup[eastern sector]
Observation: <info2.txt> (Result 1 / 1) The eastern sector extends into the High Plains and is called
the Central Plains orogeny.
Thought: The eastern sector of Colorado orogeny extends into the High Plains. **So I
need to search High Plains and find its elevation range.**
Action: Search[High Plains]
Observation: <some_file.pdf> High Plains refers to one of two distinct land regions
Thought: I need to instead search High Plains (United States).
Action: Search[High Plains (United States)]
Observation: <filea.pdf> The High Plains are a subregion of the Great Plains. <another-ref.docx> From east to west, the
High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130
m).
Thought: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer
is 1,800 to 7,000 ft.
Action: Finish[1,800 to 7,000 ft <filea.pdf>]
"""
...(略)
]

SUFFIX = """\nQuestion: {input}
{agent_scratchpad}"""

PREFIX = "Answer questions as shown in the following examples, **by splitting the question into individual search or lookup actions** to find facts until you can answer the question. " \
"Observations are prefixed by their source name in angled brackets, source names MUST be included with the actions in the answers." \
"All questions must be answered from the results from search or look up actions, only facts resulting from those can be used in an answer. "
"Answer questions as truthfully as possible, and ONLY answer the questions using the information from observations, do not speculate or your own knowledge."

これらの方法を利用することで、ある程度抽象的な質問だったり、1回のクエリで取得できる文章だけは回答できない質問に対しても、Agentが適切に分解したり、目標(検索すること)を設定しながら検索をすることで、適切な回答が得られます。よって、問題1・問題2・問題3を解決できます。(これも、プロンプト・ツールの説明・LLMの精度に大きく依存しそうであり、実際にはトライアンドエラーする必要がありそうです)

※ Askの方法(ask_approaches)として、RetrieveThenReadApproachも含まれているのですが、「通常の方法と問題点」で述べたように、これはRAGのベーシックな実装であり、Agentなどは使用されていません。

まとめ

RAGをベーシックに実装した場合の問題点を3つ挙げました。また、その解決策として2つ(クエリを考えさせる・Agentを利用する)を、Microsort Azureのサンプルをもとに処理の内容を解説しました。

補足

問題1に対しては、Two-Tower Modelという手法を利用することで、クエリのベクトル変換を学習し適切な文章に近いベクトルを得られるようです。

https://dev.classmethod.jp/articles/google-cloud-day-2022-recommend-model-two-tower-session-report/

https://cloud.google.com/vertex-ai/docs/matching-engine/train-embeddings-two-tower?hl=ja

謝辞

本記事を書くにあたって、五十嵐さん中村さんのアドバイスを参考にさせていただきました。ありがとうございました。