RAGアプリの作成でGitHub Copilotを使ってみた

はじめに

皆さんこんにちは。
CAD/AGU所属のAIエンジニアの伊藤と申します。

早速本題ですが、皆さんはGitHub Copilotを使っていますか?
リリースからしばらく経ちましたが、ARISEでもトライアル運用が始まり、周りから色々な感想が聞こえてきます。
私もトライアルで使うことができたので、今回はAIエンジニアの業務観点からGitHub Copilotをレビューしてみようと思います。

※このレビューは2024年2月時点での内容です。今はさらに性能向上しているかもしれません。

レビュー方法について

最近は生成AIが急激に普及した影響で、AIエンジニアの業務で生成AIを使うことは珍しくないと思います。
そこで今回は、GitHub Copilotを使いながら、RAGのデモアプリを作成してみようと思います。
GitHub Copilotへの質問や、補完機能を使うことでどれだけアプリ作成の効率が上がりそうか確かめようと思います。

生成AIデモアプリ作成

今回はGoogleからリリースされたLLMのGeminiと生成AIアプリケーション作成時の定番ライブラリであるLangChainを使ってWEBブラウザ上のUIにPDFのURLを入力し、そのPDFの内容についての質問に答えるチャットボットを作成していきます。

RAG作成

ということでまずはPDFの内容を参照するRAGの処理を作っていきましょう。

ちなみに、LangChainはいつからか仕様が大幅に変更されたようで、コンポーネントを”|”でつなげて書くことでChainが作成できるようになったりしたみたいです。
私は以前LangChainを使って簡単なRAGアプリを作成したことはありますが、仕様変更後のLangChainには初めて触れるので実質初心者状態です。
ライブラリの使い方の調査が楽になったか、コーディングの時間が減ったかなどを以前の経験とも比べつつ、GitHub Copilotの有用性を確かめていこうと思います。

手始めに新しいLangChainの使い方を確認したいです。
GitHub Copilotにはコーディング時の補完機能とVSCodeで開いているディレクトリのファイルを参考にコーディングに関する質問に答えてくれるチャットボット機能があります。
このチャットボットにコードの例を示してもらいましょう。
(この時点ではdevcontainer用のファイルやpoetryの設定ファイル程度だけ作成してあります。)


質問:

LangChainでRAGを作りたいのでコードの例を表示してください

回答:

LangChainを使用してRAG(Retrieval-Augmented Generation)モデルを作成するための基本的なPythonコード例は以下の通りです:

 

from langchain import LangChain
from langchain.rag import RagTokenizer, RagModel

# LangChainのインスタンスを作成
lc = LangChain()

# RAGトークナイザとモデルを作成
tokenizer = RagTokenizer.from_pretrained('facebook/rag-token-nq')
model = RagModel.from_pretrained('facebook/rag-token-nq')

# クエリを設定
query = "What is the capital of France?"

# クエリをトークナイズ
input_ids = tokenizer.encode(query, return_tensors='pt')

# モデルを使用して予測を生成
outputs = model.generate(input_ids)

# 予測をデコード
answer = tokenizer.decode(outputs[0])

print(answer)

 

このコードは、LangChainとRAGモデルを使用して質問に対する答えを生成します。質問はquery変数に設定され、その質問はトークナイザによってトークナイズされます。その後、トークナイズされた質問はモデルに渡され、答えが生成されます。最後に、生成された答えはデコードされて表示されます。

このコードは一例であり、具体的な要件に応じて調整が必要かもしれません。また、このコードを実行するには、LangChainとRAGモデルがインストールされている必要があります。

 

何言ってんだこいつは

旧仕様の使い方が出てくるとかでもなくTransformers風の謎モジュールの使い方を提示してきました。
というか最初にインスタンス化したlcとかどこにも使われてないし、存在するモジュールのコードだったとしてもおかしいのでは…?

とりあえず、GitHub CopilotにはLangChainのような比較的新しいライブラリの知識は期待できないようです。

