NeovimでLLMを使えるプラグインgen.nvimの使い方

NeovimでAIの力を享受したいと思い、gen.nvimを使ってみました。

gen.nvimとは

gen.nvimは、ローカルでLLMを動かせるOllamaをNeovimで使えるようにするプラグインです。

導入方法

プラグインの他、curlとOllama、Ollamaで扱うモデルが必要です。

Ollamaのインストール

まずはOllamaをインストールします。OSによって手順は違うため、READMEを確認しましょう。

macOS、Windowsであれば公式サイトからダウンロード、Linuxであれば次のコマンドを実行します。

Terminal window
curl -fsSL https://ollama.com/install.sh | sh

インストール中、次のようにログが表示されました。

>>> Downloading ollama...
######################################################################## 100.0%##O=# #
>>> Installing ollama to /usr/local/bin...
[sudo] password for user:
>>> Creating ollama user...
>>> Adding ollama user to render group...
>>> Adding ollama user to video group...
>>> Adding current user to ollama group...
>>> Creating ollama systemd service...
>>> Enabling and starting ollama service...
Created symlink /etc/systemd/system/default.target.wants/ollama.service → /etc/systemd/system/ollama.service.
>>> Nvidia GPU detected.
>>> The Ollama API is now available at 127.0.0.1:11434.
>>> Install complete. Run "ollama" from the command line.

ちなみに筆者の環境はWSLです。上記にNvidia GPU detected.とあるようにGPUはOllama側が勝手に見つけてくれるようです。
WSL特有の困りごとはありませんでした。

モデルのダウンロード

モデルを選んでダウンロードします。

モデルの選択

今回は「よーしパパ、Ollama で Llama-3-ELYZA-JP-8B 動かしちゃうぞー」 - Qiitaを参考に、日本語のモデルLlama-3-ELYZA-JP-8B-GGUFを用意しました。

ダウンロードリンクの取得

まずGGUFファイルのを開きCopy download linkを押し、ダウンロードリンクをコピーします。

ダウンロードリンクのコピー

ダウンロードの実行

次のコマンドで、コピーしたリンクからGGUFファイルをダウンロードします。

Terminal window
curl -L -o ~/models/Llama-3-ELYZA-JP-8B-q4_k_m.gguf https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF/resolve/main/Llama-3-ELYZA-JP-8B-q4_k_m.gguf

ファイル名やURLは適宜書き換えてください。

Modelfileの用意

Ollamaで使えるようにするための「Modelfileファイル」を用意します。

ファイル名は各自わかりやすい名前にしておきます。筆者は~/models/elyza-modelに配置しました。

前述の「よーしパパ、Ollama で Llama-3-ELYZA-JP-8B 動かしちゃうぞー」を参考に書きます。

