05. 対話型アプリケーション

この資料は 2025 年 11 月時点での田中正吾の雑感として書いたものです。あくまでもハッカソンでザっと AI の連携を作れるようにするために、動かして進められるような説明用の資料とサンプル群を用意した次第です。

継続会話と標準入力を組み合わせた、実際に使える対話型アプリケーションを作りましょう。

学べること

  • readline を使った標準入力の処理
  • 対話ループの実装パターン
  • 継続会話とツール実行の組み合わせ
  • メッセージ履歴の実践的な管理
  • エラーハンドリング

準備するもの

  • Node.js (v18 以上)
  • OpenAI API キー
  • 04 の理解

セットアップ

サンプルコードは本教材の Codespace(リンク集を参照)の 05_interactive-chat/ にあります。

フォルダ移動

まず、ターミナルに表示されているカレントディレクトリ(現在の作業フォルダ)を確認しましょう。

  • /workspaces/hackathon-ai-and-mcp-sample-202511-codespace/ の場合は、以下のコマンドでフォルダ移動します。
cd 05_interactive-chat
  • 続けて 04 から勉強している場合は、カレントディレクトリが /workspaces/hackathon-ai-and-mcp-sample-202511-codespace/04_conversational-basics で一つ階層が深いので、以下のコマンドでフォルダ移動します。
cd ../05_interactive-chat

手元の Visual Studio Code で作業されている方は、現在作業中のカレントディレクトリを確認いただいて上記を参考にフォルダを移動しましょう。

※フォルダ移動について詳しくは「Codespace でのフォルダ移動」を参照してください。

インストール

カレントディレクトリの移動が確認できたら、関連ライブラリをインストールして準備します。

npm install

これ以降はこのカレントフォルダでコマンドを打って作業していきましょう。

API キーの設定

各サンプルファイルの先頭にある API キー設定箇所を、お使いの OpenAI API キーに書き換えてください。

01_weather-interactive-chat.js の場合:

// OpenAI API キー
const OPENAI_API_KEY = "ここにあなたのAPIキーを入力";

02_memo-interactive-chat.js の場合:

// OpenAI API キー
const OPENAI_API_KEY = "ここにあなたのAPIキーを入力";

サンプル一覧

天気チャット

ファイル: 01_weather-interactive-chat.js

node 01_weather-interactive-chat.js

標準入力で場所を聞き、Function Calling で天気 API を呼び出すシンプルな対話型アプリです。

特徴:

  • readline による標準入力の処理
  • Function Calling と外部 API の連携
  • 繰り返し質問できる対話ループ

使い方:

どの場所の天気を知りたいですか? > Tokyo
(天気情報が表示される)

どの場所の天気を知りたいですか? > Osaka
(天気情報が表示される)

どの場所の天気を知りたいですか? > exit

メモチャット

ファイル: 02_memo-interactive-chat.js

node 02_memo-interactive-chat.js

メモの追加・表示・削除を自然言語で行える対話型アプリです。継続会話を活用した実践例です。

特徴:

  • 継続会話による文脈の保持
  • 複数の Tool(add_memo, list_memos, delete_memo)の使い分け
  • ツール実行後の再問い合わせパターン
  • メモリ内でのデータ保持

使い方:

あなた > 買い物リストを追加して
アシスタント: メモを追加しました。

あなた > 牛乳も追加
アシスタント: 追加しました。

あなた > メモを見せて
アシスタント: 1. 買い物リスト
            2. 牛乳

あなた > 1番目を削除
アシスタント: 削除しました。

実装パターン

標準入力の処理(readline)

Node.js の readline モジュールを使って、ユーザー入力を受け取ります。

const readline = require("readline");

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const askUser = () => {
  rl.question("あなた > ", async (answer) => {
    // 入力を処理
    console.log("あなたの入力:", answer);

    // 次の質問へ(再帰的に呼び出す)
    askUser();
  });
};

askUser(); // 開始

readline.createInterface で入出力インターフェースを作成し、rl.question でユーザー入力を受け取ります。再帰的に askUser を呼び出すことで、対話ループを実現しています。

