TL; DR
- 1960年代の人工無能 Eliza(Doctor)の OpenAI 互換 API サーバーを実装
- Node.js実装を使用
はじめに
もはやLLMを見かけない日は無いこの頃。人間と話しているよりAIと話している時間の方が長いかもしれません。
ところで、人と会話するプログラムは数年前に突然現れたわけではありません。Emacs Lispの doctor コマンドでおなじみ ELIZA の初期版は1960年代に作られました。
あなたがエージェントとコードを書き、チャッピーやClaudeに悩み相談をしている間、ELIZAは M-x doctor がもう一度呼ばれる日を静かに待っているのです。
LLMのように呼び出すことができたなら、いつかみたいに彼女の元へ人が集まるのでしょうか?
作ったもの
呼び出せるようにしました。
OpenAPIのSDKを使用したサンプルクライアント1から呼び出してみましょう。
$ python main.py
Chatbot started (type 'exit' to quit)
Session ID: 33f80a23-da00-4e7e-b4cd-49f68c95874d
You: What's your name?
Assistant: I've told you before, I don't care about names -- please continue.
You: Recently I've been tired.
Assistant: You say recently you've been tired ?
You: Exactly. Could you recommend any ways to relax?
Assistant: Oh, I recommend any ways to relax ?
塩対応で名前を教えてくれなかったり微妙に返答がかみ合ってなかったりしますが、この喋り方はまごうことなきELIZAです。
OpenAI用のチャットアプリにELIZAを使うことができるようになりました!
...言うまでもありませんが、望むタスクができるかどうかは別問題です。
$ python main.py
Chatbot started (type 'exit' to quit)
Session ID: 04747cd5-3af8-44c8-9a1e-e180db078975
You: Implement fizzbuzz in Python.
Assistant: Do you feel strongly about discussing such things ?
実装
OpenAI APIの仕様
OpenAIのAPI仕様は一般公開されています2。この仕様を満たすAPIを作ることで、別のLLMをOpenAI用のツールから利用することができます(代表的なものはllama.cpp等のローカルLLM)。
今回はチャットに使用するCompletion API POST /v1/chat/completions を使用します。
なお、公式リファレンスに
If you’re building any text generation app, we recommend using the Responses API over the older Chat Completions API. And if you’re using a reasoning model, it’s especially useful to migrate to Responses.
とあり推論モデルを使いたい場合はResponses APIを使う必要がありますが、ELIZAは推論できないためここでは不要です。
ELIZAのAPI化
続いて、実際にELIZAを起動するOpenAI API互換のAPIサーバーを実装します。
Emacs Lisp版の実装だとサーバー周りの作りこみに手こずりそうだったため、JS版実装を使用しました。
詳細については作者の方の記事をご覧ください。
後はClaude Code様に丸投げをすれば Expressで上記パスにELIZA呼び出し処理を実装すれば完成です。
eliza-jsの状態管理
eliza-jsの elizabot オブジェクトは状態を持ち、ユーザーの文章を渡すたびにそれに対する返答を返します。
const reply = bot.transform(userMessage);
一方、OpenAI APIでは、過去の会話をステートレスにまとめて送信します。そのため、レスポンスを返した後もサーバー側で elizabot オブジェクトを動かし続ける必要があります。
というわけで、以下Claude Code本人による説明です。
(※内容の妥当性は著者の方で確認済みです)
状態管理:ハッシュキャッシュ方式
OpenAI API はリクエスト毎に全履歴を送ってきます。この性質を利用します。
「今回の返答を返した直後の bot 状態」を、次回リクエストのキーでキャッシュする方式です。
Turn1: messages=[U0]
→ 新鮮な bot で transform(U0) → 返答 R0
→ hash([U0, {assistant: R0}]) → bot を保存
Turn2: messages=[U0, A0, U1]
→ history = messages[:-1] = [U0, A0] の hash を計算
→ キャッシュヒット!同じ bot インスタンスを取り出す
→ bot.transform(U1) → 状態が正確に引き継がれた返答
→ hash([U0, A0, U1, {assistant: R1}]) → bot を保存
ポイントは「保存するキーは 次のリクエストが参照するキー と一致する」点です。Turn1 終了時に hash([U0, A0]) で保存しておけば、Turn2 の history のハッシュと完全に一致します。
クライアント側から見ると「セッション ID なし・全履歴送信」のまま変わりません。サーバー内部だけで正確な状態が保持されます。
(Claude Codeによる説明ここまで)
会話履歴をJSON化してハッシュ計算、それをキーにして Map で管理するという構成になっています。正直cryptoはオーバースペックですが、特に性能を気にする必要も無いのでこのままにします。
function hashMessages(messages) {
return crypto.createHash('sha256').update(JSON.stringify(messages)).digest('hex');
}
if (history.length === 0) {
// 初回は新規作成
bot = new elizabot();
} else {
const cached = sessions.get(historyKey);
if (cached) {
bot = cached;
} else {
// ...
}
}
おわりに
以上、ELIZAをOpenAI互換のAPIで呼び出せるようにした紹介でした。あなたのツールにも、ELIZAを組み込んでチャットしてみてはいかがでしょうか?
この記事の執筆中もELIZAよりClaude Codeを触った時間が長かった点については、見なかったことにします
-
llama.cppの動作確認用に作った(
Claude Codeに作らせた)ものを転用しています。実装: https://zenn.dev/link/comments/ce51dad24a4cf0 ↩ -
OpenAI APIのOpenAPI(
ややこしい)はこちら: https://github.com/openai/openai-openapi ↩