😊

クエリ加工あれこれ。精度を上げるテクニック

2024/02/23に公開

LangChainが本日2時にツイートしてたRAGの記事についてまとめます。

https://twitter.com/LangChainAI/status/1760725914646925693

クエリを言い換えたり、分割したり、一般的な質問に置き換えたりして、
その検索結果のドキュメントをまとめたりランク付けし直すことで精度があがるようです。
最後のHyDEはちょっと新鮮なテクニックなので必見です。

notebookはこちら
かなり精度は落ちますが、プロンプトなどを日本語に訳したバージョンも作ったので置いておきます。
実行するにはLancChainAPIキーが必要。
👇こちら👇から作れます(要ログイン)。
https://smith.langchain.com/settings
無料です。

マルチクエリ

クエリの言い換えを複数個作ることで、より多様な見方の回答を得られるようにするテクニックです。

まずは質問の言い換えを5種類作ります。

プロンプト

あなたはAI言語モデルアシスタントです。
あなたの仕事は、ベクトル・データベースから関連文書を検索するために、与えられたユーザーの質問に対して5つの異なるバージョンを生成することです。
 ユーザの質問に対する複数の視点を生成することによって、あなたのゴールは、ユーザが距離ベースの類似検索の制限のいくつかを克服するのを助けることです。
改行で区切られたこれらの代替の質問を提供してください。{オリジナルの質問}

オリジナルの質問は以下です。

LLMエージェントのタスク分解とは?

対してこんなバリエーションが生まれました。

LLMエージェントのタスク分解とは何ですか?
LLMエージェントのタスク分解について教えてください。
LLMエージェントのタスク分解はどのようなものですか?
LLMエージェントのタスク分解に関する詳細を教えてください。
LLMエージェントのタスク分解についての情報を教えてください。

大切なのはクエリではなくそのクエリによる検索結果です。
質問の結果のドキュメントを取得し、重複を削除します。

def get_unique_union(documents: list[list]):
    """ 検索されたドキュメントのユニーク結合 """
    # リストのリストを平坦化し、各ドキュメントを文字列に変換する。
    flattened_docs = [dumps(doc) for sublist in documents for doc in sublist]
    # Get unique documents
    unique_docs = list(set(flattened_docs))
    # Return
    return [loads(doc) for doc in unique_docs]

# Retrieve
retrieval_chain = generate_queries | retriever.map() | get_unique_union
docs = retrieval_chain.invoke({"question":question})

今作ったretrieval_chainのドキュメントをコンテキストとして、オリジナルの質問(LLMエージェントのタスク分解とは?)に回答させます。
果たしてマルチクエリの検索結果のドキュメントをコンテキストにすることで精度は上がるのか?