仕方ないので公式のチュートリアルやGoogleのモデルの使い方埋め込みの使い方などを見ながらコーディングしてみます。
ということで下記のインポート文まで書いてみました。

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_google_genai.embeddings import GoogleGenerativeAIEmbeddings
from langchain.prompts import ChatPromptTemplate
from langchain.document_loaders import PyPDFLoader
from langchain.vectorstores.docarray import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnablePassthrough

チャットボットの時と同様LangChain自体の知識はないようで、変な補完候補ばかり表示してきました。

RunnableSerialなんてモジュールないよ…

このあたりでLangChainを題材に選んだことを後悔し始めてきましたが、もしかしたらもう少しコーディング進めたらいい感じになるかもしれないと思い続けてみます。

今回のアプリケーションではPDFを渡されたらそのベクトルDBを作成する必要があるので、まずはその処理関数を書いてみました。
すると、関数名と引数を書いたあたりで割と思っていたコードに近い補完内容がでてきました。

ちょっと惜しい

モジュールの使い方がちょっと間違っていますが、少しの修正で済みそうです。
下記のように書き直しました。(blackによるフォーマッティングがかかっています。)

def create_pdf_vector_db(pdf_url):
    pages = PyPDFLoader(pdf_url).load_and_split()
    pdf_vector_store = DocArrayInMemorySearch.from_documents(
        pages, GoogleGenerativeAIEmbeddings(model="models/embedding-001")
    )
    return pdf_vector_store.as_retriever(search_kwargs={"k": 5})

クエリとの類似度上位五件の結果を返すベクトルDBを作成する関数となっています。

補完の様子を見るに、一般的なコーディング知識やクラスや関数名などをもとに後続のコードを推測する力は高いようで、補完結果を人間が修正していくことで全く知識がないライブラリを使っていてもコーディングの手間を減らすことはできそうです。

この調子で適宜GitHub Copilotの補完を採用しつつ、最終的には下記のようなコードとなりました。

from langchain.document_loaders import PyPDFLoader
from langchain.prompts import ChatPromptTemplate
from langchain.vectorstores.docarray import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_google_genai.embeddings import GoogleGenerativeAIEmbeddings


def create_pdf_vector_db(pdf_url):
    pages = PyPDFLoader(pdf_url).load_and_split()
    pdf_vector_store = DocArrayInMemorySearch.from_documents(
        pages, GoogleGenerativeAIEmbeddings(model="models/embedding-001")
    )
    return pdf_vector_store.as_retriever(search_kwargs={"k": 5})


def create_chatbot(pdf_url):
    retriever = create_pdf_vector_db(pdf_url)
    setup_and_retrieval = RunnableParallel(
        {"context": retriever, "question": RunnablePassthrough()}
    )

    template = """Answer the question based only on the following context:
{context}

Question: {question}
"""
    prompt = ChatPromptTemplate.from_template(template)

    model = ChatGoogleGenerativeAI(
        model="gemini-pro", convert_system_message_to_human=True, temperature=0
    )

    output_parser = StrOutputParser()

    chain = setup_and_retrieval | prompt | model | output_parser

    return chain

create_chatbotはURLのPDFに関する質問内容をインプットに回答を出してくれるチェインを返す関数となりました。後述するUI側でチェインの入出力を取得・表示させるようにします。

コーディング時のGitHub Copilotの挙動ですが、新しいLangChainの記述方式についてもchain = setup_and_retrieval |を書いたあたりで察して続きを補完してくれたり、今回は採用しませんでしたが、英語のプロンプトについてもそれっぽい補完を出してくれたりしました。

ここまでやってみての感想としては、ライブラリの知識はあてにはならないがある程度コードを書いた後の補完はかなり役立つといったところでしょうか。

私の場合、この程度の作業だと30分以上かかっていたと思いますが、GitHubCopilotを使って15分程度で作れました。(LangChainが使いやすくなったせいもあるかもですが。)

UI作成

