最強ローカルLLM実行環境としてのEmacs

2024-08-04T14:32:39+0900
Emacs, Llm

Emacs meets Ollama

みなさん、ローカルLLMで遊んでいますか?

昨年末に、Ollamaが登場してから誰でも簡単にローカルLLMで遊べる時代がやってきました。そこで、僕もローカルLLMでどんなことができるんだろうと思って触りはじめたのですが、ローカルLLMを最大限に活用するためには、まずはどうやったらEmacsからローカルLLMを使えるようになるのかと考えるのはあまりにも自然な流れでした。

この記事では、ローカルLLMに関する基本的な知識から、EmacsからローカルLLMを扱う方法までを解説していきたいと思います。

ローカルLLMの基礎知識 #

ローカルLLMとは、LLM(大規模言語モデル)をローカル環境、つまり自分のパソコンで扱えるようにしたモデルです。Facebookが開発しているLlamaが業界のトップランナーで、それをベースにしたモデルを色々な組織(中には個人もいるのかも)が開発しています。

そのLlamaを動かすために開発されたのがllama.cppで、その名のとおりC++を使って動かします。llama.cppはGGUFというフォーマットのモデルを扱うことが可能で、🤗 Hugging Face というLLM界のGitHubみたいなところに上がっている多くのモデルがこのGGUFというフォーマットで公開されています。

ここらへんについてはKoRoNさんのLLaMa(.cpp) 周辺のサーベイが参考になるので、詳しく知りたい人は読んでみるとよいでしょう。

llama.cppを使ってGGUFフォーマットのLLMを動かすこと自体はそこまで難しくはないのですが、誰でも簡単にできるかというとそうでもなく、多少の面倒くささがあったのですが、それをDockerのように簡単に扱えるようにしてくれたのがOllamaでした。

Ollama

Ollamaは、OllamサーバーとCLIがセットになったLLMの動作環境で、インストールさえすれば、DockerイメージをpullするようにLLMを自動的にダウンロードして実行できるようにしてくれる素敵なソフトウェアです。

例えば、llama3.1を使ってみたければ、次のコマンドを実行すれば初回のみモデルをダウンロードして実行してくれます。

ollama run llama3.1 "What is sky blue?"

Ollamaで利用可能なモデルはOllama Modelsにあるものになります。ただ、Import GGUFのドキュメントを参考に自分でGGUFモデルをOllama用に変換すればGGUFモデルを動かせるようになっており、Ollama Modelsの一覧で公開されているもの以外も動かせるようになっています。

なのですが、実際には謎のうp職人や変換ニキがいて、世の中に新しいモデルが公開されると、ほとんど時間をあけずに色々なモデルが次々と公開されるようになっていて、一般の利用者たる我々としては「神!」だけ言って利用すればよいという古き良き属人的なエコシステムが構築されています。

EmacsとLLM #

さて、ローカルLLMについての解説がすんだところで、ここからはメインテーマである、EmacsでローカルLLMを扱う話をしていきます。

llm

EmacsでLLMを扱うためにはllmというライブラリを使います(以下、小文字のllmはこのライブラリを指します。大文字のLLMは大規模言語モデルです。)。リポジトリには以下の説明があります。

Introduction

This library provides an interface for interacting with Large Language Models (LLMs). It allows elisp code to use LLMs while also giving end-users the choice to select their preferred LLM. This is particularly beneficial when working with LLMs since various high-quality models exist, some of which have paid API access, while others are locally installed and free but offer medium quality. Applications using LLMs can utilize this library to ensure compatibility regardless of whether the user has a local LLM or is paying for API access.

LLMs exhibit varying functionalities and APIs. This library aims to abstract functionality to a higher level, as some high-level concepts might be supported by an API while others require more low-level implementations. An example of such a concept is “examples,” where the client offers example interactions to demonstrate a pattern for the LLM. While the GCloud Vertex API has an explicit API for examples, OpenAI’s API requires specifying examples by modifying the system prompt. OpenAI also introduces the concept of a system prompt, which does not exist in the Vertex API. Our library aims to conceal these API variations by providing higher-level concepts in our API.

【ローカルLLM: codestral:22b-v0.1-q4_K_S にて翻訳】

導入

このライブラリは、Large Language Models(LLMs)とのインターフェイスを提供しています。ElispコードがLLMsを使用できるようになっており、エンドユーザーは好きなLLMを選択することもできます。これは特に、高品質のモデルがいくつか存在しているため、その中にはAPIへの有料アクセスが必要なものや、ローカルにインストールされていて無料だが中程度の品質を提供するものなどがあるLLMsで役立ちます。このライブラリを使用するアプリケーションは、ユーザーがローカルのLLMを持っているかAPIへのアクセス料を払っているかに関係なく、互換性を確保できます。

