1. はじめに

「社内データを外に出さずにチャットを試したい」「オフラインで動く LLM アプリが欲しい」——そんなときに便利なのが Foundry Local です。Foundry Local は Microsoft が提供するオンデバイス AI ソリューションで、モデルの取得・ハードウェア高速化・推論をアプリのプロセス内で完結させます。

本記事では、Foundry Local を使って 端末内だけで動くマルチターン・チャットアプリを作ります。OpenAI 互換 API に対応しているため、普段 OpenAI SDK を使っている方ならほぼ同じ書き方で実装できます。

2. Foundry Local とは

Foundry Local は、ONNX Runtime を基盤とする軽量ランタイムで、次の特徴があります。

  • オンデバイス完結: プロンプトも応答も端末内で処理され、初回のモデル取得以外はネットワーク不要です。
  • ハードウェア自動選択: GPU・NPU・CPU を検出し、最適な実行プロバイダーを自動で選びます。
  • OpenAI 互換 API: OpenAI SDK や REST クライアントからそのまま呼び出せます。
  • Azure サブスクリプション不要: ローカルのハードウェアだけで動きます。

カタログには Phi・Qwen・DeepSeek・Mistral・GPT OSS などのチャットモデルが収録されています。他のランタイム(Ollama・LM Studio・llama.cpp・vLLM)との位置づけは 2026年版 ローカル LLM 実行環境の比較:Foundry Local・Ollama・LM Studio・llama.cpp・vLLM を参照してください。

3. 事前準備:Foundry Local のインストール

まず Foundry Local 本体をインストールします。OS に合わせて次のコマンドを実行してください。

# Windows
winget install Microsoft.FoundryLocal

# macOS(Apple Silicon)
brew tap microsoft/foundrylocal
brew install foundrylocal

インストールできたらバージョンとサービス状態を確認します。

foundry --version
foundry service status

サービス接続エラー(Request to local service failed など)が出た場合は、foundry service restart で復旧することが多いです。

4. CLI でチャットを動作確認する

アプリを書く前に、CLI だけでチャットを試して動作を確認しましょう。まず利用可能なモデルの一覧を表示します(初回はハードウェアに合う実行プロバイダーが自動ダウンロードされます)。

foundry model list

一覧からエイリアスを選び、対話セッションを開始します。ここでは軽量で扱いやすい Phi の小型モデルを使います。

foundry model run phi-4-mini

初回はモデルがダウンロードされ、その後プロンプトを入力すると応答が返ります。foundry model run はエイリアスを指定すると、端末のハードウェアに最適なモデルの変種(CUDA 版・NPU 版など)を自動で選択します。

phi-4-mini が見つからない場合は、foundry model list に表示されるエイリアスへ読み替えてください。とにかく軽く試すなら qwen2.5-0.5b でも動きます。

5. アプリの全体像

これから作るアプリの処理経路は次のとおりです。アプリは OpenAI SDK 経由で、Foundry Local が起動する OpenAI 互換のローカル REST サーバーに接続します。その先は Core API → ONNX Runtime → 実行プロバイダーと、すべて端末内で処理されます。

Foundry Local チャットアプリのアーキテクチャ。Python アプリのチャットループと OpenAI SDK から、Foundry Local のローカル REST サーバー(OpenAI 互換 /v1)、Core API、ONNX Runtime、実行プロバイダー(CUDA・WebGPU・NPU・CPU)へとリクエストが流れ、応答がストリーミングで返る。Foundry Local の各要素は端末内で動作する

6. Python プロジェクトの準備

仮想環境を作り、必要な 2 つのパッケージをインストールします。

python -m venv .venv
# Windows: .venv\Scripts\activate / macOS・Linux: source .venv/bin/activate
pip install -U pip
pip install foundry-local-sdk openai

Windows でより広いハードウェアアクセラレーション(Windows ML 連携)を使う場合は、foundry-local-sdk の代わりに foundry-local-sdk-winml をインストールします。

7. チャットアプリの実装

app.py を作成します。処理は「Foundry Local を初期化してモデルをロードする準備部分」と「会話を繰り返すチャットループ」に分かれます。

会話履歴を messages リストに積み上げることでマルチターンの対話になり、stream=True でトークンを逐次表示します。

# app.py
import openai
from foundry_local_sdk import Configuration, FoundryLocalManager

ALIAS = "phi-4-mini"  # foundry model list で確認。軽量に試すなら "qwen2.5-0.5b"


def setup_chat_client():
    """Foundry Local を初期化し、モデルをロードして OpenAI 互換クライアントを返す。"""
    # 1. SDK を初期化
    config = Configuration(app_name="local-chat-app")
    FoundryLocalManager.initialize(config)
    manager = FoundryLocalManager.instance

    # 2. 端末に最適な実行プロバイダー(EP)を取得・登録(初回のみ時間がかかる)
    manager.download_and_register_eps(
        progress_callback=lambda ep, pct: print(
            f"\rEP 準備中 {ep}: {pct:5.1f}%", end="", flush=True
        )
    )
    print()

    # 3. モデルを取得・ダウンロード・ロード
    model = manager.catalog.get_model(ALIAS)
    model.download(lambda pct: print(f"\rモデル取得中: {pct:5.1f}%", end="", flush=True))
    print()
    model.load()
    print(f"モデル {model.id} をロードしました。")

    # 4. OpenAI 互換のローカル REST サーバーを起動して接続
    manager.start_web_service()
    client = openai.OpenAI(base_url=f"{manager.urls[0]}/v1", api_key="none")
    return manager, model, client


