개인공부&프로젝트

퍼스널 페이지에 LLM 연동하기 1편 - 지식베이스 자동 구축

백악기작은펭귄 2025. 3. 30.
반응형

퍼스널 페이지에 LLM 연동하기 1편 - 지식베이스 자동 구축

나는 원래 개인 프로필을 노션에 정리해두고 있었다. 사실 프론트엔드나 웹 개발에는 큰 흥미도, 여유도 없었기 때문에 빠르게 쓸 수 있는 도구를 선호했기 때문이다. 하지만 이번에 연구실에 진학하면서 사정이 달라졌다. 연구 경험과 논문 실적이 자연스럽게 드러나면서도 깔끔한 구조의 퍼스널 페이지가 필요했기 때문이다.

 

나는 웹 개발 경험이 거의 없는 상태였기 때문에, 빠르고 안정적으로 정적 웹사이트를 만들 수 있는 방법을 찾다가 Jekyll을 선택했다. Markdown 파일만 잘 관리하면 사이트가 자동으로 생성되고, GitHub Pages를 통해 손쉽게 배포할 수 있는 점이 특히 마음에 들었다. 처음에는 이 정도로도 충분하다고 생각했다.

퍼스널 페이지에 LLM 연동하기 1편 - 지식베이스 자동 구축 - 퍼스널 페이지에 LLM 연동하기 1편 - 지식베이스 자동 구축

 

그런데 문득 이런 생각이 들었다. 나는 최근 LLM을 이용한 HCI 연구를 많이 하고 있고, 당분간은 이 방향을 나의 아이덴티티로 삼고자 하고 있다. 그렇다면, 내 포트폴리오 페이지에 LLM을 붙인다면 그게 곧 나의 연구 관심사를 가장 직접적으로 보여줄 수 있는 좋은 데모가 되지 않을까?

 

이 프로젝트는 바로 그 생각에서 출발했다.

 

LLM 챗봇 구조

내가 만들고자 한 챗봇은 단순히 "GPT-4에게 아무거나 물어보는" 구조가 아니다. 방문자의 질문에 대해 내 페이지 안의 콘텐츠를 검색해서 적절한 정보를 바탕으로 GPT가 대답하도록 만드는 RAG(Retrieval-Augmented Generation) 구조를 도입하고자 했다.

RAG 챗봇은 다음의 세 가지 구성 요소로 이루어진다.

 

  1. 지식베이스 구축: 내 페이지의 Markdown 문서와 논문 등의 텍스트를 벡터화하여 저장
  2. 질문-응답 API 서버: 질문을 받아 지식베이스에서 관련 정보를 검색하고 GPT에 전달
  3. 프론트엔드 인터페이스: 질문을 입력하고 응답을 보여주는 챗 UI

이번 글에서는 첫 번째 단계인 지식베이스 구축과 자동 업데이트를 다룬다. (글에서는 아주 간단한 PoC만 다룰 예정이다. 고도화는 필수!)

 

Markdown 기반 콘텐츠 수집

내 사이트는 Jekyll을 기반으로 하고 있으며, 페이지 내용은 대부분 Markdown 파일 형태로 작성되어 있다. 자기소개, 학력과 경력, 프로젝트, 논문 리스트 등은 _pages, _posts, _data 등의 디렉토리에 .md 파일로 저장되어 있다.

이 파일들에서 텍스트를 추출하기 위해 python-frontmatter 라이브러리를 사용했다. 이 라이브러리를 이용하면 YAML 메타데이터와 본문 내용을 쉽게 분리할 수 있다.

import frontmatter
import glob

def load_markdown_texts(path):
    md_files = glob.glob(f"{path}/**/*.md", recursive=True)
    texts = []

    for file in md_files:
        try:
            with open(file, 'r', encoding='utf-8') as f:
                post = frontmatter.load(f)
                content = post.content.strip()
                if content and len(content) > 50:  # 너무 짧은 문서 제외
                    texts.append({"text": content, "source": file})
        except Exception as e:
            print(f"[!] Failed to load {file}: {e}")

    return texts

 

이렇게 긁어온 텍스트는 LLM에게 넣어줄 수 있는 형태로 청킹을 수행한 후 FAISS 벡터 데이터베이스로 저장해준다.

import os
import openai
import numpy as np
import faiss

openai.api_key = os.getenv("OPENAI_API_KEY")

def chunk_text(text, max_tokens=1000):
    return [text[i:i+max_tokens] for i in range(0, len(text), max_tokens)]

