Google GeminiをEmacsから使う時の技術的なメモ

しむどん 2025-06-30

これはGoogle Geminiの機能のいくつかをEmacsから利用する際に、EmacsをハブとしてGeminiを活用するための2つの異なるアプローチと、その過程で得られた知見のメモを残す。

Gemini CLI

最近Gemini CLIが登場したので使う事にした。これはGoogle GeminiのCUI エージェントだ。競合としてはClaude CodeとかOpenAI Code XとかOpenHandsなんかが当てはまるだろう。Gemini CLIの特徴はコンテキストウィンドウが広くて、無料で使える量も多いという事だ。ただ無料で使う場合、その情報は学習に使われる事になるから、Geminiに渡すデータについては注意が必要だ。

準備と起動

まずはインストールする。

npm install -g @google/gemini-cli

起動する。

gemini --debug

Gemini CLIは --sandbox というオプションを提供している。これにより gemini はコンテナ内で実行される。本来はこれを使うと良さそうではある。

しかし僕はこれを使わず、自分でコンテナの起動コマンドを指定する事でGemini CLIを実行する事にした。そうすれば何がGeminiによって使われるのか、明らかに把握できるからだ。これは、実行環境の透明性を確保し、予期せぬ挙動を避けるための選択である。

EmacsとGemini CLI

Gemini CLI は制御コードを使ってTUI(ターミナルUI)を作っている。Emacsの標準の機能、例えば `M-x shell` や `M-x term` などでは、残念ながらこの制御コードを上手く扱えず、表示が崩れてしまう問題があった。

しかし、この手のツールは vterm を使うと上手く扱う事ができる。`vterm`はEmacs内で動作する、libvtermを利用した高性能なターミナルエミュレータであり、モダンなCLIアプリケーションが生成する複雑なTUIも適切に表示できるため、この問題の解決策として採用した。

そんなユーティリティをEmacs Lispで書いた。

(defun my/agent-gemini-cli (&optional line cwd)
  (interactive (list (progn
		       (when (use-region-p) (clipboard-kill-ring-save (region-beginning) (region-end)))
		       (read-shell-command "CMD$ "))
                     (read-directory-name "DIRECTORY: "
					  default-directory nil
					  default-directory)))
  (let ((default-directory cwd)
        (vterm-shell
	 (format
	  "docker run --volume $PWD:$PWD --volume $HOME/.gemini:/root/.gemini --workdir $PWD -it %s %s"
	  "gemini-cli"
	  line
	  ))
        (vterm-buffer-name
	 (format "*Agent: %s*" (expand-file-name
				default-directory)))
        (vterm-kill-buffer-on-exit nil))
    (vterm)))

この関数は、カレントディレクトリやプロンプトを引数に取り、`vterm`内でDockerコンテナを起動してGemini CLIを実行するラッパーとして機能する。指定しているコンテナイメージには、事前にビルドしたGemini CLIインストール済みのDockerイメージを指定している。

ただ、これで実行するとgemini-cliへの日本語の入力ができなかった。`vterm`はキー入力を直接サブプロセスのシェルに送ってしまう「char-mode」で動作するため、Emacsの入力メソッドであるSKKが介入する余地がないからだ。これにより、日本語の変換処理がEmacs側で行われず、意図した文字列が入力できないという問題が発生した。

この問題を解決するため、日本語の入力ができるように、追加でちょっとしたコマンドを書く事にした。

(defun my/send-string-to-buffer-process ()
  (interactive)
  (send-string nil (read-from-minibuffer "PROMPT: ")))

この関数は、ミニバッファで文字列を読み取り、それをカレントバッファのプロセスに送信する。ミニバッファはEmacsの通常のバッファであるため、SKKなどの入力メソッドが問題なく使える。これにより、日本語のプロンプトを`vterm`内のGemini CLIに送ることが可能になる。多少面倒な所もあるけれど、これで一通りは使えるようになった。

Gemini APIの直接利用

エージェントではなくてGeminiの機能をAPIで利用する事もできる。まあ、Gemini CLIの方が後発ではあるのだけれど。

APIキーを取得する

Googleアカウントを持っていれば、以下のページからAPIキーを生成できる。

https://aistudio.google.com/app/apikey

簡単なリクエストを送信する

まずは動作を確認するために、restclientで簡単なリクエストを送信する。これはAPIの基本的な挙動や認証方法を確認するための概念実証(PoC)として行った。

:ORIGIN = https://generativelanguage.googleapis.com
:API_KEY := google-gemini-api-key

POST :ORIGIN/v1beta/models/gemini-pro:generateContent
x-goog-api-key: :API_KEY

