DIVX テックブログ

catch-img

【RAG】検索クエリをカスタムして、RAGの検索精度を向上させる

目次[非表示]

  1. 1.はじめに
  2. 2.校閲AIとは
  3. 3.存在した課題
    1. 3.1.どう解決するか?
  4. 4.RAGの検索クエリをカスタムしてみる
    1. 4.1.準備
      1. 4.1.1.ベクトルDBの用意とドキュメントの登録
    2. 4.2.検証
      1. 4.2.1.1. プレーン
      2. 4.2.2.2. HyDE
      3. 4.2.3.3. カスタムHyDE
    3. 4.3.RAGAS評価
      1. 4.3.1.評価用の処理
    4. 4.4.検証まとめ
  5. 5.校閲AIへ導入した結果
  6. 6.おわりに
  7. 7.お悩みご相談ください
  8. 8.参考文献

はじめに

AI基礎研究チームでエンジニアをしている飯倉です!

今回、社内で開発・利用している「校閲AI」にRAG(Retrieval-Augmented Generation)を導入しました。
導入にあたって精度向上にも取り組みましたので、その内容を紹介したいと思います。

校閲AIとは

まず、校閲AIの概要を説明します。
divxではテックブログ執筆の際、社内ツールに実装された「校閲AI」を使用して記事のチェックを行っています。


この校閲AIは生成AIを利用し、以下の項目をチェックするツールです。

  • 技術チェック: 技術的な誤りがないか確認

  • 初心者表現チェック: 書き手が初心者と思われるような表現をしていないか確認
  • SEOチェック: 基本的なSEO対策の確認
  • コンプライアンスチェック: 不適切または敬意を欠く表現を使用していないかチェック

存在した課題

校閲AIの「技術チェック」に課題がありました。
技術チェックは、技術的な誤りがないか確認し、以下のフォーマットで出力されます。

  • 指摘箇所: 技術的な誤りが記述されている文
  • 指摘内容: 技術的な誤りの内容
  • 修正案: 正しく修正するための案
  • 参考URL: 指摘の元となった情報のあるURL

出力部分には、以下の課題が存在していました。

  1. 指摘内容が間違っていることがある
  2. 参考URLのリンクが切れていることがある

これらの原因は、回答がLLM(大規模言語モデル)の持っている情報からのみ生成され、学習に使用したデータが古く、最新の情報を反映できないことにあります。

どう解決するか?

最新の情報を参照できないことが原因なので、最新情報の取得が課題解決の鍵となります。

その上でどの方法を選択するかですが、冒頭にも述べている通りRAGを選択しました。
結論としてはRAGを利用することで、上記の課題を解決できました。

また、当初考えていた選択肢としては今回選んだRAG含め下記がありました。

  • ファインチューニング:技術データセットを用意し、モデルをファインチューニング
  • Webサーチ:リアルタイムでウェブから技術情報を取得
  • RAG:技術情報をベクトルDBに保存し、RAGで活用

RAGを選択した理由は以下のメリットがあるからです:

  • 信頼性の高い情報をもとに回答を生成できる
  • 実装コストと効果のバランスがいい
  • 情報源更新の手間が少ない
  • RAGの知識を蓄積できる

続いて、RAGの精度改善についてお話しします。

RAGの検索クエリをカスタムしてみる

RAGを導入時に精度を向上させるため、検索クエリの調整を行いました。
検索クエリの調整には、以下の3つの手法を試してみました。

  1. プレーン
    入力をそのまま検索クエリとして利用する方法。
  2. HyDE
    入力に対してLLMに仮説的な回答を出してもらい、その回答を検索クエリとして利用する方法。
  3. カスタムHyDE
    プレーンとHyDEを組み合わせた方法。入力とHyDEの出力をまとめて検索クエリとして利用する。 HyDE出力の関連度が低いことがあるので、そこに入力を追加することで安定性を高める。

準備

まずは準備から始めます。
検証は複数のプログラミング言語に関する文章をレビューする形で進めていきます。
評価にはRAG評価ツールであるRAGASを用いて可視化してみます。

