프로젝트에서 OpenAI를 활용해 면접 질문에 대한 답변과 그 답변의 피드백을 제공하는 기능을 구현했다.
그러나 답변의 길이가 매우 길어져 사용자가 완성된 답변을 받기까지의 대기 시간이 길어져서 사용자 경험에 부정적인 영향을 줄 것으로 판단되었다.
이를 해결하기 위해 서버에서 OpenAI와의 통신을 스트림 방식으로 처리하여, 클라이언트에게 답변을 한 글자씩 실시간으로 전송하게끔 최적화하였다.
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
@Controller('your-controller-path')
export class YourController {
@Post()
async creatQuestionFeedback(
@Body() creatAnswer: CreatAnswerDto,
@Res() res: Response,
): Promise<any> {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const creatQuestionFeedback =
await this.gptChatService.createQuestionFeedback(creatAnswer);
for await (const chunk of creatQuestionFeedback) {
for (const char of chunk.choices[0].delta.content.toString()) {
res.write(`data: ${char}\n\n`); // 한 글자씩 EventSource로 전송
await new Promise((resolve) => setTimeout(resolve, 100)); // 각 글자를 100ms 간격으로 전송
}
}
}
@Injectable()
export class GptChatService {
constructor(
@Inject('OpenAi')
private readonly openAi: OpenAI,
) {}
async openAIChatStream(content: string): Promise<any> {
const completion = await this.openAi.chat.completions.create({
messages: [
{
role: 'user',
content: `${content}`,
},
],
model: 'gpt-3.5-turbo',
stream: true,//completion을 조각으로 나누어 반환받기 위해서 stram:true 값을 줘야 한다.
});
return completion;
}
async createQuestionFeedback(creatAnswer: CreatAnswerDto): Promise<any> {
const { question, answer } = creatAnswer;
const content = `원하는 질문`;
const complet = await this.openAIChatStream(content);
return complet;
}
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');:
이 헤더 설정들은 Server-Sent Events를 올바르게 구현하고,클라이언트와의 연결을 지속적으로 유지하며,데이터의 실시간 전송을 보장하기 위한 것이다.
async openAIChatStream(content: string): Promise<any> {
const completion = await this.openAi.chat.completions.create({
messages: [
{
role: 'user',
content: `${content}`,
},
],
model: 'gpt-3.5-turbo',
stream: true,
});
return completion;
}
creatQuestionFeedback 에 openAIChatStream 함수를 통해 반환된 스트림을 할당한 후,for of 루프를 사용하여 이 스트림에서 반환되는 각 조각(chunk)을 순차적으로 처리한다.
const creatQuestionFeedback =
await this.gptChatService.createQuestionFeedback(creatAnswer);
for await (const chunk of creatQuestionFeedback) {
for (const char of chunk.choices[0].delta.content.toString()) {
res.write(`data: ${char}\n\n`); // 한 글자씩 EventSource로 전송
await new Promise((resolve) => setTimeout(resolve, 100)); // 각 글자를 100ms 간격으로 전송
}
}