# RAG
template = """
このコンテキストに基づいて、次の質問に日本語で答えてください:
{context}
質問: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(temperature=0)
final_rag_chain = (
    {"context": retrieval_chain, 
     "question": itemgetter("question")} 
    | prompt
    | llm
    | StrOutputParser()
)
final_rag_chain.invoke({"question":question})

回答は以下のとおりです。

LLMエージェントのタスク分解とは、大きなタスクを小さな管理しやすいサブゴールに分解することです。これにより、複雑なタスクを効率的に処理することが可能となります。また、エージェントは過去の行動を自己批評し、反省することができ、間違いから学び、将来のステップのためにそれを改善することで、最終的な結果の品質を向上させることができます。

多分、上がった!多分!

関連クエリ

次のテクニックは言い換えではなく、オリジナルの質問に関連する検索クエリを4つ作ってもらいます。

あなたは、1つの入力クエリに基づいて複数の検索クエリを生成する便利なアシスタントです。\n
{question}に関連する複数の検索クエリを日本語で生成します。
(4 クエリ出力):

先ほどの5件作ったのとの違いは以下の通り

  • 言い方を変えたクエリを5件作る
  • 関連するクエリを4件作る
    以下のような結果になりました。
1. LLMエージェントとは何ですか?
2. LLMエージェントのタスク分解とはどのようなプロセスですか?
3. LLMエージェントのタスク分解における重要なポイントは何ですか?
4. LLMエージェントのタスク分解を効果的に行うための方法はありますか?

関連クエリの結果のドキュメントを、今度は重複削除ではなくリランク(優先度順に並べ替える)します。
クエリ4件の検索結果として最も関連する文書をk件取ってきて、最も重複があり、かつkの数字が小さい(1位とか)ものほどランクが高くなります。

from langchain.load import dumps, loads

def reciprocal_rank_fusion(results: list[list], k=60)""" reciprocal_rank_fusionは、ランク付けされた文書の複数のリストと、RRFの式で使われるオプションのパラメータkを受け取ります。 """
    
    # 各一意な文書の融合スコアを保持する辞書を初期化する
    fused_scores = {} # 各リストを繰り返し処理する

    # ランク付けされた文書のリストを繰り返し処理する
    for docs in results:
        # リスト内の各文書を、そのランク(リスト内の位置)で繰り返し処理する
        for rank, doc in enumerate(docs)# ドキュメントを文字列形式に変換してキーとして使用する (ドキュメントはJSONにシリアライズできると仮定)
            doc_str = dumps(doc)
            # ドキュメントがまだfused_scores辞書にない場合、初期スコアを0として追加する
            if doc_str not in fused_scores:
                fused_scores[doc_str] = 0
            # もしあれば、その文書の現在のスコアを取得する。
            previous_score = fused_scores[doc_str] # 文書の現在のスコアを取得する。
            # RRF の式を用いて文書のスコアを更新する: 1 / (rank + k)
            fused_scores[doc_str] += 1 / (rank + k)

    # 最終的な再ランク結果を得るために、融合されたスコアに基づいてドキュメントを降順にソートする
    reranked_results = [
        (loads(doc), score)
        for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
    ]

    # それぞれが文書とその融合スコアを含むタプルのリストとして、再ランクされた結果を返す
    return reranked_results

retrieval_chain_rag_fusion = generate_queries | retriever.map() | reciprocal_rank_fusion
docs = retrieval_chain_rag_fusion.invoke({"question": question})
len(docs)

では、リランクした文書を使ってオリジナルの質問に回答させます。
5件のマルチクエリより回答が短くなってしまいました…

# リランクした文書を使った回答
LLMエージェントのタスク分解とは、複雑なタスクをより小さな、管理しやすいサブゴールに分解することです。これにより、エージェントは複雑なタスクを効率的に処理することができます。
# マルチクエリの結果の文書を1つにまとめた文書を使った回答
LLMエージェントのタスク分解とは、大きなタスクを小さな管理しやすいサブゴールに分解することです。これにより、複雑なタスクを効率的に処理することが可能となります。また、エージェントは過去の行動を自己批評し、反省することができ、間違いから学び、将来のステップのためにそれを改善することで、最終的な結果の品質を向上させることができます。

本来は以下のような丁寧な結果が出るんだそうです。
日本語で書いたのがまずかったかな。

LLM エージェントのタスク分解とは、複雑なタスクをより小さく単純なステップに分割するプロセスを指します。これにより、LLM (言語モデル) エージェントは、タスクの完了に必要な個々の手順をよりよく理解し、計画することができます。タスクの分解は、思考連鎖 (CoT) や思考ツリーなどの手法によって実現できます。これらの手法により、LLM は段階的に思考し、各段階で複数の推論の可能性を探ることができます。タスクの分解は、単純なプロンプト、タスク固有の指示、または人間による入力を使用して行うこともできます。

結果は残念でしたが、マルチクエリとリランクについてご理解いただけたでしょうか。

Decomposition(クエリ分解)

今度はクエリ分解を理解しましょう。
クエリ分解とは質問を、もっと単純な質問に書き換えることです。
以下のようなプロンプトを使って、3件にクエリを分解してもらいます。

あなたは、入力された質問に関連する複数のサブ質問を生成する親切なアシスタントです。\n
入力された問題を、単独で答えられるような小問題/小問題のセットに分解することが目標です。\n
に関連する複数の検索クエリを生成します: {質問}に関連する複数の検索クエリを生成します。\n
出力(3つのクエリ)

質問はこれです。

LLMを搭載した自律エージェントシステムの主な構成要素とは?

以下のようなクエリに分解されました。

  1. LLMを搭載した自律エージェントシステムの主な構成要素は何ですか?
  2. LLMを使用する自律エージェントシステムにはどのような機能が含まれていますか?
  3. LLMを搭載した自律エージェントシステムを構築する際に考慮すべき要素は何ですか?

1は元のクエリまんま、2は機能、3は考慮すべき要素を質問しています。
それぞれの回答はこちら。

  1. LLMを搭載した自律エージェントシステムの主な構成要素は何ですか?
LLMを搭載した自律エージェントシステムの主な構成要素は以下です:
- 計画
  - サブゴールと分解:エージェントは大きなタスクを小さな管理可能なサブゴールに分解し、複雑なタスクを効率的に処理できるようにします。
  - 反省と改善:エージェントは過去の行動を自己批判し、過ちから学び、将来のステップのためにそれらを改善することができます。これにより、最終結果の品質が向上します。
- メモリ
これらの要素がLLMを中心とした自律エージェントシステムの構成要素として機能します。
  1. LLMを使用する自律エージェントシステムにおいて、LLMの役割は何ですか?
LLMを使用する自律エージェントシステムにおいて、LLMの役割はエージェントの脳として機能することです。
LLMはエージェントの中心的なコントローラーとして機能し、計画や反省、改善などの重要な機能を補完する役割を果たします。
LLMは大きなタスクを小さな管理可能なサブゴールに分解し、複雑なタスクを効率的に処理するための基盤となります。
また、過去の行動を自己批判し、過ちから学び、将来のステップのためにそれらを改善することで、最終結果の品質を向上させる役割も果たします。
LLMは強力な一般的な問題解決者としての潜在能力を持っています。
  1. 自律エージェントシステムにおけるLLMの機能とは何ですか?
LLMを使用する自律エージェントシステムにおいて、LLMの機能は以下の通りです:
- エージェントの脳として機能し、中心的なコントローラーとして活動する。
- 大きなタスクを小さな管理可能なサブゴールに分解し、複雑なタスクを効率的に処理する基盤となる。
- 過去の行動を自己批判し、過ちから学び、将来のステップのために改善することで、最終結果の品質を向上させる。
- 強力な一般的な問題解決者としての潜在能力を持つ。

ちなみにもとの英語では以下のようにもっと高度なクエリに分解されるようです。ぐぬぬ。

  • LLM技術とはどのようなもので、自律エージェントシステムでどのように機能するのか?
  • LLMを搭載した自律エージェントシステムの主な構成要素は、自律的な機能を実現するためにどのように相互作用するのでしょうか?
    回答もずっと詳しい。長すぎるので真面目に読まなくていいですよ。
LLM テクノロジーは、Large Language Model の略で、自律エージェント システムのコア コントローラーとして使用できる強力なツールです。これらのシステムでは、LLM はエージェントの頭脳として機能し、計画、サブゴールの分解、反映、改良などの主要なコンポーネントによって補完されます。LLM テクノロジーにより、自律エージェントは大きなタスクをより小さな管理可能なサブ目標に分割し、複雑なタスクを効率的に処理できるようになります。エージェントは、過去の行動に対する自己批判と反省を行い、間違いから学び、将来のステップに向けて改善することもできます。この反復プロセスは、自律エージェントによって生成される最終結果の品質を向上させるのに役立ちます。さらに、自律エージェント システムに LLM テクノロジーを組み込むには、LLM+P などのさまざまなアプローチがあります。これには、長期計画に外部の古典的なプランナーを使用することが含まれます。このアプローチでは、計画ドメイン定義言語 (PDDL) を利用して計画の問題を記述し、計画ステップを外部ツールにアウトソーシングします。全体として、LLM テクノロジーは、強力な一般的な問題解決機能を提供し、エージェントが環境と対話し、推論し、効果的に行動できるようにすることで、自律エージェント システムにおいて重要な役割を果たします。

精度はともかく、次は今作った分解クエリの質問と答えをコンテキストにして、1つの文章にまとめてもらいます。
プロンプトは以下のとおりです。

template = """
ここにQ+Aのペアがあります。