{
  "contents" : [
    {
      "parts": [
        {
          "text": "こんにちわ"
        }
      ]
    }
  ]
}
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": "こんにちは。何かお手伝いできることはありますか?"
          }
        ],
        "role": "model"
      },
      "finishReason": "STOP",
      "index": 0,
      "safetyRatings": [
        {
          "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HATE_SPEECH",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HARASSMENT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
          "probability": "NEGLIGIBLE"
        }
      ]
    }
  ],
  "promptFeedback": {
    "safetyRatings": [
      {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "probability": "NEGLIGIBLE"
      },
      {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "probability": "NEGLIGIBLE"
      },
      {
        "category": "HARM_CATEGORY_HARASSMENT",
        "probability": "NEGLIGIBLE"
      },
      {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "probability": "NEGLIGIBLE"
      }
    ]
  }
}

// POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent
// HTTP/1.1 200 OK
// Content-Type: application/json; charset=UTF-8

OpenAI APIと同じような感じで、REST APIとしてリクエストを送信しレスポンスを取得する。

APIのクエリパラメータとして alt=sse を追加する事で、 Server Sent Events を使い、少しずつ結果を取得する事もできる。このあたりもOpen AI APIに似ている。たしか、OpenAI APIはリクエストのBODYの中で指定するようになっていた。

:ORIGIN = https://generativelanguage.googleapis.com
:API_KEY := google-gemini-api-key

POST :ORIGIN/v1beta/models/gemini-pro:generateContent?alt=sse
x-goog-api-key: :API_KEY

