WailsデスクトップアプリにGinサーバーを統合しローカルAIモデルAPIを提供する実装方法

アーキテクチャの設計方針

Wailsフレームワークを用いたクロスプラットフォームデスクトップアプリケーションにおいて、バックエンドで独立したHTTPサーバーを起動し、ローカル実行の大規模言語モデル(LLM)へアクセスするためのAPI層を構築する手法を解説します。本構成では、推論バックエンドとしてGGML形式に対応したライブラリをラップし、GinルーターをWailsのライフサイクルフックに組み込むことで、特定ポート上でRESTfulインターフェースを公開します。

主要な構成要素は以下の通りです:

  • Wails:GoロジックとWebフロントエンドのネイティブバインディング
  • Gin:軽量HTTPルーターおよびミドルウェア管理
  • go-chatglm.cpp:ChatGLM3モデルのC++推論エンジンGoバインディング
  • chatgpt-web:UI実装およびストリーミング通信プロトコルの参照モデル

WailsライフサイクルへのHTTPサーバー統合

Wailsアプリケーションの起動フェーズでGinルーターを初期化し、別ゴルーチンでHTTPリスナーを起動します。アプリケーション終了時にはコンテキストタイムアウトを用いたGraceful Shutdownを実行し、接続中のリクエストを安全に処理した上でリソースを解放します。

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

type DesktopEngine struct {
	runtimeCtx context.Context
	apiServer  *http.Server
}

func InitializeEngine() *DesktopEngine {
	return &DesktopEngine{}
}

// OnStartup はWailsアプリケーションの初期化フェーズで呼び出されます
func (e *DesktopEngine) OnStartup(ctx context.Context) {
	e.runtimeCtx = ctx

	router := gin.Default()
	router.Use(cors.New(cors.Config{
		AllowOrigins:     []string{"*"},
		AllowMethods:     []string{"GET", "POST", "OPTIONS"},
		AllowHeaders:     []string{"Content-Type", "Authorization"},
	}))

	router.POST("/session", handleSessionCheck)

	e.apiServer = &http.Server{
		Addr:         ":9999",
		Handler:      router,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 30 * time.Second,
	}

	go func() {
		log.Printf("ローカルAPIサーバー起動中 (ポート: 9999)")
		if err := e.apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("HTTPサーバー起動失敗: %v", err)
		}
	}()
}

// OnShutdown はアプリケーション終了直前に実行されます
func (e *DesktopEngine) OnShutdown(ctx context.Context) {
	log.Println("HTTPサーバーの安全な停止処理を開始...")
	shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	if err := e.apiServer.Shutdown(shutdownCtx); err != nil {
		log.Printf("サーバー停止エラー: %v", err)
	}
}

// handleSessionCheck は認証不要のセッション状態返却エンドポイント
func handleSessionCheck(c *gin.Context) {
	response := gin.H{
		"status":  "OK",
		"message": "",
		"data": gin.H{
			"auth":  false,
			"model": "chatglm3-ggml",
		},
	}
	c.JSON(http.StatusOK, response)
}

func (e *DesktopEngine) SayHello(username string) string {
	log.Printf("リクエスト受信: %s", username)
	return fmt.Sprintf("接続確認完了, %s", username)
}

ストリーミング応答の構築

モデル推論結果をリアルタイムでフロントエンドへ送信するには、http.ResponseWriterを直接操作し、チャンク単位でデータをフラッシュする実装が推奨されます。以下はストリーミングエンドポイントの骨子実装です。

func handleChatStream(c *gin.Context) {
	c.Writer.Header().Set("Content-Type", "text/event-stream")
	c.Writer.Header().Set("Cache-Control", "no-cache")
	c.Writer.Header().Set("Connection", "keep-alive")
	c.Writer.Flush()

	streamChan := engine.GenerateStream(c.Request.Context())

	isFirst := true
	for chunk := range streamChan {
		if chunk.Error != nil {
			sendErrorResponse(c, chunk.Error)
			return
		}

		payload := StreamPayload{
			ID:    chunk.RequestID,
			Delta: chunk.Content,
			Text:  chunk.AccumulatedText,
		}

		jsonData, err := json.Marshal(payload)
		if err != nil {
			sendErrorResponse(c, fmt.Errorf("JSONマーシャル失敗: %w", err))
			return
		}

		if !isFirst {
			c.Writer.Write([]byte("\n"))
		}
		isFirst = false

		c.Writer.Write(jsonData)
		c.Writer.Flush()
	}
}

type StreamPayload struct {
	ID    string `json:"id"`
	Delta string `json:"delta"`
	Text  string `json:"text"`
}

func sendErrorResponse(c *gin.Context, err error) {
	log.Printf("ストリーミング中断: %v", err)
	c.JSON(http.StatusOK, gin.H{
		"status":  "Error",
		"message": err.Error(),
		"data":    nil,
	})
}

上記の構成により、推論エンジンが出力するトークン列を逐次クライアントへ転送できます。フロントエンド側では fetch API または ReadableStream を用いてバイトデータをパースし、UIにリアルタイム反映させる実装が標準的です。

Linux環境での実行と検証

本アーキテクチャはLinuxディストリビューション上でネイティブ動作します。ビルド前にCGOが有効化されていることを確認し、推論ライブラリが依存するC++コンパイラおよびBLAS実装をシステムにインストールしてください。wails build コマンドでバイナリを生成後、実行ファイルを開始するとGUIウィンドウの表示と同時にポート9999のHTTPリスナーがバックグラウンドで起動します。curl -X POST http://localhost:9999/session によるセッション検証や、ストリーミングエンドポイントへのリクエスト発行を通じて、データフローとレスポンス形式の整合性を検証可能です。

タグ: wails golang gin-framework llm-inference desktop-application

7月15日 01:17 投稿