{context}

これらを使って質問に対する答えを日本語でまとめてください: {question}
"""

結果

LLMを搭載した自律エージェントシステムの主な構成要素には、計画、サブゴールと分解、反省と改善、メモリが含まれます。
これらの要素は、複雑なタスクの効率的な処理を可能にし、過去の行動を振り返り、未来のステップに向けて改善することで、最終結果の品質を向上させます。

あまり違いがわからないですね…。
でも英語のソース元だとかなり優秀な結果が出たみたいです。
分割クエリの結果からして違う。
まず分割クエリは以下のものが出るようです。

質問 1. LLM とは何ですか?また、自律エージェント システムでどのように機能しますか? 
回答 1. LLM は Large Language Model の略で、自律エージェント システムのコア コントローラーとして機能します。これらのシステムでは、LLM は計画、サブゴールの分解、反映、改良などのタスクを担当します。自然言語で推論トレースを生成し、タスク固有の個別のアクションを通じて環境と対話できます。

質問 2. 自律エージェント システムのさまざまなコンポーネントは何ですか? 
回答 2. 自律エージェント システムのさまざまなコンポーネントには、計画、タスクの分解、サブ目標と分解、反映と改良、記憶が含まれます。

質問 3. LLM はエージェント システムの自律性にどのように貢献しますか? 
回答 3. LLM は、エージェントの頭脳として機能し、サブゴールの分解とリフレクションを通じて複雑なタスクを効率的に処理できるようにすることで、エージェント システムの自律性に貢献します。これにより、エージェントは大きなタスクをより小さく管理しやすいサブ目標に分割し、過去のアクションから学習して将来の結果を向上させることができます。

質問 4. LLM を利用した自律エージェント システムとその主要コンポーネントの例を教えてください。
回答 4. LLM を利用した自律エージェント システムの例としては、AutoGPT、GPT-Engineer、BabyAGI などがあります。これらのシステムの主なコンポーネントには、計画、サブゴールと分解、反映と改良、記憶が含まれます。

(なんか4つあるな)
それを1つの文章にまとめてもらった回答がこちら。

LLM を利用した自律エージェント システムの主なコンポーネントには、計画、タスク分解、サブ目標分解、反映と改良、およびメモリが含まれます。
LLM は Large Language Model の略で、これらのシステムのコア コントローラーとして機能し、計画、サブゴールの分解、反映、改良などのタスクを担当します。
自然言語で推論トレースを生成し、タスク固有の個別のアクションを通じて環境と対話できます。
これらのコンポーネントを利用することで、LLM は複雑なタスクを効率的に処理し、
タスクをより小さなサブ目標に分割し、過去のアクションから学習して将来の結果を改善することにより、
エージェント システムの自律性を実現します。LLM を利用した自律エージェント システムの例には、AutoGPT、GPT-Engineer、BabyAGI などがあります。

「LLMを搭載した自律エージェントシステムの主な構成要素とは?」という質問に対して、非常に精度が高く多角的な回答が返ってくるようですね。

Step Backプロンプティング

次はStep Backプロンプティングです。
これは質問をもっと一般的な質問に書き換えて、答えやすくするものです。
例えば質問が「警察のメンバーは合法的な逮捕を行うことができますか?」だとすると
もっと一般的な質問は「警察のメンバーは何ができるか?」になります。

Step Backするためのプロンプト

あなたは世界の知識に精通している専門家です。
あなたの任務は、質問を一歩引いて、より一般的なステップバック質問に言い換えることです。
これにより、回答が容易になります。以下にいくつかの例を示します:

このように例を示すプロンプトを与える場合、
FewShotChatMessagePromptTemplate
というテンプレートを使うと便利なようです。

examples = [
    {
        "input": "警察のメンバーは合法的な逮捕を行うことができますか?",
        "output": "警察のメンバーは何ができるか?",
    },
    {
        "input": "Jan Sindel’の出身国は?",
        "output": "what is Jan Sindelの経歴は?",
    },
]
example_prompt = ChatPromptTemplate.from_messages(
    [
        ("human", "{input}"),
        ("ai", "{output}"),
    ]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(
    example_prompt=example_prompt,
    examples=examples,
)

このプロンプトでいつも通りchainを作って、step_back_contextというキーに入れることで回答がしやすくなるとのこと。

chain = (
{
    # 通常の質問を使ってコンテキストを取得
    "normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
    # ステップバック質問を使ってコンテキストを取得
    "step_back_context": generate_queries_step_back | retriever,
    # 質問をそのまま渡す
    "question": lambda x: x["question"],
}
    | response_prompt
    | ChatOpenAI(temperature=0)
    | StrOutputParser()
)
chain.invoke({"question": question})

回答は以下のとおりです。(長いので読まなくていry)

LLMエージェントのタスク分割は、大きなタスクを小さな管理可能なサブゴールに分解することを指します。これにより、複雑なタスクを効率的に処理することが可能となります。エージェントは、Chain of thought(CoT)やTree of Thoughtsなどの技術を使用して、タスクを段階的に分解し、複数の管理可能なタスクに変換します。これにより、モデルのパフォーマンスが向上し、タスクの解釈が可能となります。LLMエージェントは、簡単なプロンプトを使用してタスク分解を行うこともあります。例えば、「XYZのステップ」「XYZを達成するためのサブゴールは何か」といったプロンプトを使用することで、タスクを分解しています。また、特定のタスクに関する指示を使用することもあります。例えば、小説を書く場合には「ストーリーの概要を書く」といった具体的な指示を使用することがあります。さらに、人間の入力を使用してタスク分解を行うこともあります。LLMエージェントのタスク分解は、複雑なタスクをより管理可能な部分に分割し、効率的に解決するための重要なプロセスです。

HyDE

さて、最後にして表題のHyDEです。
これは今までと違い、LLMにクエリではなくコンテキストを作ってもらい、次に本物のコンテキストを渡して検索させることでより的確な回答が望めるという試みです。

これまでの検索が
「この質問の答えを探して」だとすると、HyDEは
「ドキュメントの中にこんな感じの情報があると思うからそれを探して!」
と指示する感じです。
よくわからないですよね。見ていきましょう。

まずは以下のようなプロンプトでLLMにコンテキストを作ってもらいます。

質問に答えるための論文からの抜粋を書いてください。
質問: {question}
抜粋::"""

