ABCABC Tech Catalog
研究開発

このブログの内容を踏まえた回答をLLMから取得するスクリプトを作る

LLMにこのブログの内容を聞くための実装手順

実際にLLMと自前データを組み合わせて活用したい

前回、「自前のデータを踏まえた回答をLLMから得るには」の記事で、自前のデータを踏まえてLLMに問い合わせを行う方法論をまとめました。

結論から言うと、 ベクターストアに情報を溜めて、まずは類似度検索(similarity search)を行い、出てきた類似情報と質問をLLMにまとめて渡してそれっぽい回答を得れば良い 、というものでした。(詳細は記事を読んで下さい…というか、中身ほぼこの文章で全てですが…)

しかし、人間は方法論を学ぶだけでは成長できません。やはり 実践して初めて圧倒的成長😤を得ることが出来る というのはかつて石器を作っていた頃からの常識です。

というわけで、今回は実践編です。LangChain、Python、OpenAIのEmbeddings APIおよびLLMを活用し、実際に自前データの回答を得るまでをまとめます。

自前データ…公開することを考えるとこれは難しいな、と思っていましたが、冷静に考えると このブログはMarkdownで全ての記事を書いている のでした。しかも公開しています。(「NotionをヘッドレスCMSとして使ってNext.jsでTechブログを作った」の記事シリーズ参照)

なので、 めっちゃ自前データとして使い勝手いい やん、ということに気付いたので、このブログのデータを踏まえての回答を得られるかを試してみます。

実際にどういった質問に対してどういった答えが得られるか?はまた別記事にまとめていくとして、今回はどう実装するか?に焦点をあてて記事化します。

自前データをベクターストアにロードする

Markdownデータのロード

今回は、 ./data/posts に全ての記事のMarkdownファイルが入っている想定とします。

このように複数のファイルが特定のディレクトリに格納されている場合は、LangChainのDirectoryLoaderがとても便利に使えます。

from langchain.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader

loader = DirectoryLoader("./data/posts", glob="*.md", loader_cls=UnstructuredMarkdownLoader)
documents = loader.load_and_split()

こんな感じですね。

ちなみにこのLoaderの中で必要なライブラリについては内包されておらず別途インストールが必要です。

何が必要になるかは走らせて都度確認したほうが確実です。

たとえば、Markdownの場合は、 unstructuredmarkdown がないと怒られるので、 pip install なり poetry add なりします。

あと、本筋と関係ないのであまり触れませんが、MarkdownのLoaderに関しては、絵文字が入っていると派手に文字化けするので気をつけて下さい。

この罠にハマってまあまあ苦戦しました… 除去するスクリプトを別途用意しました。

Embeddings APIの実行とベクターストアの保存

先ほどloadしたdocumentsを活用して、ベクターストアへのロードを行います。ここでは、ベクターストアとしてMeta社製のFAISSを使います。(だいたいChromaかFAISSを使うことが多いですが、正直なところどちらがどう良いかの検証まで出来てはいません)

import openai
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS

faiss_db = FAISS.from_documents(documents, OpenAIEmbeddings(client=openai.ChatCompletion))
faiss_db.save_local("./db")

これだけです。この時点で ./db フォルダ内に保存もできてしまいます。

保存しておくことで、何度も同じデータに対してEmbeddingsを走らせる必要がなくなるわけですね。

ローカルに保存したベクターストアを読み込むのはとても簡単で、

faiss_db = FAISS.load_local("./db", embeddings=OpenAIEmbeddings(client=openai.ChatCompletion))

とするだけです。

今回は簡略化して、 ./db が存在しなければ新しくEmbeddings APIを活用してベクターストアへのロードおよびローカルへの保存をするように実装します。

まとめるとこうですね。

def load_md_vector_store() -> FAISS:
    """Markdownファイルからベクターストアを作成する

    Returns:
        FAISS: ベクターストア
    """
    if not os.path.exists("./db"):
        loader = DirectoryLoader("./data/posts", glob="*.md", loader_cls=UnstructuredMarkdownLoader)
        documents = loader.load_and_split()
        faiss_db = FAISS.from_documents(documents, OpenAIEmbeddings(client=openai.ChatCompletion))
        faiss_db.save_local("./db")
    else:
        faiss_db = FAISS.load_local("./db", embeddings=OpenAIEmbeddings(client=openai.ChatCompletion))
    return faiss_db

ベクターストアを活用しながらLLMに質問を投げる

ベクターストアが準備できたら、質問を投げる部分を準備します。

とはいっても、

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI

# OpenAPIのKey設定
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")