LLMsはさまざまな機能とAPIを示しています。このライブラリーは、高いレベルで機能を抽象化することを目的としています。これは、APIがサポートする高級概念の一部がそうでありながら、他のものにはより低レベルの実装が必要だからです。そのような概念として「例」があります。クライアントはこのパターンを示すためにLLMに対する例の相互作用を提供します。GCloud Vertex API には例の明確なAPIがありますが、OpenAIのAPIではシステムプロンプトを変更することで例を指定する必要があります。OpenAIはVertex APIに存在しない「システムプロンプト」という概念も導入しています。私たちのライブラリーはこれらのAPIの違いを隠蔽することを目指し、より高レベルの概念を提供しています。

ざっくりと言えば、このライブラリは様々なLLMをEmacsから透過的に扱うためのライブラリです。このライブラリを利用することで、Open AI、Gemini、Claude、Ollama、llama.cppなどローカル、外部を問わず様々な生成AIを、Emacs上で同じように扱うAPIを用意してくれています。

ライブラリ本体はllm.elで、各種生成AIとそのLLMとの接続部分はOpen AIはllm-openai.el、Claudeはllm-claude.el、Ollamaはllm-ollama.elといったようにモジュール方式で構築されていています。

llmは使いたいモデルをプロバイダーという単位で定義します。

(make-llm-ollama :embedding-model "mistral:latest" :chat-model "mistral:latest")

これは、Ollamaを使ってmistralというLLMを利用する設定を、プロバイダーとして作成する記法です。

make-llm-ollama はOllamaを使ったLLMプロバイダを作成するための関数です。 :embedding-model "mistral:latest"は使用するEmbedding(埋め込み)モデルを指定します。:chat-model "mistral:latest"は使用するチャットモデルを指定します。

このようにしてプロバイダーを作成すれば、Emacsからいつでも任意のプロバイダーを呼び出してLLMを利用できるようになります。

さあ、それでは実際にLLMを利用してみましょう!といいたいところですが、実はllmにはインタラクティブコマンドが一切用意されていないという本当の意味でのライブラリとなっています。そのため、簡単にLLMを使ってみるという用途には向きません。

そこで登場するのがellamaです。

最強LLMフロントエンドEllama #

Ellamaは、執筆時点で僕が知るかぎりもっとも簡単にEmacs上でLLMを使えるパッケージです。

たいていの人は、OllamaとEllamaさえ入れれば、簡単にEmacs上でLLMを使えるようになるはずです。

Ellamaの設定 #

Ellamaに必要な設定はREADMEに書かれているとおりですが、参考までに僕の現在の設定を紹介しておきます。

(with-eval-after-load 'llm
  (require 'llm-ollama)
  ;; ellama-translateで翻訳する言語
  (setopt ellama-language "Japanese")
  ;; ellama-ask-selection などで生成されるファイルのネーミングルール
  (setopt ellama-naming-scheme 'ellama-generate-name-by-llm)
  ;; デフォルトのプロバイダー
  (setopt ellama-provider (make-llm-ollama
                           :chat-model "codestral:22b-v0.1-q4_K_S"
                           :embedding-model "codestral:22b-v0.1-q4_K_S"))
  ;; 翻訳で利用するプロバイダー
  (setopt ellama-translation-provider (make-llm-ollama
                                       :chat-model "aya:35b-23-q4_K_S"
                                       :embedding-model "aya:35b-23-q4_K_S"))
  ;; ellamaで使えるプロバイダー。ellama-provider-select で選択できる
  (setopt ellama-providers
          '(("codestral" . (make-llm-ollama
                            :chat-model "codestral:22b-v0.1-q4_K_S"
                            :embedding-model "codestral:22b-v0.1-q4_K_S"))
            ("gemma2" . (make-llm-ollama
                            :chat-model "gemma2:27b-instruct-q4_K_S"
                            :embedding-model "gemma2:27b-instruct-q4_K_S"))
            ("command-r" . (make-llm-ollama
                            :chat-model "command-r:35b"
                            :embedding-model "command-r:35b"))
            ("llama3.1" . (make-llm-ollama
                                  :chat-model "llama3.1:8b"
                                  :embedding-model "llama3.1:8b"))
            )))