使用する技術や環境

  • 実行環境:Google Colaboratory
  • 使用モデル:
    • gpt-4o-mini
    • text-embedding-3-large
  • ベクトルDB:ChromaDB
  • ライブラリ:langchain, ragas

ベクトルDBの用意とドキュメントの登録

ベクトルデータベースを用意し、回答に必要な情報を入れます。
入れる情報はプログラミング言語の公式ドキュメントのページ内容です。

from langchain_community.document_loaders import AsyncHtmlLoader
from langchain_community.document_transformers import MarkdownifyTransformer
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from typing import Sequence


# 分割器:再帰的チャンキング。テキストを指定した文字列のリストで分割して、指定チャンクサイズに収まるようにまとめる
recursive_character_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=3000, chunk_overlap=100, length_function=len
)

def get_md_docs_from_urls(urls: str | List[str]) -> Sequence[Document]:
    """
    URLからHTMLを取得し、マークダウンに変換してドキュメント作成
    """
    # URLからHTMLを取得
    loader = AsyncHtmlLoader(web_path=urls)
    docs = loader.load_and_split(text_splitter=recursive_character_text_splitter)
    # HTMLをマークダウンに変換
    md_transformer = MarkdownifyTransformer()
    md_docs = md_transformer.transform_documents(docs)
    return md_docs

# プログラミング言語のドキュメント
urls = [
    "https://docs.python.org/ja/3.10/tutorial/controlflow.html",
    "https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html",
    "https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html",
    "https://www.php.net/manual/ja/functions.user-defined.php",
    "https://www.php.net/manual/ja/control-structures.if.php",
    "https://www.php.net/manual/ja/control-structures.for.php",
    "https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions",
    "https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/if...else",
    "https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for",
    "https://go.dev/ref/spec",
]

md_docs = get_md_docs_from_urls(urls=urls)

ドキュメントが用意できたら、ベクトルDBの作成とドキュメントの登録を行います。
また今回使用していくモデルの定義も同時に行います。

from langchain_chroma import Chroma
from langchain_openai import ChatOpenAI, OpenAIEmbeddings


# モデルの準備
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# ベクトルDBの定義
chroma = Chroma(
    collection_name="vector_index",
    embedding_function=embeddings,
    collection_metadata={"hnsw:space": "cosine"},
)

# ベクトルDBへドキュメントを登録
chroma.add_documents(documents=md_docs)

リトリーバーの定義

# ベクトルDBからドキュメント取得するリトリーバーの準備
 retriever = chroma.as_retriever(
     search_type="similarity",
)

レビュー用処理群の定義

レビューにはOpenAIのStructured Outputsを使用し、結果を構造化データにして返す関数を定義します。
これにより、指摘箇所、指摘内容、参考URLを確実に取得できるようになります。

from pydantic import BaseModel, Field
from typing import List
from openai import OpenAI
from langchain_core.runnables import chain

# レビュー用の構造化データ
class TechnicalReview(BaseModel):
    """技術的なレビューの結果を構造化データにする"""

    review_target: str = Field(
        ...,
        description="指摘対象の原文。",
    )
    review_content: str = Field(
        ...,
        description="技術的な指摘内容。",
    )
    source: str = Field(
        ...,
        description="指摘の元となったソースURL。",
    )


class TechnicalReviewList(BaseModel):
    """技術的なレビューの結果のリスト"""

    reviews: List[TechnicalReview] = Field(
        ...,
        description="技術的なレビューの結果のリスト",
    )


# レビュー用のプロンプト
review_system_prompt = """
あなたはWebアプリケーション開発とIT技術の専門家です。下記ルールに従って文章内に技術的な誤りがあれば全て指摘してください。

- 提供する情報のみを使用して、文章の技術的な誤りを指摘してください。
- 提供する情報と関連がなければ何も出力しないでください。
- 指摘箇所と内容と根拠となるURLをセットで教えてください。
"""

openai_client = OpenAI()