def main():
    manager, model, client = setup_chat_client()
    messages = [
        {"role": "system", "content": "あなたは親切で簡潔に答える日本語アシスタントです。"}
    ]

    print("\nチャットを開始します('exit' で終了)\n")
    try:
        while True:
            user_input = input("You: ").strip()
            if user_input.lower() in {"exit", "quit"}:
                break
            if not user_input:
                continue

            messages.append({"role": "user", "content": user_input})

            # ストリーミングで応答を受け取りつつ表示
            print("AI: ", end="", flush=True)
            reply = ""
            stream = client.chat.completions.create(
                model=model.id,
                messages=messages,
                stream=True,
            )
            for chunk in stream:
                if chunk.choices and chunk.choices[0].delta.content:
                    piece = chunk.choices[0].delta.content
                    print(piece, end="", flush=True)
                    reply += piece
            print("\n")

            # 応答を履歴に追加して次のターンの文脈にする
            messages.append({"role": "assistant", "content": reply})
    finally:
        # 後片付け:モデルをアンロードし、サーバーを停止
        model.unload()
        manager.stop_web_service()
        print("モデルをアンロードしました。")


if __name__ == "__main__":
    main()

チャットループの中で messages がどう育つかを図にすると、マルチターンの仕組みがつかめます。毎ターン、ユーザー発話と AI 応答の両方を履歴へ積み上げ、その全体を毎回モデルへ送ることで文脈が保たれます。

チャットの 1 ターンのシーケンス。ユーザーが質問を入力するとアプリが messages に user を追加し、会話履歴全体を /v1/chat/completions へ送信。Foundry Local がトークンをストリーミングで返し、アプリが逐次表示したうえで assistant 応答を messages に追加する。これを会話が続く限り繰り返す

8. 実行する

仮想環境を有効にした状態で実行します。

python app.py

初回は実行プロバイダーとモデルのダウンロードに数分かかることがあります。準備が終わると対話できます。

モデル Phi-4-mini-instruct-... をロードしました。

チャットを開始します('exit' で終了)

You: 自己紹介して
AI: こんにちは。ローカルで動く AI アシスタントです。…
You: いまの説明を一言で
AI: 端末内で動く、あなた専用のアシスタントです。
You: exit
モデルをアンロードしました。

2 つ目の質問で「いまの説明を」と指示しても文脈が通じるのは、会話履歴を messages に積み上げているためです。

9. (任意)Gradio で Web UI にする

ターミナルの代わりにブラウザの UI で使いたい場合は、Gradio を使うと数行で Web チャットにできます。pip install gradio を追加し、先ほどの setup_chat_client() を再利用します。

# web.py
import gradio as gr
from app import setup_chat_client  # 7 章のセットアップ関数を再利用

manager, model, client = setup_chat_client()


def chat(message, history):
    # history は {"role", "content"} の辞書リスト(Gradio 5 の messages 形式)
    messages = [{"role": "system", "content": "あなたは親切な日本語アシスタントです。"}]
    messages.extend(history)
    messages.append({"role": "user", "content": message})

    reply = ""
    stream = client.chat.completions.create(
        model=model.id, messages=messages, stream=True
    )
    for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            reply += chunk.choices[0].delta.content
            yield reply  # 部分応答を逐次返してストリーミング表示


gr.ChatInterface(chat, type="messages", title="ローカルチャット (Foundry Local)").launch()

python web.py を実行すると、http://127.0.0.1:7860 でブラウザチャットが開きます。モデルは起動時に一度だけロードし、リクエストごとに再読込しないのがポイントです。

ノーコードでブラウザ UI を使いたいだけなら、CLI で foundry model run <model> を起動したまま、Open WebUI を Foundry Local のローカルエンドポイントに接続する方法もあります。

10. つまずきポイント

  • モデルのエイリアスが見つからない: foundry model list で実際に利用可能なエイリアス/モデル ID を確認し、ALIAS を読み替えます。
  • 初回が遅い: 実行プロバイダーとモデルのダウンロードが走るためです。2 回目以降はキャッシュから即座に起動します。
  • サービスに接続できない: foundry service restart でサービスを再起動します。状態は foundry service status で確認できます。
  • 応答品質が物足りない: 小型モデルは複雑な推論が苦手です。foundry model list でより大きめのモデルを選ぶか、用途に合うモデルへ切り替えます。
  • リソースを解放したい: 終了時に model.unload()manager.stop_web_service() を必ず呼びます(本記事のコードは finally で実行します)。

11. まとめ

  • Foundry Local なら、foundry-local-sdkopenai の 2 パッケージで端末内完結のチャットアプリを作れます。
  • 会話履歴を messages に積み上げればマルチターン、stream=True でトークンの逐次表示が実現できます。
  • モデルは初回だけダウンロードされ、以降はオフラインで動作。プロンプトも応答も端末の外に出ません。
  • Gradio を足せば、同じロジックのままブラウザ UI に拡張できます。
  • 次のアクション: 軽い処理はローカル、重い処理はクラウドへ振り分ける構成は ローカル LLM × Azure エージェント:Phi-4 と Azure を組み合わせたコスト削減構成 が参考になります。

12. 参考リンク


この記事の執筆にあたり、AI の支援を受けています。