ellama-providers の設定は、モデルを切り替えることがなければ不要です。あと、僕の設定は M3 MacBook Pro メモリ64GBの環境で動くモデルを指定しているので、基本的に大きめになっています。各自の環境にあわせて適切なモデルを選択してください。

Emacsの設定が済んだら、ターミナルから事前に必要なモデルをダウンロードしておきましょう。回線によってはけっこう時間がかかります。

ollama pull codestral:22b-v0.1-q4_K_S
ollama pull aya:35b-23-q4_K_S
ollama pull gemma2:27b-instruct-q4_K_S
ollama pull command-r:35b
ollama pull llama3.1:8b

ここまで準備できたら、Ellamaで遊んでみましょう。

Ellamaで遊ぶ #

Ellamaデモ

動画ファイル:ellama-demo.mp4

これは、Ellamaを使ってPrismaというORMのスキーマを使って遊んでいます。

最初に M-x ellama-make-table を使って、リージョン選択したPrismaスキーマのテーブル定義をMarkdownのテーブルに変換しています。
次に、 M-x ellama-make-format RET yaml RET でYAMLフォーマットに変換しています。
そして、M-x ellama-summarize で選択したテーブル定義の概要を作成してもらいました。
最後に、M-x ellama-translate で作成した概要を日本語に翻訳しています。

このようにEllamaが用意しているコマンドを使うだけで、リージョン選択しているテキストやバッファに対して、生成AIによる処理が行えるようになっています。

Ellamaの代表的なコンマンド #

Ellmaが用意している代表的なコマンドを一覧にしてみました。こちらの表もEllamaを使って作成しています。

コマンド 説明
ellama-ask-about Ellama に選択した領域または現在のバッファーについて尋ねます。
ellama-ask-line 現在の行を Ellama チャットに送信します。
ellama-ask-selection 選択した領域または現在のバッファーを Ellama チャットに送信します。
ellama-change 指定された CHANGE に従って、選択したテキストまたは現在のバッファーのテキストを変更します。
ellama-chat 会話履歴を添えて、Ellama チャットに PROMPT を送信します。
ellama-code-add DESCRIPTION に従って新しいコードを追加します。
ellama-code-complete 選択したコードまたは現在のバッファー内のコードを補完します。
ellama-code-edit 指定された CHANGE に従って、選択したコードまたは現在のバッファー内のコードを変更します。
ellama-code-improve 指定された CHANGE に従って、選択したコードまたは現在のバッファー内のコードを変更します。
ellama-code-review 選択した領域または現在のバッファー内のコードをレビューします。
ellama-complete 現在のバッファー内のテキストを補完します。
ellama-define-word 現在の単語の定義を見つけます。
ellama-generate-commit-message 変更内容に基づいてコミットメッセージを作成する。
ellama-improve-grammar 選択した領域または現在のバッファーの文法とスペルを強化します。
ellama-improve-wording 選択した領域または現在のバッファーの言葉遣いを強化します。
ellama-make-format 選択したテキストまたは現在のバッファーのテキストを NEEDED-FORMAT にレンダリングします。
ellama-make-list アクティブな領域または現在のバッファーから Markdown リストを作成します。
ellama-make-table アクティブな領域または現在のバッファーから Markdown テーブルを作成します。
ellama-provider-select Ellama プロバイダーを選択します。
ellama-summarize 選択した領域または現在のバッファーの概要を作成します。
ellama-summarize-webpage URL から取得したウェブページの要約を作成します。
ellama-translate 選択した領域またはカーソルのある単語を Ellama に翻訳するように求めます。
ellama-translate-buffer 現在のバッファーを Ellama に翻訳するように求めます。

Ellamaで作る自分専用のLLMコマンド #

Ellamaのソースコードを見てもらえばすぐに分かりますが、Ellamaの各コマンドはプロンプトのテンプレートを使って命令を実行しているだけです。

例えば、概要を作成してくれる M-x ellama-summarize は次のような中身になっています。