# Runnable化
@chain
def review(x: dict[str, str]) -> TechnicalReviewList:
    """
    技術的なレビューを行う関数
    """
    # 回答に使用する関連情報
    context = "\n".join(
        [
            f"ページ内容:{doc[0].page_content}\n\nソース:{doc[0].metadata['source']}"
            for doc in x["context"]
        ]
    )
    # 関連情報と文章を組み合わせたテキスト
    input_with_context = f"関連情報:\n{context}\n\n文章:\n{x['input']}"
    # 構造化クラスをもとに、技術レビュー結果を構造化して返す
    res = openai_client.beta.chat.completions.parse(
        model="gpt-4o-mini",
        temperature=0.0,
        response_format=TechnicalReviewList,
        messages=[
            {
                "role": "system",
                "content": review_system_prompt,
            },
            {"role": "user", "content": input_with_context},
        ],
    )
    structured_res = res.choices[0].message.parsed

    return structured_res

検証

下記の文章を検証に用います。
マークダウンヘッダー毎に異なるプログラミング言語に関する説明が記載されていて、それぞれの言語におけるif文、for文、関数の説明が記載されています。

ただし、それぞれ事実と異なる内容が混ざって記載されているので、その辺りを正しく指摘できるかも見ていきます。

# Pythonにおけるif文
Pythonのif文は、プログラムの制御を行うための基本的な方法です。
`if`文は条件が真の場合に特定のコードブロックを実行します。以下は、数値に基づいて出力を制御する例です。

```python
x = int(input("Please enter an integer: "))
if x < 0:
    x = 0
    print('Negative changed to zero')
elif x == 0:
    print('Zero')
else:
    print('More')
```

この構文により、複数の条件を簡潔に扱うことが可能です。特に、`elif``else if`の短縮形であり、条件の組み合わせを容易にします。
また、if文はPythonの唯一の制御フロー構造であるため、他の条件文と組み合わせて使用することはできません。


# Rubyにおけるif文
Rubyのif文は条件分岐の基本です。Rubyでは条件式が真のときに実行すべきコードを記述できます。
例えば、整数の変数が特定の値以上であれば特定のメッセージを表示するということができます。以下はその例です。

```ruby
if age > 18 then
  puts "成人です"
else
  puts "未成年です"
end
```

if文は値を返すため、条件式が真であればその結果がそのまま返されます。これにより、値を利用した処理が可能になります。
さらに、Rubyでは`if`節の代わりに`elif`を使用する点にも注意が必要です。多くの他の言語と異なり、Rubyでの`if`文は単純で柔軟な構文を持っています。

```ruby
result = if score >= 60 then "合格" else "不合格" end
```

このように、Rubyのif文は簡潔で直感的に使用でき、条件に基づいた処理の流れを明確に示します。


# PHPにおける関数
PHPにおけるユーザー定義関数は、特定の機能を持つコードの集合であり、再利用を促進します。関数は次のように定義されます。引数を使う場合もあり、以下のような構文となります。

```php
<?php
function sampleFunction($input1, $input2)
{
    return $input1 + $input2;
}
?>
```

関数名は、文字またはアンダースコアで始まり、その後に任意の構成要素が続く必要があります。
関数の定義は呼び出し前に記述する必要があります。

また、関数名の大文字小文字の違いは区別されるため、開発者が特別に気を付ける必要があります。


# JavaScriptの`for`文
JavaScriptの`for`文は、効率的なループ処理のための基本的な構文を提供します。基本的な構文は以下の通りです。

```javascript
for (let i = 0; i < 10; i++) {
  console.log(i);
}
```

ここで、初期化、条件、後処理の3つの部分から構成されます。興味深い点は、`initialization`を省略できるだけでなく、ループの最初の部分から全ての式を省略することも可能です。
これにより、無限ループを簡単に作成することができます。

```javascript
for (;;) {
  console.log('This will run forever');
}
```

なお、`for`文内で宣言された変数は、ループ外からアクセス可能です。

