iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
生成式 AI

用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰系列 第 3

Day 03 - OpenAI API 快速上手:實作 CLI 聊天機器人

  • 分享至 

  • xImage
  •  

在前一篇文章中,我們已經認識了 OpenAI API 的基本功能與使用方式。今天要正式進入實作,透過最簡潔的程式碼範例,快速體驗如何呼叫 API,並將它應用在一個可以即時互動的專案中。

這個專案是一個 CLI 聊天機器人,能在終端機中輸入問題並立即得到 AI 的回覆。雖然功能相對簡單,但它完整涵蓋了 API 串接的核心步驟,也能作為後續進一步擴充應用的基礎範本。

實作:簡易 CLI 聊天機器人

今天的目標是串接 OpenAI 的 GPT 模型,並用 Node.js 打造一個最簡單的終端機聊天機器人。透過這個範例,你可以快速體驗「輸入問題 → 呼叫 API → 取得回應」的完整流程。

在開始之前,請先初始化一個新的專案,命名為 gpt-chatbot,我們將在這個專案中完成實作內容。

如果你對 Node.js 專案初始化流程還不熟悉,可以先回顧 Day 01 中「建立 Node.js 專案與 TypeScript 開發環境」的內容。

安裝依賴套件

建立專案環境後,請在專案根目錄中執行以下指令,安裝所需依賴套件:

npm install openai dotenv
  • openai:官方提供的 OpenAI API 套件,用來呼叫 GPT 模型。
  • dotenv:用來載入 .env 檔案中的環境變數,例如 API 金鑰。

設定 API 金鑰

為了讓程式能成功呼叫 API,我們需要在專案根目錄建立一個 .env 檔案,並填入你的 OpenAI API 金鑰:

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

OpenAI SDK 預設會從 process.env.OPENAI_API_KEY 讀取金鑰。如果你使用 .env 搭配 dotenv,在程式啟動時就會自動載入並生效。若你希望使用不同的變數名稱,也可以在建立 OpenAI client 時,手動指定 apiKey

建立 CLI 主程式

接下來,我們撰寫一個互動式命令列程式,讓使用者可以在終端機中輸入訊息並即時獲得 GPT 回覆。

請打開 src/index.ts 並輸入以下程式碼:

// src/index.ts
import 'dotenv/config';
import readline from 'readline';
import { OpenAI } from 'openai';

async function main() {
  const openai = new OpenAI();

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

  console.log('GPT Chatbot 已啟動,輸入訊息開始對話(按 Ctrl+C 離開)。\n');
  rl.setPrompt('> ');
  rl.prompt();

  rl.on('line', async (input) => {
    try {
      const response = await openai.chat.completions.create({
        model: 'gpt-4o-mini',
        messages: [{ role: 'user', content: input }],
      });

      console.log(`\n${response.choices[0]?.message?.content}\n`);
    } catch (err) {
      console.error(err);
    }

    rl.prompt();
  });
}

main();

在上述程式中,我們主要完成了以下工作:

  1. 使用 new OpenAI() 初始化 API Client,並自動載入 .env 檔案中的 OPENAI_API_KEY
  2. 透過 Node.js 的 readline 模組建立命令列介面,用於持續接收使用者輸入並輸出結果。
  3. 每當使用者輸入文字時,程式即呼叫 OpenAI 的 Chat API,將輸入內容傳遞給模型並取得回應。
  4. 最終,模型的回覆會即時顯示在終端機中,形成一個可互動的對話流程。

這樣就完成了一個最簡單的 CLI 聊天機器人,可以即時和 GPT 模型對話。

測試執行結果

完成程式後,就可以來測試看看效果了。在專案根目錄下執行以下指令啟動程式:

npm run dev

終端機會顯示以下訊息,代表聊天機器人已經啟動成功:

GPT Chatbot 已啟動,輸入訊息開始對話(按 Ctrl+C 離開)。

> Hello!

Hello! How can I assist you today?

