Clova Studio
- 클로바 스튜디오가 무엇일까?
- 사실 생소한 단어지만 GPT 네이버 버전이라고 생각하면 된다.
- 즉 네이버에서 제공하는 LLM 서비스입니다.
- 이번 프로젝트에서 AI에게 채팅으로 질문을 할 수 있고 이를 통해 Clova Studio로 부터 답변을 받을 수 있었습니다.
NOSQL을 활용해서 컨텍스트 기억하기
- GPT를 쓰면 우리가 메시지를 입력할때 해당 메시지 이전 내용도 분석하여 답변하는 모습을 볼 수 있을것입니다.
- GPT가 이전 대화 컨텍스트를 가지고 있다가 요청이 오게 되면 이전 컨텍스트 내용도 같이 보내는 것입니다.
- 따라서 NOSQL을 통해 대화 방에 대한 컨텍스트를 유지하게 하였습니다.
왜 NOSQL을 선택했을까?
- 메모리와 잦은 삽입과 삭제 때문입니다.
- 저희는 방마다 대화 컨텍스트를 가지도록 하였습니다.
- 따라서 방을 생성하면 대화 컨텍스트가 생겨나고 모두 방에 나가게 되면 대화 컨텍스트가 삭제됩니다.
- 즉 삽입과 삭제가 잦게 발생하는데 이때 이 과정이 RDB보다는 NOSQL이 적합하다고 생각했습니다.
- 또한 REDIS 인메모리 DB를 활용해볼 생각을 했지만 메모리와 데이터 구조의 이유로 NOSQL을 선택했습니다.
- REDIS는 메모리에 데이터를 저장하므로 휘발성이 크고
(물론 AOF나 RDB 기능이 있긴하지만...) 메모리 용량에 의존적입니다. 만약 사용자가 많아진다면 메모리 용량에 효율적이지 않다고 생각했습니다.
- 또한 구조상 룸 마다 채팅 컨텍스트를 가지고 채팅 컨텍스트 안에 채팅에 대한 정보가 들어가 있습니다. 즉 채팅 컨텍스트는 채팅 Object를 가지고 있어야 하는데 Document 구조가 더 적합하다고 생각되었습니다.
export type LLMHistoryDocument = Document & LLMHistory;
const options: SchemaOptions = {
timestamps: true,
};
@Schema(options)
export class LLMHistory {
@Prop({ required: true })
room: string;
@Prop({
required: true,
type: [
{
role: { type: String, required: true },
content: { type: String, required: true },
},
],
})
messages: LLMMessageDto[];
}
export const LLMHistorySchema = SchemaFactory.createForClass(LLMHistory);
- 이런 LLMHistory라는 Document 모델을 만들고 이를 삽입하도록 하였습니다.
LLM 서비스로 요청 보내기
- Clova Studio로 요청을 보내는 것은 API 호출하는 것이기 때문에 크게 어렵지 않습니다.
- NOSQL을 조회하여 대화 컨텍스트를 가지고 온 후 여기에 새롭게 질문한 채팅을 더하여 이 내용을 ClovaStudio로 보냅니다.
- 하지만 문제는 반환값에서 발생합니다.
- 반환이 stream 형태로 오고 이를 한번에 보여주기 원했으므로 적절히 응답을 가공했어야 했습니다.
- 따라서 저희가 생각한 방식은 stream으로 오는 데이터를 data 이벤트와 end 이벤트로 나누어 관리했습니다.
- data 이벤트가 올때 새 배열에 값을 넣어주고 모든 stream이 오면 end 이벤트가 호출됩니다.
- end 이벤트에서는 stream으로 온 데이터 중 event가 result인 데이터를 찾아 해당 데이터를 반환합니다.
async processAIResponse(
room: string,
message: string,
socketId: string,
): Promise<LLMMessageDto> {
const llmHistoryStream = await this.useLLM(room, message, socketId);
const response = [];
llmHistoryStream.on('data', (chunk) => {
const lines = chunk.toString().split('\n\n');
lines.forEach((line) => {
response.push(line);
});
});
return new Promise((resolve, reject) => {
llmHistoryStream.on('end', () => {
const resultIndex = response.findIndex((line) =>
line.includes('event:result'),
);
const resultLine = response[resultIndex];
const data = resultLine.split('data:')[1].trim();
try {
const dataJson = JSON.parse(data);
const message = dataJson.message;
resolve({
role: message.role,
content: message.content,
});
} catch (error) {
reject('JSON parsing error');
}
});
});
}