1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】LLM + Gradio でローカルチャットアプリを作ってみた

Last updated at Posted at 2025-06-01

LLM + Gradio でローカルチャットアプリを作ってみた

はじめに

最近 LLM で色々と遊んでみています.
今回は LLM と Gradio を用いて,ローカル + CPU で簡易的なチャットアプリを作成してみました.
使用するモデルやセットアップに関しては,以下の過去記事で紹介しているので,未読の方は先に目を通していただくとスムーズかと思います.

👉 ローカル+CPU+Python で Large-Language Model(LLM)を動かしてみた

アプリ構想

使用したモデル・ライブラリ

モデル・ライブラリ 説明
llama-cpp-python .gguf 形式のモデルを Python で動かすための bindings
Phi-3-mini-4k-instruct-q4.gguf Microsoft の開発した軽量言語モデル
量子化(q4)なら CPU でも十分動作可能
Gradio 機械学習モデルの Web アプリやデモ UI を簡単に作れる Python ライブラリ

構成

バックエンド(モデル実行部分)

  • llama-cpp-python を利用し,.gguf 形式の LLM モデルを Python 上で実行
  • 入力されたプロンプトと履歴を元に LLM が応答を生成
  • モデルには Phi-3-mini-4k-instruct-q4.gguf を使用(軽量でCPUでも動作)

フロントエンド(UI表示部分)

  • Gradio を用いてチャット UI を構築
  • Textbox() による入力,Chatbot() による吹き出し形式の応答表示
  • save ボタンで会話を .csv 形式で保存可能

処理の流れ

Textbox() にユーザー入力 → 送信 → Python 関数(バックエンド)コール → llama-cpp-python で推論 → Chatbot() に応答表示
  • Gradio UI 上で,Textbox() にメッセージを入力
  • 送信後,バックエンドで履歴を考慮したプロンプトを組み立て,llm() を呼び出して推論
  • 応答が生成された後,再び Gradio 上の Chatbot() に返して画面に表示

このようなシンプルな構成でアプリを作成しました.

コード

コード全体像

import argparse

import gradio as gr
from llama_cpp import Llama
import pandas as pd


class LLMChatModel:
    def __init__(self, model_path, n_ctx, n_threads, n_keep = 5):
        self.model_path = model_path
        self.llama = Llama(
            model_path=self.model_path,
            n_ctx=n_ctx,
            n_threads=n_threads,
        )
        self.n_keep = n_keep
        self.raw_conversation_list = []
        self.current_conversation_list = []
        self.summary = ""
        self.concept = (
            "あなたは日本語で答えるアシスタントです。親切で簡潔に答えます。\n"
            "以下は回答時の制約です。\n"
            "- 日本語のみ(英語や記号表現なし)\n"
            "- 「Instruction:」、「Step:」、「指示1:」などの表現は**使わない**\n"
            "- 本文から自然に始める\n"
            "- 128文字以内で簡潔に\n"
            )
    
    def summarize_old_conversation(self):
        summary_input = self.summary + "\n\n"
        for m, r in self.current_conversation_list[:self.n_keep]:
            summary_input += f"ユーザ:{m}\nアシスタント:{r}\n"
        summary_prompt = "以下の内容を簡潔にまとめてください:\n" + summary_input + "\n要約:"
        result = self.llama(summary_prompt, max_tokens=256)
        self.summary = "これまでの会話の要約:" + result["choices"][0]["text"].strip() + "\n"
        self.current_conversation_list = self.current_conversation_list[self.n_keep:]
    
    def create_conversation(self):
        prompt = ""
        if len(self.current_conversation_list) > 2*self.n_keep:
            self.summarize_old_conversation()
        for m, r in self.current_conversation_list:
            prompt += self.make_prompt(m, r)
        prompt = self.summary + prompt
        return prompt

    def make_prompt(self, query, reply = "", is_brief = False):
        return (
            "<|user|>\n"
            "制約を守って質問に答えてください。\n\n"
            f"質問:{query}\n"
            "<|end|>\n"
            "<|assistant|>\n"
            f"{reply + '\n<|end|>\n' if reply != '' else ''}"
        )
    
    def chat(self, message, history):
        whole_prompt = self.create_conversation()
        prompt = self.make_prompt(message, is_brief=True)
        prompt = self.concept + whole_prompt + prompt
        # tokens = self.llama.tokenize(prompt.encode("utf-8"))
        # print("トークン数:", len(tokens))
        reply = self.llama(
            prompt, 
            max_tokens=256, 
            stop=["<|end|>", "<|user|>", "<|assistant|>", "\n\n", "Instruction", "Step"]
            )
        reply = reply["choices"][0]["text"].strip()
        self.raw_conversation_list.append((message, reply))
        self.current_conversation_list.append((message, reply))
        history.append((message, reply))
        return "", history
    
    def dump_history(self):
        df = pd.DataFrame(self.raw_conversation_list, columns=["User", "Assistant"])
        df.to_csv("chat_history.csv", index=False, encoding="utf-8-sig")