此時你可以在 > 後面輸入任何訊息,例如提問或聊天,GPT 都會即時回覆你。程式會持續等待輸入,直到你按下 Ctrl+C 才會中止。

提升對話體驗:使用串流模式

一般情況下,呼叫 GPT API 時,模型會先生成完整回覆,再一次性返回。這樣雖然簡單,但當回覆內容很長時,使用者往往需要等待一段時間才能看到結果,體驗上不夠即時。

為了改善這個問題,OpenAI 提供了 串流模式(Streaming)。在串流模式下,模型會一邊生成、一邊回傳部分內容,類似即時「逐字打字」的效果。這有幾個好處:

  • 更即時:使用者可以立即看到回覆開頭,而不必等完整結果。
  • 體驗更自然:對話感受更像真人輸入訊息。
  • 適合長文回應:當回答內容很長時,可以避免一次性等待的延遲。

使用串流模式能顯著提升互動體驗,讓使用者更快獲得回饋。接下來,我們就要來改造聊天機器人,加入串流支援。

實作:支援串流模式的 CLI 聊天機器人

在前一個版本中,我們的聊天機器人只能等到模型完整生成回應後,才一次性輸出到終端機。這對於短訊息影響不大,但如果回覆內容較長,就會造成明顯的等待時間。

現在,我們要改造這個程式,啟用 串流模式。透過串流,模型可以邊生成、邊回傳部分內容,讓使用者即時看到 AI 的回覆逐步出現,互動體驗也會更流暢。

修改主程式

請將 src/index.ts 改寫如下:

// src/index.ts
import 'dotenv/config';
import readline from 'readline';
import { OpenAI } from 'openai';

async function main() {
  const openai = new OpenAI();

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

  console.log('GPT Chatbot 已啟動,輸入訊息開始對話(按 Ctrl+C 離開)。\n');
  rl.setPrompt('> ');
  rl.prompt();

  rl.on('line', async (input) => {
    try {
      const stream = await openai.chat.completions.create({
        model: 'gpt-4o-mini',
        messages: [{ role: 'user', content: input }],
        stream: true,
      });

      process.stdout.write('\n');
      for await (const chunk of stream) {
        const content = chunk.choices[0]?.delta?.content || '';
        process.stdout.write(content);
      }
      process.stdout.write('\n\n');
    } catch (err) {
      console.error(err);
    }

    rl.prompt();
  });
}

main();

在上述程式中,我們主要完成了以下工作:

  1. 使用 new OpenAI() 初始化 API Client,並自動載入 .env 檔案中的 OPENAI_API_KEY
  2. 透過 Node.js 的 readline 模組建立命令列介面,用於持續接收使用者輸入並輸出結果。
  3. 每當使用者輸入文字時,程式即呼叫 OpenAI 的 Chat API,並啟用 stream: true 以獲取串流回應。
  4. 透過 for await...of 即時讀取模型逐步輸出的內容,並逐字顯示在終端機中。
  5. 在每次回覆完成後,重新提示使用者輸入,形成持續的對話流程。

這樣一來,即使是長篇回覆,使用者也能立刻獲得回饋,整體體驗更貼近真實對話。

執行測試

輸入一個需要較長回覆的問題,例如:

> 請幫我解釋什麼是 Node.js 的 Event Loop?

終端機就會即時輸出模型逐步生成的內容,看起來就像 AI 正在「打字」一樣,互動效果更自然。

加入多輪對話支援:維持上下文歷程

目前我們的 CLI 聊天機器人雖然能正常互動,但有個明顯的限制:它完全沒有記憶。換句話說,每次輸入訊息,模型都把它當成一場全新的對話,完全不知道你之前問過什麼或聊過什麼,因此回應常常顯得不夠自然。

為什麼需要「對話歷程」?

GPT 模型本身是「無狀態」的,並不會自動記住先前的對話紀錄。若要讓模型理解上下文,就必須在每次呼叫 API 時,一併提供完整的對話歷程。這樣模型才能將當前的問題與先前的對話串聯起來,生成更符合邏輯的回覆。