続いてはアプリのUI部分を作成してみましょう。
今回はStreamlitと呼ばれるPythonで簡単にUIを作れるライブラリを利用します。
細かい調整は難しいですが、デモレベルのアプリであれば十分機能するので非常に便利です。

UIの要素として、PDFのURL入力欄、チャット入力欄、チャット履歴表示欄を用意したいです。
RAGの時と同様にまずはGitHub Copilotのチャットボットを使ってみようと思います。


質問:

streamlitを使ってチャットボットのUIを作成したいです。

回答:

Streamlitを使用してチャットボットのUIを作成するためには、まずStreamlitをインストールする必要があります。次に、StreamlitのAPIを使用してUIを作成します。以下に基本的な例を示します。

 

import streamlit as st

def chatbot_ui():
    st.title('Chatbot')
    user_input = st.text_input("Please enter your question:")
    if st.button('Submit'):
        if user_input:
            # `create_chatbot` function from your `app.py` file
            chatbot = create_chatbot(pdf_url)
            response = chatbot.run({"question": user_input})
            st.text_area("Chatbot response:", value=response)

このコードは、ユーザーが質問を入力し、その質問に対するチャットボットの応答を表示する基本的なUIを作成します。create_chatbot関数は、あなたのapp.pyファイルから取得され、チャットボットを作成します。ユーザーが質問を入力し、「Submit」ボタンをクリックすると、チャットボットは質問に対する応答を生成し、その応答がテキストエリアに表示されます。

このコードは基本的な例であり、実際の使用には適応させる必要があります。例えば、pdf_urlは適切なPDFのURLに置き換える必要があります。また、create_chatbot関数が期待する他のパラメータがある場合、それらも適切に提供する必要があります。

質問:

今までのチャット履歴の表示もしたいです

回答:

Streamlitでは、チャット履歴を表示するために、テキストエリアを使用してチャットの各交換を追加することができます。以下にその例を示します。

import streamlit as st

def chatbot_ui():
    st.title('Chatbot')
    chat_history = st.empty()
    user_input = st.text_input("Please enter your question:")
    if st.button('Submit'):
        if user_input:
            # `create_chatbot` function from your `app.py` file
            chatbot = create_chatbot(pdf_url)
            response = chatbot.run({"question": user_input})
            # Update the chat history
            chat_history.text_area("Chat History:", value=f"{user_input}\n{response}", height=200)

このコードでは、st.empty()を使用して動的に更新できるプレースホルダを作成しています。ユーザーが新しい質問を送信するたびに、チャット履歴が更新され、新しい交換が追加されます。

このコードは基本的な例であり、実際の使用には適応させる必要があります。例えば、pdf_urlは適切なPDFのURLに置き換える必要があります。また、create_chatbot関数が期待する他のパラメータがある場合、それらも適切に提供する必要があります。


LangChainよりはStreamlitの方が知識を持ってそうですが、まだ不完全そうです。
というのも、Streamlitはユーザーの入力などで画面が更新されるたびに上からコードが実行しなおされるため、chat_history = st.empty()といった初期化を行ってしまうと過去の履歴が消し飛んでしまいます。
そのため、実際にはst.session_stateという画面更新されても変わらない辞書ライクな変数を利用して履歴を保存する必要がありますが、その点は把握していないようです。

とはいえ、その点さえ修正してしまえば動きそうです。
このコードをベースに下記のように修正しました。

import streamlit as st
from langchain.document_loaders import PyPDFLoader
from langchain.prompts import ChatPromptTemplate
from langchain.vectorstores.docarray import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_google_genai.embeddings import GoogleGenerativeAIEmbeddings


def create_pdf_vector_db(pdf_url):
    pages = PyPDFLoader(pdf_url).load_and_split()
    pdf_vector_store = DocArrayInMemorySearch.from_documents(
        pages, GoogleGenerativeAIEmbeddings(model="models/embedding-001")
    )
    return pdf_vector_store.as_retriever(search_kwargs={"k": 5})


