Raspberry Pi 5 でローカルLLMを使う簡易AIアシスタントを作ってみた

クラウドのAIサービスは便利ですが、ネットワーク前提にしたくない場面や、まずは手元だけで小さく試したい場面もあります。たとえば、短い文章の下書き、箇条書きの整理、簡単な要約、メモの言い換えなどです。そこで今回も、「エレクトロニクスを簡単に」をテーマにRaspberry Pi 5上でローカルLLM を動かし、ブラウザから使える簡易AIアシスタント Web アプリを試作してみました。
Raspberry Pi

今回の構成では、推論エンジンにllama.cpp、モデルに SmolLM2-1.7B-Instruct の GGUF版を使います。llama.cpp は、最小限のセットアップで幅広いハードウェア上のLLM推論を可能にすることを目的としたプロジェクトとして案内されており、ローカル実行との相性がよい構成です。SmolLM2は、135M / 360M / 1.7Bのコンパクトなモデル群で、on-device 実行も意識された系列として紹介されています。
GitHub
Hugging Face

目次

なぜ Raspberry Pi 5 でローカルAIアシスタントなのか

Raspberry Pi 5 は、公式情報では Broadcom BCM2712 の 2.4GHz quad-core Arm Cortex-A76を搭載し、前世代比で2~3倍、最大3倍の高速化がうたわれています。さらに、USB 3.0、PCIe 2.0 x1、MIPI カメラ/ディスプレイ、最大 16GB RAM構成など、エッジ側で小さなAI処理を動かすための要素も揃っています。巨大モデルを快適に動かす用途ではありませんが、小型量子化モデルを使った簡易アシスタントには試しやすいバランスです。
Raspberry Pi

今回のような「短い相談に答える」「下書きを支援する」といった用途では、クラウドへ毎回投げなくてもよい構成にしておくと、全体像がとても分かりやすくなります。まずはローカルだけで完結する最小構成を作り、そこから必要に応じてRAGや音声入力へ広げていく流れが扱いやすいです。

今回の基本構成

今回の構成はとてもシンプルです。

  1. llama.cpp server がローカルLLMを推論する

  2. Flask が簡易UIと /chat APIを提供する

  3. ブラウザが Fetch APIでFlask に質問を送る

  4. Flask が llama.cpp server の OpenAI互換エンドポイントに問い合わせる

  5. 返ってきた応答を画面に表示する

llama.cpp server の READMEでは、server は fast, lightweight, pure C/C++ HTTP server であり、さらにLLM REST APIs と Web UI を備えると説明されています。また、OpenAI互換の chat completions / responses / embeddings routes に対応していることも案内されています。つまり、Webアプリ側は「ローカルにある OpenAI互換APIを呼ぶ」感覚で組み立てられます。
GitHub

ブラウザ側の通信には fetch() を使います。MDN では Fetch APIを、ネットワーク上のリソース取得のための、より強力で柔軟なインターフェースと説明しています。小さなチャットUIを作るにはちょうどよい構成です。
developer.mozilla.org

モデルとランタイムの選定

ランタイムは llama.cpp

llama.cpp は、READMEでminimal setupwide range of hardware を強く打ち出しており、ローカルLLM実行の定番の一つです。また、モデルはGGUF形式が必要と明記されています。
GitHub

さらに build ドキュメントでは、Arm CPU 向けに KleidiAI の最適化マイクロカーネルを有効化できることが説明されています。KleidiAI は Arm CPU 向けAIワークロード最適化ライブラリで、dotprod、int8mm、SVE、SME などのCPU機能を活用し、利用可能な最適カーネルをランタイム時に選択します。Pi5のようなArm ベース環境では、この最適化を意識した構成にしておくと試しやすいです。
GitHub (ggml-org/llama.cpp)

モデルは SmolLM2-1.7B-Instruct

SmolLM2 は、135M、360M、1.7B の3サイズで構成される compact language model family として案内されており、軽量で on-device 実行しやすいのが特徴です。Instruct版は、文章の書き換え、要約、function calling なども意識した調整が入っていると説明されています。
Hugging Face

