Skip to content

2주차 발표

ez edited this page Feb 14, 2025 · 1 revision

기획 배경 및 의도

서비스 소개

image (51)

Octodocs는 실시간 문서 협업 및 시각화 서비스로 다양한 문서들을 작성하고 서로 연관성이 있는 문서들을 연결할 수 있는 서비스입니다.

workspace에 존재하는 노드를 클릭하여 문서 열람과 수정을 할 수 있습니다.

발견한 문제

문서들을 시각적으로 연결하고 있어서 연결되어 있는 문서들이 많아지면 한 눈에 문서들의 정보를 확인하기 번거롭습니다.

예를 들어 회의록이라는 노드에 연결된 여러 개의 회의들이 존재할 때 모든 회의들의 내용을 확인하기 위해서 모든 노드를 하나씩 클릭하여 페이지를 열어야 하는 번거로움이 있습니다.

image (52)

기획 의도

연결된 많은 페이지의 정보를 기반으로 AI에게 질의를 할 수 있는 기능을 제공하려고 합니다.

사용자가 AI에게 요청 사항을 말하면 AI가 요청 사항과 관련된 문서를 검색해서 가져온 다음 가져온 문서를 기반으로 사용자의 요청 사항을 수행합니다.

예를 들어 **“회의록에 있는 내용들을 요약해줘”**라는 요청을 보내면 AI가 “회의록”과 관련된 문서들을 가져온 다음 그 문서들의 내용을 요약해줄 수 있습니다.

image (53)

스트리밍을 통한 RAG 실시간 답변 생성

제목 없는 동영상 - Clipchamp로 제작

사용성을 향상하기 위해 스트리밍 기능을 적용했습니다.

기존에는 사용자의 요청을 LLM의 입력으로 넣어주면 전체 응답을 받아 바로 클라이언트에게 반환해주었습니다.

그런데 이러한 방식은 모든 응답이 생성될 때까지 기다려야 하는 단점이 있었습니다.

그래서 LLM이 제공해주는 streaming 기능을 활용해서 chunk 단위로 클라이언트에게 응답을 내려주기로 결정했습니다.

LLM이 chunk 단위로 응답을 전해주면 서버에서는 클라이언트에게 text/event-stream type으로 계속 chunk를 전달해주었습니다.

이 과정에서 이슈가 하나 있었습니다.

저희 프로젝트에서는 reverse proxy server로 Nginx를 사용했는데 Nginx는 기본적으로 buffering 기능이 활성화 되어 있어서 chunk 단위로 전달하더라도 Nginx가 buffering으로 모두 모아두었다가 완성되었을 때 클라이언트에게 전달해주었습니다.

image (54)

langchain API의 location block에 buffering 옵션을 꺼서 이슈를 해결할 수 있었습니다.

로컬 임베딩 모델로 변경

저희 프로젝트는 실시간 문서 동시 편집 기능을 지원하기 때문에 문서의 변경 사항이 잦을 수밖에 없습니다.

그리고 문서의 변경 사항이 발생할 때마다 임베딩을 다시 적용하여 새로운 vector를 만들어야 합니다.

기존에는 변경 사항이 발생할 때마다 OpenAI API를 사용하여 임베딩을 진행했더니 비용이 많이 부과되는 문제가 있었습니다.

그래서 채팅 모델은 OpenAI API를 그대로 사용하되 임베딩은 로컬 모델로 진행하기로 결정했습니다.

모델은 HuggingFaceTransformersEmbeddings을 사용하여 로컬 cpu로 임베딩을 진행하여 변경 사항이 발생할 때마다 비용이 부과되는 문제를 해결할 수 있었습니다.

문서 유사도 검색 성능 향상

기존에는 오직 요청 vector와 문서 vector의 유사도를 기반으로 문서를 조회했습니다.

하지만 사용자의 요청에 포함된 키워드에 대한 문서를 잘 가져오지 못 하는 현상이 발생했습니다.

