在前一篇文章中,我們已經認識了 OpenAI API 的基本功能與使用方式。今天要正式進入實作,透過最簡潔的程式碼範例,快速體驗如何呼叫 API,並將它應用在一個可以即時互動的專案中。
這個專案是一個 CLI 聊天機器人,能在終端機中輸入問題並立即得到 AI 的回覆。雖然功能相對簡單,但它完整涵蓋了 API 串接的核心步驟,也能作為後續進一步擴充應用的基礎範本。
今天的目標是串接 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,我們需要在專案根目錄建立一個 .env
檔案,並填入你的 OpenAI API 金鑰:
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OpenAI SDK 預設會從
process.env.OPENAI_API_KEY
讀取金鑰。如果你使用.env
搭配dotenv
,在程式啟動時就會自動載入並生效。若你希望使用不同的變數名稱,也可以在建立OpenAI
client 時,手動指定apiKey
。
接下來,我們撰寫一個互動式命令列程式,讓使用者可以在終端機中輸入訊息並即時獲得 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();
在上述程式中,我們主要完成了以下工作:
new OpenAI()
初始化 API Client,並自動載入 .env
檔案中的 OPENAI_API_KEY
。readline
模組建立命令列介面,用於持續接收使用者輸入並輸出結果。這樣就完成了一個最簡單的 CLI 聊天機器人,可以即時和 GPT 模型對話。
完成程式後,就可以來測試看看效果了。在專案根目錄下執行以下指令啟動程式:
npm run dev
終端機會顯示以下訊息,代表聊天機器人已經啟動成功:
GPT Chatbot 已啟動,輸入訊息開始對話(按 Ctrl+C 離開)。
> Hello!
Hello! How can I assist you today?
此時你可以在 >
後面輸入任何訊息,例如提問或聊天,GPT 都會即時回覆你。程式會持續等待輸入,直到你按下 Ctrl+C
才會中止。
一般情況下,呼叫 GPT API 時,模型會先生成完整回覆,再一次性返回。這樣雖然簡單,但當回覆內容很長時,使用者往往需要等待一段時間才能看到結果,體驗上不夠即時。
為了改善這個問題,OpenAI 提供了 串流模式(Streaming)。在串流模式下,模型會一邊生成、一邊回傳部分內容,類似即時「逐字打字」的效果。這有幾個好處:
使用串流模式能顯著提升互動體驗,讓使用者更快獲得回饋。接下來,我們就要來改造聊天機器人,加入串流支援。
在前一個版本中,我們的聊天機器人只能等到模型完整生成回應後,才一次性輸出到終端機。這對於短訊息影響不大,但如果回覆內容較長,就會造成明顯的等待時間。
現在,我們要改造這個程式,啟用 串流模式。透過串流,模型可以邊生成、邊回傳部分內容,讓使用者即時看到 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();
在上述程式中,我們主要完成了以下工作:
new OpenAI()
初始化 API Client,並自動載入 .env
檔案中的 OPENAI_API_KEY
。readline
模組建立命令列介面,用於持續接收使用者輸入並輸出結果。stream: true
以獲取串流回應。for await...of
即時讀取模型逐步輸出的內容,並逐字顯示在終端機中。這樣一來,即使是長篇回覆,使用者也能立刻獲得回饋,整體體驗更貼近真實對話。
輸入一個需要較長回覆的問題,例如:
> 請幫我解釋什麼是 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();
在上述程式中,我們主要完成了以下工作:
OpenAI()
初始化 API Client,並讀取 .env
中的 OPENAI_API_KEY
。messages
陣列,並透過 system
訊息設定助理的角色,使模型能根據上下文維持一致的行為。readline
模組建立互動式命令列介面,持續接收使用者輸入。messages
陣列,並呼叫 OpenAI Chat API 啟用 stream: true
,以串流方式獲取模型回應。for await...of
即時逐字輸出模型回覆,同時累積內容存入 reply
。assistant
訊息形式加入 messages
陣列,確保後續對話具備上下文。這樣,我們的 CLI 聊天機器人就具備了 多輪對話能力。不論你輸入多少次訊息,它都能根據前後脈絡來生成更合理的回覆,而不再是單純的「問一句 → 回一句」。
在多輪對話的設計中,每次呼叫 GPT 模型時,messages
陣列裡的所有訊息都會被送進 API,並計入 token 使用量。隨著對話歷程逐漸累積,可能會出現以下問題:
gpt-4o-mini
的上限為 128,000 tokens(包含輸入與輸出),超過就會導致請求失敗。為了避免這些問題,常見的解決方式有以下幾種:
最直接的方式,就是限制對話長度,只保留「近期」的訊息,避免整個歷程不斷累積而變得過長。
例如,若我們只想保留最近三輪使用者與助理的互動,可以這樣寫:
const recentMessages = [
messages[0], // system 設定
...messages.slice(-6), // 最近三輪對話
];
這段程式碼會保留最前面的 system
設定(用來維持助理的角色與行為規則),以及最近三個 user
與 assistant
的訊息對。
這樣一來,模型仍能維持自然的對話流暢度,同時避免把冗長的歷程每次都送進 API。對於大部分聊天應用,保留少量上下文已經足以讓回應看起來合情合理。
在多輪對話中,並不是所有訊息都需要逐字保存。像是「使用者的角色、任務目標、偏好設定」這些背景資訊,通常在對話過程中不會頻繁變動,就可以透過「摘要」的方式壓縮,取代冗長的歷程紀錄。
這些資訊可以濃縮成一段簡短的 system
prompt,而不是保留原始的長篇對話。例如:
{ role: 'system', content: '使用者名叫小明,剛完成旅遊行程規劃。你是他的 AI 助理。' }
這種做法能大幅減少 token 使用量,同時保留必要的語境,讓模型仍能理解互動脈絡。比起單純刪除歷程,摘要能確保重要背景資訊不會遺失,是多輪對話中常見且非常實用的最佳實務。
當對話內容具有長期價值(例如:客服紀錄、醫療諮詢、專案討論),就不能單純依靠刪除或摘要,否則可能遺失關鍵資訊。這時候可以採取以下策略:
透過這些方法,我們能兼顧資訊完整性與效能表現,讓聊天機器人具備更真實的「長期記憶」。相關的進階技巧會在後續章節中再深入探討。
今天我們實作了一個能夠即時互動的 CLI 聊天機器人,並逐步升級它的功能:
openai
SDK 串接 GPT 模型,建立基礎的問答流程。readline
建立命令列介面,支援即時輸入與回覆。messages
陣列維持上下文,讓聊天機器人具備 多輪對話能力。從零開始,我們打造了一個能即時互動、能維持上下文的聊天機器人。這雖然只是入門範例,但已經涵蓋了 API 串接的核心觀念,可以作為後續進階應用的基礎。