def get_embedding(text):
    try:
        response = client.embeddings.create(
            model="text-embedding-ada-002",
            input=[text]
        )
        return np.array(response.data[0].embedding, dtype="float32")
    except Exception as e:
        print(f"[!] Embedding failed: {e}")
        return None

def build_index(embeddings, metadata_list):
    if not embeddings:
        print("[!] No embeddings to index.")
        return

    dimension = len(embeddings[0])
    index = faiss.IndexFlatL2(dimension)
    index.add(np.array(embeddings))

    # 인덱스 저장
    faiss.write_index(index, "vector.index")

    # 메타데이터 저장
    with open("metadata.json", "w", encoding="utf-8") as f:
        json.dump(metadata_list, f, ensure_ascii=False, indent=2)

 

이제 임베딩이 완료되었다. 이 임베딩을 바탕으로 제대로 응답이 가능한지 간단하게 확인을 해보도록 하자.

import os
import openai
import numpy as np
import faiss
import json
from update_knowledge_base import get_embedding

client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def get_embedding(text):
    try:
        response = client.embeddings.create(
            model="text-embedding-ada-002",
            input=text
        )
        return np.array(response.data[0].embedding, dtype="float32")
    except Exception as e:
        print(f"[!] Embedding 오류: {e}")
        return None

def load_index_and_metadata(index_path="vector.index", metadata_path="metadata.json"):
    index = faiss.read_index(index_path)
    with open(metadata_path, "r", encoding="utf-8") as f:
        metadata = json.load(f)
    return index, metadata

def search_similar_docs(query, index, metadata, top_k=3):
    query_embedding = get_embedding(query)
    if query_embedding is None:
        return []

    query_embedding = np.expand_dims(query_embedding, axis=0)
    distances, indices = index.search(query_embedding, top_k)
    return [metadata[i]["text"] for i in indices[0] if i < len(metadata)]

def generate_answer(query, context_texts):
    prompt = (
        "다음 문서를 참고하여 질문에 답해줘.\n\n"
        "문서:\n" + "\n---\n".join(context_texts) +
        f"\n\n질문: {query}\n답변:"
    )

    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "너는 친절한 블로그 도우미야."},
                {"role": "user", "content": prompt}
            ],
            temperature=0.7
        )
        return response.choices[0].message.content.strip()
    except Exception as e:
        print(f"[!] Chat 생성 오류: {e}")
        return "답변을 생성하는 데 실패했습니다."

def ask(query):
    index, metadata = load_index_and_metadata()
    context_texts = search_similar_docs(query, index, metadata)
    if not context_texts:
        return "관련 문서를 찾을 수 없습니다."
    answer = generate_answer(query, context_texts)
    return answer

if __name__ == "__main__":
    query = "이 사람의 현재 어떤 일을 하고 있나요? 주요 관심사는 무엇인가요?"
    print(f'질문: {query}')
    print(f'답변: {ask(query)}')

퍼스널 페이지에 LLM 연동하기 1편 - 지식베이스 자동 구축 - 퍼스널 페이지에 LLM 연동하기 1편 - 지식베이스 자동 구축 - Markdown 기반 콘텐츠 수집

 

조금 응답이 어색하긴하지만.. 뭐 좋다. 어차피 고도화는 한참 더 해야하니까.. 기본적인 벡터 데이터베이스 구축 코드는 작성이 완료되었다.

 

GitHub Action으로 임베딩 자동화하기

현재의 방법은 블로그 내용을 사전에 벡터 데이터베이스화 해두고 이를 바탕으로 답변을 하고 있다. 이 경우 깃허브 블로그 내용이 바뀐다면 우리는 임베딩 작업을 다시 해주어야 한다. 이 작업을 매번 수동으로 하기에는 귀찮으니 GitHub Actions를 이용해 자동화를 할 수 있도록 해보자.

name: Update Knowledge Base

on:
  push:
    paths:
      - '_pages/**'
  workflow_dispatch:

jobs:
  update:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: Install dependencies
        run: |
          pip install -r requirements.txt

      - name: Run embedding pipeline
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python _scripts/update_knowledge_base.py

      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: knowledge-base
          path: |
            vector.index
            docs.txt

 

이제 깃헙 블로그의 주요 내용을 담당하는 부분인 '_pages' 내의 .md 파일이 변경되면 GitHub Action 워크플로우가 자동으로 실행되어서 Knowledge-base의 업데이트를 수행할 것이다.

 

여기까지 기본적인 세팅을 진행해 보았다. 다음에는 이 지식베이스를 바탕으로 주어진 질문에 답변하는 LLM 챗봇을 구현해 보도록 하겠다. 근데 연구가 바빠져서 언제 다시 시작할지는 미지수다..

반응형

댓글