この「論文(の抜粋)」がコンテキストです。
LLMについての質問ならLLMの論文、心理学の質問なら心理学の論文(の抜粋)を事前知識から「こんな感じかな〜」と作ってもらいます。
例えば質問が「子供が人の心を理解するのは何歳くらいからですか?」だとすると、
「…また、一般的に幼児は3,4歳ぐらいからサリーアン課題を解けるようになると言われる。これは他人が自分と違う視点を持っていることを理解しないと解けない問題であり…」みたいなコンテキストが作られます。
(例です。本当はもっと回答と見分けがつかないようなコンテキストが生成されます)

from langchain.prompts import ChatPromptTemplate

# HyDE document genration
template = """
質問に答えるための論文からの抜粋を書いてください。
質問: {question}
抜粋::"""
prompt_hyde = ChatPromptTemplate.from_template(template)

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

generate_docs_for_retrieval = (
    prompt_hyde | ChatOpenAI(temperature=0) | StrOutputParser() 
)

# Run
question = "LLMエージェントのタスク分解とは?"
generate_docs_for_retrieval.invoke({"question":question})

つくってもらったコンテキストです。ほぼ回答。

LLMエージェントのタスク分解は、大きな課題を小さな部分に分割し、それぞれの部分を個別に解決することを指します。
このアプローチにより、複雑な問題をより管理しやすい形に分割することができ、各部分を個別に解決することで全体の問題を解決することが可能となります。
LLMエージェントは、タスク分解を行う際に、各部分の関連性や依存関係を考慮し、最適な解決策を見つけるための手がかりを得ることが重要です。

