ChatGPTの機能をEmacsから使う

しむどん 2025-02-27

最近、AIという言葉をよく聞く。AIは人みたいに考えるプログラムのことだ。AIに質問すると答えてくれたり、動画を作ってほしいと言うと作ってくれる。今はさまざまなAIと、それに関連するサービスやアプリがたくさん作られている。その中で代表的なものの一つにChatGPTがある。

ChatGPTはAIを簡単に使うことができるWebサービスで、Webブラウザから質問を入力するとAIに答えてくれる。https://chatgpt.com/にGoogle ChromeなどのWebブラウザでアクセスすれば、質問を投稿して回答してもらう事ができるだろう。またChatGPTの機能はOpenAI APIという使い方もできるようになっていて、いろいろな道具からAIの力を使える。

僕はEmacsというツールが好きだから、この機能をEmacsから使えるようにしたい。そこで必要な事を調べる事にした。

使えるモデル

このようなAIのサービスではたいてい使用するモデルを選べるようになっている。モデルというのは学習済みのデータの事なのだけれど、あえて喩えるなら脳ミソだろうか。つまり脳ミソを交換できる。ChatGPT、OpenAI APIでも使えるモデルはいくつかある。そこで使う可能性のあるモデルをいくつかピックアップしてまとめる事にした。今回は会話で使えるものを対象にする。

Group 入出力サイズ 入出力サイズ 料金 料金 料金
Name 入力1 出力2 入力3 入力c4 出力T5
gpt-4o-2024-05-13 128000 4096 5.000 - 15.000
gpt-4o-2024-08-06 128000 16384 2.500 1.250 10.000
gpt-4o-2024-11-20 128000 16384 2.500 1.250 10.000
gpt-4o-mini-2024-07-18 128000 16384 0.150 0.075 0.600
o1-2024-12-17 200000 100000 15.000 7.500 60.000
o1-preview-2024-09-12 128000 32768 15.000 7.500 60.000
o1-mini-2024-09-12 128000 65536 1.100 0.550 4.400
o3-mini-2025-01-31 200000 100000 1.100 0.550 4.400

OpenAIのWebサイトから値を収集してまとめて表にした67

この表の入出力サイズの入力はコンテキストウィンドウの事で、モデルが1度に入力できるトークンの数を表している1。入出力サイズの出力は最大出力トークンで、モデルが1度に出力できるトークンの数を表している2。これらの単位はトークンで表している。

ところどころで出てくるトークンというのはデータを処理する際の最小単位の事なのだけれど、おおよそ単語の単位と考えることができ、1000トークンは約750単語と考えられる8

どのモデルを使うかによって応答の特徴や品質、応答速度、料金が変わってくる。大ざっぱに考えると、次のようになる。

  • Largeモデル

    • 高いレベルの知性を持つ。
    • それなりの速度で応答を返す。
    • トークンあたりの料金が高い。
  • Miniモデル

    • Largeモデルと比較すると知性は低い。
    • 高速に応答を返す。
    • トークンあたりの料金は、Largeモデルと比較すると安い。
  • 推論モデル

    • 結果を返すのが遅い。
    • LargeモデルやMiniと比較すると、より大くのトークンを使用する。
    • より多くのトークンを使用するという事は、"考える"事ができる。
    • 高度な推論、コーディング、マルチステップ計画ができる。

作業の特性によってもあらかじめ使用するモデルを制限する方法を用意しておきたい。

これらをふまえて考えると、普段使いするのは gpt-4o-mini-2024-07-18 、少し複雑な問題を考える時には o3-mini-2025-01-31 、しっかり考えないような複雑な問題の時には o1-2024-12-17 を使うのがよさそうだ。

Emacsに組み込む

OpenAI APIを使う」でチャットの機能のためのAPIの使い方について調べた。結果を一括で取得する方法と、 Server Sent Events を用いて結果を徐々に受け取るストリーミングのような方法を調べた。

Server Sent Events を受け取るためにはリクエストのBODYのパラメータ streamtrue を設定する必要がある。

今回はこの方法を使い、ChatGPTと似たような使用感になるように、Emacsを拡張する事にした。

