개인공부&프로젝트

퍼스널 페이지에 LLM 연동하기 3편 – RAG 연결과 Langfuse 기반 모니터링

백악기작은펭귄 2025. 4. 6.
반응형

퍼스널 페이지에 LLM 연동하기 3편 – RAG 연결과 Langfuse 기반 모니터링

TL;DR
Astro 기반 챗봇 UI와 Gemini API를 연결한 이후, 본격적으로 벡터 검색 기반 RAG 기능을 통합했다. Pinecone을 통해 사용자의 질문과 유사한 컨텍스트를 찾아내고, Gemini로부터 응답을 생성하는 구조를 구현했으며, Langfuse를 활용해 전 과정을 트레이싱하고 분석할 수 있도록 했다.

 

앞선 글에서 챗봇 UI와 Gemini API를 연동한 구조까지 소개했다면, 이번 글에서는 드디어 1편에서 구축한 벡터 지식베이스를 챗봇과 연결하고, 실시간 RAG(Retrieval-Augmented Generation)를 수행하는 기능을 구현하는 과정을 정리한다. 또한, 챗봇 시스템의 행동을 추적하고 분석하기 위해 도입한 Langfuse 기반 모니터링도 함께 소개한다.

 

RAG 기반 문맥 검색 및 응답 시스템 구현

이전 글에서 Astro 기반 챗봇 UI와 Gemini API 연동 구조를 정리했다면, 이번에는 그 위에 RAG 구조를 얹었다. 즉, 벡터 검색을 통해 문맥을 찾아서 LLM 응답에 활용하는 본격적인 흐름을 구축한 것.

전체 구성은 다음 세 가지 컴포넌트로 이루어졌다:

  • Pinecone: 벡터 임베딩 기반 유사 문서 검색
  • Gemini API: 검색된 문맥을 바탕으로 응답 생성
  • Langfuse: 이 모든 과정을 추적하고 분석

각 서비스의 키는 Astro 프로젝트 내 환경변수로 관리하고, 초기화는 아래처럼 설정했다:

const pinecone = new Pinecone({ apiKey: import.meta.env.PINECONE_API_KEY });
const genAI = new GoogleGenerativeAI(import.meta.env.GEMINI_API_KEY);
const embedModel = genAI.getGenerativeModel({ model: "embedding-001" });

const langfuse = new Langfuse({
  publicKey: import.meta.env.LANGFUSE_PUBLIC_KEY,
  secretKey: import.meta.env.LANGFUSE_SECRET_KEY,
  baseUrl: import.meta.env.LANGFUSE_HOST || 'https://cloud.langfuse.com'
});

 

사용자의 질문이 들어오면 가장 먼저 수행해야 할 일은 해당 질문을 벡터화(embedding) 해서 벡터 DB에서 유사한 정보를 찾아내는 것이다. Langfuse에서 제공하는 span() 메서드를 활용해 이 검색 프로세스를 추적할 수 있도록 했다.

async function searchVectorDB(query: string, trace: any) {
  const searchSpan = trace.span({ name: 'vector-search' });
  const queryEmbedding = await embedText(query);
  const results = await index.query({ ... });

  const contexts = results.matches.map(match => ({
    text: match.metadata?.pageContent || "No content available",
    ...
  }));

  searchSpan.end({
    input: { query },
    output: {
      resultCount: contexts.length,
      topResults: contexts.slice(0, 3).map(context => ({
        ...
      })),
    },
  });

  return contexts;
}

 

실제로 작동해보면 PDF, 블로그, 발표자료 등 다양한 타입의 문서가 검색되며, 각 문맥은 text, contentType, source 등으로 구성되어 후속 응답 생성에 활용된다.

 

이제 검색된 문맥들을 바탕으로 Gemini에게 응답 생성을 요청한다. 프롬프트 안에 문맥 정보, 대화 이력, 질문을 함께 포함시키는 방식이다. 이때도 Langfuse의 generation() 메서드를 통해 요청과 응답 내용을 기록한다.


특히 여기서는 이전 대화 이력도 함께 전달함으로써 대화 흐름을 유지하도록 했다.

const prompt = `
  You are Kangbeen Ko(고강빈)...  
  ### Context Information:
  ${contextText}
  ...
  ### User Question:
  ${query}
`;

const result = await generativeModel.generateContent(prompt);
const responseText = result.response.text();

trace.generation({
  name: 'chat-response',
  type: 'chat',
  model: 'gemini-1.5-pro',
  input: query,
  output: responseText,
  metadata: {
    ...
  }
});

 

여기서 주목할 점은 단순히 질문-응답만 남기는 게 아니라, 참고한 문서 수, 출처, 대화 이력 길이 등 다양한 메타데이터를 함께 저장한다는 점이다. 이런 세부 정보 덕분에 나중에 챗봇 응답 품질을 분석하거나 디버깅할 때 큰 도움이 된다.

 

 

Langfuse를 활용한 트레이싱 및 로깅

Langfuse는 단순한 로깅 도구가 아니다. 하나의 트레이스(trace) 안에 여러 개의 이벤트(event), 스팬(span), 응답(generation)을 계층적으로 쌓을 수 있어, 하나의 챗봇 세션이 어떤 흐름으로 진행되었는지를 시각적으로 분석할 수 있다.

const trace = langfuse.trace({
  name: 'chat-session',
  userId,
  metadata: { ... }
});

trace.event({ name: 'chat-request', input: { ... } });
trace.event({ name: 'error', input: { ... } });

 

Langfuse의 대시보드는 꽤 직관적이다. 각 응답의 latency, 입력 길이, 사용된 문서 출처 등도 한눈에 확인할 수 있고, 어디서 병목이 생겼는지도 쉽게 파악된다.

실제로 챗봇 응답이 이상하거나, 사용자 피드백이 있을 때 바로 Langfuse에서 세션 로그를 열어보면, 어느 단계에서 문제가 있었는지 거의 대부분 잡아낼 수 있다.

 

다음 글 예고

지금까지는 수동으로 벡터화한 자료를 Pinecone에 올려야 했지만, 다음 글에선 이걸 자동화하는 파이프라인을 소개할 예정이다.

Astro 프로젝트 내에 새로운 Markdown이나 PDF 파일이 생기면, 이를 자동으로 처리해 벡터 DB에 반영하는 구조다. CI/CD와 연동해서 “문서만 추가하면 챗봇이 알아서 읽고 대답하는 구조”로 진화하는 게 목표다.

반응형

댓글