継続会話とツール実行の組み合わせ

const messages = [
  { role: "system", content: "システムメッセージ" }
];

// 1. ユーザー入力を履歴に追加
messages.push({ role: "user", content: userInput });

// 2. API 呼び出し
const completion = await openai.chat.completions.create({
  messages: messages,
  model: "gpt-4o-mini",
  tools: tools,
  tool_choice: "auto"
});

// 3. ツール実行
if (completion.choices[0].message.tool_calls) {
  // アシスタントのメッセージを履歴に追加
  messages.push(completion.choices[0].message);

  // ツール実行結果を履歴に追加
  messages.push({
    role: "tool",
    tool_call_id: toolCall.id,
    content: result
  });

  // 4. もう一度 API を呼び出して最終的な返答を得る
  const finalCompletion = await openai.chat.completions.create({
    messages: messages,
    model: "gpt-4o-mini"
  });
}

ツール実行結果は role: "tool" で履歴に追加します。そのとき、tool_call_id を指定してツール実行結果を紐付けます。そしてもう一度 API を呼び出すことで、ツール実行結果を踏まえた最終的な返答を得ることができます。

対話ループの終了

const askUser = () => {
  rl.question("あなた > ", async (answer) => {
    // 終了コマンド
    if (answer.toLowerCase() === "exit") {
      console.log("チャットを終了します。");
      rl.close();
      return;
    }

    // 処理を続ける
    // ...

    askUser();
  });
};

ユーザーが exit と入力したら、対話を終了します。

04 との違い

項目 04_conversational-basics 05_interactive-chat
入力 ハードコード 標準入力(readline)
会話 3-5ターン固定 ユーザーが終了するまで継続
目的 仕組みの理解 実用的なアプリケーション
ツール なし Function Calling を活用

04 では仕組みを理解するために固定の会話を実装しましたが、05 では実際に使える対話型アプリを作ります。

コードの説明

完全な実装例

const OpenAI = require("openai");
const readline = require("readline");

const OPENAI_API_KEY = "your-api-key";
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

// 会話履歴
const messages = [
  { role: "system", content: "あなたは親切なアシスタントです。" },
];

const askUser = () => {
  rl.question("あなた > ", async (answer) => {
    if (answer.toLowerCase() === "exit") {
      console.log("チャットを終了します。");
      rl.close();
      return;
    }

    // ユーザー入力を履歴に追加
    messages.push({ role: "user", content: answer });

    // API 呼び出し
    const completion = await openai.chat.completions.create({
      messages: messages,
      model: "gpt-4o-mini",
    });

    // アシスタントの返答を履歴に追加
    const assistantMessage = completion.choices[0].message;
    messages.push(assistantMessage);

    console.log("アシスタント:", assistantMessage.content);

    // 次の質問へ
    askUser();
  });
};

console.log("チャットを開始します。終了するには 'exit' と入力してください。");
askUser();

このコードを実行すると、ターミナルで AI とチャットできます。

知っておくと良いこと

エラーハンドリング

実用的なアプリでは、エラーハンドリングがあると安心です。

try {
  const completion = await openai.chat.completions.create({
    messages: messages,
    model: "gpt-4o-mini",
  });
  // ...
} catch (error) {
  console.error("エラーが発生しました:", error.message);
}

API 呼び出しが失敗した場合でも、アプリが停止せずに続けられるようになります。

会話履歴の管理

会話が長くなると、トークン数が増加します。必要に応じて、古い履歴を削除する方法もあります。

// 最新 10 件のメッセージのみ保持
if (messages.length > 10) {
  messages.splice(1, messages.length - 10); // system メッセージは残す
}

まずは動かしてみて、トークン数が気になるようなら検討してみてください。

ツール実行の非同期処理

readline.question のコールバック内で await を使う場合、async を付けるのを忘れないようにしてください。

rl.question("あなた > ", async (answer) => {
  // ここで await が使える
  const completion = await openai.chat.completions.create({ ... });
});

これがないと await が使えず、エラーになります。

次のステップ

対話型アプリケーションが作れましたね!次は Web API サーバーとして実装する方法を見ていきましょう。