openai-chat-question でチャットを開始する時に、前置きの部分に > を挿入するようにした。

   (list (read-string-from-buffer
	  "sQuestion"
	  (if (region-active-p)
	      (let ((txt (buffer-substring-no-properties
			  (region-beginning) (region-end))))
		(with-temp-buffer
		  (insert txt)
		  (goto-char (point-min))
		  (replace-regexp "^" "> ")
		  (buffer-string)))

Emacs Lispを置いておく。

openai.el

;;; openai --- OpenAI API Utility -*- lexical-binding: t -*-

;; Copyright (C) 2025 TakesxiSximada

;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Repository:
;; Version: 3
;; Package-Version: 20250227.0000
;; Package-Requires: ((emacs "28.0")
;; Date: 2025-02-27

;; This file is not part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Code:

(defvar openai-api-key nil)

(setq openai-chat-result-buffer-name "*ChatGPT Result*")
(setq openai-chat-request-buffer-name "*ChatGPT Request*")
(setq openai-chat-response-buffer-name "*ChatGPT Response*")
(setq openai-chat-response-error-buffer-name "*ChatGPT Response Error*")

(setq openai-chat-send-request-process nil)

(setq openai-chat-chars-q nil)

(setq openai-chat-chars-timer nil)

(defgroup openai-chat nil
  "OpenAI API Utility for Emacs."
  :prefix "openai-chat-"
  :group 'tools
  :link '(url-link :tag "Source" "https://www.symdon.info/ja/posts/1708258091"))

(defvar openai-chat-model-list
  '("gpt-4o-2024-05-13"
    "gpt-4o-2024-08-06"
    "gpt-4o-2024-11-20"
    "gpt-4o-mini-2024-07-18"
    "o1-2024-12-17"
    "o1-mini-2024-09-12"
    "o1-preview-2024-09-12"
    "o3-mini-2025-01-31")
  "See https://platform.openai.com/docs/models")

(defcustom openai-chat-current-model "o1-mini"
  "Use completions api model"
  :type 'string
  :group 'openai-chat)

(defun openai-chat-select-current-model ()
  (interactive)
  (customize-set-value
   'openai-chat-current-model
   (completing-read "OpenAI Chat Current Model: " openai-chat-model-list
		    nil t nil nil openai-chat-current-model)))

(defun openai-chat-sync-results ()
  (while openai-chat-chars-q
    (with-current-buffer (get-buffer-create openai-chat-result-buffer-name)
      (goto-char (point-max))
      (let ((ch (car openai-chat-chars-q)))
	(if (string-equal "" ch)
	    (insert "\n")
	  (insert ch)))
      (end-of-buffer)

      (setq openai-chat-chars-q (cdr openai-chat-chars-q))))
  (pop-to-buffer openai-chat-result-buffer-name))

(defun openai-chat-chars-start-timer ()
  (interactive)
  (unless openai-chat-chars-timer
    (setq openai-chat-chars-timer (run-with-idle-timer 1 t #'openai-chat-sync-results))))

(defun openai-chat-chars-cancel-timer ()
  (interactive)
  (cancel-timer openai-chat-chars-timer)
  (setq openai-chat-chars-timer nil))

(require 'json)


(defcustom openai-chat-system-pre-sentence nil
  "APIに送信する前置きの文章"
  :type 'string
  :group 'openai
  )

(defun openai-chat-create-request (sentence)
  (interactive "sSentence: ")

  (with-current-buffer (get-buffer-create openai-chat-result-buffer-name)
    (goto-char (point-max))
    (insert "\n\n------------------------------------------------\n")
    (insert "ME: ")
    (insert sentence)
    (insert "\n------------------------------------------------\n")
    (insert "AI: ")
    )

  (with-current-buffer (get-buffer-create openai-chat-request-buffer-name)
    (erase-buffer)
    (insert
     (format "{
    \"model\": \"%s\",
    \"messages\": [
	{
            \"role\": \"user\",
            \"content\": %s
	},
	{
            \"role\": \"user\",
            \"content\": %s
	}
    ],
    \"stream\": true
}" openai-chat-current-model (json-encode-string openai-chat-system-pre-sentence) (json-encode-string sentence)))))

(defun openai-chat-parse-response ()
  (interactive)
  (with-current-buffer openai-chat-response-buffer-name
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward "^data: \\(.*\\)

" nil t)
	(if-let ((txt (buffer-substring-no-properties
		       (match-beginning 1) (match-end 1))))
	    (progn
	      (delete-region (point-min) (match-end 1))
	      (if-let ((content
			(cdr (assoc 'content
				    (cdr (assoc 'delta
						(seq-first (cdr (assoc 'choices
								       (json-read-from-string txt))))))))))
		  (setq openai-chat-chars-q (append openai-chat-chars-q (list content))))))))))


(defun openai-chat-send-request ()
  (interactive)
  (if-let ((req-body (with-current-buffer openai-chat-request-buffer-name (buffer-string))))
      (progn
	(setq openai-chat-send-request-process
	      (make-process
	       :name "*OpenAI Chat*"
	       :buffer openai-chat-response-buffer-name
	       :command `("curl" "https://api.openai.com/v1/chat/completions"
 	      		  "-X" "POST"
 	      		  "-H" "Content-Type: application/json"
 	      		  "-H" ,(format "Authorization: Bearer %s" openai-api-key)
 	      		  "-d" "@-")
 	       :connection-type 'pipe
 	       :coding 'utf-8
	       :filter (lambda (process output)
			 (with-current-buffer (process-buffer process)
			   (goto-char (point-max))
			   (insert output)
			   (openai-chat-parse-response)
			   (openai-chat-sync-results)))
	       :sentinel (lambda (process event)
			   (openai-chat-parse-response)
			   (openai-chat-sync-results))
 	       :stderr openai-chat-response-error-buffer-name))

	(process-send-string openai-chat-send-request-process req-body)
	(process-send-eof openai-chat-send-request-process))))

;;;###autoload
(defun openai-chat-question (question)
  (interactive
   (list (read-string-from-buffer
	  "sQuestion"
	  (if (region-active-p)
	      (let ((txt (buffer-substring-no-properties
			  (region-beginning) (region-end))))
		(with-temp-buffer
		  (insert txt)
		  (goto-char (point-min))
		  (replace-regexp "^" "> ")
		  (buffer-string)))
	    ""))))
  (when (and question (not (string-blank-p question)))
    (openai-chat-create-request question)
    (openai-chat-send-request)))

;;;###autoload
(defun openai-chat-create-and-send-request-for-assist (content)
  (interactive)
  (when content
    (openai-chat-create-request content)
    (openai-chat-send-request)))

(provide 'openai)
;;; openai.el ends here

使用しているリクエストのパラメータ

この openai.el では messagemodelstream のみを使用している。現状でも特に不満はないけれど、パラメータを妥当な値で細かく調整できれば、もっと応答の品質を上げられるだろう。そこでAPIのリクエストのボディに含めるパラメータを必要な分だけ学んでいく事にする。

これはOpenAIのChat Completion APIの公式ドキュメントを元にしたものだが、この文書では必要に応じて私見などが含まれている事に注意が必要だ。だから正しい情報を得たい場合には公式ドキュメントを参照すると良い9

model

string
必須 Required

使用するモデルのIDを文字列で指定する。使用可能なモデルについてはモデルエンドポイント互換性テーブルで確認できる10

そのテーブルによると現在、 /v1/chat/completions では以下のモデルを使用できる。

  • All o-series
  • GPT-4o (except for Realtime preview)
  • GPT-4o-mini
  • GPT-4
  • GPT-3.5 Turbo models
  • and their dated releases
  • chatgpt-4o-latest dynamic model
  • Fine-tuned versions of

    • gpt-4o
    • gpt-4o-mini
    • gpt-4
    • gpt-3.5-turbo

このモデルのうちどれを使用するかという事が、応答の品質と料金に大きく関わる。そのため何が使用可能なのかを正確に把握し、素早く切り替えられるようにする事と便利そうだ。

現状では curl で使用しているテンプレートにハードコーディングしている。このモデルを切り替える手段を実装する。 (追記: これは実装した)

stream

boolean or null
必須 no
デフォルト false

この属性にtrueが設定された場合、生成されたメッセージは部分的に徐々に、サーバ送信イベントとして返される。メッセージのストリームは [DONE] というメッセージで終了を表す。

まとめ

今回はChatGPTの機能をEmacsから使うために、OpenAI APIをEmacsから呼び出して利用する仕組みを実装した。必要に応じて使っていきたい。


1

コンテキストウィンドウ(Context Window)。モデルが1度に入力できるトークンの数。(単位: トークン)

2

最大出力トークン(Max Output Tokens)。 モデルが1度に出力できるトークンの数。(単位: トークン)

3

入力トークン当たりの料金 (単位: $/1Mトークン)

4

キャッシュ済入力トークン当たりの料金 (単位: $/1Mトークン)

5

出力トークン当たりの料金 (単位: $/1Mトークン)

8