説明が間違っていて、指摘してほしい箇所については以下です。

  1. if文はPythonの唯一の制御フロー構造であるため、他の条件文と組み合わせて使用することはできません。
    • 実際には他の条件文と組み合わせて使用することができます。
  2. Rubyではif節の代わりにelifを使用する点にも注意が必要です。
    • 実際にはelifではなくelsifを使用します。(elifはPythonの構文です)
  3. 関数の定義は呼び出し前に記述する必要があります。
    • 実際にはPHPでは呼び出し前でも記述可能です。
  4. 関数名の大文字小文字の違いが区別されるため、開発者が特別に気を付ける必要があります。
    • 実際には大文字小文字を区別しません。
  5. なお、for文内で宣言された変数は、ループ外からアクセス可能です。
    • 実際にはletconstを使用した場合、ループ外からアクセスできません。

こちらの文章をマークダウンヘッダーごとにチャンクとして分割し、レビューに用います。

# 文章をマークダウンヘッダーごとに分割
markdown_header_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[("#", "#")], strip_headers=False
)
text_chunks = markdown_header_splitter.split_text(sample_md)

1. プレーン

プレーンの実装です。
文章をそのまま検索クエリとして使用し、関連ドキュメントの検索を行います。


from langchain_core.runnables import RunnablePassthrough


# リトリーバーチェイン
retrieve_chain = (lambda x: (x["input"])) | retriever


# RAGチェイン
rag_chain_with_source = (
    RunnablePassthrough().assign(context=retrieve_chain).assign(answer=review)
)


# プレーンのレビュー結果を格納
plane_results = []
for chunk in text_chunks:
    result = rag_chain_with_source.invoke({"input": chunk.page_content})
    plane_results.append(result)

実行した結果を少し整えて表示してみます。

レビュー1:
指摘箇所: また、`if`文はPythonの唯一の制御フロー構造であるため、他の条件文と組み合わせて使用することはできません。
指摘内容: `if`文はPythonの唯一の制御フロー構造ではなく、`for`文や`while`文など他の制御フロー構造と組み合わせて使用することができます。
参考URL: https://docs.python.org/ja/3.10/tutorial/controlflow.html


レビュー2:
指摘箇所: Rubyでは`if`節の代わりに`elif`を使用する点にも注意が必要です。
指摘内容: Rubyでは`if`の代わりに`elif`を使用することはできません。正しくは`elsif`です。
参考URL: https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html


レビュー3:
指摘箇所: 関数の定義は呼び出し前に記述する必要があります。
指摘内容: PHPでは、関数は参照される前に定義されている必要はありません。条件付きで関数が定義される場合を除き、関数がコールされる前に定義されている必要があります。
参考URL: https://www.php.net/manual/ja/functions.user-defined.php


レビュー4:
指摘箇所: 関数名の大文字小文字の違いが区別されるため、開発者が特別に気を付ける必要があります。
指摘内容: 関数名はASCII文字AからZで構成されている場合、大文字小文字を区別しません。通常は関数宣言時と同じ名前で関数をコールする方が好ましいです。
参考URL: https://www.php.net/manual/ja/functions.user-defined.php


レビュー5:
指摘箇所: なお、`for`文内で宣言された変数は、ループ外からアクセス可能です。
指摘内容: `for`文内で宣言された変数は、`let``const`を使用した場合、ブロックスコープを持ち、ループ外からはアクセスできません。`var`を使用した場合は、関数スコープまたはグローバルスコープを持ち、ループ外からアクセス可能です。
参考URL: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for

全て検出できていて、指摘内容も正しいですね。参考URLも正しく記載されています。
回答の精度自体はそのままでも問題なさそうです。

2. HyDE

HyDEの実装です。
HyDEで生成された回答を検索クエリとし、関連ドキュメントの検索を行います。
レビューに必要な情報を取得したいので、文章に書かれている技術の説明をHyDEで生成するようにします。

from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


hyde_prompt = ChatPromptTemplate.from_template(
    """文章に書かれているIT技術について説明してください。
    文章:{input}
    回答:
    """
)

# HyDEチェイン
hyde_chain = hyde_prompt | llm | StrOutputParser()


# リトリーバーチェイン
retrieve_chain = hyde_chain | retriever


# RAGチェイン
rag_chain_with_source = (
    RunnablePassthrough().assign(context=retrieve_chain).assign(answer=review)
)


hyde_results = []
for chunk in text_chunks:
    result = rag_chain_with_source.invoke({"input": chunk.page_content})
    hyde_results.append(result)

