古いEmacs (26.3) を最新のローカルLLMサーバーと連携させる最短経路

この記事を読むのに掛かる時間: 8

私は,ことサーバー運用に関しては保守的な人間です.
今回,Xubuntu 20.04環境のEmacs 26.3という「枯れた」環境から,常時稼働している llama.cppLM Studio のAPIを叩いて,執筆やコーディングを爆速にする仕組みを構築しました.

モダンなパッケージ(=gptel= 等)が依存関係で動かない環境でも,標準機能と curl だけで動作する「枯れた実装」の決定版です.

スポンサーリンク
336×280

1. サーバー側:モデル選択・起動自動化スクリプト

サーバー側では,モデルファイルを自動スキャンし,ホスト名(CPU/GPU環境)に応じて適切なバイナリとコンテキスト長を選択するスクリプトを運用します.
--host 0.0.0.0 を指定することで,外部のEmacsからの接続を許可するのがポイントです.

#! /bin/bash

# --- LLM Config ---
LM_MODEL_DIR='/home/yourname/LLM/llama_models'

#サーバー名を見て,環境を切り替える
case $(hostname) in
    gate)       LM_HOME_DIR="/home/yourname/LLM/llama.cpp/build/bin"
        LM_NGL=0 # CPU
        ;;
    powerfull)  LM_HOME_DIR="/home/yourname/LLM/llama.cpp/build-cuda/bin"
        LM_NGL=99 # GPU
        ;;
esac

LM_N=1024 # 打ち止め
LM_TEMP=0.1 # 正確さ

