Go 言語でローカル向け LLM を扱いたい
Google や DeepL などの翻訳 API を叩くのもいいのですが、すぐに無料枠を使い切ったり、有料ライセンスにしても意図せず叩きすぎて、ビックリする課金で逆ビンタを喰らうのが怖いのです。
そこで、M4 Mac mini (16GB) で Ollama は動くし、llama3.2 程度であれば普通にサクサク動くので、ローカルで翻訳 API を Go 言語(以下 golang)で構築できないかなと思いました。つまり、golang で Ollama の API を叩き JSON で受け取りたい人向けの記事です。
Ollama とは
Ollama とは、恐れずに一言で説明すると「GGUF フォーマットの LLM をローカルで実行できるアプリ」です。Docker と同じく golang で書かれており、docker コマンドと同じ感覚で使えます。
GGUF とは
GGUF とは、Meta 社(旧 Facebook 社)の学習モデル LLaMA をベースとした、オープンな学習モデル Llama に「推論の実行に必要な情報」を埋め込んだバイナリ・フォーマットです。
Georgi Gerganov 氏が、オリジナルの LLaMA 向けに GGML (Georgi Gerganov Machine Learning) として発案し、LLaMA の派生学習モデル(Llama 系の学習モデル)でも使えるように互換を持たせた後継のフォーマットが GGUF です。
学習モデルの拡張子は .gguf で、Ollama の公式からダウンロードできるモデル以外にも、Hugging Face に公開されている GGUF の野良モデルもダウンロードして手動で設置すると利用できます。
このバイナリ・データを、同氏が開発した軽量推論プログラム「llama.cpp」に読み込むだけで推論を実行できます。llama.cpp は C/C++ で書かれており、FFI としても機能するため他のプログラム言語からも利用できます。Ollama も cgo としてバックエンドに llama.cpp を使っています。
llama.cpp の特筆すべき点は、NVIDIA の GPU がなくても実行できることです。
Apple Silicon の Metal(M 系 Mac の GPU ライブラリ)、AMD の GPU や Linux の Vulkan にも対応しており、いずれの GPU 機能が使えない場合は CPU でマルチスレッド動作すると言う、非 Windows/NVIDIA ユーザーの救世主です。
ここまで読んで「すげー」と感じた方は、ぜひ llama.cpp のリポジトリに「いいね」しに行ってあげてください。
Ollama の公式 Go クライアント・ライブラリについて
Ollama の公式 Go クライアント・ライブラリについて
Ollama の公式クライアント・ライブラリに Python などはあるものの、golang の記載がありませんでした。
3rd パーティ製を使うのかと思ったのですが、本体のソースコードのパッケージにありました。当然すぎるのか、どこにも明記されていないので自分のググラビリティ(備忘録)として。
動作環境(macOS)
$ sw_vers
ProductName: macOS
ProductVersion: 26.0.1
BuildVersion: 25A362
$ ollama --version
ollama version is 0.12.5
$ go version
go version go1.25.2 darwin/arm64
$ go list -m all | grep github.com/ollama/ollama
github.com/ollama/ollama v0.12.5
この記事では macOS を前提としていますが、Ollama のアプリ自体は Windows 版や Linux 版もあるのでダウンロード以外は、大きな違いはないと思います。
TL; DR (今北産業)
ポイントは、Ollama API を叩く際のリクエストで Format フィールドに、レスポンスの JSON スキーマを指定することです。これにより、レスポンスからデータを取り出す手間が圧倒的に減ります。
// Ollama API クライアントのパッケージ
import "github.com/ollama/ollama/api"
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/ollama/ollama/api"
)
// Translation は Ollama から返される JSON レスポンスをマッピングする構造体です。
type Translation struct {
English string `json:"english"`
}
func main() {
// ========================== 設定 ==========================
const (
// 使用モデル
modelName = "llama3.2"
// 翻訳対象の日本語文
japaneseText = "私は昨日、友達と映画を見に行きました。"
// プロンプト・テンプレート
promptTemplate = "Translate this Japanese sentence to English: %s"
// 温度(0.0 = 決定論的)
temperature = 0.0
// レスポンスのパース用 JSON スキーマ
schema = `{
"type": "object",
"properties": {
"english": { "type": "string" }
},
"required": ["english"],
"additionalProperties": false
}`
)
// ========================== Ollama クライアント ==========================
client, err := api.ClientFromEnvironment()
if err != nil {
log.Fatalf("Ollama クライアントの作成に失敗: %v", err)
}
// ========================== リクエスト作成 ==========================
req := &api.GenerateRequest{
Model: modelName,
Prompt: fmt.Sprintf(promptTemplate, japaneseText),
Format: json.RawMessage(schema), // ← ここがポイント
Options: map[string]any{
"temperature": temperature,
},
}
// ========================== レスポンス受信 ==========================
var responseBuilder strings.Builder
err = client.Generate(
context.Background(),
req,
func(resp api.GenerateResponse) error {
_, err := responseBuilder.WriteString(resp.Response)
return err
},
)
if err != nil {
log.Fatalf("生成処理に失敗: %v", err)
}
rawOutput := strings.TrimSpace(responseBuilder.String())
// ========================== 結果出力 ==========================
fmt.Println("=== Raw Output ===")
fmt.Println(rawOutput)
fmt.Println()
// JSON パース
var result Translation
if err := json.Unmarshal([]byte(rawOutput), &result); err != nil {
log.Fatalf("JSON パースエラー: %v\nRaw output:\n%s", err, rawOutput)
}
// 最終結果表示
fmt.Println("🇯🇵 原文:", japaneseText)
fmt.Println("🗣️ 英語翻訳:", result.English)
}
% go run .
=== Raw Output ===
{
"english": "I went to the movies with my friends yesterday."
}
🇯🇵 原文: 私は昨日、友達と映画を見に行きました。
🗣️ 英語翻訳: I went to the movies with my friends yesterday.
翻訳 API と言っちゃうと、たいそうなイメージですが、基本的に Ollama の API を叩いた結果を確実に JSON 形式で受け取れれば、何でも機械的に処理できるので、翻訳に使ってみようというものです(この記事では、本格的な API 実装はしていませんが、JSON で受け取れれば後は何とかなるという系です)。
流れ的には以下をするだけ。
- Ollama 本体のソースコードに API クライアントの GO パッケージが内包されているので、それを利用する
- ローカルに立ち上げた Ollama サーバーへ翻訳のリクエストをする
- レスポンスは JSON で出力させ、パースした翻訳結果を出力する
つまり、どうすれば安定した JSON フォーマットでレスポンスを LLM から受け取れるかがポイントになります。
プロンプトに「レスポンスは以下の JSON 形式で」のように書いても大抵は動きますが、翻訳結果を JSON データだけで欲しいのに、モデル・温度・翻訳言語によって「承知しました。以下が日本語を⚫︎⚫︎に翻訳した JSON ...」と返ってくることがあります。
そこで、リクエスト時の Format フィールドに JSON の構造(スキーマ)が指定すれば、可能な限りその JSON 形式でレスポンスが返ってきます。あとは用途にあわせて自由に実装できるという寸法です。
その他のコマンド
$ go get github.com/ollama/ollama
$ # Apple Silicon の場合 Metal 対応版が入り Mac の GPU が利用できる
$ # Intel Mac の場合は CPU で動作する
$ brew install ollama
$ # Windows/Linux --> https://ollama.com/download
$ # 別ターミナルで実行すること
$ ollama serve
$ ollama pull llama3.2
pulling manifest
pulling dde5aa3fc5ff: 100% ▕████████████████████████████████████████████████████████▏ 2.0 GB
pulling 966de95ca8a6: 100% ▕████████████████████████████████████████████████████████▏ 1.4 KB
pulling fcc5a6bec9da: 100% ▕████████████████████████████████████████████████████████▏ 7.7 KB
pulling a70ff7e570d9: 100% ▕████████████████████████████████████████████████████████▏ 6.0 KB
pulling 56bb8bd477a5: 100% ▕████████████████████████████████████████████████████████▏ 96 B
pulling 34bb5ab01051: 100% ▕████████████████████████████████████████████████████████▏ 561 B
verifying sha256 digest
writing manifest
success