こちらも同様に結果を表示してみます。

レビュー1:
指摘箇所: また、`if`文はPythonの唯一の制御フロー構造であるため、他の条件文と組み合わせて使用することはできません。
指摘内容: `if`文はPythonの唯一の制御フロー構造ではなく、`elif``else`と組み合わせて使用することができるため、他の条件文と組み合わせて使用することが可能です。
参考URL: https://docs.python.org/ja/3.10/tutorial/controlflow.html


レビュー2:
指摘箇所: 文章: Rubyにおけるif文
指摘内容: Rubyでは`elif`は使用できず、正しくは`elsif`を使用する必要があります。
参考URL: https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html


レビュー3:
指摘箇所: 関数の定義は呼び出し前に記述する必要があります。
指摘内容: PHPでは、関数は参照される前に定義されている必要はありません。ただし、条件付きで関数が定義される場合には、その関数定義は関数がコールされる前に行われなければなりません。
参考URL: https://www.php.net/manual/ja/functions.user-defined.php


レビュー4:
指摘箇所: 関数名の大文字小文字の違いが区別されるため、開発者が特別に気を付ける必要があります。
指摘内容: 関数名はASCII文字AからZで構成されている場合、大文字小文字を区別しません。通常は関数宣言時と同じ名前で関数をコールする方が好ましいです。
参考URL: https://www.php.net/manual/ja/functions.user-defined.php


レビュー5:
指摘箇所: なお、`for`文内で宣言された変数は、ループ外からアクセス可能です。
指摘内容: `for`文内で`let``const`を使って宣言された変数は、ブロックスコープを持つため、ループ外からはアクセスできません。`var`を使った場合は、関数スコープまたはグローバルスコープになるため、ループ外からアクセス可能ですが、`let``const`ではアクセスできません。
参考URL: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for

プレーンとほぼ同じ結果が得られていて、こちらも正しくレビューできていそうです。

3. カスタムHyDE

カスタムHyDEの実装です。
HyDEの結果と入力文章を組み合わせて、関連ドキュメントの検索を行います。

from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


# プログラミング言語の場合は、<言語>:<説明>の構造で回答してください。
hyde_prompt = ChatPromptTemplate.from_template(
    """文章に書かれているIT技術についての説明を、<技術>:<説明>の構造で回答してください。
    文章:{input}
    回答:
    """
)

# HyDEチェイン
hyde_chain = hyde_prompt | llm | StrOutputParser()


# リトリーバーチェイン
retrieve_chain = (
    RunnablePassthrough().assign(technical_entities=hyde_chain)
    | RunnableLambda(lambda x: f"{x['technical_entities']}\n\n{x['input']}")
    | retriever
)


# RAGチェイン
rag_chain_with_source = (
    RunnablePassthrough().assign(context=retrieve_chain).assign(answer=review)
)


# RAGチェインの実行
custom_hyde_results = []
for chunk in text_chunks:
    result = rag_chain_with_source.invoke({"input": chunk.page_content})
    custom_hyde_results.append(result)

他と同じく結果を表示してみます。

レビュー1:
指摘箇所: また、`if`文はPythonの唯一の制御フロー構造であるため、他の条件文と組み合わせて使用することはできません。
指摘内容: Pythonには`if`文以外にも制御フロー構造が存在します。例えば、`while`文や`for`文などがあります。これらは条件に基づいてループ処理を行うために使用されます。
参考URL: https://docs.python.org/ja/3/tutorial/controlflow.html


レビュー2:
指摘箇所: 文章: Rubyにおけるif文
指摘内容: Rubyでは`elif`ではなく`elsif`を使用する必要があります。
参考URL: https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html


レビュー3:
指摘箇所: 関数の定義は呼び出し前に記述する必要があります。
指摘内容: PHPでは、関数は参照される前に定義されている必要はありません。ただし、条件付きで関数が定義される場合には、その関数定義は関数がコールされる前に行われなければなりません。
参考URL: https://www.php.net/manual/ja/functions.user-defined.php


