💨

【LangChain】RAGとchain周りを理解する

2024/02/23に公開

以前の記事で、RAGで回答を生成していましたが、公式チュートリアルの通りに書けば動いたというだけで、あまり書き方は理解しきれていませんでした。
https://python.langchain.com/docs/expression_language/cookbook/retrieval

もう少しRAGやchainの書き方の理解を深めるべく、「今週のヒット曲を答えさせる」というプログラムを書いて学習しました。

※ 個人的なワガママですが、技術系の本では、アリスやらボブやら外人の名前用いられたり、マスタングなど高級車の名前が用いられることが多いので、これらに疎い私には途中で誰が誰だったかわからなくなってくるので、もっと覚えやすい例を使ってくれたらいいのにと思ってます

やりたいこと

chatGPTもそうですが、openAIは基本的に事前に学習した内容からしか回答することしかできません。
なので、「今週のヒット曲一位を教えてください」と聞いても、回答してくれません。
(そもそもヒット曲といっても、メディアやジャンルによって結果は変わってきます。)

今回はユーザーがヒット曲を尋ねる際に、大阪のローカルラジオ局であるFM802のHOT100を取ってきて、そのランキングから回答させるようにします。
(全国的にはCDTVなどのほうが有名でしょうが、CDTVは一時期AKBばかりが上位に来ていたこともあり、コレジャナイ感が半端ないです)

ちなみに執筆時点ではcreepy nutsのBling-Bang-Bang-Bornが1位でした
https://www.youtube.com/watch?v=210R0ozmLwg

LCELのchainを理解する

引用で恐縮ですが、こちらの説明が世界一わかりやすいです。
https://secon.dev/entry/2024/01/11/100000/

これ以上の説明ができる気がしませんので割愛します。

LCELでRAGを実装

データを渡さないで回答させる

chainを作成するにあたり、最小構成はpromptとmodelだけです。

from langchain.prompts import PromptTemplate

# promptの作成
prompt = PromptTemplate.from_template("""
質問に答えてください

質問: {question}""")

# modelの作成
model = ChatOpenAI()

# chainの作成
chain = prompt | model

chain.invokeで、これらを実行することができます。

chain.invoke({'question':'あいみょんとは誰ですか'})

#-> AIMessage(content='あいみょんは日本のシンガーソングライターであり、女性アーティストです。彼女は独自の歌詞やメロディーで人気を集めており、幅広い年齢層から支持を受けています。彼女の楽曲は感情豊かであり、多くの人々に共感を呼び起こしています。')

回答のAIMessageが気になるなら、chainにStrOutputParser()を渡せば消すことができます

chain = prompt | model | StrOutputParser()

さて、この質問に「今週の1位は?」と聞いても答えてくれません。まあ、当たり前ですね。

chain.invoke({'question':'今週の1位は?'})

#=> '申し訳ありませんが、私は情報を更新する能力を持っていないため、今週の1位についてはわかりません。お手数ですが、他の情報源をご確認いただくか、別の質問があればお知らせください。何かお手伝いできることがあればお知らせください。'

そこで、音楽チャートについて聞いていることをpropmtで明らかにさせます

prompt = PromptTemplate.from_template("""
質問に対して、音楽チャートのランキングを回答してください

質問: {question}""")

この状態なら、音楽チャートの一位を答えようとしてくれますが、残念ながら以下のような回答となります

chain.invoke({'question':'今週の1位は?'})

#=> '申し訳ありませんが、私は情報を更新する能力を持っていないため、今週の1位についてはわかりません。お手数ですが、他の情報源をご確認いただくか、別の質問があればお知らせください。何かお手伝いできることがあればお知らせください。'

データを渡して回答させる

音楽チャートのデータを渡してあげましょう。
今回はwebから情報を取得するので、WebBaseLoaderを使います。

# FM802から今週のランキングを取得
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://funky802.com/hot100/")

documents = loader.load()

ここで得られるdocumentsは、Documentオブジェクトの配列です。Documentとは、LangChainで文を処理するためのオブジェクトです。

もし得られたDocumentのサイズが大きい場合はテキスト分割する必要がありますが、今回はしなくても後の処理ができました

次に、retrieverを作成します。(retrieverとは自然言語をベクトル化したものと捉えてください)

# retrieverの作成
embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
vector = FAISS.from_documents(documents, embeddings)
retriever = vector.as_retriever()

さて、データの準備ができましたので、このデータをもとに回答するようにpromptを書いてやります。
このように渡すデータを習慣的にcontextと呼びます。

# promptの作成
template = """
質問に対して、音楽チャートのランキングを回答してください
音楽チャートはcontextを参考にしてください:{context}
質問: {question}
"""
prompt = ChatPromptTemplate.from_template(template)

chainは以下のようになります。
先ほど作成したpromptには、2つの変数:contextとquestionを渡す必要があります。
contextはFM802のサイトから引っ張ってきたデータをベクトル化したもの(retriever)、
questionは、chain.invoke()で渡されるテキストをそのまま渡せばよいので、RunnablePassthrough()を設定します。

# chainを作成
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

以上をまとめると、コードは以下のようになります。

# FM802から今週のランキングを取得
loader = WebBaseLoader("https://funky802.com/hot100/")

documents = loader.load()

# retrieverの作成
embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
vector = FAISS.from_documents(documents, embeddings)
retriever = vector.as_retriever()

# promptの作成
template = """
contextに従って回答してください:{context}
質問: {question}
"""
prompt = ChatPromptTemplate.from_template(template)

# modelの作成
model = ChatOpenAI()

# chainを作成
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

実行すると、結果が得られるはずです。

chain.invoke('今週の一位は?')

#=> 今週の一位はCreepy Nutsの「Bling-Bang-Bang-Born」です。

いろいろ試してみましょう。

chain.invoke("あいみょんの曲は何位ですか?")

#-> あいみょんの曲「リズム64」は3位です。

# templateをいじると、答え方が変わる
template = """
質問に対して、音楽チャートのランキングを回答してください。
音楽チャートはcontextを参考にしてください:{context}

また、回答に対して、そのアーティストの情報を付け加えてください。

質問: {question}
"""

chain.invoke('今週の一位は?')

#->
今週の一位はCreepy Nutsの「Bling-Bang-Bang-Born」です。

Creepy Nuts(クリーピーナッツ)は、日本のヒップホップユニットで、メンバーはDJ松永とR-指定です。彼らは独自の音楽スタイルとユニークなライブパフォーマンスで知られており、日本国内外で活動しています。Creepy Nutsの楽曲は多様な要素を取り入れた斬新なサウンドで、幅広いリスナーから支持を集めています。

Discussion