C#, Qdrant, Ollama를 이용해서 로컬 자원만으로 구동되는 RAG 시스템을 만들어볼 수 있지 않을까? 하는 생각으로 맨바닥에서부터 구현을 시작해보고 있습니다.
감사하게도 위키백과에서 이런 시스템을 구현하기에 적합한 데이터셋을 제공하고 있어서 시작해보고 있습니다.
생성형 AI에서 중요한 부분이라 할 수 있는 검색 증강 생성 (RAG)은 (1) float 배열을 저장한 뒤 유사도 검색을 수행할 수 있도록 돕는 벡터 DB, (2) 원본 데이터 (텍스트, 이미지, 동영상 따위)에서 의미에 맞는 float 배열 값을 생성하는 임베딩 모델, (3) 벡터 DB에서 찾은 내용과 연결되는 원본 데이터를 보관하는 관계형 DB 혹은 NoSQL DB, (4) 마지막으로 언어 모델에 전달할 프롬프트의 설계, 이렇게 네 가지 요소가 결합되어 만들어지는 시스템으로 알고 있습니다.
쉽게 생각하면, 기존에 자연어 검색을 위해서 문서를 인덱싱하던 개념을 대체하는 것과 비슷한 상황인데, 이 관점에서 실제로 RAG 시스템을 시험삼아 구현해보고 테스트해보면 좋겠다고 생각하여 맨바닥에서 시작해보고 있습니다.
첫 MVP를 만들어본 다음에는 파켓 파일을 통해서 Qdrant와 Ollama를 디커플링해보고, 임베딩을 할 때 더 많은 부가 데이터를 넣어 매칭 가능성을 높이는 옵션을 추가하는 등 여러 가지 재미있는 아이디어를 더해서 하나의 완성된 데스크톱 소프트웨어로도 디자인해보려 합니다.
LLM 자체를 쓰는 것보다는 부하가 덜하지만, 결국 텍스트 임베딩 연산도 GPU가 있고 없고의 차이가 크다는 것을 알았습니다. 임베딩 연산 역시 CPU 연산만으로는 역시 꽤 느립니다.
첫 루프를 돌아봤는데, 검색 결과로 매칭된 문서들의 연관성이 많이 떨어져보이는 것 같습니다.
RAG에서 또한 중요한 부분이 chunking이라는 것을 알 수 있었는데, 단순히 문단 단위로 나누어 embedding 하는 것으로는 충분하지 않고, 맥락 형성을 위해 일정한 토큰 갯수 유지 + 오버랩 지정이 필요하다는 것을 알게 되어서, 이 부분의 도움을 받기 위해 Semantic Kernel의 TextChunker 구현체를 사용해보기로 합니다.
Ollama 모델 갤러리에 올라온 임베딩 전용 모델 중에 nomic-embed-text를 사용했었는데, 다시 보니 영어에만 특화된 모델이라 한국어 처리에는 문제가 있었습니다. Ollama에서 사용하려면 GGUF (llama.cpp용 포맷)로 가공된 임베딩 모델이 필요한데, 한국어에 대한 처리 능력이 있으면서 GGUF 형식으로 가공된 모델은 매우 숫자가 드물다는 것을 알게 되었습니다.
그래도 일단은 hf.co/KeyurRamoliya/multilingual-e5-large-GGUF 모델을 사용해보기로 결정했습니다. Ollama 공식 레지스트리에 없어도 허깅페이스에서 GGUF 모델을 pull 할 수 있어 테스트가 편리한 점은 매우 유용합니다.
임베딩 모델은 짧은 문장의 경우에는 multilingual-e5-large로 충분하지만,
긴 문장은 bge-m3사용해보는 것도 나쁘지 않습니다.
검색된 결과를 다시 정렬할 때 Reranker Model까지 쓰면 성능이 더 좋아집니다.
데이터 청킹할 때는 여러 문장을 묶고 일부를 오버랩해서 저장하는 게 좋습니다
문장 분리는 한국어는 KSS나 Kiwi (.NET Wrapper 있음),
영어는 PragmaticSegmenterNet (PragmaticSegmenter의 닷넷 재구현) 이나 BlingFire 같은 걸 쓰면 됩니다.
아니면 그냥 LLM한테 시키는 것도 방법입니다…