プログラミング

Gradioで簡単!ローカルLLMを使ったチャット画面の作り方




前回はローカルPCでサイバーエージェント社が日本語データを学習させたDeepSeekのモデルを動かしてみました。

使ったモデルは「cyberagent/DeepSeek-R1-Distill-Qwen-14B-Japanese」です。

その発展として、Gradioを使ってローカルLLMとチャットできる画面を作成します。

本記事の内容

  • GradioのChatInterfaceを使ってチャット画面を作成
    • ローカル環境にあるDeepSeekと、Gemma 2を切り替えてチャットができる



前回と同様に、OllamaでローカルPCでもLLMを動かせるような環境を使います。

やり方については前回の記事を参考にしていただければと思います。


Gradioとは


今回使うGradioは「機械学習モデルをデモするためのWebインターフェース」です。

(日本語の記事ではグラディオって読むと書いてあるのですが、英語ではグレイディオ呼びでした…)


画像・動画・音声など様々なデータを扱えるコンポーネントが用意されています。


また、Hugging Face Spacesで公開されているアプリをAPI経由で使えるgradio_clientもあります。

AI界のGitHubと言われている「Hugging Face」のアプリが使えるあたり、機械学習向けのライブラリという感じがしますね!


Gradioと同じように簡単にWebアプリが作れるライブラリとして「Streamlit」があります。


私の感覚としては、Streamlitの方が直観的でわかりやすく、デフォルトのデザインがオシャレなので そのまま使えます。

Gradioはデフォルトのデザインは正直微妙ですが、CSSを適用しやすいので デザインにこだわることができます。

今回使用するChatInterfaceはデフォルトでも いい感じのデザインです。

Chatinterfaceチュートリアル


まずは gradioの公式ドキュメントにあるサンプルコードを使っています。

ユーザから何か入力されると「Yes」または「No」のどちらかを返却するコードです。

ChatInterfaceでは実行する関数を指定します。ここでは「chat_response」がその関数です。


関数は引数にユーザから入力される文字列と、会話履歴を受け取る必要があります。

会話履歴はOpenAIで使用されている辞書形式のリストになります。

実際にコードを動かして、関数の引数で受け取っている「message」と「history」の中身を確認します。

import random
import gradio as gr

# chatの関数 引数にはユーザ入力と会話履歴が必須
def chat_response(message, history):
    print("message", message)
    print("history", history)
    return random.choice(["Yes", "No"])

demo = gr.ChatInterface(chat_response, type="messages", autofocus=False)

if __name__ == "__main__":
    demo.launch()


printで出力した結果は以下になります。

message あいうえお
history []
message かきく
history [{'role': 'user', 'metadata': {'title': None, 'id': None, 'parent_id': None, 'duration': None, 'status': None}, 'content': 'あいうえ お', 'options': None}, {'role': 'assistant', 'metadata': {'title': None, 'id': None, 'parent_id': None, 'duration': None, 'status': None}, 'content': 'No', 'options': None}]


ChatInterfaceを使うとチャット画面が作成され、ユーザから入力された文章と会話履歴が関数に渡されています

「gr.ChatInterface」だけでここまでやってくれるのは かなり便利ですね。


現在のコードは単純にYesか、Noかを返却しているので、この部分を生成AIからの応答に変えたいと思います。


ローカルLLMとチャットする画面の作成


本題のチャット画面になります。

画面上でチャットに使用するローカルLLMの選び、チャットを開始できる画面を作ってみます!

ローカルLLMは「DeepSeek」と「Gemma2」の2つで、DeepSeekをデフォルトで選択するようにします。


事前に以下のモデルをダウンロードしておきます。


DeepSeekの場合、<think></think>というタグで囲まれたAIの思考内容も含めて返却されます。

この<think>タグがあると、ChatInterfaceではうまく表示できませんでした。

そこで、<think>タグを置換することで対処しました。

