ここがポイント！RAGを活用した生成AIボットの検索精度向上について

こんにちは。Supership プロダクト統括部の @ps010 です。
普段は広告・マーケティング領域で、分析業務や広告セグメントの作成を担当しています。

:::note warn
この記事は、[Supershipグループ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/supership) の13日目の記事になります。
:::


# はじめに
今年は、自然言語の生成AIに注目が集まりましたね。私の所属チームでもLLMを用いた生産性向上に取り組んでますが、個人的には、QAボットに使われる RAG に関心があります。少し前まで夫がカスタマーサポート勤務だったのですが、人手不足のせいか残業が多かったので、QAボットで効率化できないものか？と思い、興味を持ち始めました。
RAGを調べると、簡単にはよい回答が得られないようなので、今回は、RAGのサンプルを作って、どこが精度向上のポイントになるのか解明していきたいと思います。

# 記事の目的
この記事は、RAGの仕組みと改善ポイントの把握を目的とします。サンプルデータを用いて改善案の実験を行い、ベースラインと結果を比較します。記事は以下4つのパートで構成されています。本文が長いので、リンクや目次から気になるところへ飛んでください。
1. [RAGの概要](#ragの概要)： RAGとは何か、仕組みや機能の説明
1. [RAGサンプル](#ragサンプル)： RAGの作り方(サンプルコード)とベースラインの結果
1. [改善案の実験](#改善案の実験)： 改善案の手法紹介、作り方(サンプルコード)とその結果
1. [まとめ](#まとめ)： 改善案の実験結果からわかったこと


# 忙しい方のための要約（伝えたいこと）
1. RAGとは、情報検索とLLMによる回答生成の2つのコンポーネントを組み合わせたAIフレームワークです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290054/5cefb77d-1aca-3059-e9ec-b1fca5d4b069.png) 
1. 精度改善のテストの結果、3つのことがわかりました（テスト結果は[こちら](#まとめ)）
    - 検索対象のテキスト(チャンク[^1])は、知識がまとまっている単位で分割するのがよい
        - 今回は400字程度、段落区切りが Good!!
    - 質問とチャンクの類似度をあげる工夫が効果的
        - 検索に仮の回答を渡す [HyDE](#13-hyde) は効果がありそう🙆‍♀️
        - [今回試せなかった](#今回できなかったこと)ベクトル検索やチャンクの要約の工夫に改善の余地が大きそう
    - 回答生成のLLMの影響も大きい


では、次の章からRAGの説明に入ります。

# RAGの概要
はじめに、RAGの仕組みについて簡単に説明します。

## RAGとは
Retrieval Augmented Generation(RAG。検索拡張生成)は、情報検索(Retrieval)とテキスト生成(Generation)を組み合わせたフレームワークです。通常、LLMはトレーニング時に得た情報に基づいて質問に回答します。そのため、最新情報や独自知識が必要な質問には不正確な回答をすることがあり、これを解決する手段としてRAGが用いられます。RAGを使うことで、LLMは自身が保持する既存の知識に加え、外部のデータベースから情報を引き出せるようになり、より精度の高い回答を提供することが可能になります。

RAGの応用例としては、カスタマーサービスでの自動応答システム、社内情報の検索、医療情報の提供、技術サポートなどがあります。

## アーキテクチャ
RAGは、情報検索と回答生成という二つの主要なコンポーネントで構成されています。各コンポーネントは、AI開発フレームワークの LangChain を通じて制御されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290054/5cefb77d-1aca-3059-e9ec-b1fca5d4b069.png) 

### 情報検索
情報検索とは、大量のテキストデータから関連する情報を見つけ出すプロセスを指します。RAGでは、質問が与えられるとLLMは関連文書や情報をデータベースから検索します。このプロセスでは、質問と関連性が高い情報を効率的に抽出するために、ベクトル空間モデルや類似性検索アルゴリズムを使用します。

### 回答生成
次に回答生成のプロセスでは、検索で得られた情報を基にして、LLMが新たなテキストを生成します。この過程では、Transformerベースのモデル（例えば、GPT-3など）が利用されることが多く、検索された文書と質問を入力として受け取り、それに基づいた回答を生成します。


RAGのユニークな点は、情報検索と回答生成の統合にあります。これにより、LLMは実際のデータを基に「情報に裏打ちされた」回答を生成することができます。このアプローチは、正確性の高い回答が求められる状況で威力を発揮します。


# RAGサンプル
ここからは、実際にRAGのサンプルを作って、改善ポイントを探りたいと思います。

## データソース
今回はWikipediaの[「葬送のフリーレン」](https://ja.m.wikipedia.org/wiki/%E8%91%AC%E9%80%81%E3%81%AE%E3%83%95%E3%83%AA%E3%83%BC%E3%83%AC%E3%83%B3)のページに関するQAボットを作ります。Wikipediaは著作権フリーなので、こういうテストには重宝します。データはWikipediaのAPIで取得します。API取得のサンプルコードを載せているので、折りたたみを開いてご参照ください。
<details><summary>サンプルコードを表示（折りたたみ）</summary><div>

```python:extract_data.py
import requests

def get_wikipedia_page(title):
    """
    Wikipediaから指定ページのテキストを抽出する
    
    title: Wikipediaページのタイトル
    return: ページのテキスト
    """
    url = "https://ja.wikipedia.org/w/api.php"
    params = {
        "action": "query",
        "format": "json",
        "titles": title,
        "prop": "extracts",
        "exlimit": 1,
        "explaintext": True,
        "redirects": 1,
    }

    response = requests.get(url, params=params)
    data = response.json()
    page = next(iter(data['query']['pages'].values()))
    
    return page.get("extract", "")


# 「葬送のフリーレン」のページを取得
page_content = get_wikipedia_page("葬送のフリーレン")
file_path = "./output.txt"
with open(file_path, 'w', encoding='utf-8') as file:
    file.write(page_content)
```

</div></details>

## 使用モデル
プライベートでChatGPT plusを契約してるので、埋め込みや回答生成のモデルはOpenAIを使いました。回答生成は、モデルのバージョンによって回答が変わる可能性もあるので、2通り試します。情報検索は、オープンソースでデータベースサーバを立てる必要のないFAISSライブラリにしました。

### 情報検索
- ベクトル埋め込み: [text-embedding-ada-002(OpenAI)](https://platform.openai.com/docs/models/embeddings)
- ベクトル検索: [FAISS(Facebook)](https://github.com/facebookresearch/faiss)

### 回答生成
- LLM: [gpt-3.5-turbo、gpt-4-1106-preview(OpenAI)](https://platform.openai.com/docs/models)

## ベースライン
では、QAボットの作成に入りましょう。今回はベースラインのスクリプトを基にして、パラメータなどを変えながら改善案を探ろうと思います。スクリプトは折りたたみのサンプルコードをご参照ください。

<details><summary>サンプルコードを表示（折りたたみ）</summary><div>

```python:extract_data.py
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain.chains import RetrievalQA
from langchain import PromptTemplate
from langchain.embeddings import OpenAIEmbeddings
from langchain.chat_models import ChatOpenAI


# utils.configsで管理するOpenAIのAPIキーの読み込み
import os
import sys
sys.path.append(os.pardir)
from utils import configs

openai_config = configs.set_openai_config()
os.environ["OPENAI_API_KEY"] = openai_config['API_KEY']


# 葬送のフリーレンのテキストファイルの読み込み
file_path = "./output.txt"
loader = TextLoader(file_path, encoding='utf-8')
documents = loader.load()


# 情報検索
# テキストをチャンクに分割
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    separator = "。", # 区切り文字
    chunk_size = 200, # チャンクの文字数
    chunk_overlap = 50, # チャンクオーバーラップの文字数
)
texts = text_splitter.split_documents(documents)

# embeddingモデルを定義
embs_model = OpenAIEmbeddings(model="text-embedding-ada-002")

# テキストを埋め込み、ベクトルDBに格納
db = FAISS.from_documents(texts, embs_model)
save_path = "./db_freelen"
db.save_local(save_path)
# DBのロード
db = FAISS.load_local(save_path, embs_model)

# retriever設定。質問に類似するチャンクのロード数を変数kに設定
retriever = db.as_retriever(search_kwargs={"k": 3})


# 回答生成
# プロンプトテンプレート
DEFAULT_SYSTEM_PROMPT = "参考情報を元に、ユーザーからの質問にできるだけ正確に答えてください。"
text = "{context}\nユーザからの質問は次のとおりです。{question}"
template = "{system}{prompt}".format(
    system=DEFAULT_SYSTEM_PROMPT,
    prompt=text,
)

# プロンプトの設定
PROMPT = PromptTemplate(
    template=template,
    input_variables=["context", "question"],
    template_format="f-string"
)
chain_type_kwargs = {"prompt": PROMPT}


# RAG
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
qa = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=retriever,
    chain_type="stuff",
    return_source_documents=True,
    chain_type_kwargs=chain_type_kwargs,
    verbose=True,
)
```
</div></details>

### 各種設定(処理、パラメータ)

**処理の流れ**
情報検索、回答生成、RAGの順に以下の設定を行います。
- 情報検索
    - ドキュメントのチャンク[^1]への分割方法
        - 区切り文字、チャンクの文字数、オーバーラップする文字数など
    - 検索して取り出すチャンク数（類似チャンク数）
    - 埋め込みに使うモデル
    - retriever（検索モデル）
- 回答生成
    - 回答生成のLLMモデル
- RAG
    - 情報検索、 回答生成の設定
    - その他の設定（ソースの表示など）

[^1]:チャンクとは、意味のかたまりごとに区切られたテキストのことです

ベースラインのパラメータ設定は以下の通りです。200文字ずつ文章単位でテキストを分けて、類似する3つのチャンクを検索する方針です。

**パラメータ設定**
- 区切り文字 : `。`
- チャンクの文字数 : 200
- 検索して取り出すチャンク数 : 3
- 回答生成のLLMモデル : gpt-3.5-turbo


### 質問
葬送のフリーレンQAボットには、フリーレンと師匠フランメ、仲間の勇者パーティに関するエピソードから、3つの質問をしてみます。

Q1. フランメが弟子にしたのは誰？
　→ フリーレンが正解です

Q2. フリーレンがフランメの弟子になったきっかけは何？
　→ 魔族から助けてもらったのがきっかけです（登場人物 `フリーレン` の以下の引用が正解）。
>故郷であるエルフの集落が魔族に襲われ死にかけた際に大魔法使いフランメに助けられ、彼女の弟子になる

Q3. フリーレンが勇者パーティー4人と交わした約束は？
　→ 次回も一緒に半世紀流星を見ることです（あらすじの以下の引用が正解）。
>50年に一度降るという「半世紀（エーラ）流星」を見た4人は、次回もそれを見る約束を交わしてパーティーを解散する

↓はQAボットの呼び出しイメージです。
```python:QAボットの呼び出し
## 質問
result = qa("フリーレンの異名が「葬送のフリーレン」になったきっかけは？")
print('回答:', result['result'])
print('='*10)
print('ソース:')
for d in result['source_documents']:
    print(d)

# 回答
回答: フリーレンの異名が「葬送のフリーレン」になったきっかけは、編集部会議で副編集長が提案したタイトル案の中に「葬送のフリーレン」があり、最終的に山田鐘人とアベツカサが決めたためです。このタイトル案が採用されたことで、現在の題名となりました。
==========
ソース:
page_content='本作のタイトルの由来は、山田が考えたタイトル案がありながら、編集部でも検討をし、編集部会議で「いいタイトルが決まったら自腹で賞金1万円出します」と担当編集者が募ったところ、副編集長が出したタイトル案の中に「葬送のフリーレン」があり、最終的に山田、アベに決めてもらい現在の題名となった。\n2022年9月に単行本9巻が発売、ほぼ同時にアニメ化が発表され、同年11月に展開媒体がテレビアニメであることが発表された。\n2023年10月時点で単行本の累計発行部数は1100万部を突破している。\n== あらすじ ==' metadata={'source': './output.txt'}
page_content='『葬送のフリーレン』（そうそうのフリーレン、英: Frieren: Beyond Journey’s End）は、山田鐘人（原作）、アベツカサ（作画）による日本の漫画。『週刊少年サンデー』（小学館）にて、2020年22・23合併号より連載中。\n第14回マンガ大賞、第25回手塚治虫文化賞新生賞受賞作。\n== 概要 ==\n魔王を倒した勇者一行の後日譚を描くファンタジー。\n第2巻が発売された際に有野晋哉、浦井健治、江口雄也、小出祐介、近藤くみこ、須賀健太、鈴木達央、豊崎愛生が本作にコメントを寄せている。' metadata={'source': './output.txt'}
page_content='困った人を決して見捨てない性格で、その生涯をかけて人助けに奔走したようである。死後も作中での存在感は大きく、「ヒンメルならどうしたか」がフリーレンを始めとする元勇者パーティーメンバーの行動基準の一つであり続けており、フリーレンの回想の中でも、度々その勇者ぶりが振り返られる。かつてフリーレンに恋心を抱いていたと思わせるようなエピソードも多い。幼い頃に森で道に迷った時にフリーレンと出会っており、花畑の魔法を見せてもらったことがある。エルフであるがゆえにはるかに長命である彼女の未来を慮って銅像に細かく注文をしたり、故郷を魔族に奪われて孤独に生きてきた彼女の孤独を思いやったりと、何かとフリーレンを気にかけていた。' metadata={'source': './output.txt'}
```
QAボットは、ソースの3つの文章(`"本作のタイトルの由来は..."` 、`"'『葬送のフリーレン』（そうそうのフリーレン..."`、`"困った人を決して見捨てない性格で..."`)を参照して、質問 `"フリーレンの異名が「葬送のフリーレン」になったきっかけは？"` に回答をしています。

### 結果
それではベースラインの結果を見てみましょう。

**質問 1**
```python: 実行結果
# 質問
"フランメが弟子にしたのは誰？"
# 回答
フランメが弟子にしたのはフェルンです。
==========
ソース:
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した' metadata={'source': './output.txt'}
page_content='最晩年、戦災孤児であるフェルンを引き取り、訪ねてきたフリーレンにちょっとした謀を仕掛けてフェルンを彼女の弟子として託すことで、自分の死後の悩みを解決した。かつては自身が孤児だったことから、孤児院の復興資金を自ら出すなどしたこともあった' metadata={'source': './output.txt'}
page_content='大魔法使いフランメは言葉を話す魔物を「魔族」と名付けた' metadata={'source': './output.txt'}
```
**結果**：×
違います。フェルンではありません



**質問 2**
```python:実行結果
# 質問
"フリーレンがフランメの弟子になったきっかけは何？"
# 回答
フリーレンがフランメの弟子になったきっかけは、フェルンを引き取った最晩年に、フェルンを彼女の弟子として託すことで自分の死後の悩みを解決するために謀を仕掛けたことです。
==========
ソース:
page_content='最晩年、戦災孤児であるフェルンを引き取り、訪ねてきたフリーレンにちょっとした謀を仕掛けてフェルンを彼女の弟子として託すことで、自分の死後の悩みを解決した。かつては自身が孤児だったことから、孤児院の復興資金を自ら出すなどしたこともあった' metadata={'source': './output.txt'}
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した' metadata={'source': './output.txt'}
page_content='フリーレンに曾祖父が世話になったからと食事に誘い、盗まれた家宝の宝剣の奪還を依頼する（元々は魔族の物であり、曾祖父の代も一度盗難に遭っていた）' metadata={'source': './output.txt'}
```

**結果**：×
不正解。フリーレンがフェルンを弟子にした時のエピソードです


**質問 3**
```python:実行結果
# 質問
"フリーレンが勇者パーティー4人と交わした約束は？"
# 回答
フリーレンが勇者パーティー4人と交わした約束は、フリーレンがエルフであるために長命であり、定期的に魔物が湧く里の魔物を退治するために、半世紀に一度フリーレンが訪れることを約束していたということです。
==========
ソース:
page_content='後にフリーレンがメトーデから僧侶枠として同行しようかと聞かれた際は、「このパーティの僧侶の席は（ザインのために）空けておきたいから」と断っている。\n\n\n=== 勇者一行 ===\nヒンメル\n声 - 岡本信彦\nフリーレン、ハイター、アイゼンと共に魔王を討伐した勇者であり、種族は人間' metadata={'source': './output.txt'}
page_content='女神が授けたとされる「勇者の剣」を守護する役目があり、里近辺には定期的に魔物が湧くため、半世紀に一度は退治してもらうようフリーレンと約束を結んでいるが、実際にフリーレンが訪れたのは前回から80年後だったため、彼女に文句を零した' metadata={'source': './output.txt'}
page_content='== あらすじ ==\n魔王を倒して王都に凱旋した勇者ヒンメル、僧侶ハイター、戦士アイゼン、魔法使いフリーレンら勇者パーティー4人は、10年間もの旅路を終えて感慨にふけっていたが、1000年は軽く生きる長命種のエルフであるフリーレンにとって、その旅はきわめて短いものであった' metadata={'source': './output.txt'}
```
**結果**：×
これもNG。半世紀に一度の魔物退治が選ばれましたが、別人との約束です


3つとも全て間違えました。関連文書に選ばれるエピソードが誤っているようで、例えば質問2には、フェルンがフリーレンの弟子になる時のエピソードが混ざってました。


# 改善案の実験
ベースラインは全てのQAが間違っていました。今回実施できそうな精度向上の工夫として、情報検索、回答生成、クエリの観点から以下の案が考えられます。順に試してみましょう。
- 情報検索の工夫
    - 検索して取り出すチャンク数（類似チャンク数）
    - ドキュメントのチャンク分割（区切り文字、チャンク文字数）
    - その他の工夫
        - 検索に渡すクエリの工夫（HyDE）
        - チャンクの文章の工夫（要約）
- 回答生成の工夫
    - 使用モデル
- クエリの工夫
    - 質問の仕方

## 改善案1. 情報検索の工夫
手始めに、情報検索の工夫を試してみましょう。

### 1.1. 検索して取り出すチャンク数（類似チャンク数）
ベースラインは関連文書が3つでした。この数を増やしたら正解が増えるでしょうか。

**パラメータ設定**
- 区切り文字 : `。`
- チャンクの文字数 : 200
- 検索して取り出すチャンク数 : 5（3→5）
- 回答生成のLLMモデル : gpt-3.5-turbo


**結果**
**質問 1**
```python: 実行結果
# 質問
"フランメが弟子にしたのは誰？"
# 回答
フランメが弟子にしたのはフェルンです。
==========
ソース:
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した' metadata={'source': './output.txt'}
page_content='最晩年、戦災孤児であるフェルンを引き取り、訪ねてきたフリーレンにちょっとした謀を仕掛けてフェルンを彼女の弟子として託すことで、自分の死後の悩みを解決した。かつては自身が孤児だったことから、孤児院の復興資金を自ら出すなどしたこともあった' metadata={'source': './output.txt'}
page_content='大魔法使いフランメは言葉を話す魔物を「魔族」と名付けた' metadata={'source': './output.txt'}
page_content='1000年以上昔、故郷であるエルフの集落が魔族に襲われ死にかけた際に大魔法使いフランメに助けられ、彼女の弟子になる。師匠のフランメからは闘いの技術や魔力制御の方法を伝授された。天才的な資質に加えて1000年以上に渡り魔法を研鑽したため、魔法使いとしての実力は極めて強大で圧倒的な魔力を誇る' metadata={'source': './output.txt'}
page_content='フリーレン達が受験した一級魔法使いの三次試験では急きょ試験官を務め、直感により受験者に合否を下した。これまで数多く人間の弟子を取ったが、皆ゼーリエの足元にも及ばないまま先立っていったとのこと' metadata={'source': './output.txt'}
```
**結果**：×
あらら。また、フェルンが選ばれました


**質問 2**
```python:実行結果
# 質問
"フリーレンがフランメの弟子になったきっかけは何？"
# 回答
フリーレンがフランメの弟子になったきっかけは、フェルンという戦災孤児を引き取り、彼女をフランメの弟子として託すことで、自分の死後の悩みを解決するためでした。
==========
ソース:
page_content='最晩年、戦災孤児であるフェルンを引き取り、訪ねてきたフリーレンにちょっとした謀を仕掛けてフェルンを彼女の弟子として託すことで、自分の死後の悩みを解決した。かつては自身が孤児だったことから、孤児院の復興資金を自ら出すなどしたこともあった' metadata={'source': './output.txt'}
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した' metadata={'source': './output.txt'}
page_content='フリーレンに曾祖父が世話になったからと食事に誘い、盗まれた家宝の宝剣の奪還を依頼する（元々は魔族の物であり、曾祖父の代も一度盗難に遭っていた）' metadata={'source': './output.txt'}
page_content='かつてフリーレンに恋心を抱いていたと思わせるようなエピソードも多い。幼い頃に森で道に迷った時にフリーレンと出会っており、花畑の魔法を見せてもらったことがある' metadata={'source': './output.txt'}
page_content='フリーレン達が受験した一級魔法使いの三次試験では急きょ試験官を務め、直感により受験者に合否を下した。これまで数多く人間の弟子を取ったが、皆ゼーリエの足元にも及ばないまま先立っていったとのこと' metadata={'source': './output.txt'}
```

**結果**：×
違います。さっきと同じエピソードが選ばれました



**質問 3**
```python:実行結果
# 質問
"フリーレンが勇者パーティー4人と交わした約束は？"
# 回答
フリーレンが勇者パーティー4人と交わした約束は、ヒンメルと共に魔王を討伐するために旅立つことです。また、フリーレンは定期的に魔物が湧く里の魔物を退治するためにフリーレンと約束を結んでいます。
==========
ソース:
page_content='後にフリーレンがメトーデから僧侶枠として同行しようかと聞かれた際は、「このパーティの僧侶の席は（ザインのために）空けておきたいから」と断っている。\n\n\n=== 勇者一行 ===\nヒンメル\n声 - 岡本信彦\nフリーレン、ハイター、アイゼンと共に魔王を討伐した勇者であり、種族は人間' metadata={'source': './output.txt'}
page_content='女神が授けたとされる「勇者の剣」を守護する役目があり、里近辺には定期的に魔物が湧くため、半世紀に一度は退治してもらうようフリーレンと約束を結んでいるが、実際にフリーレンが訪れたのは前回から80年後だったため、彼女に文句を零した' metadata={'source': './output.txt'}
page_content='== あらすじ ==\n魔王を倒して王都に凱旋した勇者ヒンメル、僧侶ハイター、戦士アイゼン、魔法使いフリーレンら勇者パーティー4人は、10年間もの旅路を終えて感慨にふけっていたが、1000年は軽く生きる長命種のエルフであるフリーレンにとって、その旅はきわめて短いものであった' metadata={'source': './output.txt'}
page_content='フリーレンはここの領主一族は無理難題を押しつけてくると乗り気ではなかったが、結局は魔道書を報酬に依頼を引き受け、宝剣を盗み出していた剣の魔族を討伐し、依頼を達成した。\nファス\nビーア地方の街に住んでいるドワーフ。過去に勇者一行とも出会ったことがあり、フリーレンと80年ぶりに再会する' metadata={'source': './output.txt'}
page_content='うち七崩賢3人を討ち取るが、シュラハトと相打ちで死亡したとされる。\n魔王討伐の旅立ちの前、フリーレンをパーティーに誘うも断わられた過去がある' metadata={'source': './output.txt'}
```
**結果**：×
不正解。魔王討伐と魔物退治は、4人と交わした約束ではありません


結果は変わりませんでした。どれもソースに想定箇所が選ばれていません。Wikipediaの登場人物の章は、対象の固有名詞が説明に入らない傾向が見られるので、その影響かもしれません（例：フリーレンの項目に、対象の`フリーレン`という単語が一切出てこない）

### 1.2. チャンクの長さを増やす
今度は、少しまとまった単位（原稿用紙1枚程度）でチャンクを作ってみましょう。Wikipediaの記述をみると、大体は1段落につき1主題なので、区切り文字も改行に変更します。
**パラメータ設定**
- 区切り文字 : `\n`
- チャンクの文字数 : 400 (200→400)
- 検索して取り出すチャンク数 : 3
- 回答生成のLLMモデル : gpt-3.5-turbo

**結果**
**質問 1**
```python: 実行結果
# 質問
"フランメが弟子にしたのは誰？"
# 回答
フランメの唯一の弟子はフリーレンです。
==========
ソース:
page_content='=== その他 ===\nフランメ\n声 - 田中敦子\n魔法史に登場する伝説の大魔法使いであり、人類の魔法の開祖。今ではおとぎ話の人物とも言われて存在すら疑われているが1000年ほど前に実在しており、その唯一の弟子がフリーレンとされる。また、エルフの大魔法使いゼーリエの弟子でもある。これまでに発見されている彼女の魔導書はことごとく偽物とされる。' metadata={'source': './output.txt'}
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した。\n魔力を制限した状態でさえ見た者を恐れさせるほどの絶大な魔力を放っている。かつて黄金郷のマハトを軽くあしらって子供扱いしたり、フリーレンを「歳のわりには技術の甘い魔法使い」と評し、さらにフランメを「失敗作」と切り捨てるなど、作中でも頭抜けた実力者であることを示唆する描写は多い。' metadata={'source': './output.txt'}
page_content='「俺の弟子はとんでもない戦士になる」とアイゼンに言わしめさせるほどの素質を持つ。修行の成果か素養からか尋常ならざる頑強さを持ち、フェルンから化け物かと疑われることもあるが、間違いなく純粋な人間であるため、時間の感覚はフェルンと同じ。彼女からは「シュタルク様」と呼ばれているが、落ち着かないためやめて欲しいと思っている。フェルンが「服が透けて見える魔法」で彼の下半身を見たところ、「ちっさ」との感想を漏らした。後に18歳の誕生日を迎える。好物はハンバーグ。年齢より子供っぽい所があり、女性の扱いは不得手。親しみやすく誰にでも慕われる人柄であるが、特に変な人に好かれやすい。' metadata={'source': './output.txt'}
```
**結果**：○
正解！



**質問 2**
```python:実行結果
# 質問
"フリーレンがフランメの弟子になったきっかけは何？"
# 回答
フリーレンがフランメの弟子になったきっかけは、フリーレンが9歳の頃にハイターの元を訪ねてきた際に出会ったことです。フリーレンは彼女に魔法の教えを請い、4年間の修業を経て一人前の魔法使いに成長しました。
==========
ソース:
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した。\n魔力を制限した状態でさえ見た者を恐れさせるほどの絶大な魔力を放っている。かつて黄金郷のマハトを軽くあしらって子供扱いしたり、フリーレンを「歳のわりには技術の甘い魔法使い」と評し、さらにフランメを「失敗作」と切り捨てるなど、作中でも頭抜けた実力者であることを示唆する描写は多い。' metadata={'source': './output.txt'}
page_content='現時点でフリーレン唯一の弟子である人間の少女。9歳→19歳。南側諸国の戦災孤児だったが、両親を失い絶望して飛び降り自殺を図ろうとした所を勇者パーティーの僧侶ハイターに助けられる。幼少期をハイターの元で過ごしながら、「一人で生きていける力」を得る為に魔法の修行を始める。その後、9歳の頃にハイターを訪ねてきたフリーレンと出会って彼女に魔法の教えを請い、4年間の修業を経て一人前の魔法使いに成長する。ハイターの死後、15歳の頃にフリーレンの弟子として共に旅立つ。フリーレンのことを師として尊敬し慕ってはいるものの、人間であるためエルフの彼女とは時間の感覚がズレており、長期滞在に辟易したりすることもある。また、フリーレンの散財を戒めたり、私生活がだらしのない彼女の面倒を見るうちに、自分のやっていることが完全に「お母さん」であることに気付いたりと、気苦労も多い。フリーレンに似てあまり感情を表に出さず、怒った時も静かに不機嫌になるので却ってフリーレンやシュタルクを畏れさせている。発言も結構辛辣で、特にシュタルクには容赦無い。甘いものが好きなようで、食事のシーンでは甘味を食べていることが多い。' metadata={'source': './output.txt'}
page_content='50年後、すっかり年老いたヒンメルと再会したフリーレンは、ハイターやアイゼンとも連れ立って再び流星群を観賞する。まもなくヒンメルは亡くなるが、彼の葬儀でフリーレンは自分がヒンメルについて何も知らず、知ろうともしなかったことに気付いて涙する。その悲しみに困惑したフリーレンは、人間を知るためと魔法収集のために旅に出る。\nそれから20年後、フリーレンは老い先短いハイターを訪ねる。ハイターは魔導書の解読と戦災孤児のフェルンを弟子にすることをフリーレンに依頼。その4年後、フリーレンは魔導書の解読を終え、フェルンは1人前の魔法使いとなる。ハイターの最期を看取った2人は諸国を巡る旅に出発する。' metadata={'source': './output.txt'}
```

**結果**：×
うーん。。これもフェルンがフリーレンの弟子になった時のエピソードです



**質問 3**
```python:実行結果
# 質問
"フリーレンが勇者パーティー4人と交わした約束は？"
# 回答
フリーレンが勇者パーティー4人と交わした約束は、「半世紀（エーラ）流星」を次回も一緒に見ることです。
==========
ソース:
page_content='== あらすじ ==\n魔王を倒して王都に凱旋した勇者ヒンメル、僧侶ハイター、戦士アイゼン、魔法使いフリーレンら勇者パーティー4人は、10年間もの旅路を終えて感慨にふけっていたが、1000年は軽く生きる長命種のエルフであるフリーレンにとって、その旅はきわめて短いものであった。そして、50年に一度降るという「半世紀（エーラ）流星」を見た4人は、次回もそれを見る約束を交わしてパーティーを解散する。' metadata={'source': './output.txt'}
page_content='フリーレンと初対面の際に彼女の強さを直感で見抜き、冒険の仲間としてスカウトした。魔族の本質を知らなかった為にフリーレンの忠告を無視して人を食った魔族の少女を見逃したことがあるが、後に彼女がまた人を殺めたと言う苦い経験をする。また、勇者のみが抜ける「勇者の剣」がある里を訪れた際はそれに挑戦するも、結局抜くことは出来なかった。だが、「偽物の勇者でもかまわない、魔王を倒せば偽物でも本物でも関係ない」と話して実際にそれを成し遂げ、フリーレンも彼を「あんな剣が無くてもヒンメルは本物の勇者」だと評し、剣が抜けなかったという一件は秘匿された。' metadata={'source': './output.txt'}
page_content='魔族としては穏健派で戦いを好まないが、一級魔法使いを含む精鋭魔法使いの集団を一瞬で皆殺しにするなど桁外れの圧倒的な戦闘能力を誇る。また、600年前にフリーレンを負かしたことがある。今のフリーレンでさえ匙を投げるほどの凄まじい実力を持ち、七崩賢で最強とされる。昔、自身が滅ぼしたある村の神父との出会いが切っ掛けで人類を理解したいと思うようになり、人類が持つ「悪意」や「罪悪感」と言う感情を理解したいと考え、「人類との共存」を願いながら、人類を殺戮する日々を送った。' metadata={'source': './output.txt'}
```
**結果**：○
正解です

質問1と3が正解になりました。質問1は人物紹介のフランメの項目、3はあらすじの想定箇所を含むようになったので、正解したのでしょう。


### 1.3. HyDE
次はHyDEという手法を試します。HyDEは、クエリから仮の回答を作成して、それに似たドキュメントを検索する方法です。QAにおいて、ユーザーの質問とそれに対応するチャンクの文章自体は、似ているとは言えません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290054/a7e9b7c2-edf9-7259-d161-356138a31eff.png)

この問題を解決するために、HyDEは質問から想定される仮の回答を用意して、類似するチャンクを検索します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290054/774a8383-3a60-0909-840a-9927b055ca90.png)

HyDEのサンプルコードと、呼び出しサンプルを載せたので、ご参照ください。

<details><summary>サンプルコードを表示（折りたたみ）</summary><div>

```python:hyde_params_setting.py
from langchain.chains import HypotheticalDocumentEmbedder, LLMChain
from langchain import PromptTemplate
from langchain.embeddings import OpenAIEmbeddings

from langchain.chat_models import ChatOpenAI
from langchain.chains.question_answering import load_qa_chain

# texts 作成まではベースラインと一緒のため、省略

# LLM chainの設定
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
prompt_template = """参考情報を元に、ユーザーからの質問にできるだけ正確に答えてください。
質問：{question}
回答："""
prompt = PromptTemplate(
    input_variables=["question"], 
    template=prompt_template
)
llm_chain = LLMChain(llm=llm, prompt=prompt, verbose=True)

# 質問に対する仮の文書を生成し、それを埋め込む
embeddings = HypotheticalDocumentEmbedder(
    llm_chain=llm_chain, 
    base_embeddings=OpenAIEmbeddings(), 
)
db_Hyde = FAISS.from_documents(texts, embeddings)
```
</div></details>

```python:HyDE 呼び出しサンプル
# HyDEのembeddingsを使って vector indexを作成
hyde_query = "フリーレンの異名が「葬送のフリーレン」になったきっかけは？"
docs = db_Hyde.similarity_search(hyde_query)
print(docs[0].page_content)

chain = load_qa_chain(ChatOpenAI(), chain_type="stuff")
print("----------------")
print(chain({"input_documents": docs, "question": hyde_query}, return_only_outputs=True))

# 仮の回答
ゼーリエに「臆病な坊や」と揶揄される一方、その強さは高く評価されている。また、魔法使い試験の受付会場にて一目見ただけでフリーレンの魔力制限を見抜くなど、能力の高さは一級魔法使いの中でも群を抜ている。師であるゼーリエへの想いから、歴史に名を残す為にフリーレンを襲撃した。フリーレンの防御魔法を破壊し手傷を負わせるほどの力を見せるが、フリーレンには「時間の無駄」と手合わせを拒否された。
----------------
# 回答
{'output_text': 'フリーレンの異名が「葬送のフリーレン」となったきっかけは、ヒンメルの葬儀で彼が自分がヒンメルについて何も知らず、知ろうともしなかったことに気付いて涙したことに関連しています。この出来事によって、フリーレンは人間を知るために旅に出ることを決意し、その旅の中で魔法収集も行うことになります。このきっかけから、「葬送のフリーレン」という異名が生まれたのでしょう。'}
```
質問から仮の回答を作成した後に、LLMに `質問` と `仮の回答` を渡して回答を生成しています。チャンクの文字数は長い方が結果がよいので、400文字で試してみます。

**パラメータ設定**
- 区切り文字 : `\n`
- チャンクの文字数 : 400
- 検索して取り出すチャンク数 : ー （仮の回答を作るため、なし）
- 回答生成のLLMモデル : gpt-3.5-turbo

**結果**
**質問 1**
```python: 実行結果
# 質問
"フランメが弟子にしたのは誰？"
# 仮の回答
=== その他 ===
フランメ
声 - 田中敦子
魔法史に登場する伝説の大魔法使いであり、人類の魔法の開祖。今ではおとぎ話の人物とも言われて存在すら疑われているが1000年ほど前に実在しており、その唯一の弟子がフリーレンとされる。また、エルフの大魔法使いゼーリエの弟子でもある。これまでに発見されている彼女の魔導書はことごとく偽物とされる。
==========
# 回答
{'output_text': 'フランメの唯一の弟子はフリーレンとされています。'}
```
**結果**：○
正解です！



**質問 2**
```python:実行結果
# 質問
"フリーレンがフランメの弟子になったきっかけは何？"
# 仮の回答
才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した。
魔力を制限した状態でさえ見た者を恐れさせるほどの絶大な魔力を放っている。かつて黄金郷のマハトを軽くあしらって子供扱いしたり、フリーレンを「歳のわりには技術の甘い魔法使い」と評し、さらにフランメを「失敗作」と切り捨てるなど、作中でも頭抜けた実力者であることを示唆する描写は多い。
==========
# 回答
{'output_text': 'フリーレンがフランメの弟子になったきっかけは、フリーレンが9歳の頃にハイターに出会い、彼女に魔法の教えを請い、4年間の修業を経て一人前の魔法使いに成長した後です。ハイターの死後、15歳の頃にフリーレンの弟子として共に旅立つことになりました。'}
```

**結果**：×
不正解ですね。またもやフェルンのエピソードが選ばれました



**質問 3**
```python:実行結果
# 質問
"フリーレンが勇者パーティー4人と交わした約束は？"
# 仮の回答
== あらすじ ==
魔王を倒して王都に凱旋した勇者ヒンメル、僧侶ハイター、戦士アイゼン、魔法使いフリーレンら勇者パーティー4人は、10年間もの旅路を終えて感慨にふけっていたが、1000年は軽く生きる長命種のエルフであるフリーレンにとって、その旅はきわめて短いものであった。そして、50年に一度降るという「半世紀（エーラ）流星」を見た4人は、次回もそれを見る約束を交わしてパーティーを解散する。
==========
# 回答
{'output_text': 'フリーレンと勇者パーティー4人が交わした約束は、彼らが次回の「半世紀（エーラ）流星」を見るために再び集まることです。'}
```
**結果**：○
正解しました


質問1と3は正解しました。質問2は、またも拾えていません。


### 1.4. 要約
ドキュメントの工夫の一つに、要約が挙げられます。これもHyDEと同じ着想で、質問の多くは1〜2行程度の短文ですが、ドキュメントは一定の長さを持つため、似ているとは言えません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290054/1571fe27-1c0f-d729-53cd-cf566c3156de.png)

チャンクを要約することで冗長性が減り、質問との類似度が増すことが期待されます。今回はチャンクに分けた後、一文ずつ要約する手法を採用します(下図参照)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290054/0b5f9251-47b8-f6e8-7d6a-54cf00a75da5.png)

要約のサンプルコードを載せておきます。パラメータは、チャンクの文字数を増やした時と同じ設定にします。
<details><summary>サンプルコードを表示（折りたたみ）</summary><div>

```python:extract_data.py
from langchain.chains.summarize import load_summarize_chain
from langchain.docstore.document import Document

## チャンク分割まではベースラインと同じ
# 情報検索
# テキストをチャンクに分割
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    separator = "\n", # 区切り文字
    chunk_size = 400, # チャンクの文字数
    chunk_overlap = 50, # チャンクオーバーラップの文字数
)
docs = text_splitter.split_documents(documents)


## 以下、要約の箇所
# テンプレートの定義
map_template = """Webページの一節が与えられます。セクションは三重のバックスティック(```)で囲まれています。文章には バックスラッシュn(`\n`) が含まれますが、無視してください。
あなたの目標は、セクションの内容を読者が理解できるように、要約を述べることです。内容を網羅するよう、心がけてください。
なお、文章の冗長性を減らすため、要約は文章の整理に留め、セクションの説明は避けてください。文章はできるだけ簡潔にしましょう:
```{doc}```
完全な要約:"""

# プロンプトテンプレートの作成
map_prompt_template = PromptTemplate.from_template(map_template)

# 要約チェインのロード
map_chain = load_summarize_chain(
    llm=llm, 
    chain_type='stuff',
    prompt=map_prompt_template,
    document_variable_name="doc"  # `document_variable_name` を明示的に指定
)

# 文書の要約
summary_list = []
for i, doc in enumerate(docs):
    chunk_summary = map_chain.run([doc])
    summary_list.append(chunk_summary)
    print(f"Chunk {i+1} ")


# 分割した長文を各文章ごとにDocumentオブジェクト化
docs = [Document(page_content=summary) for summary in summary_list]


## 以下、ベースラインと同じ
# embeddingモデルを定義
embs_model = OpenAIEmbeddings(model="text-embedding-ada-002")

db = FAISS.from_documents(docs, embs_model)
save_path = "./db_freelen_1"
db.save_local(save_path)
## 以下省略
```

</div></details>

**パラメータ設定**
- 区切り文字 : `\n`
- チャンクの文字数 : 400
- 検索して取り出すチャンク数 : 3
- 回答生成のLLMモデル : gpt-3.5-turbo

**結果**

**質問 1**
```python: 実行結果
# 質問
"フランメが弟子にしたのは誰？"
# 回答
フランメが弟子にしたのはフリーレンです。
==========
ソース:
page_content='フランメは、魔法史に登場する伝説の大魔法使いであり、人類の魔法の開祖です。彼女は1000年ほど前に実在しており、唯一の弟子はフリーレンです。彼女の魔導書はすべて偽物とされています。'
page_content='フェルンは戦災孤児であり、最晩年に彼女を引き取ったフリーレンによって彼女の弟子として育てられました。フリーレンはかつて自身も孤児であり、孤児院の復興資金を提供するなど、フェルンにとって重要な存在でした。ザインからも、フリーレンは優しく頼りがいのある理想的な大人と評されていました。'
page_content='1000年以上前、エルフの集落が魔族に襲われた際、主人公は大魔法使いフランメに助けられ、彼女の弟子になる。フランメからは闘いの技術や魔力制御の方法を学び、天才的な資質と長年の魔法の研鑽により、非常に強力な魔法使いとなる。彼は魔力を隠匿する技術を習得し、魔族を過小評価させて倒すことが得意である。彼は「葬送のフリーレン」として知られ、魔族から恐れられているが、敗北経験もある。'
```
**結果**：○
正解！



**質問 2**
```python:実行結果
# 質問
"フリーレンがフランメの弟子になったきっかけは何？"
# 回答
フリーレンがフェルンの弟子になったきっかけは、フェルンが戦災孤児であり、自殺を図ったところを勇者パーティーの僧侶に助けられたことです。フリーレンは彼女の過去を知り、彼女を引き取り、育てることを決めました。
==========
ソース:
page_content='このセクションは、フリーレンの唯一の弟子である人間の少女について述べています。彼女は戦災孤児であり、自殺を図ったところを勇者パーティーの僧侶に助けられました。彼女はハイターの元で幼少期を過ごし、魔法の修行を始めました。彼女はフリーレンと出会い、彼女に魔法を教えてもらい、一人前の魔法使いに成長しました。彼女はフリーレンの弟子として旅立ち、彼女を尊敬していますが、彼女との時間の感覚のズレやフリーレンの行動に悩んでいます。彼女は辛辣な発言をし、甘いものが好きです。'
page_content='フェルンは戦災孤児であり、最晩年に彼女を引き取ったフリーレンによって彼女の弟子として育てられました。フリーレンはかつて自身も孤児であり、孤児院の復興資金を提供するなど、フェルンにとって重要な存在でした。ザインからも、フリーレンは優しく頼りがいのある理想的な大人と評されていました。'
page_content='フリーレンとの戦いで、主人公はフリーレンの強さに驚き、最後は自害して死亡する。'
```

**結果**：×
また、フェルンのエピソードが（略）



**質問 3**
```python:実行結果
# 質問
"フリーレンが勇者パーティー4人と交わした約束は？"
# 回答
フリーレンが勇者パーティー4人と交わした約束は、「半世紀流星」を見るためにパーティーを解散し、次回の再会を約束することでした。
==========
ソース:
page_content='勇者パーティーの4人は、長い旅を終えて王都に凱旋しました。特にエルフの魔法使いフリーレンにとっては、旅は短いものでした。彼らは「半世紀流星」を見るためにパーティーを解散し、次回の再会を約束しました。'
page_content='フリーレンは強さを持っていると直感し、彼女を仲間に加えた。彼は魔族の本質を知らず、彼女の忠告を無視して人を食った魔族の少女を見逃してしまった。後に彼女が再び人を殺したことを知り、苦い経験をした。彼らは「勇者の剣」がある里を訪れ、挑戦したが抜くことはできなかった。しかし、フリーレンは「偽物の勇者でも魔王を倒せば関係ない」と話し、実際にそれを成し遂げた。フリーレンは彼を本物の勇者と評し、剣が抜けなかったことは秘密にされた。'
page_content='フリーレンとの戦いで、主人公はフリーレンの強さに驚き、最後は自害して死亡する。'
```
**結果**：△
エピソードは正しいですが、文章がおかしいです。約束の内容が「流星を見るためにパーティを解散し、次回の再会を約束すること」だと、意味が通りません。



質問1は正解しました。質問3の回答は、対象は合っていますが文章が少し壊れてます。ソースを見ると、要約された文がおかしくなっていることがわかります。
要約に使うLLMモデルを賢くするべきでしょうが、課金額が膨らみそうで試せませんでした（ちなみに今回の課金額は、現時点で$9.80、145円換算で1420円になります😇）。



## 改善案2. 回答生成の工夫
次に、回答生成の工夫として、賢いモデルに変えてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290054/d25fb919-fc4e-2156-c855-7f83ec9a7de7.png)

### 2.1. モデル変更
LLMのモデルを `gpt-3.5-turbo` から `gpt-4-1106-preview` に変えてみます。
**パラメータ設定**
- 区切り文字 : `\n`
- チャンクの文字数 : 400
- 検索して取り出すチャンク数 : 3
- 回答生成のLLMモデル : gpt-4-1106-preview

**結果**
**質問 1**
```python: 実行結果
# 質問
"フランメが弟子にしたのは誰？"
# 回答
フランメの唯一の弟子はフリーレンとされています。
==========
ソース:
page_content='=== その他 ===\nフランメ\n声 - 田中敦子\n魔法史に登場する伝説の大魔法使いであり、人類の魔法の開祖。今ではおとぎ話の人物とも言われて存在すら疑われているが1000年ほど前に実在しており、その唯一の弟子がフリーレンとされる。また、エルフの大魔法使いゼーリエの弟子でもある。これまでに発見されている彼女の魔導書はことごとく偽物とされる。' metadata={'source': './output.txt'}
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した。\n魔力を制限した状態でさえ見た者を恐れさせるほどの絶大な魔力を放っている。かつて黄金郷のマハトを軽くあしらって子供扱いしたり、フリーレンを「歳のわりには技術の甘い魔法使い」と評し、さらにフランメを「失敗作」と切り捨てるなど、作中でも頭抜けた実力者であることを示唆する描写は多い。' metadata={'source': './output.txt'}
page_content='「俺の弟子はとんでもない戦士になる」とアイゼンに言わしめさせるほどの素質を持つ。修行の成果か素養からか尋常ならざる頑強さを持ち、フェルンから化け物かと疑われることもあるが、間違いなく純粋な人間であるため、時間の感覚はフェルンと同じ。彼女からは「シュタルク様」と呼ばれているが、落ち着かないためやめて欲しいと思っている。フェルンが「服が透けて見える魔法」で彼の下半身を見たところ、「ちっさ」との感想を漏らした。後に18歳の誕生日を迎える。好物はハンバーグ。年齢より子供っぽい所があり、女性の扱いは不得手。親しみやすく誰にでも慕われる人柄であるが、特に変な人に好かれやすい。' metadata={'source': './output.txt'}
```
**結果**：○
ご名答！



**質問 2**
```python:実行結果
# 質問
"フリーレンがフランメの弟子になったきっかけは何？"
# 回答
フリーレンがフランメの弟子になったきっかけは、フリーレンが幼い頃にフランメに出会い、その圧倒的な魔力と魔法の技術に憧れを抱いたことにあります。フランメは才能ある者や野心ある者を好む性格であり、フリーレンはその条件に合致していたため、フランメはフリーレンを弟子として受け入れました。フリーレンはフランメの下で厳しい修業を積み、やがて人類の魔法の開祖としての地位にまで上り詰めることになるフランメの教えを受け継ぎ、一人前の魔法使いとして成長していきます。フランメの死後、フリーレンは自身の旅を続けながら、フランメから受け継いだ魔法の知識と技術を活かしていくことになります。
==========
ソース:
page_content='才ある者や野心ある者を好み、弟子達への愛情はあるが素直に気持ちを伝えられない不器用な一面もある。人類の魔法の開祖に上り詰めた弟子フランメが亡くなった際にフリーレンにエルフと人間の時間感覚の違いを説き、「千年で人間の時代がやってくる」と予言した。\n魔力を制限した状態でさえ見た者を恐れさせるほどの絶大な魔力を放っている。かつて黄金郷のマハトを軽くあしらって子供扱いしたり、フリーレンを「歳のわりには技術の甘い魔法使い」と評し、さらにフランメを「失敗作」と切り捨てるなど、作中でも頭抜けた実力者であることを示唆する描写は多い。' metadata={'source': './output.txt'}
page_content='現時点でフリーレン唯一の弟子である人間の少女。9歳→19歳。南側諸国の戦災孤児だったが、両親を失い絶望して飛び降り自殺を図ろうとした所を勇者パーティーの僧侶ハイターに助けられる。幼少期をハイターの元で過ごしながら、「一人で生きていける力」を得る為に魔法の修行を始める。その後、9歳の頃にハイターを訪ねてきたフリーレンと出会って彼女に魔法の教えを請い、4年間の修業を経て一人前の魔法使いに成長する。ハイターの死後、15歳の頃にフリーレンの弟子として共に旅立つ。フリーレンのことを師として尊敬し慕ってはいるものの、人間であるためエルフの彼女とは時間の感覚がズレており、長期滞在に辟易したりすることもある。また、フリーレンの散財を戒めたり、私生活がだらしのない彼女の面倒を見るうちに、自分のやっていることが完全に「お母さん」であることに気付いたりと、気苦労も多い。フリーレンに似てあまり感情を表に出さず、怒った時も静かに不機嫌になるので却ってフリーレンやシュタルクを畏れさせている。発言も結構辛辣で、特にシュタルクには容赦無い。甘いものが好きなようで、食事のシーンでは甘味を食べていることが多い。' metadata={'source': './output.txt'}
page_content='50年後、すっかり年老いたヒンメルと再会したフリーレンは、ハイターやアイゼンとも連れ立って再び流星群を観賞する。まもなくヒンメルは亡くなるが、彼の葬儀でフリーレンは自分がヒンメルについて何も知らず、知ろうともしなかったことに気付いて涙する。その悲しみに困惑したフリーレンは、人間を知るためと魔法収集のために旅に出る。\nそれから20年後、フリーレンは老い先短いハイターを訪ねる。ハイターは魔導書の解読と戦災孤児のフェルンを弟子にすることをフリーレンに依頼。その4年後、フリーレンは魔導書の解読を終え、フェルンは1人前の魔法使いとなる。ハイターの最期を看取った2人は諸国を巡る旅に出発する。' metadata={'source': './output.txt'}
```

**結果**：×
ブブー！不正解です。他人のエピソードをうまいこと混ぜ合わせてます。



**質問 3**
```python:実行結果
# 質問
"フリーレンが勇者パーティー4人と交わした約束は？"
# 回答
フリーレンが勇者パーティー4人と交わした約束は、50年に一度降る「半世紀（エーラ）流星」を次回も一緒に見るという約束です。
==========
ソース:
page_content='== あらすじ ==\n魔王を倒して王都に凱旋した勇者ヒンメル、僧侶ハイター、戦士アイゼン、魔法使いフリーレンら勇者パーティー4人は、10年間もの旅路を終えて感慨にふけっていたが、1000年は軽く生きる長命種のエルフであるフリーレンにとって、その旅はきわめて短いものであった。そして、50年に一度降るという「半世紀（エーラ）流星」を見た4人は、次回もそれを見る約束を交わしてパーティーを解散する。' metadata={'source': './output.txt'}
page_content='フリーレンと初対面の際に彼女の強さを直感で見抜き、冒険の仲間としてスカウトした。魔族の本質を知らなかった為にフリーレンの忠告を無視して人を食った魔族の少女を見逃したことがあるが、後に彼女がまた人を殺めたと言う苦い経験をする。また、勇者のみが抜ける「勇者の剣」がある里を訪れた際はそれに挑戦するも、結局抜くことは出来なかった。だが、「偽物の勇者でもかまわない、魔王を倒せば偽物でも本物でも関係ない」と話して実際にそれを成し遂げ、フリーレンも彼を「あんな剣が無くてもヒンメルは本物の勇者」だと評し、剣が抜けなかったという一件は秘匿された。' metadata={'source': './output.txt'}
page_content='魔族としては穏健派で戦いを好まないが、一級魔法使いを含む精鋭魔法使いの集団を一瞬で皆殺しにするなど桁外れの圧倒的な戦闘能力を誇る。また、600年前にフリーレンを負かしたことがある。今のフリーレンでさえ匙を投げるほどの凄まじい実力を持ち、七崩賢で最強とされる。昔、自身が滅ぼしたある村の神父との出会いが切っ掛けで人類を理解したいと思うようになり、人類が持つ「悪意」や「罪悪感」と言う感情を理解したいと考え、「人類との共存」を願いながら、人類を殺戮する日々を送った。' metadata={'source': './output.txt'}
```
**結果**：○
ピンポン！正解です。



質問1と3は正解しました。質問2は、一見、フリーレンとフランメの話のように見えますが、ソースはゼーリエとフェルンの人物紹介の文章で、不正確な情報です。これまでより尤もらしく見えるのは、GPT4のせいでしょうか。


## 改善案3. クエリの工夫
最後に、質問の仕方を工夫してみましょう。


### 3.1. 質問の仕方
ここまで正解がない質問2`フランメがフリーレンを弟子にしたきっかけは？`について、質問の仕方を変えてみましょう。元の文章が
>故郷であるエルフの集落が魔族に襲われ死にかけた際に大魔法使いフランメに助けられ、彼女の弟子になる。

なので、Wikipediaの言葉を使って、

>大魔法使いフランメがフリーレンを弟子にしたきっかけは？

に変えてみます（以降、この質問は **質問2'**, **Q2'** と呼びます）。

**結果**
複数の案に試したところ、ベースライン、HyDE、モデル変更で正解になりました。
| 項目             |結果 |内容                                            | 
|:----------------|:----|:----------------------------------------------|
|0. ベースライン |○| 大魔法使いフランメがフリーレンを弟子にしたきっかけは、フランメの故郷であるエルフの集落が魔族に襲われ、死にかけていたときに助けられたことです...(省略)...|
|1.2. チャンク数変更     |×| 大魔法使いフランメがフリーレンを弟子にしたきっかけは、フリーレンが幼少期から魔法の研鑽を積み、彼女に出会ったばかりの時点で既に卓越した魔力の...(省略)...|
|1.3. 要約|×|大魔法使いフランメがフリーレンを弟子にしたきっかけは、フリーレンが戦災孤児であり、自殺を図ったところを勇者パーティーの僧侶に助けられたことです...(省略)...|
|1.4. HyDE|○|フランメがフリーレンを弟子にしたきっかけは、フランメがエルフの集落を魔族の襲撃から救った際に、フリーレンが助けられたことです...(省略)...|
|2.1. モデル変更|○|大魔法使いフランメがフリーレンを弟子にしたきっかけは、フリーレンが故郷であるエルフの集落が魔族に襲われ死にかけた際にフランメに助けられたことです...(省略)...|

# 今回できなかったこと
今回、時間 & お金の都合で、やりたいことがいくつかできませんでした。
## 1. 要約の工夫(モデル変更など)
要約の章でも述べましたが、要約に使うLLMのグレードアップを試せませんでした。gpt-3.5-turboで一文ずつ要約させたら、処理に10分ほどかかってしまったので、グレードアップした時の課金額を想像して、ビビってしまったのが正直なところです。
要約はデータの前処理などにも工夫の余地がありそうで、もう少し調査をしたいところです。

## 2. ベクトル検索の工夫
ベクトル検索の工夫は、時間と勉強が足りなくて手をつけることができませんでした。
ざっと調べただけでも、採用するライブラリの検討やモデルのfine-tune、Re-rankモデルの活用など、いろいろと改善の余地がありそうです。

## 3. 埋め込みモデルの工夫
ドキュメントの埋め込みに使うモデルをより性能の良いものにすると、精度が上がるかもしれません。

## 4. 評価方法の検討
今回は単純に正答数を数えただけですが、本格的に取り組む場合は、評価指標の検討が必要です。今回は時間の都合で、そこまで手が回りませんでした。

# まとめ
質問応答の結果をまとめると、以下のようになります（Q1〜Q3の質問内容は[こちら](#質問)、Q2'は[こちら](#31-質問の仕方)を参照）。
|項目                |Q1|Q2|Q3|Q2'|チャンク文字数,区切り文字, <br> 類似チャンク数,モデル|
|:-------------------|:---|:---|:---|:----|:------------------------|
|0. [ベースライン](#ベースライン)| ×  | ×   | ×  | ○  |200, 。, k=3, gpt-3.5-turbo  |
|1.1. [類似チャンク数](#11-検索して取り出すチャンク数類似チャンク数)| ×  | ×   | ×  | ×  |200, 。, k=5, gpt-3.5-turbo|
|1.2. [チャンク文字数](#12-チャンクの長さを増やす)  | ○  | ×   | ○  | ×  |400, \n, k=3, gpt-3.5-turbo  |
|1.3. [HyDE](#13-hyde)| ○  | ×    | ○  | ○  |400, \n, なし, gpt-3.5-turbo  |
|1.4. [要約](#14-要約) | ○  | ×   | △  | ×  |400, \n, k=3, gpt-3.5-turbo  |
|2.1. [モデル変更](#21-モデル変更)| ○ | ×    | ○  | ○  |400, \n, k=3, gpt-4-1106-preview|

いずれも全問正解にはなりませんでした。最も成績がよかったのはHyDEとモデル変更で、Q2'で質問の仕方を変えることで正解になりました。今回の実験でわかったことをまとめると、以下3点になります。
- チャンクは、知識がまとまっている単位で分割するのがよい
    - 今回は400字程度、段落区切りが Good!!
- 質問とチャンクの類似度をあげる工夫が効果的
    - 検索に仮の回答を渡す [HyDE](#13-hyde) は効果がありそう🙆‍♀️
    - [今回試せなかった](#今回できなかったこと) ベクトル検索や要約の工夫などに改善の余地が大きそう
- 回答生成のLLMの影響も大きい


RAGにおいては、質問とチャンクの類似度を上げる工夫が要だとわかりました。
また、RAGのフレームワークは応用が効きそうで、LLMの働きを工夫すればレコメンドにも有用ではないかと思っています。今後の発展が楽しみな技術です。

# 宣伝
:::note info
**最後に宣伝です。**

Supership ではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
[Supership 採用サイト](https://supership.jp/recruit/)
是非ともよろしくお願いします。
:::

# 参考
- [RAGにおけるドキュメント検索精度向上について(概要編)
](https://zenn.dev/sompojapan_dx/articles/eb755a18e893ce)
- [RAG における埋め込みモデルの比較](https://note.com/alexweberk/n/ncccfdab3f4bb)
- [ベクトル検索で欲しい情報が得られないときの問題点と改良方法を考えてみた](https://dev.classmethod.jp/articles/problem-and-improve-methods-of-vector-search/)
- [Retrieval-based LM (RAG system) ざっくり理解する](https://speakerdeck.com/smiyawaki0820/retrieval-based-lm-rag-system-zatukurili-jie-suru)
- [LangChain への OpenAIのRAG戦略の適用](https://note.com/npaka/n/n62cd25213679)
- [LangChainから学ぶLLMを使ったアプリケーションの工夫](https://jobs.layerx.co.jp/4e73a991e0b449d2b8739c50b24ca8f0)
- [LangChainを使ってHyDEによるクエリ変換の効果を検証する](https://hironsan.hatenablog.com/entry/information-retrieval-with-hyde)
- [LangChain API reference(公式ドキュメント)](https://api.python.langchain.com/en/latest/api_reference.html)
- [HyDE公式サンプルコード](https://github.com/langchain-ai/langchain/blob/master/cookbook/hypothetical_document_embeddings.ipynb)
- [ベクトル検索ライブラリ Faiss を試す](https://note.com/npaka/n/nb766e344a4fc)
- [最近話題のVector Searchを実現するFaissって何？ #1](https://note.com/masuidrive/n/n5dc6da6dd2b6)
- [LangChainでCognitive SearchのベクトルDBと連携させたRAGを構築する](https://qiita.com/tmiyata25/items/bfb7f4f5f22ec659c791)
- [LangChainで HyDE による質問応答を試す](https://note.com/npaka/n/n0be34a51395d)