def create_chatbot(pdf_url):
    retriever = create_pdf_vector_db(pdf_url)
    setup_and_retrieval = RunnableParallel(
        {"context": retriever, "question": RunnablePassthrough()}
    )

    template = """Answer the question based only on the following context:
{context}

Question: {question}
"""
    prompt = ChatPromptTemplate.from_template(template)

    model = ChatGoogleGenerativeAI(
        model="gemini-pro", convert_system_message_to_human=True, temperature=0
    )

    output_parser = StrOutputParser()

    chain = setup_and_retrieval | prompt | model | output_parser

    return chain


def chatbot_ui():
    if "history" not in st.session_state:
        st.session_state.history = []

    st.title("Chatbot")
    pdf_url = st.text_input("Please enter the URL of the PDF file:")
    user_input = st.text_input("Please enter your question:")
    if st.button("Submit"):
        for role, message in st.session_state.history:
            st.write(f"{role}: {message}")
        if user_input:
            st.write(f"user: {user_input}")
            chatbot = create_chatbot(pdf_url)
            response = chatbot.invoke(user_input)
            st.write(f"ai: {response}")
            st.session_state.history.append(("user", user_input))
            st.session_state.history.append(("ai", response))


chatbot_ui()

さきほどのコードをコピペして、session_stateへの履歴格納などの修正を行う過程でもは、そのまま採用できるコードを提示してくれました。
私が同様の機能を持ったコードを一から書こうと思うと30分はかかりそうなところですが、GitHub Copilotを利用すると、チャットボットでベースとなるコードを作成できること、補完による修正作業の効率化の恩恵で5分程度でここまで作成することができました。

さて、ひとまずはこれで出来上がったので、実際に動かしてみます。
Streamlitはstreamlit run .pyファイルでローカルにWebサーバーを立ち上げてくれます。
その結果が下記の画像です。

見た目・性能ともにもうちょっと改善の余地がありそう。

Attention is All You Needの論文のURLを貼り付け、いくつか質問してみました。
RAGのプロンプトなどが適当なせいか、Attentionについて答えられないなど若干低性能ですが、動作していることが確認できます。

ただし、見た目はチャットUIとしてはかなり貧弱です。
実はStreamlitにはこういったチャットボットを構築するためのモジュールがあるのですが、GitHub Copilotはそのことは把握していないようでした。。。
が、動作はするのでとりあえずこれで良しとします。

本当はもっときれいにできる(公式サイトより)

まとめ

今回はRAGチャットボットを構築しつつ、GitHub Copilotをレビューしてみました。
まとめると下記のようになると思います。

レビュー観点 改善度
ライブラリの使い方調査での有用性 変化なし(自力でやる必要がある)
RAGのコーディング時間の削減度合い(LangChainで実装) 30分⇒15分
RAGのクオリティへの影響(LangChainで実装) 変化なし
チャット画面のコーディング時間の削減度合い(Streamlitで実装) 30分⇒5分
チャット画面のクオリティへの影響(Streamlitで実装) 悪化(ライブラリの機能がフル活用できていない)

また、今回RAGアプリを作成してみて思ったこととしては下記のようになります。

良い点

  • (自分で書いたコード量がある程度あれば)機能追加などの際に少しの修正で動くコードを提示してくれる
  • コードの修正などに使う際にはそのまま動かせるコードを提示してくれることが多い

悪い点

  • ライブラリの知識はあまりあてにならない(有名どころだとまた違うかも)

私の結論としては、「最低限コーディングの手間の削減には役立つが、比較的新しいライブラリを使うときは人間がそのライブラリの知識を持つ必要がある」といったところでしょうか。
特に生成AI周りはライブラリも含め日進月歩で未成熟なので、成熟したライブラリをよく利用する職種と比べるとAIエンジニアが受けられる恩恵は少ないように思えます。

とはいえ、間違いなく既存の補完機能よりは優れていると言えると思うので今後に期待したいと思います。
そしていつの日か全部AIに任せればOKとなって人間を仕事から解放してほしいです。