這也是 OpenAI 的 Chat Completions API 設計 messages 陣列的原因。每一則訊息都會標註 角色(role),藉此描述對話的來源與意圖:

  • system:系統提示,用來設定模型的角色或行為,例如「你是一個樂於助人的 AI 助理」。
  • user:使用者輸入的訊息,代表人類的提問或指令。
  • assistant:模型先前的回應,必須一併保存,以便模型能持續延續對話脈絡。

不同角色的訊息共同構成一段有邏輯的對話歷程,是模型理解上下文的基礎。

範例:維持對話上下文

假設我們建立一段對話歷程:

[
  { role: 'system', content: '你是一個樂於助人的 AI 助理。' },
  { role: 'user', content: '我今天心情不好。' },
  { role: 'assistant', content: '怎麼了呢?要不要跟我說說?' },
  { role: 'user', content: '工作壓力很大。' },
]

在這個例子中,GPT 就能理解「工作壓力很大」是承接使用者前面提到的「心情不好」,而不是一個毫無關聯的全新問題。

透過維持對話歷程,我們就能讓聊天機器人具備「記憶感」,回應也會更自然、更貼近人類對話的模式。

實作:支援多輪對話的聊天機器人

接下來,我們要改寫程式,讓聊天機器人能夠「記住」先前的對話,並在後續回應中考慮上下文。這樣使用者就能體驗到更接近真實聊天的互動效果。

修改主程式

請打開 src/index.ts,將原本的單輪對話邏輯改成以下內容:

// src/index.ts
import 'dotenv/config';
import readline from 'readline';
import { OpenAI } from 'openai';
import { ChatCompletionMessageParam } from 'openai/resources';

async function main() {
  const openai = new OpenAI();

  const messages: ChatCompletionMessageParam[] = [
    { role: 'system', content: '你是一個樂於助人的 AI 助理。' },
  ];

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

  console.log('GPT Chatbot 已啟動,輸入訊息開始對話(按 Ctrl+C 離開)。\n');
  rl.setPrompt('> ');
  rl.prompt();

  rl.on('line', async (input) => {
    messages.push({ role: 'user', content: input });

    try {
      const stream = await openai.chat.completions.create({
        model: 'gpt-4o-mini',
        messages,
        stream: true,
      });

      let reply = '';

      process.stdout.write('\n');
      for await (const chunk of stream) {
        const content = chunk.choices[0]?.delta?.content || '';
        process.stdout.write(content);
        reply += content;
      }
      process.stdout.write('\n\n');

      messages.push({ role: 'assistant', content: reply });
    } catch (err) {
      console.error(err);
    }

    rl.prompt();
  });
}

main();

在上述程式中,我們主要完成了以下工作:

  1. 使用 OpenAI() 初始化 API Client,並讀取 .env 中的 OPENAI_API_KEY
  2. 建立 messages 陣列,並透過 system 訊息設定助理的角色,使模型能根據上下文維持一致的行為。
  3. 利用 Node.js 的 readline 模組建立互動式命令列介面,持續接收使用者輸入。
  4. 每次接收到輸入時,將訊息加入 messages 陣列,並呼叫 OpenAI Chat API 啟用 stream: true,以串流方式獲取模型回應。
  5. 透過 for await...of 即時逐字輸出模型回覆,同時累積內容存入 reply
  6. 回覆完成後,將 AI 的輸出以 assistant 訊息形式加入 messages 陣列,確保後續對話具備上下文。
  7. 最後重新顯示提示符,等待下一次使用者輸入,形成持續互動的多輪對話流程。

這樣,我們的 CLI 聊天機器人就具備了 多輪對話能力。不論你輸入多少次訊息,它都能根據前後脈絡來生成更合理的回覆,而不再是單純的「問一句 → 回一句」。

訊息過長怎麼辦?