レビュー4:
指摘箇所: 関数名の大文字小文字の違いは区別されるため、開発者が特別に気を付ける必要があります。
指摘内容: 関数名はASCII文字AからZで構成されている場合、大文字小文字を区別しませんが、通常は関数宣言時と同じ名前で関数をコールする方が好ましいです。
参考URL: https://www.php.net/manual/ja/functions.user-defined.php


レビュー5:
指摘箇所: なお、`for`文内で宣言された変数は、ループ外からアクセス可能です。
指摘内容: `for`文内で宣言された変数は、`let`や`const`を使用している場合、ブロックスコープを持ち、ループ外からはアクセスできません。`var`を使用した場合は、関数スコープまたはグローバルスコープを持ち、ループ外からアクセス可能です。
参考URL: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for

先ほどの2つの手法と同様に、指摘箇所も正しく検出できていて、指摘内容、参考URLも正しいです。

RAGAS評価

RAGの検索精度を可視化するためにRAGASを用いて評価を行います。
今回は検索精度に関する以下の評価指標を使用します。

  • コンテキスト精度(Context Precision)
    • 取得されたコンテキスト中における関連性の高い情報の割合を測定する指標です。
  • コンテキスト再現率(Context Recall)
    • 関連するドキュメントや情報のうち、どれだけ多くが実際に取得されたかを測定する指標です。
  • コンテキストエンティティ再現率(Context Entity Recall)
    • 参照情報(reference)に含まれるエンティティのうち、どれだけが取得されたコンテキストに含まれているかを測定する指標です。

RAGASの評価指標の詳細についてはこちらをご覧ください。

評価用の処理

処理を用意する前に、評価に使用する正しい回答(期待する回答)を用意します。

correct_answers = [
    "指摘箇所: `if`文はPythonの唯一の制御フロー構造であるため、他の条件文と組み合わせて使用することはできません。\n指摘内容: `if`文はPythonの唯一の制御フロー構造ではなく、他の制御フロー構造と組み合わせて使用することができます。\n参考URL: https://docs.python.org/ja/3.10/tutorial/controlflow.html",
    "指摘箇所: Rubyでは`if`節の代わりに`elif`を使用する点にも注意が必要です。\n指摘内容: Rubyでは`elif`でなく、`elsif`を使用する必要があります。\n参考URL: https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html",
    "指摘箇所: 関数の定義は呼び出し前に記述する必要があります。\n指摘内容: PHPでは、関数は参照される前に定義されている必要はありません。ただし特定の条件下では、関数がコールされる前に定義されている必要があります。\n参考URL: https://www.php.net/manual/ja/functions.user-defined.php",
    "指摘箇所: 関数名の大文字小文字の違いは常に無視されるため、開発者が特別に気を付ける必要があります。\n指摘内容: 関数名はASCII文字AからZで構成されている場合、大文字小文字を区別しませんが、通常は関数宣言時と同じ名前で関数をコールする方が好ましいです。\n参考URL: https://www.php.net/manual/ja/functions.user-defined.php",
    "指摘箇所: `for`文内で宣言された変数は、ループ外からアクセス可能です。\n指摘内容: `for`文内で宣言された変数は、`let`や`const`を使用した場合、ループ外からはアクセスできません。ただし`var`を使用した場合は、ループ外からアクセス可能です。\n参考URL: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for",
]

次に評価指標とサンプル作成用の処理を用意します。

from ragas.metrics import (
    ContextPrecision,
    ContextRecall,
    ContextEntityRecall,
)
from langchain_openai import ChatOpenAI
from ragas.llms import LangchainLLMWrapper


# 評価器モデルの定義
evaluator_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o"))

# 評価指標の定義
metrics = [
    ContextPrecision(llm=evaluator_llm),
    ContextRecall(llm=evaluator_llm),
    ContextEntityRecall(llm=evaluator_llm),
]