function llama-run() {
    # 1. モデルファイルを自動スキャン (.ggufのみ)
    local FILES=($(ls "$LM_MODEL_DIR"/*.gguf 2>/dev/null | xargs -n 1 basename))

    if [ ${#FILES[@]} -eq 0 ]; then
    echo "Error: No .gguf files found in $LM_MODEL_DIR"
    return 1
    fi

    # 2. メニュー表示
    echo "--- Found Models in $LM_MODEL_DIR ---"
    for i in "${!FILES[@]}"; do
    echo "$((i+1))) ${FILES[$i]}"
    done
    read -p "Select number: " MODEL_CH

    # 3. 選択したモデルの設定
    local SELECTED_FILE="${FILES[$((MODEL_CH-1))]}"
    if [ -z "$SELECTED_FILE" ]; then echo "Invalid selection"; return 1; fi

    LM_MODEL="$SELECTED_FILE"

    # 4. VRAM容量に配慮した動的コンテキスト長設定
    if [[ "$LM_MODEL" =~ [7-9][Bb] ]]; then
    LM_CTX=1024
    echo "Notice: Large model detected. Setting CTX to $LM_CTX for VRAM safety."
    else
    LM_CTX=2048
    fi

    echo "Launching: $LM_MODEL (CTX: $LM_CTX)"
    read -p "Mode? [1: CLI (default), 2: Server]: " MODE_CH

    if [ "$MODE_CH" = "2" ]; then
    # ポート8765でAPIサーバーを起動
    "$LM_HOME_DIR/llama-server" -m "$LM_MODEL_DIR/$LM_MODEL" --host 0.0.0.0 --port 8765 -ngl $LM_NGL -c $LM_CTX
    else
    "$LM_HOME_DIR/llama-cli" -m "$LM_MODEL_DIR/$LM_MODEL" -c $LM_CTX -n $LM_N --temp $LM_TEMP -ngl $LM_NGL
    fi
}

llama-run

2. Emacs側:API連携設定 (init.el)

Emacs 26.3の動的スコープ環境でも,非同期でLLMの出力を受け取るための実装です.
.emacs などに保存して下さい.
文章(プロンプト)をリージョン選択した状態で M-[RET] でLLMに送信します.

(require 'json)

(defun my-llama-complete ()
  "LLMサーバー設定を一括管理し、選択範囲または直前のテキストを補完する。"
  (interactive)
  (let* (;; === [設定項目] ここを変更してください ===
     (server-ip   "127.0.0.1")       ; サーバーのIPアドレス
     (server-port "8765")            ; サーバーのポート番号 (起動スクリプトと合わせる)
     (max-gen     2000)               ; AIが返してくる最大文字数 (max_tokens)
     (ctx-size    2000)              ; AIに送る過去の文脈文字数 (prompt length)
     (timeout     300)                ; 応答を待つ秒数 (タイムアウト)
     ;; ==========================================

     ;; 接続先URLの構築
     (server-url (format "http://%s:%s/v1/completions" server-ip server-port))
     ;; 選択範囲があれば優先し、なければカーソル直前から指定文字数分を取得
     (prompt (if (use-region-p)
             (buffer-substring-no-properties (region-beginning) (region-end))
           (buffer-substring-no-properties (max (point-min) (- (point) ctx-size)) (point))))
     ;; 送信用JSONデータの構築
     (json-data (json-encode `((prompt . ,prompt) 
                   (max_tokens . ,max-gen) 
                   (stream . :json-false))))
     (target-buffer (current-buffer))
     (output-buffer " *llama-temp*"))

    (message "Llama 推論中... (最大 %d トークン)" max-gen)
    (when (get-buffer output-buffer) (kill-buffer output-buffer))

    ;; 非同期プロセス(curl)の実行
    (let ((proc (start-process "llama-curl" output-buffer "curl"
                   "--max-time" (number-to-string timeout)
                   "-s" "-X" "POST"
                   "-H" "Content-Type: application/json"
                   "-d" json-data
                   server-url)))

      ;; 実行時のバッファ情報をプロセスに紐付け
      (process-put proc :target-buffer target-buffer)

      ;; プロセス終了時のコールバック
      (set-process-sentinel proc
    (lambda (p event)
      (when (string-match-p "finished" event)
        (let ((response (with-current-buffer (process-buffer p) (buffer-string)))
          (dest-buffer (process-get p :target-buffer)))
          (with-current-buffer dest-buffer
        (condition-case err
            (let* ((json-object-type 'alist)
               (decoded (json-read-from-string response))
               (choices (cdr (assoc 'choices decoded)))
               (first-choice (and choices (> (length choices) 0) (elt choices 0)))
               (content (and first-choice (cdr (assoc 'text first-choice)))))
              (if content
              (progn 
                (goto-char (point-max)) 
                (insert content) 
                (message "補完完了 (残りコンテキストに注意)"))
            (insert (format "\n[抽出失敗]: %s" response))))
          (error (insert (format "\n[JSONエラー]: %S\n[生データ]: %s" err response))))))
        (when (get-buffer (process-buffer p)) (kill-buffer (process-buffer p)))))))))

;; キーバインド
;; Alt + Enter に割り当てる(直感的)
(global-set-key (kbd "M-RET") 'my-llama-complete)

3. 工夫した点とハマりどころ

  • ポート番号の一致: 起動スクリプトで 8765 を指定した場合,Emacs側の server-url も合わせる必要があります.
  • 変数のロスト: 非同期 sentinel 内で変数が消失する問題に対し,=process-put= を使ってバッファ情報を保持.
  • OpenAI互換API: LM Studio 等との親和性を考え,階層の深い /v1/completions 形式をパース.

これで,手元のレガシーな Emacs が,最新の AI アシスタントを搭載した強力なエディタに進化しました.

スポンサーリンク
336×280
336×280

書いた人

me 小津雪ヲ: 生きるとゆう事は,雑多な問題に対処するとゆう事.
ライフハック,DIY,木工,鉄工,ソルトルアー,ロードバイク,オートバイ,家庭菜園,自給自足,料理,Linux,ガジェット,写真,フラメンコギターなど.
日常の様々な創意工夫やお役立ち情報を発信しています.

記事のシェア,ツイッターフォロー,ブックマーク,RSS登録 お願いします.
あとランキングも押して頂けると助かりますm(__)m

ブログランキング・にほんブログ村へ blogranking
blogranking

フォローする