私は,ことサーバー運用に関しては保守的な人間です.
今回,Xubuntu 20.04環境のEmacs 26.3という「枯れた」環境から,常時稼働している llama.cpp や LM Studio のAPIを叩いて,執筆やコーディングを爆速にする仕組みを構築しました.
モダンなパッケージ(=gptel= 等)が依存関係で動かない環境でも,標準機能と curl だけで動作する「枯れた実装」の決定版です.
スポンサーリンク
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-run2. 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 アシスタントを搭載した強力なエディタに進化しました.