また、GGUF版として SmolLM2-1.7B-Instruct-GGUFも用意されており、smollm2-1.7b-instruct-q4_k_m.gguf のような量子化済みファイルを llama.cpp でそのまま扱えます。Pi5では、まず q4_k_m あたりから試すと、サイズと扱いやすさのバランスを取りやすいと思います。
huggingface.co

実装方針

今回のPoCでは、できるだけ短いコードで全体像が見えるように、次の方針にしています。

1. 推論は llama.cpp server に任せる

Flask アプリの中に推論ロジックを持たせず、llama.cpp server を別プロセスで起動して、Webアプリ側は HTTP で呼ぶだけにします。こうすると、推論層とUI層が分かれて、構成がかなり分かりやすくなります。
GitHub

2. Flask は薄いプロキシにする

Flask は、HTMLを返すことと /chat の中継だけに集中させます。Flask 自体は公式に lightweight WSGI web application framework と説明されており、このくらいの小さなWebアプリにはちょうどよいです。
Flask Documentation

3. 最初はテキスト応答だけに絞る

音声、Wake Word、RAG、function calling まで最初から入れると構成が急に重くなります。今回はまず、「質問すると短く答えるローカルAIアシスタント」 を最小構成で作ります。

4. 会話履歴はブラウザ側で軽く持つ

PoCでは、会話履歴をサーバーやDBで管理せず、ブラウザ側の配列で直近数ターンだけ保持します。

ディレクトリ構成

最小構成は以下です。

pi-local-ai-assistant/
├── app.py
├── requirements.txt
└── templates/
    └── index.html

LLM 本体は llama.cpp 側で動かすので、Webアプリ側はかなり小さく保てます。

サンプルコード

今回は、Pi5上で動かしやすいシンプルな Web UI + プロキシ構成にしています。
ブラウザは Flaskの /chat に POSTし、Flask はローカルの llama.cpp server の /v1/chat/completions に問い合わせます。llama.cpp serverのREADME では、OpenAI互換の /v1/chat/completions が言及されています。
GitHub
※本プログラムは生成AIを用いて実装しております。

app.py

import os
import requests
from flask import Flask, jsonify, render_template, request

app = Flask(__name__)

LLAMA_SERVER_URL = os.getenv(
    "LLAMA_SERVER_URL",
    "http://127.0.0.1:8080/v1/chat/completions"
)
MODEL_NAME = os.getenv("LOCAL_MODEL_NAME", "local-model")

SYSTEM_PROMPT = """
あなたは Raspberry Pi 5 上で動作するローカルAIアシスタントです。
日本語で簡潔に、まず要点を答え、そのあと必要なら短い補足を加えてください。
不明な点は推測しすぎず、必要なら確認事項を示してください。
""".strip()

@app.get("/")
def index():
    return render_template("index.html")

@app.post("/chat")
def chat():
    data = request.get_json(force=True)
    messages = data.get("messages", [])
    if not messages:
        return jsonify({"error": "messages is required"}), 400

    payload = {
        "model": MODEL_NAME,
        "messages": [{"role": "system", "content": SYSTEM_PROMPT}] + messages,
        "temperature": 0.7,
        "max_tokens": 256,
        "stream": False
    }

    try:
        res = requests.post(LLAMA_SERVER_URL, json=payload, timeout=120)
        res.raise_for_status()
        body = res.json()
        reply = body["choices"][0]["message"]["content"]
        return jsonify({"reply": reply})
    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.get("/health")
def health():
    return jsonify({"ok": True})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=False)

