昨天,我們使用Sheet Modal為文法檢查建立了互動式的視窗。今天,我們會加入GPT模型,來對文法錯誤給予詳細的說明。這次的方法與之前的有所不同,我們將利用【Day - 8】提到的Stream串流模式。通過GPT API的Server Sent Event (SSE),我們可以逐步接收GPT模型回傳的資料。這方式不僅加速了資料傳輸,還大幅縮短了回應等待時間,也讓我們能夠實現與ChatGPT相似的即時交互體驗。
為了使用Server Sent Event (SSE),我們可以使用瀏覽器內建的EventSource API。不過要注意的是,它只支持url
和withCredentials
這兩個參數。這表示我們不能透過EventSource API傳遞header或body的資料,而且它僅適用於Get方法。
所以,最後決定使用由微軟提供的fetch-event-source套件。該套件不只可以支援自訂的header和傳輸body,還能使用Fetch API的所有功能。
我們可以透過以下的指令安裝:
npm i @microsoft/fetch-event-source
在安裝完畢後,在OpenAI Service的openai.service.ts
中導入該套件。同時,我們還要加入一個AbortController
物件,這可以用於取消正在進行的fetch請求。有關AbortController的功能,可以參考MDN文件:
import { fetchEventSource } from '@microsoft/fetch-event-source';
.
.
.
//中斷信號
private ctrl = new AbortController();
我們需要準備兩個方法,分別用於建立header和body。特別要注意,當與GPT API進行通信時,header中的Content-Type必須設定為application/json
,否則會無法使用。而當取得body方法時,請帶入從【Day - 14】中所建立的ChatMessageModel
。最後,切記要將stream
的值設定為true
:
private getFetchHeader() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer {你的Token}'
};
}
private getFetchBody(chatMessage: ChatMessageModel[]) {
return {
model: 'gpt-4',
messages: chatMessage,
temperature: 0.7,
top_p: 1,
stream: true
};
}
接下來,我們需要建立chatStreamAPI()
方法。這個方法會回傳一個新的Observable,且在其中會執行fetchEventSource()
方法。在onmessage()
中,我們可以接收到每次返回的訊息。如果返回的訊息不是「[DONE]」(這表示GPT API所有的訊息都已經發送完畢),那麼這些訊息會透過觀察者發送出去:
public chatStreamAPI(chatData: ChatMessageModel[]) {
return new Observable<ChatResponseModel>(observer => {
fetchEventSource('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: this.getFetchHeader(),
body: JSON.stringify(this.getFetchBody(chatData)),
signal: this.ctrl.signal,
//接收訊息事件
onmessage(msg) {
//如果訊息不是[DONE]則將訊息發送給觀察者
if (msg.data !== '[DONE]') {
observer.next(JSON.parse(msg.data));
}
},
//結束事件
onclose() {
console.log('%c Open AI API Close', 'color: red');
observer.complete();
},
//錯誤處理事件
onerror(err) {
observer.error(new Error(err));
}
});
});
}
使用Stream功能時,其回傳的Response結構和非Stream的回應有所不同。在非Stream模式下,結果回傳於choices[0].message.content
中;而在Stream模式下,則是於choices[0].delta.content
中。
因此,我們需要對【Day - 14】中的ChatChoicesModel
進行相應的調整:
export interface ChatChoicesModel {
index: number;
message: ChatMessageModel;
//加入delta
delta?: {
content: string
}
finish_reason: string;
}
在【Day - 23】建立的GrammerMistake元件中,我們做了以下調整:在grammermistake.component.ts
檔案中,注入OpenAI Service和新增了兩個變數,一個用於判斷是否正在執行,另一個則用於儲存GPT模型的回應:
//是否正在執行API
isGettingExplane = false;
//解釋文本
mistakExplaneText = '';
constructor(private statusService: StatusService,
private openAIService: OpenAIService) {
}
然後,我們加入了一個getMistakeChatMessageData()
的方法,該方法會返回ChatMessageModel
。在這個方法中,我們使用的提示,可以讓GPT模型提供文法錯誤的詳細解釋:
private getMistakeChatMessageData(contentData: string): ChatMessageModel[] {
return [
{
role: 'system',
content: '你是英文文法老師,請用繁體中文詳細解釋為什麼這句英文有錯誤。'
},
{
role: 'user',
content: contentData
}
];
}
利用【Day - 23】中介紹的「ionModalDidPresent」,這個事件會在Sheet Modal完全開啟後立即執行,我們將在開啟後,直接訂閱chatStreamAPI()
以取得詳細解釋:
onIonModalDidPresent(event: Event) {
if (this.mistakExplaneText === '') {
this.isGettingExplane = true;
this.statusService.grammerStatus$.pipe(
take(1),
switchMap(grammerStatusResult => this.openAIService.chatStreamAPI(this.getMistakeChatMessageData(grammerStatusResult.mistakeSentence))),
finalize(() => {
this.isGettingExplane = false;
})
).subscribe(result => this.mistakExplaneText += result.choices[0].delta?.content ? result.choices[0].delta?.content : '');
}
}
最後在Ion Model元件裡,我們使用內嵌繫結來顯示mistakeExplaneText
變數的內容。同時,在資料正在取得的階段,我在文字的末尾加入了「…」的動畫效果,用來提示API正在執行中,以增強使用者的體驗。
<ion-modal #modal [initialBreakpoint]="0.6" [breakpoints]="[0, 0.6, 1]"
(ionModalDidPresent)="onIonModalDidPresent($event)">
<ng-template>
<ion-header>
<ion-toolbar>
<ion-title>AI文法解析</ion-title>
<ion-buttons slot="end">
<ion-button (click)="modal.dismiss()">
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="flex flex-col p-4">
<div class="mb-2"><span class="bg-rose-400 text-white font-bold rounded-2xl px-2 py-1">錯誤句型:</span></div>
<div class="mb-6 underline pl-2 text-gray-600">{{ (grammerStatus$ | async)?.mistakeSentence }}</div>
<div class="mb-2"><span class="bg-lime-500 text-white font-bold rounded-2xl px-2 py-1">錯誤解析:</span></div>
<div class="pl-2 align-baseline">
<p class="text-gray-800">
{{ mistakExplaneText }}
<ng-container *ngIf="isGettingExplane">
<span class="dot text-xl font-bold text-purple-400" style="--i:1"> .</span>
<span class="dot text-xl font-bold text-orange-400" style="--i:2"> .</span>
<span class="dot text-xl font-bold text-blue-400" style="--i:3"> .</span>
<span class="dot text-xl font-bold text-amber-400" style="--i:4"> .</span>
</ng-container>
</p>
</div>
</div>
</ion-content>
</ng-template>
</ion-modal>
此為「…」的CSS動畫:
.dot {
opacity: 0;
animation: fadeDot 2.5s infinite;
animation-delay: calc(0.5s * var(--i));
}
@keyframes fadeDot {
0%,
80%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
最後完成編譯後,在實體機上進行測試,我故意講了一段常見的錯誤文法「Where are you come from ?」,來驗證看看整個運作的效果吧!
今天我們終於完成了文法解析功能的整合。其實最初的想法是讓AI在對話中即時解釋文法問題,但因為【Day - 19】提到的GPT-3.5模型和Function Calling的問題,導致這功能難以實現之外,英文能力較弱的使用者,譬如「我」,在同時理解對話和文法解釋時也是頗為吃力。但隨著功能的完成,我發現將文法提示做為附加資訊,可以讓使用者能更集中在聽力訓練上,並在必要時深入了解文法上的錯誤。這樣的方式不僅為應用帶來了更多的互動,也讓使用者得到了更精確且具深度的文法解析呢!
Github專案程式碼:Ionic結合ChatGPT - Day24