OpenAI API 응답 스트리밍 구현

Yi suho·2023년 9월 25일
0
post-thumbnail

프로젝트에서 OpenAI를 활용해 면접 질문에 대한 답변과 그 답변의 피드백을 제공하는 기능을 구현했다.
그러나 답변의 길이가 매우 길어져 사용자가 완성된 답변을 받기까지의 대기 시간이 길어져서 사용자 경험에 부정적인 영향을 줄 것으로 판단되었다.
이를 해결하기 위해 서버에서 OpenAI와의 통신을 스트림 방식으로 처리하여, 클라이언트에게 답변을 한 글자씩 실시간으로 전송하게끔 최적화하였다.

구현 전 API 요청

OpenAI API 응답 스트리밍 구현

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;
  }
}
  • 서버 설정
    먼저 서버에서 openAi를 호출하여 스트림을 얻고 이 스트림에서 데이터를 한글자씩 읽어서 클라이언트에 전송하도록 하였다.
    이때 EventSource(Server-Sent Events)또는 WebSocket을 사용하여 구현할 수 있다.
    이번엔 EventSource를 사용하여 구현해 보았다.

EventSource 헤더 설정

res.setHeader('Content-Type', 'text/event-stream');
  • Content-Type : 이 헤더는 클라이언트에게 응답의 내용 형식을 알려준다.
  • text/event-stream :
    이 값은 Server-Sent Event (SSE)를 위한 MIME 타입이다. 이를 통해 클라이언트(보통 웹 브라우저)는 서버로 부터 받는 데이터를 SSE로 해석하게 된다.
    이로 인해 서버에서 보내는 메시지들을 클라이언트가 실시간으로 받을 수 있게 된다.
res.setHeader('Cache-Control', 'no-cache');
  • Cache-control: 이 헤더는 응답 데이터의 캐싱 동작을 제어한다.
  • no-cache:
    이 값은 클라이언트에게 응답 데이터를 캐시에 저장하지 말라고 지시하는것이다.
    이렇게 함으로써,서버에서 실시간으로 보내는 데이터가 오래된 캐시 데이터로 대체되는 것을 방지 한다.
res.setHeader('Connection', 'keep-alive');:
  • Connection: 이 헤더는 네트워크 연결의 제어에 사용된다.
  • keep-alive:
    이 값은 네트워크 연결을 지속적으로 유지하도록 지시한다.HTTP 연결은 기본적으로 요청-응답이 완료되면 종료 되도록 설계되어 있지만,keep-alive 값을 사용함으로써 연결을 계속 유지할 수 있다.
    SSE에서는 서버가 클라이언트에게 지속적으로 메세지를 보내야 하므로 이 옵션이 필요하다.

이 헤더 설정들은 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;
  }
  • openAIChatStream 함수에서는 stream:true 옵션을 사용하여 OpenAI chat stream API로부터 연속적인 스트임을 받는다.이렇게 반환된 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 간격으로 전송
      }
    }
  • chunk.choices[0].delta.content.toString():
    creatQuestionFeedback의 chunk에서 선택된 내용을 문자열로 변환한다.
  • res.write(data: ${char}\n\n):
    EventSource 를 사용하여 클라이언트에 한 글자씩 전송한다.
  • await new Promise((resolve) => setTimeout(resolve, 100)):
    각 글자를 전송하기 전에 100ms의 지연을 생성한다.이는 클라이언트에서 데이터를 실시간으로 수신할 때
    사용자에게 자연스러운 느낌을 제공하기 위해서 이다.

구현 후 API 요청

0개의 댓글