그래서 keyword 기반 full text search를 구현하고 RRF 방식을 적용하여 keyword와 벡터 유사도를 모두 활용하여 문서를 가져올 수 있게 구현했습니다.

image (55)

full text search의 경우 문서의 내용을 tsvector로 변환하여 tsvector 컬럼에 저장하여 특정 키워드를 포함하는 문서의 종류를 가져올 수 있는 구조로 만들었습니다.

vector 유사도의 경우 문서의 내용을 vector로 변환하여 pgvector 컬럼에 저장하였고 vector 유사도를 계산할 수 있는 구조로 만들었습니다.

fetch를 활용한 실시간 스트림 데이터 처리

기존 프로젝트는 axios를 이용하여 통신을 진행했지만 스트리밍 관련 레퍼런스는 모두 fetch를 사용하였습니다.

브라우저 환경에서는 axios가 내부적으로 XMLHttpRequest(XHR)를 사용하기 때문에 전체 응답 데이터를 모두 수신한 후에야 데이터를 처리할 수 있습니다. 따라서 스트리밍 응답을 다루기가 어렵습니다.

반면, 브라우저에 내장된 fetch API는 스트림 응답을 처리할 수 있도록 ReadableStream 인터페이스를 제공해줍니다.

따라서 클라이언트에서 실시간 스트림 데이터를 처리하려면 fetch를 사용하는 것이 더 적합하다고 판단하여 스트리밍의 경우만 fetch를 활용하기로 결정하였습니다.

const response = await postLangchain(query);

if (!response.body) return;

const reader = response.body
    .pipeThrough(new TextDecoderStream())
    .getReader();

while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    const convertValue = value.replace(/\n/g, "");

    updateData(convertValue);
}
  • 응답의 body 스트림을 TextDecoderStream을 통해 디코딩하고, getReader()를 사용하여 리더를 생성합니다.
  • while 루프를 통해 스트림에서 데이터를 읽으며, done 상태가 되면 루프를 종료합니다.

UI/UX 개선

RAG 패널을 닫고 열어도 질문이 유지되도록 개선

제목 없는 동영상 - Clipchamp로 제작 (1)

기존에는 패널을 다시 렌더링 할 경우 질문과 답변이 초기화되었습니다. 그래서 사용자가 이전의 문답을 다시 확인할 수 없는 불편함이 존재했습니다.

zustand를 활용하여 전역에서 관리하여 유지되도록 개선하였습니다.

  • React의 context API와 zustand에서 고민하였으나, 기존 로직에서도 zustand를 활용하고 있어 일관성을 위해 zustand로 결정하였습니다.

답변의 경우 스트리밍을 통해서 업데이트되므로 실시간 갱신할 필요가 있었습니다.

그래서 상태 업데이트가 이전 상태를 기반으로 이루어지는 functional update를 적용하였습니다.

export const useLangchainStore = create<langchainStore>((set) => ({
  question: "",
  answer: "",
  setQuestion: (question: string) => set({ question }),
  setAnswer: (answerOrUpdater: string | ((prev: string) => string)) =>
    set((state) => ({
      answer:
        typeof answerOrUpdater === "function"
          ? answerOrUpdater(state.answer)
          : answerOrUpdater,
    })),
}));

setAnswer((*prev*) => *prev* + *chunk*);

예시 질문 컴포넌트 추가

안내 문구만으로는 사용자가 RAG 기능을 사용하기에 어려움이 있을 수 있다고 판단하였습니다. 그래서 사용자가 기능 사용에 도움이 되도록 예시 질문 컴포넌트를 추가하였습니다.

워크스페이스마다 기반이 되는 문서에 따라 질문의 내용이 다르다고 판단하였습니다. 그래서 클릭하면 질문을 하는 등의 상호작용은 없습니다.

image (56)

결과

저희는 2주차에 UI/Ux 개선 및 스트리밍 기능 구현으로 사용성을 향상할 수 있었고 검색 기능을 수정하여 사용자의 요청을 잘 처리할 수 있는 문서를 가져올 수 있게 되었습니다. image (57)

Clone this wiki locally