今つくったコンテキスト生成用のチェーンに本物の論文のretrieverをパイプでつなげます。

# Retrieve
retrieval_chain = generate_docs_for_retrieval | retriever 
retireved_docs = retrieval_chain.invoke({"question":question})
retireved_docs

retireved_docsにはウソコンテキストと近いベクトルを持った本物の論文の抜粋が出てきます。
これをコンテキストに渡していつも通り質問させます。

# RAG
template = """このコンテキストに基づいて、次の質問に日本語で答えてください:
{context}

Question: {question}
"""

prompt = ChatPromptTemplate.from_template(template)

final_rag_chain = (
    prompt
    | llm
    | StrOutputParser()
)

final_rag_chain.invoke({"context":retireved_docs,"question":question})

以下のような回答が得られました。
論文を実際に使ってる!と実感するような結果になりましたね。

LLMエージェントのタスク分解は、効率的な計画よりも幻覚がより一般的な失敗であることを考慮して、
効率的な計画と幻覚を区別するためのヒューリスティック関数を使用することを含みます。
このヒューリスティック関数は、効率的でない軌道や幻覚を含む軌道を停止すべきタイミングを決定します。
また、自己反省は、LLMに2つの例を示し、それぞれが(失敗した軌道、将来の計画の変更を導くための理想的な反射)のペアである2ショットの例を示すことで作成されます。
その後、反射はエージェントの作業メモリに追加され、最大3つまで使用され、
LLMへのクエリのコンテキストとして使用されます。

Discussion