ExcelからローカルLLM(Ollama)を呼べるか試してみた →Power Queryだけでどこまでできる?

最近、ローカルLLMを使用していると、いろいろなアプリケーションから使いたいなと思っていて、ふと「Excelから直接ローカルLLMを呼べないかな?🤔」と思ったんですよね。

ExcelからLLMといえばVBAを使う方法が真っ先に浮かびますが、正直なところ新規でVBAを書くのはちょっと遠慮したい…😫という気持ちがあります。VBA自体は1990年代からの技術で、Microsoftも新規開発には推奨していないような状況ですし。

そこで調べてみたら、Power Query(M言語) を使えばVBAを書かずにAPIアクセスができることがわかりました。実際にやってみた体験をまとめておこうと思います。

実験環境

  • OS … Windows 11
  • Excel … Microsoft 365
  • Ollama … GPUのないローカルPCで起動(http://localhost:11434
  • モデル … gemma3:4b

OllamaはインストールしてPCで起動している前提にしています(現在のバージョンをインストールすると自動的に起動します)。ブラウザで http://localhost:11434 にアクセスして、Ollama is running と表示されればOKです。

Power Queryって何?

Excelの「データ」タブにある機能で、外部データソースからデータを取得・変換してテーブルに展開してくれるものです。内部ではM言語という関数型言語が使われていて、GUIで操作すると裏側でM言語が自動生成される仕組みになっています。

普段は「CSVを読み込む」とか「データベースに接続する」といった用途で使われることが多いんですが、実は Web.Contents という関数でHTTPリクエスト(POST含む)を送信することができます。つまり、OllamaのREST APIにもアクセスできるということです(つまるところ、OpenAI互換のAPIも可能ですよね)。

まずは接続確認から

いきなりLLMを呼ぶ前に、まずExcelからOllamaに到達できるか確認します。まずはExcelで新規ファイルを作成し、

【データ】タブ →【Webから】 でURLに http://localhost:11434 を入力し、【OK】ボタンをクリックします。

⚠️処理中にプライバシーレベルを聞かれたら匿名を選択して接続してください。

Power QueryエディターOllama is running が表示されました。ただ、テキストがスペース区切りで「Ollama」「is」「running」と3列に分かれてしまいましたが(笑)、接続確認なのでこれで十分です。

ローカルLLMを使用してテキスト生成をやってみる

ここからが本番となります。OllamaのAPIにPOSTリクエストを送って、LLMの回答を取得してみます。

【データ】タブ →【データの取得】→【その他のデータソースから】→【空のクエリ】 で詳細エディターを開き、以下のM言語のコードを入力します。

let
    requestBody = Json.FromValue([
        model = "gemma3:4b",
        stream = false,
        messages = {
            [
                role = "system",
                content = "あなたは親切なアシスタントです。回答は簡潔に日本語で行ってください。"
            ],
            [
                role = "user",
                content = "日本の四季について50文字で説明してください。"
            ]
        }
    ]),

    response = Web.Contents(
        "http://localhost:11434/api/chat",
        [
            Headers = [#"Content-Type" = "application/json"],
            Content = requestBody,
            Timeout = #duration(0, 0, 3, 0)
        ]
    ),

    json = Json.Document(response),
    message = json[message],

    result = Table.FromRecords({[
        response = message[content],
        model = json[model],
        duration_sec = Number.Round(json[total_duration] / 1000000000, 2)
    ]})
in
    result

👉️ポイント!  stream = false の指定です。Ollamaのデフォルトはストリーミングモードなんですが、Power Queryではストリーミングレスポンスを受信処理できないので、これの指定を入れる必要があります。あと、LLMの推論には時間がかかるため Timeout も3分に設定しています。

ローカルLLMの回答がExcelで取得できています🤩🤩🤩

実用的なプロンプト管理シートを作る

1つのプロンプトを試すだけなら上のコードで十分ですが、もうちょっと実用的にしたい。そこで、プロンプトをExcelのテーブルに入力して、一括でLLMに投げる仕組みを作ってみました。

入力テーブルの準備

まず、シートに以下のようなテーブルを作ります。A1セルにプロンプトと入力して、A2セル以降にプロンプト文を入力し、Ctrl + T でテーブル化。テーブル名はテーブル デザインタブで InputPrompts に変更します。

ちなみに、テーブルに名前を付けるのはエイリアスのような仕組みで、Power Queryからこの名前で参照できるようになります。セルの位置を後から動かしても名前が追従してくれるので便利です。

Power Queryで一括処理

【データ】タブ →【データの取得】→【その他のデータソースから】→【空のクエリ】を作成して、詳細エディターに以下を入力します。

let
    source = Excel.CurrentWorkbook(){[Name="InputPrompts"]}[Content],

    filtered = Table.SelectRows(source, each
        [プロンプト] <> null and Text.Trim(Text.From([プロンプト])) <> ""
    ),

    addResult = Table.AddColumn(filtered, "生成結果", each
        let
            requestBody = Json.FromValue([
                model = "gemma3:4b",
                stream = false,
                messages = {
                    [
                        role = "system",
                        content = "あなたは親切なアシスタントです。回答は簡潔に日本語で行ってください。"
                    ],
                    [
                        role = "user",
                        content = Text.From([プロンプト])
                    ]
                }
            ]),
            raw = Web.Contents(
                "http://localhost:11434/api/chat",
                [
                    Headers = [#"Content-Type" = "application/json"],
                    Content = requestBody,
                    Timeout = #duration(0, 0, 3, 0),
                    IsRetry = true
                ]
            ),
            json = Json.Document(raw)
        in
            json[message][content],
        type text
    ),

    addModel = Table.AddColumn(addResult, "モデル", each "gemma3:4b", type text),

    addTimestamp = Table.AddColumn(addModel, "生成時刻", each DateTime.LocalNow(), type datetime)
in
    addTimestamp

👉️IsRetry = truePower Queryのキャッシュを無効化するための設定です。これがないと、Power Queryは「URL + ヘッダー + ボディ」の組み合わせが前回と同じ場合、実際にはOllamaにアクセスせず前回のレスポンスを再利用してしまいます。同じプロンプトでも毎回新しい回答が欲しい場合は必須の設定となります。

クエリ名LLM生成結果に変更して、【閉じて読み込む】をクリックします。

3つのプロンプトに対してちゃんと回答が返ってきています。

ボタンを付けてアプリっぽくする

ここまでで、【プロンプトを入力】 → 【データタブの"すべて更新"で実行】という流れはできましたが、もうちょっとアプリっぽくしたいですよね。シート上に【LLM生成実行】ボタンを置いて、クリックで実行できるようにしてみます。

ただ、実は問題があります。Power Queryデータを取得・変換する仕組みのため、ボタンクリック等のUIイベントは扱えないです😫ここだけは最小限のVBAが必要になります。

Alt + F11 でVBAエディターを開き、標準モジュールに以下のコードを追加。

Sub LLM生成実行()
    ThisWorkbook.RefreshAll
End Sub

この1行のみです。これを図形(「挿入」タブ →「図形」)に右クリック → 「マクロの登録」で割り当てるだけ。

これでボタンをクリックするだけでLLM生成が走るようになります。

VBAは「更新ボタンを押す」という最小限の役割のみです。処理ロジックはすべてPower Query側にあります。

⚠️マクロを含むため、ファイルの保存時は .xlsm(マクロ有効ブック) を選択してください。.xlsx で保存するとマクロが消えてしまうので注意です。

systemプロンプトもセルで管理できる

systemプロンプト(LLMへの指示)がコード内にハードコードされているのは少し不便なので、これもセルから変更できるようにしてみます。

任意のセルにsystemプロンプトを入力して、【数式】タブ →【名前の定義】で SystemPrompt という名前を付けます。あとはPower Queryコードの先頭でこのセルの値を取得して、messages 配列で参照するだけ。

let
    // systemプロンプトをセルから取得
    sysTable = Excel.CurrentWorkbook(){[Name="SystemPrompt"]}[Content],
    sysValue = Text.From(sysTable{0}[Column1]),

    // 入力テーブルからプロンプト一覧を取得
    source = Excel.CurrentWorkbook(){[Name="InputPrompts"]}[Content],

    // 空行を除外(プロンプトが未入力の行はスキップ)
    filtered = Table.SelectRows(source, each
        [プロンプト] <> null and Text.Trim(Text.From([プロンプト])) <> ""
    ),

    // 各プロンプトに対してOllama APIを呼び出し
    addResult = Table.AddColumn(filtered, "生成結果", each
        let
            requestBody = Json.FromValue([
                model = "gemma3:4b",
                stream = false,
                messages = {
                    [
                        role = "system",
                        content = sysValue
                    ],
                    [
                        role = "user",
                        content = Text.From([プロンプト])
                    ]
                }
            ]),
            raw = Web.Contents(
                "http://localhost:11434/api/chat",
                [
                    Headers = [#"Content-Type" = "application/json"],
                    Content = requestBody,
                    Timeout = #duration(0, 0, 3, 0),
                    IsRetry = true
                ]
            ),
            json = Json.Document(raw)
        in
            json[message][content],
        type text
    ),

    // モデル名の列を追加
    addModel = Table.AddColumn(addResult, "モデル", each "gemma3:4b", type text),

    // 生成時刻の列を追加
    addTimestamp = Table.AddColumn(addModel, "生成時刻", each DateTime.LocalNow(), type datetime)
in
    addTimestamp

ただし、この方法を使う場合は クエリのオプションでプライバシーレベルの設定変更が必要 です。 【データ】タブ →【データの取得】→【クエリのオプション】→【現在のブック】→【プライバシー】で「プライバシーレベルを無視する」に変更してください。

この設定はPower Queryが複数のデータソース(InputPromptsテーブル、SystemPromptセル、Ollama API)の組み合わせに対してプライバシーチェックを行います。通常は「同じレベルのデータソース同士は結合できるが、異なるレベルのデータソース同士は結合できない」というルールがあるため、SystemPromptセルを参照する場合はこのチェックを無効化する必要があります。

ExcelからローカルLLMを使う意味

正直なところ「ExcelとローカルLLMの組み合わせって便利なの?」と思う方もいると思います。自分もやってみるまでは半信半疑でした。

実際にやってみて感じたメリットは以下のような点でしょうか。

  • 取得結果をExcelで使用できる … これがやりたいことですよね。
  • データが外に出ない … ローカルLLMなのでクラウドに送信されない。業務データを含むプロンプトでも安心
  • 追加コストゼロ … API利用料がかからない
  • Excelという既存UIをそのまま使える … 新しいツールを覚える必要がない
  • ファイルを配布すれば他の人も使える … Ollamaさえ起動していれば、ファイルを開いてボタンを押すだけ

一方で、UIイベントの処理にどうしても最小限のVBAが必要だったり、大量処理には向かないなど、制約もあります。Excelの標準機能だけでは「アプリっぽく使う」には限界があるなというのが正直な感想です🙄

おわりに

ExcelのPower QueryでローカルLLM(Ollama)に接続する方法を試してみました。

「ExcelからLLMを使う」というと大げさに聞こえますが、やっていることは Web.Contents でHTTPリクエストを送ってJSONを展開しているだけなので、Power Queryの基本的な機能の延長線上です。

VBAを使わずにAPIアクセスできるPower Queryはなかなか便利で、今回のようなREST API連携には十分使える印象です。ExcelとローカルLLMを繋げるというのはあまり聞かないパターンだと思いますが、業務でExcelを使いつつローカルLLMも活用したいという方には、一つの選択肢になるのではないでしょうか。

とはいえ、本格的にUI付きで使いたいなら別の手段を検討した方が自由度は高いと思います。用途に合わせて使い分けるのがよさそうですね😊