~/models/elyza-model
FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>
{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>
{{ .Response }}<|eot_id|>"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|reserved_special_token"

1行目のFROMの後./Llama-3-ELYZA-JP-8B-q4_k_m.ggufがダウンロードしたGGUFファイルの名前です。

モデルの作成

オプション-fにさきほど作成した「Modelfileファイル」の場所を書きます。

Terminal window
ollama create elyza:jp8b -f elyza-model

elyza:jp8bの部分が、今回のモデルにつける名前です。後で使います。

実行すると次のようなログが出ます。

transferring model data
省略
writing manifest
success

作成に成功したようです。

モデルの実行

モデルの名前を指定して実行します。

ollama run elyza:jp8b

プロンプト>>>が表示されるまで少し時間がかかります。
適当に質問して確認してみましょう。

ollamaの実行例

終了するにはCtrl+d/byeを入力します。

プラグインのインストール

ようやくgen.nvimのインストールです。各自のプラグインマネージャーに合わせて記述します。

lazy.nvimの例
{ "David-Kunz/gen.nvim" },

筆者は次のように設定しました。デフォルト値以外の行をハイライトしています。

require("gen").setup({
model = "elyza:jp8b", -- The default model to use.
quit_map = "q", -- set keymap for close the response window
retry_map = "<c-r>", -- set keymap to re-send the current prompt
accept_map = "<c-cr>", -- set keymap to replace the previous selection with the last result
host = "localhost", -- The host running the Ollama service.
port = "11434", -- The port on which the Ollama service is listening.
display_mode = "split", -- The display mode. Can be "float" or "split" or "horizontal-split".
show_prompt = true, -- Shows the prompt submitted to Ollama.
show_model = false, -- Displays which model you are using at the beginning of your chat session.
no_auto_close = false, -- Never closes the window automatically.
hidden = false, -- Hide the generation window (if true, will implicitly set `prompt.replace = true`)
init = function(options)
pcall(io.popen, "ollama serve > /dev/null 2>&1 &")
end,
-- Function to initialize Ollama
command = function(options)
local body = { model = options.model, stream = true }
return "curl --silent --no-buffer -X POST http://"
.. options.host
.. ":"
.. options.port
.. "/api/chat -d $body"
end,
-- The command for the Ollama service. You can use placeholders $prompt, $model and $body (shellescaped).
-- This can also be a command string.
-- The executed command must return a JSON object with { response, context }
-- (context property is optional).
-- list_models = '<omitted lua function>', -- Retrieves a list of model names
debug = false, -- Prints errors and the command which is run.
})

modelに作成したモデルの名前elyza:jp8bを指定しました。

表示はsplitを指定しました。プロンプトの表示させています。

gen.nvimを動かす

:Gen Fooのようにコマンドで実行します。

たとえば、開いているバッファのコードをレビューしたいならReview_Codeです。

:Gen Review_Code

Neovimを立ち上げてから1回目の:Gen Fooの実行では、起動のために少々時間がかかる点に注意です。

プロンプトの種類

用意されているプロンプトはgen.nvim/lua/gen/prompts.luaから確認できます。

よく使いそうなものを表にまとめました。

名前内容バッファの書き換え
Generate入力をそのままモデルに聞くあり
Chat入力をそのままモデルに聞くなし
Askバッファの内容に関して質問するなし
Summarizeバッファの内容を要約なし
Review_Codeバッファの内容をレビューさせるなし
Change_Codeバッファの内容を入力に合わせて書き換えるあり
Make_Listバッファの内容をMarkdownのリストにするあり
Make_Tableバッファの内容をMarkdownのテーブルにするあり

バッファの内容に関係するなら:Gen Ask、関係ないなら:Gen Chatといった具合です。

自分用のプロンプトを設定する

プロンプトは自由に変更可能です。

たとえば、次のようにプロンプトを日本語化できます。

require("gen").prompts["Summarize"] = {
prompt = "以下を要約してください:\n$text",
replace = true,
}
require("gen").prompts["Ask"] = { prompt = "以下の文章について、$input:\n$text" }
require("gen").prompts["Make_List"] = {
prompt = "文章をマークダウンリストに変換してください:\n$text",
replace = true,
}
require("gen").prompts["Make_Table"] = {
prompt = "文章をマークダウンテーブルに変換してください:\n$text",
replace = true,
}
require("gen").prompts["Review_Code"] = {
prompt = "以下のコードをレビューし、簡潔な提案を行ってください:\n```$filetype\n$text\n```",
}
require("gen").prompts["Enhance_Code"] = {
prompt = "以下のコードを改良し、結果は```$filetype\n...\n```という形式で出力してください:\n```$filetype\n$text\n```",
replace = true,
extract = "```$filetype\n(.-)```",
}
require("gen").prompts["Change_Code"] = {
prompt = "以下のコードについて、$input、結果は```$filetype\n...\n```という形式で出力してください:\n```$filetype\n$text\n```",
replace = true,
extract = "```$filetype\n(.-)```",
}

「プロンプトを日本語化したほうがいいのか、しない方がいいのか」についてはよく分かっていません。LLM周りで詳しい人いたら教えてください。

次のように独自のプロンプトも追加できます。

require("gen").prompts["Tailwind"] = {
prompt = "$input をTailwindCSSで実現する方法を教えていただけないでしょうか?",
}

あくまでもモデルの学習の範囲内で答えが返ってきます。

textlintの設定

gen.nvimで開かれるバッファのfiletypeはMarkdownです。textlintを有効化していると不要な実行・通知が出てきてしまいます。

そこで、LSP側でバッファの名前がgen.nvimだった場合に無効化する設定を書きます。

たとえばnone-ls.nvimなら次のように設定します。

null_ls.setup({
-- ...
should_attach = function(bufnr)
return not vim.api.nvim_buf_get_name(bufnr):match("gen%.nvim")
end,
})

以上、gen.nvimの使い方でした。