(defcustom ellama-summarize-prompt-template "Text:\n%s\nSummarize it."
  "Prompt template for `ellama-summarize'."
  :group 'ellama
  :type 'string)

(defun ellama-summarize ()
  "Summarize selected region or current buffer."
  (interactive)
  (let ((text (if (region-active-p)
          (buffer-substring-no-properties (region-beginning) (region-end))
        (buffer-substring-no-properties (point-min) (point-max)))))
    (ellama-instant (format ellama-summarize-prompt-template text))))

また、フォーマット変換する M-x ellama-make-format は次のようになっています。

(defcustom ellama-make-format-prompt-template "Render the following text as a %s:\n%s"
  "Prompt template for `ellama-make-format'."
  :group 'ellama
  :type 'string)

(defun ellama-make-format (needed-format)
  "Render selected text or text in current buffer as NEEDED-FORMAT."
  (interactive "sSpecify required format: ")
  (let* ((beg (if (region-active-p)
          (region-beginning)
        (point-min)))
     (end (if (region-active-p)
          (region-end)
        (point-max)))
     (text (buffer-substring-no-properties beg end)))
    (kill-region beg end)
    (ellama-stream
     (format
      ellama-make-format-prompt-template
      needed-format text)
     :point beg)))

この2つから分かるように、ellama-instantellama-stream の2つの関数が鍵となっていて、これを使えば、誰でも簡単に自分専用のAIコマンドを作ることができます。

ellama-instant: バッファを作成して結果を出力する #

ellama-instant 関数は、新規でバッファを作成して結果を出力するコマンドです。

(ellama-instant "Why is the sky blue?")

このS式を評価するだけで、バッファを作成してデフォルトのLLMを使った実行結果を出力します。

ellama-stream: カーソル位置に結果をストリーム出力する #

ellama-stream 関数はカーソル位置に結果を出力するコマンドです。そのため、リードオンリーバッファでは使うことができません。

(ellama-stream "What is today's date?")

このS式を評価するだけで、現在カーソルのある位置を起点にして、LLMを使った実行結果を出力します。

シェルコマンドの結果を利用したコマンド作成例 #

例えば、これは M-x ellama-generate-commit-messagegit diff --cached ではなく、 git diff でコミットメッセージを生成していたので、使いものにならなかったときに僕が作成したコマンドです。

(defun my-ellama-generate-commit-message ()
  "git diff --cached の結果を使ってコミットメッセージを生成する。"
  (interactive)
  (ellama-stream (format ellama-generate-commit-message-template
                         (shell-command-to-string "git diff --cached"))))

たったこれだけで、自分でLLMを使ったコマンドが作れるのは、とても夢が広がるかと思います。

なお、この問題はすでに Fix commit message generation for partial commits #146 で修正済みなのでこのコマンドは不要になりました。

モデルを使い分けたコマンド作成例 #

他にも、翻訳実行するためのモデルとして、僕が標準で設定しているaya:35bは結果は優秀なのですが、重たいため手軽に翻訳してみたいときには不便です。

そこで、もっと軽量なモデル、例えばgemma2:9bを使って翻訳するコマンドを作成すれば、精度と速度を使いわけて翻訳が可能となります。

(when (require 'ellama nil t)
  (setopt my-ellama-translation-quick-provider (make-llm-ollama
                                       :chat-model "gemma2:9b-instruct-q4_K_S"
                                       :embedding-model "gemma2:9b-text-q4_K_S"))
  (defun my-ellama-translate-quick ()
    "Quick version ask ellama to translate selected region or word at point."
    (interactive)
    (let ((text (if (region-active-p)
                    (buffer-substring-no-properties (region-beginning) (region-end))
                  (thing-at-point 'word))))
      (ellama-instant
       (format ellama-translation-template
               ellama-language text ellama-language)
       :provider my-ellama-translation-quick-provider))))

具体的にはこんな感じでコマンドを定義することで、 M-x my-ellama-translate-quick を実行すると、gemma2:9bを使った高速な翻訳が行えるようになました。

まとめ #

このように、Emacsはllmという標準ライブラリと、Ellamaというインターフェイスを得て、見事に最強といえるローカルLLMの実行環境になりました。

今回の記事では、Ellamaを紹介しましたが、同じ作者のs-kostyaev氏がELISA (Emacs Lisp Information System Assistant)というものを作成しており、こちらも面白そうなので、今後EmacsのAI活用がますます発展していきそうです。

さまざまなアプリケーションやOSがAI機能を搭載することに力を注いでいる昨今ですが、このテキスト情報の扱いにかけては天下一品であるEmacsも、ちゃくちゃくAI機能との統合を整えてきており、AI時代の到来によってEmacsの黄金期がやってきたのではないかというくらい、便利になってきていますので、まだLLMに触れてない方は、ぜひこの機会に試してみてもらえればと思います。

宣伝:エンジニアの楽園 vim-jpラジオ #

最後にちょっとだけ宣伝です。

先月、エンジニアの楽園 vim-jpラジオという本格的ラジオ番組を作成しました。タイトルにVimが含まれていますが、半分はEmacsなので、実質Emacsラジオだと思って、ぜひみなさん聞いてみてください。