はじめに
OpenAIのChatGPT APIをつかったチャットとかを作っていると、公式みたいにチャットの文字をストリーミングつかって徐々に出したいっていうことあると思います。短い文章ならまだいいとして、特に長い文章だと、応答が遅くなるだけでストレスですものね。
というわけで、ChatCompletionのstreamオプションをtrueにした際の実装の注意点を書き残したいと思ます。ちなみにサーバサイドはFastAPIで、フロントエンドはSvelteです。
サーバサイド(FastAPI)
ChatGPTのstreamオプションをtrueにする
ChatGPTをコールする際に、stream=Trueを指定します。engineのところ、AzureOpenAIの場合のコードになっています。素のOpenAIの場合はmodel="gpt-3.5-turbo"
ですね。
response = openai.ChatCompletion.create(
engine=settings.AOAI_MODEL, # engine = "deployment_name".
messages=messages,
temperature=temperature,
stream = True,
)
ChatGPTのresponseをStreamingResonseで返す
stream=Trueにするとresponseのなかがchunk構成になっています。公式の内部構造を参考に、ここから応答の文字列をストリームで返します。以下みたいな感じですね。これでサーバからは応答文字列がストリームで返ってきます。
from fastapi.responses import StreamingResponse
def chatgpt_stream(response):
for chunk in response:
if chunk is not None:
content = chunk["choices"][0]["delta"].get("content")
if content is not None:
yield content
@router.post("/test")
def message(messages: List[Message]):
response = call_chatgpt([m.dict() for m in messages])
return StreamingResponse(
chatgpt_stream(response),media_type="text/event-stream"
)
フロントエンド(Svelte)
フロントエンドでストリームを受け取る
Svelte側ではストリームで受け取った内容を、取得でき次第更新していきます。今回作りが配列を表示している関係で配列=配列とかやってます(けど、もう少しうまくできそうではありますが)
ざっくり以下みたいなコードです。TextDecoderをつかって、文字をとって、表示文字を更新する感じです。
const response = await fetch("/simple/stream", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(send_chats)
});
const latest_chat = {"role":"assistant","content":""}
chats = [...chats, latest_chat]
if(response.body){
const decoder = new TextDecoder();
const reader = response.body.getReader();
while(true){
const {done, value} = await reader.read();
if(done){
break;
}
const text = decoder.decode(value);
latest_chat.content += text;
chats = chats
}
}
おわりに
FastAPIとSvelteを使っている場合に、ChatGPTのレスポンスをどうストリーム化するかのポイントを紹介しました。ただ、個人的には別にぐるぐるローダーが回っていてもそんなに気にならないので、ここはもうUIどうするか次第ですけどね。
あと、Azure OpenAIの方はフィルターの設定次第では固まりの粒度が大きくなるとかあるみたいなので、これまた検証しないといけないです。
ひとまず文字がにょろにょろ出るようにはなりましたということで、以上です。