🛡Ollama と Gradio でセキュアなローカル専用 AI チャットアプリを作る
Building a Secure Local-Only AI Chat App with Ollama x Gradio.
1. 概要 (Overview)
ローカル AI チャットアプリの実装方法について、私が実装したものをベースに紹介します。
アプリは簡単に言うと "ChatGPT のオフライン版" みたいなもので、
手元の PC 内で直接生成 AI を動かしてセキュアにチャットができる、ローカル専用アプリです。
外部にはデータを一切送信しないので、例えば機密情報みたいなものも安心して扱えます。
この記事では Python やアプリ開発にはある程度慣れた方向けに、
以下の具体的な実装の解説をします。
- LangChain を利用して Ollama 経由で AI モデルを扱う部分
- Gradio を利用して UI を構築する部分
アプリの全ソースコードと実行のさせ方などは GitHub で公開しています。
2. アプリの構成 (App Architecture)
3. 実装例 (Implementation Example)
※以降は Python 3.12.10、langchain 0.3.24、langchain-core 0.3.57、
langchain-ollama 0.3.2、gradio 5.29.0 で動作を確認しています
3.1. Ollama 経由で AI モデルを扱う
※事前に Ollama のインストールと、任意の AI モデルの DL をしておく必要があります (お手軽)
LangChain を利用して Ollama 経由で AI モデルを扱う例です。
以下のようにモデルを初期化します。
from langchain_ollama import ChatOllama
llm = ChatOllama(
model="gemma3:27b", # LLM のモデル名
base_url="http://localhost:11434/", # Ollama のエンドポイント
temperature=0.7, # LLM の出力の多様性
)
※パラメータは実践では .env
で指定する想定です
※base_url
を明示的に指定しない場合は Ollama デフォルトの http://localhost:11434/
になります
以下のように LLM を呼び出します。
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# システムプロンプトとユーザーメッセージを設定する例
messages = [
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content="What is the capital of Japan?"),
]
# プロンプトと LLM、パーサーを連結する
prompt = ChatPromptTemplate.from_messages(messages)
chain = prompt | llm | StrOutputParser()
# LLM を呼び出す
response = chain.invoke({})
print(response) # e.g. The capital of Japan is Tokyo.
また、以下のように LLM に渡す会話履歴の最大数の設定機能もあると、
コンテキストが長くなるとモデルの性能が低下する場合に有用です。
オープンソースで小さいモデルを利用すると、モデルの性能が低下する場合がよくあります。
LangChain の trim_messages
を利用するとスマートに実装できます。
from langchain_core.messages import (
AnyMessage,
AIMessage,
HumanMessage,
SystemMessage,
trim_messages,
)
# システムメッセージを含めて最大 3 件のメッセージを残す設定例
messages_trimmer: RunnableLambda[list[AnyMessage], list[AnyMessage]] = (
trim_messages(
# token_counter に len を指定して、メッセージ数をトークン数として扱う
max_tokens=3,
token_counter=len,
strategy="last",
allow_partial=False,
include_system=True,
)
)
# システムプロンプト
instructions = "You are a helpful assistant." # 残る
# 会話履歴
history: list[AnyMessage] = [
HumanMessage(content="What is the capital of France?"), # 消える
AIMessage(content="The capital of France is Paris."), # 消える
HumanMessage(content="What is the capital of Japan?"), # 消える
AIMessage(content="The capital of Japan is Tokyo."), # 残る
]
# ユーザーメッセージ
message = "What is the capital of Brazil?" # 残る
messages: list[AnyMessage] = [SystemMessage(content=instructions)]
messages.extend(history)
messages.append(HumanMessage(content=message))
trimmed_messages = messages_trimmer.invoke(messages)
# SystemMessage と末尾の AIMessage、HumanMessage が残る
print(trimmed_messages)
3.2. Gradio で UI を構築する
以下のような機能を簡単に実装できます。
- ユーザーと AI の会話履歴 (会話例) の事前入力
- AI の出力の内容、文字数などの誘導に有用
- 会話履歴の編集
- 会話が長くなると文字数が増えて来たり、システムプロンプトの指示から
ブレて来るケースなど、AI の出力を軌道修正するのに有用
- 会話が長くなると文字数が増えて来たり、システムプロンプトの指示から
- LLM の呼び出し
- システムプロンプトの動的更新
AI のメッセージの誘導は、ロールプレイなどにも欲しくなる機能だと思います。
3.2.1. ユーザーと AI の会話履歴 (会話例) の事前入力
Gradio の Chatbot
コンポーネントの初期値として会話履歴 (会話例) を渡します。
import gradio as gr
from gradio.components.chatbot import Message
# 会話履歴 (会話例)
message_examples: list[gr.MessageDict | Message] = []
message_examples.append(
gr.MessageDict(role="user", content="What is the capital of France?")
)
message_examples.append(
gr.MessageDict(role="assistant", content="The capital of France is Paris.")
)
with gr.Blocks() as ui:
# 会話履歴の UI (記事冒頭のスクショの会話やり取り部分)
gr.Chatbot(message_examples, type="messages")
ui.launch(inbrowser=True, share=False, pwa=True)
3.2.2. 会話履歴の編集機能
Gradio の Chatbot
コンポーネントの editable
オプションで編集可能に設定できます。
with gr.Blocks() as ui:
# 会話履歴の UI
gr.Chatbot(message_examples, type="messages", editable="all")
3.2.3. LLM の呼び出し
Gradio の Textbox
コンポーネントのサブミットイベントでユーザーメッセージと会話履歴を
コールバックに引き渡して、LLM を呼び出します。
def chat_callback(
user_message: str, history: list[gr.MessageDict]
) -> tuple[str, list[gr.MessageDict]]:
# LLM を呼び出す
# 注: invoke_llm() の中では前述のように LLM を呼び出していると仮定します
ai_res = invoke_llm(user_message, history)
# 更新した会話履歴を UI に返す
history.append(gr.MessageDict(role="user", content=user_message))
history.append(gr.MessageDict(role="assistant", content=ai_res))
# "" を返すと入力ボックスがクリアされる
return "", history
with gr.Blocks() as ui:
# 会話履歴の UI
chatbot = gr.Chatbot(message_examples, type="messages", editable="all")
# ユーザーメッセージ入力の UI (記事冒頭のスクショの下側のテキスト入力ボックス)
input_box = gr.Textbox(
placeholder="Shift + Enter で改行", show_label=False
)
input_box.submit(
chat_callback,
inputs=[input_box, chatbot],
outputs=[input_box, chatbot],
)
3.2.4. システムプロンプトの動的更新
Gradio の Textbox
コンポーネントに初期値としてシステムプロンプトを渡しつつ編集可能に
設定し、Button
コンポーネントのクリックイベントで最新のシステムプロンプトを
コールバックに引き渡して更新します。
def update_instructions(instructions: str) -> None:
# システムプロンプトを更新する
# 注: ここで前述の SystemMessage として LLM に渡せるように保存するなどします
~略~
instructions = "You are a helpful assistant."
with gr.Blocks() as ui:
with gr.Row():
with gr.Column(scale=1):
# システムプロンプト表示・入力 UI
# (記事冒頭のスクショの左側のテキスト入力ボックス)
instructions_box = gr.Textbox(
instructions,
lines=10,
label="Instructions (editable)",
interactive=True, # 編集可能に設定
)
# システムプロンプト更新の UI (記事冒頭のスクショの左側の Upadte ボタン)
# Update ボタン押下で instructions_box に入力されているテキストを取得して
# システムプロンプトを更新する
gr.Button("Update").click(
lambda txt: update_instructions(txt),
inputs=instructions_box,
)
with gr.Column(scale=4):
# 会話履歴の UI
gr.Chatbot(message_examples, type="messages", editable="all")
4. まとめ (Conclusion)
ローカル AI チャットアプリの実装方法について、私が実装したものをベースに紹介しました。
LangChain を利用して Ollama 経由で AI モデルを扱い、Gradio を利用して UI を構築した、
セキュアにチャットができるローカル専用アプリの作り方を解説しました。
自分のニーズに合わせた、機能の細かいカスタマイズも手軽にできます。
Discussion