templates/index.html

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pi Local AI Assistant</title>
  <style>
    body {
      margin: 0;
      padding: 24px;
      font-family: system-ui, sans-serif;
      background: #f6f8fb;
      color: #1f2937;
    }
    .container {
      max-width: 920px;
      margin: 0 auto;
      background: #fff;
      border-radius: 16px;
      padding: 24px;
      box-shadow: 0 10px 30px rgba(0,0,0,.08);
    }
    h1 { margin-top: 0; margin-bottom: 8px; }
    .summary { color: #4b5563; line-height: 1.8; margin-bottom: 20px; }
    .chat {
      border: 1px solid #e5e7eb;
      border-radius: 14px;
      background: #fafafa;
      padding: 16px;
      min-height: 420px;
      max-height: 60vh;
      overflow-y: auto;
    }
    .msg {
      margin-bottom: 14px;
      padding: 12px 14px;
      border-radius: 12px;
      line-height: 1.7;
      white-space: pre-wrap;
    }
    .user { background: #dbeafe; }
    .assistant { background: #ecfdf5; }
    .form {
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 12px;
      margin-top: 16px;
    }
    textarea {
      width: 100%;
      min-height: 90px;
      resize: vertical;
      border: 1px solid #d1d5db;
      border-radius: 12px;
      padding: 12px;
      font: inherit;
      box-sizing: border-box;
    }
    button {
      border: 0;
      border-radius: 12px;
      padding: 0 18px;
      background: #111827;
      color: #fff;
      font-weight: 700;
      cursor: pointer;
      height: 48px;
    }
    .status { margin-top: 12px; color: #6b7280; font-size: 0.95rem; }
    @media (max-width: 720px) {
      .form { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
<div class="container">
  <h1>Pi Local AI Assistant</h1>
  <p class="summary">
    Raspberry Pi 5 上で動くローカルLLMを使った簡易AIアシスタントです。
    ブラウザから質問を送り、同じ端末上で動く llama.cpp server 経由で応答します。
  </p>

  <div id="chat" class="chat"></div>

  <div class="form">
    <textarea id="input" placeholder="質問や依頼を入力してください"></textarea>
    <button id="send">送信</button>
  </div>
  <div id="status" class="status">待機中</div>
</div>

<script>
  const chatEl = document.getElementById("chat");
  const inputEl = document.getElementById("input");
  const sendEl = document.getElementById("send");
  const statusEl = document.getElementById("status");

  const messages = [];

  function addMessage(role, text) {
    const div = document.createElement("div");
    div.className = `msg ${role}`;
    div.textContent = text;
    chatEl.appendChild(div);
    chatEl.scrollTop = chatEl.scrollHeight;
  }

  async function sendMessage() {
    const text = inputEl.value.trim();
    if (!text) return;

    messages.push({ role: "user", content: text });
    addMessage("user", text);

    inputEl.value = "";
    sendEl.disabled = true;
    statusEl.textContent = "ローカルLLMが応答中です...";

    try {
      const recent = messages.slice(-10);
      const res = await fetch("/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ messages: recent })
      });

      const data = await res.json();
      if (!res.ok) {
        throw new Error(data.error || "request failed");
      }

      messages.push({ role: "assistant", content: data.reply });
      addMessage("assistant", data.reply);
      statusEl.textContent = "待機中";
    } catch (err) {
      addMessage("assistant", "エラーが発生しました。llama.cpp server の起動状態やURL設定を確認してください。");
      statusEl.textContent = "エラー";
      console.error(err);
    } finally {
      sendEl.disabled = false;
    }
  }

  sendEl.addEventListener("click", sendMessage);
  inputEl.addEventListener("keydown", (e) => {
    if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
      sendMessage();
    }
  });

  addMessage("assistant", "こんにちは。Raspberry Pi 5 上のローカルAIアシスタントです。質問をどうぞ。");
</script>
</body>
</html>

requirements.txt

Flask>=3.1
requests>=2.32

セットアップ手順

1. llama.cpp をビルドする

llama.cppの build ドキュメントでは、Arm CPU向け最適化として KleidiAI を有効化できることが案内されています。Pi5でもまずはこれを試す構成にすると分かりやすいです。
GitHub (ggml-org/llama.cpp)
※ビルドに10分程度時間がかかりました。

sudo apt update
sudo apt install -y git cmake build-essential python3-venv python3-pip

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DGGML_CPU_KLEIDIAI=ON
cmake --build build --config Release -j4

2. GGUF モデルをダウンロードする

SmolLM2 の GGUFページでは、smollm2-1.7b-instruct-q4_k_m.gguf のような量子化済みファイルを使って llama.cpp を起動する例が案内されています。ここでは q4_k_m を例にします。
huggingface.co
※量子化の手法により、モデルのサイズや精度が変わります。

python3 -m pip install -U huggingface_hub
mkdir -p models
hf download HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF \
  smollm2-1.7b-instruct-q4_k_m.gguf \
  --local-dir models

3. llama.cpp server を起動する

llama.cpp serverは、README にある通り REST API と Web UI を持つ HTTP サーバーです。今回はCPU 実行前提なので -ngl 0 としています。
GitHub

./build/bin/llama-server \
  -m models/smollm2-1.7b-instruct-q4_k_m.gguf \
  --host 0.0.0.0 \
  --port 8080 \
  -c 2048 \
  -ngl 0

4. Flask アプリを起動する

別ターミナルで、今回のWeb アプリを起動します。

cd ~/pi-local-ai-assistant
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
python app.py

起動後、同一ネットワーク上のブラウザから以下へアクセスします。

http://<raspberry pi="">:5000</raspberry>

使い方

操作の流れはシンプルです。

  1. Raspberry Pi 5でllama.cpp server を起動する

  2. Flask アプリを起動する

  3. ブラウザで画面を開く

  4. 質問を入力して送信する

  5. ローカルLLMの返答を確認する

これだけで、Pi5上だけで完結する簡易AIアシスタントが動きます。

今回は日本語で入力していることも影響しているのか、現時点ではあまり精度が高くないように感じられます。

今後の改善案

今回の PoC はテキスト対話だけですが、改善の余地はかなりあります。

まず、ストリーミング表示を入れると、返答が長いときの待ち時間を体感的に短くできます。llama.cpp server はOpenAI互換の chat completions や responses routes を持つので、段階的に応答を画面へ出す構成にも広げやすいです。

GitHub

次に、RAG 連携です。FAQ、手順書、装置マニュアルなどのローカル資料を検索して、その結果をプロンプトに渡す構成にすれば、小型モデルでも「手元資料に強いアシスタント」に発展させやすくなります。

また、Pi 5 のハードウェア面ではPCIe 2.0 x1USB 3.0MIPI カメラが使えるので、将来的にはカメラ付きローカル対話端末音声入力付き補助端末にも広げやすいです。

Raspberry Pi

最後に、モデルサイズの調整です。より軽快さを優先するならさらに小さいサイズ、返答品質を優先するなら別の量子化や別モデル、という調整が必要になります。SmolLM2 自体が compact family として設計されているので、用途に応じた比較もしやすいです。

Hugging Face

また、LLMなどのAI処理への対応として、RenesasはRZシリーズを展開しています。今回の作例では実装までは行っていませんが、今後、RZシリーの活用も有効な選択肢になると感じました。

RZファミリ 32ビットおよび64ビット・マイクロプロセッサ(MPU)

まとめ

今回は、Raspberry Pi 5上でローカルLLMを動かし、ブラウザから使える簡易AIアシスタントを作ってみました。 構成としては、Pi5の上でllama.cpp server を動かし、その上に Flask の薄い Web UI を載せるだけなので、見た目以上にシンプルです。llama.cppは最小セットアップで幅広いハードウェアに対応するローカルLLM実行系として設計されており、SmolLM2のような小型モデルと組み合わせることで、Pi5のような小型マシンでも十分PoCを組み立てられます。
GitHub
Hugging Face

大きな構成にする前に、「ローカルで質問すると返事が返る」 という基本形を小さく作ってみる。それだけでも、現場端末にどう組み込めそうかのイメージはかなり掴みやすくなります。そこから必要に応じて、ストリーミング、RAG、音声、カメラ連携へ広げていくのが扱いやすい進め方だと感じました。

参考リンク

Raspberry Pi 5 公式ページ
Raspberry Pi 5 Product Brief PDF
llama.cpp GitHub Repository
llama.cpp server README
llama.cpp build documentation
SmolLM2-1.7B-Instruct
SmolLM2-1.7B-Instruct-GGUF
Flask Documentation
MDN Fetch API
RZファミリ 32ビットおよび64ビット・マイクロプロセッサ(MPU)