RAGを用いてSake Diploma対策を効率化する

（これは [Livesense Advent Calendar 2024](https://adventar.org/calendars/10408) DAY 22 の記事です。）

## はじめに
リブセンスの転職ドラフト事業部で、マーケティングやプロダクトマネジメントを担っている中垣です。
3年前にリブセンスを退職したのですが、スタートアップやフリーランスなどを転々としたのち、**昨年(2023年) リブセンスに出戻りしました。**

<br>

そんなことはどうでもよくて、大学時代から**日本酒が大好きです**。酒好きが高じて、明治~昭和期の酒に関する古本を集めることにハマっています。最近は酒を飲むより、酒を読むほうが多いかもしれない。

## 試験対策を効率化したい!
昨年、日本酒のソムリエ資格である **J.S.A. Sake Diploma** を取得しました。

Sake Diplomaの試験には、1次と2次があります。

- **1次試験：選択式問題（CBT方式）**
- **2次試験：テイスティングと論述問題（2~3問, 各200字以内）**

特に、2次試験の論述問題が手強く、ニッチなお題も出題されますが、ちゃんと得点を稼ぎたいところです。


当時はフリーランスで、時間的余裕はたくさんあったので、**学習ノートを丁寧に作成していました**。いま読み返してもなかなか良いコンテンツだと思いますので、ぜひ参考にしてください（もちろん無料です!）。

https://note.com/sake_diploma_23/m/ma32f86659037

サラリーマンになった今ならば、もっと効率的に試験対策したい...
質問したら模範回答が返ってくるchatbotとかないかな...

ということで、**生成AIを活用して効率化しよう!** と思ったのですが...

## ChatGPTは、嘘つきがち。

いわゆる **ハルシネーション** です。模範回答に嘘があってはなりません。
まず試しに、ChatGPTに過去問を解いてもらいました。モデルは `GPT-4o` です。


**山廃‧生酛の現状と将来の展望について、200字以内で述べよ。（2017年 過去問）**


<br>

さて、返ってきた答えは...

![スクリーンショット 2024-12-14 15.38.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214391/77715f2a-ee27-7730-19d3-3952a9976147.png)

:::note info
山廃・生酛は、伝統的な製法による複雑な風味と深みが評価され、近年再評価が進んでいます。熟成による独特の旨味や酸味が国内外で注目を集め、特に自然派志向の消費者に支持されています。しかし、製造に手間がかかるため効率化が課題であり、将来的には技術革新により、生産の安定化とコスト削減が求められるでしょう。また、発酵に関わる微生物の多様性を生かした新しい味わいの創出も期待されています。
:::


<br>

さすが、ChatGPT。
**何も知らない人よりは、それっぽい回答が返ってきます。**

<br>

ただ、やはり懸念していた **ハルシネーション** が起きています。

- **生酛‧山廃は、たしかに熟成との相性が良いが、「生酛‧山廃 = 熟成」という誤解がある**
- **自然派志向の消費者の支持は、わからんでもないが、だいぶ根拠が怪しい**
- **技術革新はすでに起きており、1910年に速醸が発明されているが、言及されていない**

また、回答として **外せないポイント** がいくつも抜け落ちています。

- **山廃・生酛についての説明（乳酸添加をしていないこと）**
- **現状のシェア（速醸90%、山廃9%、生酛1%）**
- **速醸の発明（1910年）**
- **ペアリングの相性（中華や洋食）**

<br>

ネット上に転がっている情報は限定的で、かつ誤情報も多いので、もっと **試験対策のための知識** を学習してもらう必要がありそうです。


そこで、**RAGを用いて、より回答の精度を高めてみようと思います**。

## RAGってなんだ
RAG（Retrieval-Augumented Generation）とは、大規模言語モデル（LLM）に不足している知識を補うために、外部情報を検索して、その情報を元に回答を生成させる技術のことです。

つまり、

- **地頭 = LLM**
- **知識 = 外部データソース**

このように組み合わせることで、

- **自然な日本語で回答を生成できるLLMが、より精度の高い回答を行うために、外部データを知識として活用できる**

という、まさに **鬼に金棒** なのです。

<br>

ちょうど日本酒の勉強会に参加していた際にRAGの存在を知り、これはめちゃくちゃ役に立ちそう! と思ったので、実装してみることにしました。


## さぁやっていくぞ

RAGの実装のために、以下の記事を参考にさせていただきました（ありがとうございました!）。

1. Pythonの環境構築
https://zenn.dev/umi_mori/books/python-programming/viewer/python-setup-macos

2. LangChainを用いずにRAG実装
https://zenn.dev/spiralai/articles/8af7cbf526c2e1

3. LangChainを用いたRAG実装
https://python.langchain.com/docs/tutorials/rag/

<br>
この順番にならって、解説をしていきます。


### 1. Pythonの環境構築
ここは省略します。Homebrew経由でPythonを入れました。
Pythonのバージョンは `3.11.1` です。

非エンジニアなので、こういう地味なところでつまづきがち。

### 2. LangChainを用いずにRAG実装
少し調べてみると、`LangChain` という便利なライブラリを使うと、簡単にRAGが実装できるということがわかりました。


**でも、それだとRAGの仕組みが理解ができないのでは...!**


まずは素手で戦ってみよう。

ということで、LangChainを用いずにRAGを実装してみます。

<br>
まずはOpenAIに課金して、API Keyを取得。

![スクリーンショット 2024-12-14 15.40.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214391/7d37f30f-cac6-cee3-4ba3-4591cfa59fd5.png)


~~課金って気持ちいいいいいいいいいいい~~


<br>

さて、RAGの処理のプロセスは以下になります。

:::note info
1. データをベクトル化し、元テキストと共にjsonファイルとして保存(疑似DB)
2. ユーザークエリを受け取ったら、それをベクトル化
3. 疑似DBからデータを読み取り、ベクトル近傍検索を行う
4. 検索結果をプロンプトに埋め込みGPTに投げる
5. 得た回答を表示する
(引用元：https://zenn.dev/spiralai/articles/8af7cbf526c2e1 )
:::

これを自分なりに翻訳してみると...

1. まずは、外部知識を使いやすい状態にするために、**ベクトル化** を行う。これによって、文字情報はベクトルデータで表され、**言葉間の意味の類似度が計算しやすくなる**。それを、元テキストと共にjsonファイルとして保存する。
2. ユーザーが文字で質問を投げた際にも、そのままだと扱いにくいので、ベクトル化する。これによって、**外部知識と質問の類似度を計算できる**ようになった。
3. 外部知識の中で、質問と類似しているものを検索する（**ベクトル近傍検索**）。意味の類似度を計算して、「**この質問に類似する知識はこれだ!**」という外部知識をいくつか抽出する。ちなみに、この計算には **コサイン類似度** が用いられるらしい。三角関数ってこんなところで活用されているのか...
4. 抽出した外部知識を **プロンプトに付与** してあげて、地頭であるLLM（ここではGPT）のAPIに投げる
5. APIから返ってきた回答を表示する

<br>

これで、おおむねの仕組みは理解できました。

外部知識にはいったん、自分が作成した模範回答を5つほど差し込んでみます。
本問題とは関係ない回答も含めてみます。

```json:外部知識
    texts = [
        "山廃・生酛は、醸造用乳酸を添加せずに乳酸菌を増殖させる伝統的な醸造方法である。1910年に速醸酛が開発されたことで、手間のかかる生酛や山廃は少数派になり、現状のシェアは速醸90%、山廃9%、生酛1%である。しかし、豊かで複雑みのある香味を求めて、生酛系酒母に取り組む蔵が目立ってきている。和食だけでなく、中華や洋食との相性も良いため、幅広いペアリングの対象として好まれる可能性がある。",
        "セルレニン耐性酵母は、リンゴ様の香り・カプロン酸エチルを多く生成する酵母で、「香り酵母」とも呼ばれる。1990年代中期に全国に広まった。代表的な酵母として、アルプス酵母やきょうかい1801号が挙げられる。香り酵母は全国各地で開発されており、地域色を打ち出したものも増えている。フルーティーな香りは日本酒初心者にも好まれ、新たな消費者が日本酒を飲むきっかけとなる可能性がある。",
        "村米制度は、酒造家と農家が直接契約して酒米を栽培する制度であり、「山田錦」の故郷である兵庫県では明治20年代から行われていた。農家は酒造家が好む酒米を生産するために品質向上を図る。テロワールによる集落ごとの格付けも行われ、集落内外での競争が活発化した。現在は「特A-a地区」と「特A-b地区」に分けられ、「特A-a地区」は吉川町、口吉川町、東条、社の91集落で構成されている。",
        "美山錦は、1978年に長野県農事試験場で「たかね錦」の種籾にγ線を照射して生み出された突然変異種の酒米である。醸造用玄米の中では「山田錦」「五百万石」に次ぎ生産量第３位を誇る。大粒で心白発現率が良いため、高精白が可能である。また耐冷性があるため、長野のほか東北地方が主な産地となっている。「亀ノ尾」など歴史ある品種を先祖にもち、「出羽燦々」「越の雫」「秋の精」など他県が開発した品種の親株でもある。",
        "奈良県は清酒発祥の地とされている。日本最古の神社・大神神社は酒造りの神で、杉玉の発祥の地でもある。奈良時代には造酒司が設けられ、酒造りの中心地となった。室町時代には酒母製法の一つである「菩提酛」が菩提山正暦寺で生み出された。菩提酛は「そやし水」と呼ばれる乳酸酸性水を使用して酒母を作る製法で、近年奈良県内の蔵元が再現している。酒米は自県産より他県からの移入が多いが、「露葉風」の生産量は日本一である。",
    ]
```


なお、実装したコードは元記事と全く同じなので割愛します（ありがとうございました!）
[https://zenn.dev/spiralai/articles/8af7cbf526c2e1](https://zenn.dev/spiralai/articles/8af7cbf526c2e1)

### 実行！

![496d49f8-7e4c-9a8c-b0ff-f3964758baae.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214391/415e87b7-60b6-a2f0-099f-c043d2ee5a48.png)

:::note info
【AIの返答】
現在、山廃と生酛は速醸に比べてシェアは低くなっていますが、豊かで複雑な香りを求める消費者が増えており、生酛系酒母に取り組む蔵も増えています。将来的には、より洗練された味わいや香りを楽しみたい消費者が増える可能性があり、山廃や生酛の需要が拡大する可能性があります。地域色を打ち出した香り酵母の開発や新たな消費者層の開拓により、山廃や生酛が注目されることが期待されています。
:::

**できました！**

かなり良くなっています... まぁそりゃそうで、模範回答をそのまま突っ込んでますからね。

これで、
- **ベクトル近傍検索によって、質問と類似する外部知識をプロンプトに付与する**
- **その知識を用いて、LLMが自然な日本語で回答を行う**

というRAGの基本処理を理解することができました。

ちなみに、今回は類似度上位2つ（ `topk=2` ）の知識をプロンプトに付与するように指示したため、**本問題とは関係ない「セルレニン耐性酵母について」の模範回答もプロンプトに埋め込まれました。**

その知識に惑わされた結果、「洗練された味わいや香り」や「地域色を打ち出した香り酵母」など、**本問題には不適切なキーワードも回答に含まれてしまっています**。

<br>
さて、ここまでの学びをまとめてみます。

- **RAGを素手で書いたことで、すごく学びが多かった**
    - おそらく、LangChainを使えばよしなにやってくれちゃうはず。
    - 順を追って書いたことで、RAGの処理プロセスを理解することができた。
    - 学習においては、車輪の再発明によって追体験することは大事だと思う。
- **外部知識の精度が、そのままアウトプットの精度に影響する模様**
    - RAGを活用するにしても、結局は外部知識の質とその与え方が大事なのだろうと思った。
- **RAGすごくたのしい**
    - ~~三角関数がこうやって役に立つってことを高校時代に知っていたら、もっと人生バラ色になっていたのかもしれない。~~

### 3. LangChainを用いたRAG実装
RAGの処理プロセスをなんとなく理解したので、ついに `LangChain` を使ってみます。

LangChainとは、LLMを用いたアプリケーション開発を効率的に行うためのライブラリで、要は **RAGなどを実装する上で必要なものを、すでに色々用意してくれている便利ツール** です。

色々記事を調べては試してみましたが、結局、**公式チュートリアル** が一番わかりやすかったです。以下、チュートリアルを参考に実装しました。


```python:main.py
import bs4
from langchain import hub
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_openai import ChatOpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.schema import Document

llm = ChatOpenAI(model="gpt-4o")

# 自分のnoteを参照する
loader_note = WebBaseLoader(
    web_paths=(
        "https://note.com/sake_diploma_23/n/n55a458e78273",
    ),
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("note-common-styles__textnote-body")
        )
    ),
)

docs = loader_note.load()

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())

# Retrieve and generate using the relevant snippets of the blog.
retriever = vectorstore.as_retriever()
prompt = hub.pull("rlm/rag-prompt")

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

print(rag_chain.invoke("山廃‧生酛の現状と将来の展望について、200字以内で述べよ。"))

# cleanup
vectorstore.delete_collection()
```
また、parseまわりの実装は以下のサイトを参考にしました（ありがとうございました!）
[https://kondou.com/BS4/](https://kondou.com/BS4/)

### 実行！

![スクリーンショット 2024-12-14 16.45.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214391/1b859f2c-57d6-cff1-dd3a-afbf6398d1f2.png)

:::note info
山廃・生酛は、伝統的な醸造法であり、現状では速醸が約90%を占め、山廃が約9%、生酛が約1%となっています。しかし、豊かな香味を求める動きが増え、特に中華や洋食との相性の良さから、生酛系酒母の人気が再評価されています。今後も若い造り手が山廃・生酛を選ぶ傾向が強まると予想されます。
:::

**できました！**

文句なしの満点ですね。まぁそりゃそうで、**模範回答を記載した僕のnoteをそのまま知識として載せてますからね。。** 回答の精度がさっきより高いのは、外部知識としてのnoteを1つしか渡してないことに起因していると思います。

なにより、**実装がシンプル**。外部知識のベクトル化、質問のベクトル化、ベクトル近傍検索、プロンプトに埋め込んでGPTへの問い合わせ... これらの複雑な処理を、**コード数行書くだけ**で実行できてしまいます。**恐るべし、LangChain。**

<br>
さて、ここまでの学びをまとめてみます。

- **LangChainやばい**
    - LangChainを使わずに素手で戦った際は、4ファイルに分割して処理を書いたのに、LangChainを使ったら数行になった。
- **わからないことは、いったんChatGPTにそのまま質問する**
    - `ModuleNotFoundError: hogehoge` など、コピペしてChatGPTに質問したら、全部解決しました。恐るべし、ChatGPT。



## これにて終了!
あれ、当初の目的だった **試験対策の効率化** までは全然到達してないのでは？と思ったあなた。おっしゃる通りです。

あとは **外部知識のDB化** と、**精度・パフォーマンス改善** とかやったらいいと思うんですが、ちょっと疲れちゃったので、ここまでにします。**十分楽しかったし(?)**。

- **外部知識のDB化**
    - AzureのAI Search、AWSのKendra, Bedrock Knowledge Bases など
- **精度・パフォーマンス改善**
    - ReRanking、HyDEなど

年が明けたら、その時の自分がきっとやってくれるでしょう...。

<br>
ということで、以上です！ 最後まで読んでいただき、ありがとうございました。