# モデル設定
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-3.5-turbo-16k-0613")
MODEL_TEMPERATURE = float(os.getenv("MODEL_TEMPERATURE", 0.9))

model = ChatOpenAI(model=MODEL_NAME, temperature=MODEL_TEMPERATURE, client=openai.ChatCompletion)

def run_query_with_index(db: VectorStore, question: str) -> str:
    """ベクターストアの内容を活用しながらLLMに質問を投げる

    Args:
        db (VectorStore): ベクターストア
        question (str): 質問本文

    Returns:
        str: 回答
    """
    qa = RetrievalQA.from_chain_type(llm=model, chain_type="stuff", retriever=db.as_retriever())
    res = qa.run(question)
    return res

これだけです。

RetrievalQA でベクターストアをretriverとして読ませてあげればあとはLangChain側で良いようにしてくれる、ようです。

今回は実験的にやっているのでこのままでいってしまおうと思いますが、ちゃんとチューニングしたければもう少し掘ってプロンプトを確認したほうがいいでしょう。

ちなみにこの RetrievalQAchain_type="stuff" というのは全部一気にAPIに投げてしまえ的な設定値なので、ドキュメントの量によってはtokenの上限に達してしまいます。

今回は言うても知れているので、token上限がちょっと緩い gpt-3.5-turbo-16k-0613 を使ってパワープレイに持ち込んでいますが、実際は適切な chain_type を当ててやる必要があるシーンもあると思います。(また記事化できそうなネタ)

実行してみる

というわけで、実験の準備が整ったので、早速実行してみます。

インターンの詳細を聞いてみる

今夏実施予定のインターンの記事を上げていたので、とりあえずインターンの詳細を聞いてみます。

if __name__ == "__main__":
    question = "ABCGHDの2023年夏のインターンシップの詳細を教えて下さい"
    print(f"Q: {question}")

    faiss_db = load_md_vector_store()
    response = run_query_with_index(faiss_db, question)
    print(f"A: {response}")

出力はこうなりました。

Q: ABCGHDの2023年夏のインターンシップの詳細を教えて下さい
A: ABCGHDでは、2023年の夏に長期のインターンシップを実施します。以下に詳細をまとめます。

期間: 8月末〜9月の3〜4週間

対象: 学部3年生/4年生・大学院に在学中の学生の方

業務内容: データ基盤の整備・構築に関する業務(データエンジニアの業務)

必須条件: Pythonの基本を学んでいること、SQLの初歩的な知識(SELECTやJOINなど)を持っていること

募集要項: ABCGHD公式サイトの募集要項ページをご確認ください(締切は2023年6月30日)

参加者への提供: 新しい知識や技術の習得、退屈な作業ではなく興味深い業務の経験、スタジオ見学などの特典

応募方法: ABCGHD公式サイトの応募用のGoogleフォームから応募し、技術課題に回答する方法での応募となります(技術課題の回答期限は1週間)

インターンシップの終了後も、データ分析基盤の構築に関する知識や技術を身につけることができるでしょう。

詳細や応募方法については、ABCGHD公式サイトの募集要項ページをご確認ください。

勝手に公式サイトとなってしまっているのは微妙な感じですが、良い感じじゃないでしょうか?

撮れ高の匂いがプンプンします よね。

というわけで、他にも色々聞いてみて別記事にしようと思いますので少々お待ちください。

まとめ

今回は自前データを活用してLLMから回答を得るための実装手順についてまとめました。

LangChainを活用しているおかげもあって、短いコードで簡単に試せてしまうのが恐ろしいところです。(APIに課金は必要ですが・・)

実際に手を動かして回答が得られてみると面白いものですね。このあたりを試した結果はまた後日記事にしますのでお楽しみに。

ちなみに今回は既にNotionからMarkdownに落とし込んでいる資産があったのでMarkdownからの読み込みとしましたが、ブログを作っていなかったとしても、LangChainにはNotion DBからのLoaderがあるので、そちらを使ってもいいのかと思っています。

普通に社内ナレッジDBを繋いだりとかすると便利になる予感がしますね。

AUTHOR

伴 拓也

朝日放送グループホールディングス株式会社 デジタル・アーキテック局 データ戦略チーム

アプリケーションからインフラ、ネットワーク、データエンジニアリングまで幅広い守備範囲が売り。最近はデータ基盤の構築まわりに力を入れて取り組む。 主な実績として、M-1グランプリ敗者復活戦投票システムのマルチクラウド化等。

WORK@ABC

技術力を培うための
環境と文化

ABCに昔から根付く「自分たちで開発する」文化を支える環境や取り組みをご紹介します
ABCについてもっと知る