def app_run(model_path, n_ctx, n_threads):
    llm_chat_model = LLMChatModel(
        model_path=model_path,
        n_ctx=n_ctx,
        n_threads=n_threads,
    )
    with gr.Blocks() as demo:
        with gr.Row():
            with gr.Column():
                msg = gr.Textbox(
                    placeholder="メッセージを入力...", 
                    lines=10,
                    )
                send_btn = gr.Button("Send")
                save_btn = gr.Button("Save")
            chatbot = gr.Chatbot()
        send_btn.click(
            fn=llm_chat_model.chat,
            inputs=[msg, chatbot],
            outputs=[msg, chatbot],
        )
        msg.submit(
            fn=llm_chat_model.chat,
            inputs=[msg, chatbot],
            outputs=[msg, chatbot],
        )
        save_btn.click(
            fn=llm_chat_model.dump_history,
            inputs=None,
            outputs=None,
        )
    demo.launch()

def get_args():
    args = argparse.ArgumentParser()

    args.add_argument('--model-path', default="./models/Phi-3-mini-4k-instruct-q4.gguf",
                        type=str, help='path to model')
    args.add_argument('--n-ctx', default=2048,
                        type=int, help='n_ctx')
    args.add_argument('--n-threads', default=4,
                        type=int, help='n_threads')
    return args.parse_args()

if __name__ == "__main__":
    args = get_args()
    app_run(
        args.model_path,
        args.n_ctx,
        args.n_threads,
    )
関数・クラス 説明
LLMChatModel() LLM の推論・会話履歴管理・要約等を行うクラス
app_run() Gradio での UI 定義と LLMChatModel 初期化

使い方

  1. 環境
    1. Python:3.13.1
    2. pandas:2.2.3
    3. gradio:5.31.0
    4. llama-cpp-python:0.3.9
  2. ライブラリインストール
    $ pip install pandas gradio llama-cpp-python
    
  3. モデル配置
    1. こちらからモデルをダウンロード
    2. 下記のフォルダ構成でモデルを配置
      ./playground/
          ┣━ models/           # モデル格納用フォルダ
          ┃    ┗━ Phi-3-mini-4k-instruct-q4.gguf
          ┗━ chat_app.py         # 実行スクリプト
      

軽量 LLM で自然なチャットを実現するための工夫点

  • 会話履歴管理

    • 1 つ前,2 つ前,... のやりとりを返答に反映させるため,会話のやり取りを記録
    • プロンプトにそれらを含めて回答を得る
  • 1 回のやり取りを端的に

    • モデルの英語での出力や勝手な会話を抑制するため,1 回のやり取りを端的にするように制約を追加

          self.concept = (
              "あなたは日本語で答えるアシスタントです。親切で簡潔に答えます。\n"
              "以下は回答時の制約です。\n"
              "- 日本語のみ(英語や記号表現なし)\n"
              "- 「Instruction:」、「Step:」、「指示1:」などの表現は**使わない**\n"
              "- 本文から自然に始める\n"
              "- 128文字以内で簡潔に\n"
              )
              
          "制約を守って質問に答えてください。\n\n"
      
    • 無駄な出力を抑制するため stop_token の強化

      stop=["<|end|>", "<|user|>", "<|assistant|>", "\n\n", "Instruction", "Step"]
      
  • 定期的な過去の会話の要約

    • 過去の会話を必要とするため,やりとりが長くなるとプロンプトのトークン数が増大
    • 抑制のため一定のやり取りをした場合,最新 5 回のやり取り以外を要約
    • 要約した内容をプロンプトに追加して推論を実行

デモ

アプリ実行時

img1.png

  • 左側にテキストボックス送信ボタン,会話の保存ボタンを配置
  • 右側にチャット画面を配置

グラフィカルな UI で構築されており,ユーザーの入力とモデルの応答が左右で明確に見える構成

チャット時の画面

img2.png

  • ユーザ入力(例):「プログラミングを勉強したい!」
             「おすすめのプログラミング言語はある?」
  • モデルの応答(例):「Python人気」

実際の使用映像

output.gif

  • 実際にチャットっぽくやりとりができている
  • 質問にも答えてくれている
  • 一回の推論で 30 秒から 90 秒ぐらい
  • 割と制約を強くしたので淡白な会話に
  • 時々おかしな日本語も

この辺りはもっと大きいマシン・サーバにすると精度の高いモデルも動かせるようになるので改善の余地あり

おわりに

今回は LLM + CPU + gradio でローカルでチャットアプリを作ってみました.
LLM のライブラリやアプリ開発のライブラリを使用すると簡単に実装することが可能です.

LLM に関してはあとは RAG をチャットアプリに取り入れたりしたいと思っているので,また完成したら記事にします.

また,過去に LLM,RAG についての記事も書いているので,興味がある方はぜひご覧ください.
👉 Retrieval-Augmented Generation(RAG) を使って "ブラック企業の社内規則チャットボット" を作ってみた
👉 ローカル+CPU+Python で Large-Language Model(LLM)を動かしてみた

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?