最近、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モデル
推論モデル
作業の特性によってもあらかじめ使用するモデルを制限する方法を用意しておきたい。
これらをふまえて考えると、普段使いするのは gpt-4o-mini-2024-07-18
、少し複雑な問題を考える時には o3-mini-2025-01-31
、しっかり考えないような複雑な問題の時には o1-2024-12-17
を使うのがよさそうだ。
「OpenAI APIを使う」でチャットの機能のためのAPIの使い方について調べた。結果を一括で取得する方法と、 Server Sent Events
を用いて結果を徐々に受け取るストリーミングのような方法を調べた。
Server Sent Events
を受け取るためにはリクエストのBODYのパラメータ stream
に true
を設定する必要がある。
今回はこの方法を使い、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
では message
、 model
、 stream
のみを使用している。現状でも特に不満はないけれど、パラメータを妥当な値で細かく調整できれば、もっと応答の品質を上げられるだろう。そこでAPIのリクエストのボディに含めるパラメータを必要な分だけ学んでいく事にする。
これはOpenAIのChat Completion APIの公式ドキュメントを元にしたものだが、この文書では必要に応じて私見などが含まれている事に注意が必要だ。だから正しい情報を得たい場合には公式ドキュメントを参照すると良い9。
型 | string |
必須 | Required |
使用するモデルのIDを文字列で指定する。使用可能なモデルについてはモデルエンドポイント互換性テーブルで確認できる10。
そのテーブルによると現在、 /v1/chat/completions
では以下のモデルを使用できる。
Fine-tuned versions of
このモデルのうちどれを使用するかという事が、応答の品質と料金に大きく関わる。そのため何が使用可能なのかを正確に把握し、素早く切り替えられるようにする事と便利そうだ。
現状では curl
で使用しているテンプレートにハードコーディングしている。このモデルを切り替える手段を実装する。
(追記: これは実装した)
型 | boolean or null |
必須 | no |
デフォルト | false |
この属性にtrueが設定された場合、生成されたメッセージは部分的に徐々に、サーバ送信イベントとして返される。メッセージのストリームは [DONE]
というメッセージで終了を表す。
今回はChatGPTの機能をEmacsから使うために、OpenAI APIをEmacsから呼び出して利用する仕組みを実装した。必要に応じて使っていきたい。