# 評価用サンプルの作成
def create_samples(results):
    """
    評価用サンプルを作成する関数
    レビュー結果を受け取り、評価用サンプルを生成します。
    """
    samples = []
    for result in results:
        input = result["input"]
        reviews = result["answer"].reviews
        contexts = [
            f"ページ内容:{doc_and_score.page_content}\n\nソース:{doc_and_score.metadata['source']}"
            for doc_and_score in result["context"]
        ]

        for review in reviews:
            response = f"指摘箇所:{review.review_target}\n指摘内容:{review.review_content}\n参考URL:{review.source}"
            samples.append(
                SingleTurnSample(
                    user_input=f"{review_system_prompt}\n\n文章:{input}:",  # レビューのシステムプロンプトと文章を組み合わせて入力とする
                    reference=correct_answers[len(samples)],
                    response=response,
                    retrieved_contexts=contexts,
                )
            )
    return samples

評価の実行は以下のように行えます。

from ragas import evaluate


# サンプル作成
samples = create_samples(plane_results)

# 評価データセット作成
samples_dataset = EvaluationDataset(samples=samples)

# 評価の実行
samples_results = evaluate(dataset=samples_dataset, metrics=metrics)

評価実行

では3つの手法の結果をRAGASで評価してみましょう。
わかりやすいようにスコアをグラフで表示していきます。

コンテキスト精度の比較

若干カスタムHyDEが高いですが、全ての手法で適切な回答ができていたため、ほぼ誤差の範囲だと思います。

コンテキスト再現率

こちらはカスタムHyDE、HyDE、プレーンの順でスコアが高くなりました。

回答に必要な情報がそろっているかを評価するので、取得したいドキュメントに類似した検索クエリを生成するHyDEの強みが出ていそうです。

HyDE出力の関連度が低いことがあるので、そこに入力を追加することで安定性を高める。

また上記の意図もしっかり数値として反映されていそうです。

コンテキストエンティティ再現率の比較

こちらはプレーンが低く、HyDEとカスタムHyDEが高くなりました。

回答に必要なキーワードがちゃんと含まれているかを評価するので、こちらも同様に取得したいドキュメントに寄せた検索クエリを生成するHyDEの強みが現れていそうです。

検証まとめ

複数のプログラミング言語に関する文章をレビューし、RAGASという評価ツールを用いて可視化して検証を行いました。
結論として、3つのアプローチ全てで正確な指摘が可能でした。回答に必要な情報の取得自体は全ての手法で問題なくできていそうです。
検索精度に関しては、特にカスタムHyDEは安定性と精度が良く、取得する情報の質が高そうです。

校閲AIへ導入した結果

RAGの導入により校閲AIの技術チェックの精度が向上し、誤った指摘やリンク切れの問題を解決することができました。

画像は実際の校閲AIの出力画面です。

今回の成果の導入は、私の所属するAI基礎研究チームと連携をとる、AIサクセスチームのメンバーが行なっています。
導入に際し、処理の実行を並列化して処理時間の短縮したりと、様々な工夫がなされています。

  • AI基礎研究チーム:最新のAI技術に関する調査・検証と社内ナレッジの蓄積
  • AIサクセスチーム:研究成果を社内AIツールや製品へ、工夫しつつ落とし込んでいく

おわりに

今回、RAGを用いることで、最新の情報に基づいた高信頼性の回答を生成できるようになり、以前の課題である誤った指摘とリンク切れを解決できました。
また、RAGの検索精度を改善するための、検索クエリのカスタムについてもご紹介させていただきました。みなさまの参考になれば幸いです。

今後の課題として、ベクトルDBに登録したドキュメントのメンテナンスが挙げられます。
こちらに対する改善を含め、RAGの研究を実施した際にはまた記事にしたいと思います。

最後までご覧いただきありがとうございました!

お悩みご相談ください

  ご相談フォーム | 株式会社divx(ディブエックス) DIVXのご相談フォームページです。 株式会社divx(ディブエックス)

参考文献

Chroma | LangChain

Ragas

西見公宏, 吉田真吾, 大嶋勇樹. (2024). LangChainとLangGraphによるRAG・AIエージェント[実践]入門


お気軽にご相談ください


ご不明な点はお気軽に
お問い合わせください

サービス資料や
お役立ち資料はこちら

DIVXブログ

テックブログ タグ一覧

人気記事ランキング

関連記事

GoTopイメージ