アーキテクチャの設計方針
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 によるセッション検証や、ストリーミングエンドポイントへのリクエスト発行を通じて、データフローとレスポンス形式の整合性を検証可能です。