在多輪對話的設計中,每次呼叫 GPT 模型時,messages 陣列裡的所有訊息都會被送進 API,並計入 token 使用量。隨著對話歷程逐漸累積,可能會出現以下問題:

  • 超出模型限制:例如 gpt-4o-mini 的上限為 128,000 tokens(包含輸入與輸出),超過就會導致請求失敗。
  • 成本增加:對話越長,每次請求需要處理的內容就越多,費用自然升高。
  • 速度變慢:輸入太長不僅增加延遲,還可能因過大 payload 而出錯。

為了避免這些問題,常見的解決方式有以下幾種:

僅保留最近幾輪對話

最直接的方式,就是限制對話長度,只保留「近期」的訊息,避免整個歷程不斷累積而變得過長。

例如,若我們只想保留最近三輪使用者與助理的互動,可以這樣寫:

const recentMessages = [
  messages[0], // system 設定
  ...messages.slice(-6), // 最近三輪對話
];

這段程式碼會保留最前面的 system 設定(用來維持助理的角色與行為規則),以及最近三個 userassistant 的訊息對。

這樣一來,模型仍能維持自然的對話流暢度,同時避免把冗長的歷程每次都送進 API。對於大部分聊天應用,保留少量上下文已經足以讓回應看起來合情合理。

對背景訊息進行摘要

在多輪對話中,並不是所有訊息都需要逐字保存。像是「使用者的角色、任務目標、偏好設定」這些背景資訊,通常在對話過程中不會頻繁變動,就可以透過「摘要」的方式壓縮,取代冗長的歷程紀錄。

這些資訊可以濃縮成一段簡短的 system prompt,而不是保留原始的長篇對話。例如:

{ role: 'system', content: '使用者名叫小明,剛完成旅遊行程規劃。你是他的 AI 助理。' }

這種做法能大幅減少 token 使用量,同時保留必要的語境,讓模型仍能理解互動脈絡。比起單純刪除歷程,摘要能確保重要背景資訊不會遺失,是多輪對話中常見且非常實用的最佳實務。

儲存歷程並根據需求載入

當對話內容具有長期價值(例如:客服紀錄、醫療諮詢、專案討論),就不能單純依靠刪除或摘要,否則可能遺失關鍵資訊。這時候可以採取以下策略:

  • 存檔保存:將完整對話歷程寫入檔案或資料庫,確保資料不會遺失,方便日後查詢或稽核。
  • 選擇性載入:在呼叫 API 時,根據使用者的提問,動態載入「相關」的對話片段,而不是一次丟出所有紀錄。
  • 進階方式:結合 記憶系統(Memory)知識檢索(Retrieval) 技術,自動挑選並提供最相關的上下文,讓模型能夠回應更精準。

透過這些方法,我們能兼顧資訊完整性與效能表現,讓聊天機器人具備更真實的「長期記憶」。相關的進階技巧會在後續章節中再深入探討。

小結

今天我們實作了一個能夠即時互動的 CLI 聊天機器人,並逐步升級它的功能:

  • 使用 openai SDK 串接 GPT 模型,建立基礎的問答流程。
  • 透過 Node.js readline 建立命令列介面,支援即時輸入與回覆。
  • 加入 串流模式,讓回應能逐字輸出,改善等待體驗。
  • 建立 messages 陣列維持上下文,讓聊天機器人具備 多輪對話能力
  • 探討訊息過長的問題,並介紹三種解法: 保留最近幾輪訊息將背景訊息做摘要 以及 儲存完整歷程並依需求載入 等策略。

從零開始,我們打造了一個能即時互動、能維持上下文的聊天機器人。這雖然只是入門範例,但已經涵蓋了 API 串接的核心觀念,可以作為後續進階應用的基礎。


上一篇
Day 02 - 認識 OpenAI API:模型、功能、使用限制
下一篇
Day 04 - 提示工程與角色設計:打造回應清楚的 AI 助理
系列文
用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言