{
  "contents" : [
    {
      "parts": [
        {
          "text": "こんにちわ"
        }
      ]
    }
  ]
}
#+BEGIN_SRC js
data: {"candidates": [{"content": {"parts": [{"text": "こんにちは。"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}}


// POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?alt=sse
// HTTP/1.1 200 OK
// Content-Type: text/event-stream
// Content-Disposition: attachment

ここまで把握できた。

他の実装済みのライブラリを検討する

EmacsからGeminiを使用するための拡張機能は、自分で実装しなくても既に十分な実装がある。簡単な調査の結果、次のライブラリが適していそうだ。

https://github.com/karthink/gptel

gptelは複数の生成系AIのサービスに対応している。特に理由がない場合は、これを使うと良さそうだ。

なぜgptelや他の実装済みの拡張を使わないか

先程gptelが良さそうだと記述したが、僕はgptelを使わない事にした。理由は以下の通りだ。

  • 自分の使用する機能以外のコード(ライブラリも含む)を出来る限り持ちたくない。 近年のプログラミング言語や、そのエコシステムの状況を見ていると、多くのライブラリに依存する傾向がある。標準ライブラリにも、サードパーティライブラリにも、たくさん依存して様々な機能を実現し提供している。またサードパーティライブラリはそれ自体が、たくさんのサードパーティライブラリに依存している事もある。その結果、ソフトウェアを管理しているプログラマーが、内部で何が起きているのかを把握できない状態となる。そしてトラブルが発生した時、どうしようもなくなってしまう。 もちろん、アップデートしなくとも最新のコードを取得し、進化していくという利点はあるけれど、最新のコードが常に正しいと誰が決めたんだ?アップデートのたびに確認すれば良いという意見もあるかもしれないが、現実にはアップデートは確認せずに適用されることが多い。実情はノールックアップデートだ。 体制を整えれば、アップデートの確認も可能かもしれないが、現時点で私が関心があるのは自分用のEmacsの拡張であり、そのようなものに対して体制を整備するのは馬鹿げている。 ゆえに自分が利用する機能以外のコードはできるだけ減らしたい。
  • 機能を追加したい場合、固有のライブラリの使い方や内部実装を調べる手間を省きたい。 実現したい機能のイメージがあり、APIの使用も理解したとしても、すぐに実装は実装できない。既存の実装を知る必要があるからだ。それが自分の書いた実装なら把握もしやすいが、誰かが書いたライブラリやツールであれば、これはそこそこ大変な作業になる。この作業で右往左往するのだが、別に僕はそこで右往左往したいわけじゃない。誰かの作った実装のほうが高機能であったとしても、その機能の全てを僕が使いたい訳じゃない。使いたい機能なんて本当に少ししかないはずだ。
  • GeminiのAPIの使い方に関する知識を得たい。 今回の目的の一つはGeminiのAPIを把握する事でもある。ライブラリやツールは変われど、結局の所GeminiのAPIに追従する形になるからだ。だからライブラリを学ぶのではなく、GeminiのAPIを学びたい。

これらの理由により、僕は自分で実装することにした。

Emacs拡張を実装する

Emacs拡張を実装していく。実装方法もいくつか考えられるだろうけれど make-processcurl コマンドをサブプロセスとして起動する方法を使う。

なぜHTTPライブラリではなくmake-processとcurlを使うのか

HTTPリクエストを送信する時、通常はEmacs Lispで実装された request.elplz など、HTTPクライアントライブラリを使う。もしかしたらEmacsで標準ライブラリとして組み込まれている url-retrieve を使う事もあるかもしれない。それぞれ長短があったりはするが、概ね良くできている。しかし問題が発生した時、新しく機能を追加したい時では、それぞれのライブラリを知っておく必要がある。しかし、Emacs Lispのライブラリについて知りたいかと言われると、正直そんなに知りたい訳ではない。

その一方、 make-processcurl コマンドをサブプロセスとして起動する方法では、問題を curl に寄せる事ができる。 curl は全ての人が熟知しているツールであるため、トラブルを解決しやすい。この方法の問題点はcurlがインストールされていないと動かないという事だろう。 request.elplz.el は、 curl があれば curl を使うし、無ければ url-retrieve にフォールバックする。これは素晴しいが、僕の環境で curl が使えない事は基本的にないので、それについては考えない事にする。curl、入っているよね?入れればいいよね?だから今回も、この方法で行う。

ちなみにlibcurlを使うという方法もあるかもしれないが、そちらはビルドが必要な事、リクエスト送信中にブロックしてしまう事などを考えると、curlをサブプロセスで起動する方が、Emacsのスタイルには馴染むと思う。

実装の方針

おおまかな実装の方針としては、以前実装したopenai.elやdeepl.elと同じような実装にする。箇条書きにすると以下のようになる。

  • make-processでcurlを起動してAPIにリクエストを送信する。
  • レスポンスは Server Sent Events で受け取り、解析可能な範囲から解析していく。
  • 解析した文字列は、解析済みテキスト保持用のQueue変数に詰める。
  • 解析済みテキスト保持用のQueue変数からタイマー処理で解析済みテキストを取り出し、出力用バッファに挿入する。

データやコンポーネント、関数などごちゃまぜの概念の構成図を書いた。概ねこのようなイメージになる。

+-------------------------+   +-------------------------+
|                         |   |                         |
| read-string-from-buffer |   | api key                 |
|                         |   |                         |
+----------+--------------+   +----------+--------------+
           |                             |
           +-----------------------------+
           |
           v
+----------+------+
|                 |
| curl config     |
|                 |
+----------+------+
           |
           |         +-----------------+             +-----------------+
           |         |                 |             |                 |
           --------->+ curl process    |             | Gemini API      |
                     |                 | HTTPs POST  |                 |
                     |                 +------------>+                 |
                     |                 |             |                 |
                     |                 | Server Sent |                 |
                     |                 | Event       |                 |
   +-----------------+                 +<------------+                 |
   |                 |                 |             |                 |
   |                 |                 | Server Sent |                 |
   |                 |                 | Event       |                 |
   |  +--------------+                 +<------------+                 |
   |  |              |                 |             |                 |
   |  |              |                 | Server Sent |                 |
   |  |              |                 | Event       |                 |
   |  |  |-----------+                 +<------------+                 |
   |  |  |           |                 |             |                 |
   |  |  |           |                 |             |                 |
   |  |  |           |                 |             |                 |
   |  |  |           +-------+---------+             +-----------------+
   |  |  |                   |
   |  |  |                   |
   v  v  v                   |          push
+--+--+--+------+    +-------+---------+    +-----------------+
|               |    |                 |    |                 |
| Process       |    | Process         |    | Message Queue   |
|  Buffer       +--->+  Filter         |--->+                 |
|               |    |                 |    |                 |
|               |    |                 |    |                 |
+---------------+    +-------+---------+    +-------+---------+
                             |                      | pop
                             |          kick        v
                     +-------+---------+    +-------+---------+
                     |                 |    |                 |
                     | Process         |    | Timer           |
                     |  Sentinel       +--->+                 |
                     |                 |    |                 |
                     +-----------------+    +-------+---------+
                                                    |
                                                    v
                                            +-------+---------+
                                            |                 |
                                            | Result Buffer   |
                                            |                 |
                                            +-----------------+

実装

google-gemini.el

名前被り

実は世の中にはGoogleではない、全く別のGeminiがある。しかも、少なくとも2つ(Emacs関連のプロジェクトと、暗号通貨関連のサービス)はある。名前被りはある程度は仕方がないんだけれど、一般的な名詞をプロジェクトに付けるのは、正直避けた方がいい。Babelとかも同じ事が言える。本当にどれの事について言っているのか分からなくなる。

まとめ

この記事では、EmacsからGoogle Geminiを利用するための2つのアプローチをそれぞれの実装と課題解決の過程を記録した。

Gemini CLIは、Emacsと`vterm`を経由して利用した。TUIの制御コードや入力メソッドとの衝突といった、Emacsと外部ターミナルアプリケーションとの間で発生する問題に対処するため、ミニバッファを介してコマンドを送信するという小さなヘルパー関数がを実装した。

APIを直接利用する方法では、初期実装の手間はかかるものの、依存関係を最小限に抑え、API仕様への深い理解を深められた。`curl`を非同期プロセスとして呼び出し、その結果をEmacs内で捌くアーキテクチャは、他のWeb APIを扱う際にも応用可能で汎用性が高い。使いにくさもあったけど。

どちらのアプローチが優れているというわけではないし、用途や思想に応じて最適な手段は異なる。適宜、状況を鑑み選んでいけばいい。