そもそも<think>の内容が不要なら、長くなるので削除する方が良いと思います。


デフォルトだとブラウザいっぱいにチャット画面が表示されなかったため、「gr.Blocks(fill_height=True)」で対応しています。


また、モデルの名前を取得するための関数を用意してます。

changeによってモデルが切り替えられたらグローバル変数のmodel_nameを更新するようにしています。

Gradioはコンポーネントを通じてデータのやり取りが行われるため、「model_selected」にはコンポーネントが入っています。

そのため、コンポーネントから値を取り出すための関数が必要になります。

(ここがちょっとややこしかった…)

import gradio as gr
import ollama
import re

model_name = "DeepSeek"

# chatの関数 引数にはユーザ入力と会話履歴が必須
def chat_response(
    message,
    history,
):
    # 会話履歴を含めたチャットメッセージの作成
    if len(history) != 0:
        chat_messages = [
            {"role": item["role"], "content": item["content"]} for item in history
        ]
        chat_messages.append({"role": "user", "content": message})
    else:
        chat_messages = []
        chat_messages.append({"role": "user", "content": message})

    # ローカルLLMの呼び出し
    match model_name:
        case "DeepSeek":
            response = ollama.chat(
                model="hf.co/mmnga/cyberagent-DeepSeek-R1-Distill-Qwen-14B-Japanese-gguf:Q4_K_M",
                messages=chat_messages,
            )
            # レスポンスに<think>が入っているとChatInterfaceでうまく表示できない問題の対処
            ai_message = re.sub(
                r"<think>\n", "[think Start]:", response["message"]["content"]
            )
            ai_message = re.sub(r"</think>\s*", "[think End]\n", ai_message)
            return ai_message

        case "Gemma 2":
            response = ollama.chat(
                model="gemma2:latest",
                messages=chat_messages,
            )
            return response["message"]["content"]

# ラジオボタンで選択したモデル名をmodel_nameに格納
def get_model_name(value):
    global model_name
    model_name = value

# 画面の構築
# https://github.com/gradio-app/gradio/issues/7714
with gr.Blocks(fill_height=True) as demo:
    gr.Markdown("## チャット画面")
    gr.Markdown(
        "ローカルLLMとチャットでやり取りする画面です。使用するモデルを選択してください。"
    )
    model_selected = gr.Radio(
        choices=["DeepSeek", "Gemma 2"],
        value="DeepSeek",
        label="使用モデル",
        type="value",
    )
    model_selected.change(get_model_name, inputs=model_selected, outputs=None)

    chat_area = gr.ChatInterface(chat_response, type="messages")

if __name__ == "__main__":
    demo.launch()


Deepseekはユーザの質問の意図などを思考してから返答するため、応答に時間がかかります。

処理が終わるまでは「・・・」と表現してくれるので、処理中なのがわかります。

こういう細かな表現をGradioでやってくれるのはありがたいですね!



ブラウザをリロードすると会話履歴(history)はリセットされます。

新しく会話を始めたいときはリロードしましょう!



DeepSeekは<think>で思考が見れるのが面白いですが、チャット画面に表示するには長いので工夫した方が良さそうですね。

比較すると、Gemma2の方が簡潔でチャット画面として見やすいなと思いました。


おわりに

今回、Gradioを使ったら、簡単にチャット画面を作成することができました!

データのやり取りがコンポーネントを通じて行われるため、ラジオボタンで選択されたモデルの名前を取り出すのに苦戦しました。

それ以外は簡単に実装できましたし、機械学習向けのコンポーネントが用意されているので便利なライブラリですね。


Streamlitに比べると記事があんまりないような気がしたので、誰かのお役に立てたら幸いです。


ここまで読んでいただき、ありがとうございました。

  • この記事を書いた人

ねぎねず

IT企業で働くエンジニア6年目。プログラミングや生成AIを勉強中。勉強や生活するなかで役に立った情報を発信していきます。

-プログラミング