RAG를 활용한 코드 Q&A 봇 구축기 – 성공 사례와 실패 사례
요약
사내 마이크로서비스 문서 기반의 코드 Q&A 봇을 구축하며 겪은 RAG(검색 증강 생성)의 시행착오와 성공 사례를 다룹니다. 단순 프롬프팅, 키워드 검색, 단순 청킹의 한계를 분석하고 효과적인 RAG 파이프라인 구축 방법을 제시합니다.
핵심 포인트
- 단순 프롬프팅은 토큰 제한과 환각, 높은 비용 문제를 야기함
- TF-IDF 기반 키워드 검색은 유의어 및 코드 특유의 용어 처리에 한계가 있음
- 단순 청킹 방식은 문맥 단절로 인해 검색 정확도가 떨어짐
- 효과적인 RAG를 위해 중첩(overlap)을 포함한 청킹과 벡터 검색이 필수적임
몇 달 전, 우리 팀은 문제에 직면했습니다. 모든 신입 개발자가 우리 마이크로서비스(microservices)들이 서로 어떻게 통신하는지 이해하기 위해 흩어져 있는 문서, 오래된 슬라이드 덱, 그리고 Slack 스레드를 뒤지며 며칠을 허비해야 했습니다. 저는 생각했습니다. '이 질문들에 답할 수 있는 챗봇을 만들면 어떨까? 우리 내부 문서로 학습된 미니 GPT 같은 것 말이야.'
스포일러를 하자면, 유용한 결과물을 얻기 전까지 저는 수많은 실수를 저질렀습니다. 여기 코드 문서화를 위한 검색 증강 생성 (RAG, Retrieval-Augmented Generation) 봇을 구축하며 겪었던 막다른 길, 작동했던 방식, 그리고 제가 더 일찍 알았더라면 좋았을 트레이드오프 (trade-offs)를 포함한 솔직한 이야기를 담았습니다.
문제점: 컨텍스트 과부하 (context overload)
처음에는 간단하게 시작했습니다. 모든 Markdown 파일을 하나의 프롬프트에 쏟아붓고 GPT-4에게 다음과 같이 요청했습니다. "이 텍스트를 바탕으로 질문에 답하세요." 이 방식은 정확히 단 한 개의 질문에만 작동했습니다. 그 후 토큰 제한 (token limit)에 걸렸고, 모델은 무작위 서비스 이름을 환각 (hallucinating)하기 시작했으며, 쿼리 하나당 0.15달러의 비용이 발생했습니다. 20명의 개발자로 구성된 팀이라면 일주일 만에 예산을 다 써버릴 수준이었습니다.
저는 백과사전 전체가 아니라, 관련 있는 문서 조각들만 검색해올 방법이 필요했습니다.
시도했지만 실패했던 것들
1. 키워드 검색 (TF-IDF)
기본적인 TF-IDF 인덱스를 구축했습니다. 어느 정도 작동은 했습니다. 만약 개발자가 정확히 _"auth service가 무엇을 반환하나요?"_라고 묻고 문서에 해당 단어들이 포함되어 있다면, 올바른 단락을 찾아냈습니다. 하지만 유의어, 의역, 그리고 코드 특유의 용어(예: "auth service" vs "login endpoint")는 완전히 놓쳤습니다. 짧은 쿼리는 실패했고, 긴 쿼리는 초점이 흐려졌습니다.
2. 오버랩 (overlap) 없는 단순 청킹 (Naive chunking)
문서를 500자 단위의 청크 (chunks)로 나누고, 작은 모델로 임베딩 (embedded)한 뒤, 코사인 유사도 (cosine similarity)를 사용하여 상위 3개의 청크를 찾았습니다. 결과는 무작위적이었습니다. 검색된 청크가 문장 중간에서 끊기거나, 두 단락을 연결하는 결정적인 문장을 놓치는 경우가 빈번했습니다.
3. 찾을 수 있는 가장 큰 모델만 사용하기
저는 _“당신은 개발자 어시스턴트입니다. 간결하게 답변하세요.”_와 같은 시스템 프롬프트(system prompt)와 함께 GPT-4에 모든 것을 던져 넣어보았습니다. 적절한 검색(retrieval) 없이는, 모델이 관련 없는 컨텍스트(context)를 무시하거나 정답을 찾지 못했을 때 내용을 지어내는 문제가 발생했습니다. 또한 속도도 느렸습니다. 응답당 5~10초가 소요되었습니다.
마침내 효과를 본 방법: 검색 증강 생성 (RAG)
돌파구는 LLM을 검색 엔진(search engine)으로 취급하는 것을 멈추고, 전용 검색 인덱스(search index) 상에서 작동하는 **독해 엔진 (reading comprehension engine)**으로 취급하기 시작했을 때 찾아왔습니다.
제가 최종적으로 결정한 파이프라인(pipeline)은 다음과 같습니다:
- 문서 청킹 (Chunk documentation): 중첩되는 조각들로 나눕니다 (~300 토큰, 50 토큰 중첩).
- 각 청크 임베딩 (Embed each chunk): 벡터(vector)로 변환합니다.
- 벡터 저장 (Store vectors): 빠른 유사도 검색 인덱스(similarity search index)에 저장합니다 (처음에는 FAISS를 사용했으나, 이후 호스팅되는 벡터 스토어(vector store)로 이동했습니다).
- 쿼리 시점 (At query time): 질문을 임베딩하고, 가장 유사한 상위 5개 청크를 찾아 해당 내용만 LLM 프롬프트(prompt)에 입력합니다.
- 답변 생성 (Generate answer): LLM이 청크를 읽고 답변하거나 (모를 경우) “모릅니다”라고 말합니다.
여기에 전체 코드베이스를 다 쏟아놓지는 않겠지만, 차이를 만들어낸 핵심 Python 코드 스니펫(snippet)은 다음과 같습니다:
import openai
from sentence_transformers import SentenceTransformer
import faiss
...
이 스니펫 하나만으로 환각(hallucination)을 약 80% 줄였고, 쿼리당 비용을 0.01달러 미만으로 절감했습니다 (전체 GPT-4 대신 gpt-4o-mini를 사용함으로써).
보일러플레이트(boilerplate)를 건너뛰게 도와준 도구
문서가 계속 늘어나고 누군가 Confluence 페이지를 업데이트할 때마다 매번 재인덱싱(re-indexing)하고 싶지 않았기 때문에, 결국 로컬 FAISS 인덱스에서 관리형 솔루션(managed solution)으로 전환했습니다. ai.interwestinfo.com (청킹, 임베딩, 검색을 즉시 처리할 수 있는 서비스)을 포함하여 몇 가지 서비스를 시도해 보았습니다. 하지만 중요한 것은 기술입니다. 벡터 검색(vector search)과 LLM 통합을 제공하는 플랫폼이라면 무엇이든 유사하게 작동할 것입니다.
교훈 및 트레이드오프 (trade-offs)
청크 크기(Chunk size)는 매우 중요합니다.
- 150 tokens: 컨텍스트가 너무 적어 LLM이 전체적인 그림을 볼 수 없습니다.
- 1000 tokens: 노이즈가 너무 많아 검색 품질(retrieval quality)이 떨어집니다.
- 저는 50 overlap을 적용한 300 tokens로 결정했습니다. 이것이 정밀도(precision)와 재현율(recall) 사이에서 가장 좋은 균형을 제공했습니다.
Overlap(중첩)은 선택 사항이 아닙니다.
Overlap이 없으면 섹션 간의 연결 고리를 놓치게 됩니다. 15~20%의 Overlap을 적용했을 때, 리트리버(retriever)가 더 일관되게 올바른 청크를 찾아냈습니다.
임베딩 모델(Embedding model) 선택:
- 경량 모델(all-MiniLM-L6-v2)은 빠르고 무료이지만, 코드 비중이 높은 문서에서는 정확도가 떨어집니다.
- 더 큰 모델(text-embedding-3-large)은 비용은 증가하지만 검색 성능을 향상시킵니다.
- 내부 개발 문서의 경우, 작은 모델로도 충분했습니다. 완벽함이 아닌 90% 이상의 재현율(recall)만 필요했기 때문입니다.
지연 시간(Latency): 전체 파이프라인은 약 2초가 소요되었습니다: 쿼리 임베딩에 200ms, FAISS 검색에 10ms, LLM 호출에 1.5초가 걸렸습니다. 채팅 인터페이스로서는 수용 가능한 수준입니다.
비용(Cost):
- 무료 모델로 10,000개 청크 임베딩: $0.
- 호스팅된 벡터 스토어(Hosted vector store): 크기에 따라 월 $5~20.
- LLM 호출 (gpt-4o-mini): 쿼리당 약 $0.002. 하루 200개 쿼리 기준, 월 $12입니다.
다음에 다시 한다면 다르게 할 점
- 검색(retrieval)을 제대로 평가하겠습니다. 50개의 쿼리를 수동으로 확인했는데, 그것으로는 충분하지 않습니다. 정답이 알려진 작은 테스트 세트를 구축하고 recall@k를 측정하겠습니다.
- 하이브리드 검색(hybrid search)을 사용하겠습니다. 순수 벡터 검색(pure vector search)은 정확한 키워드 일치를 놓칠 수 있습니다. 다음에는 BM25와 벡터 유사도(vector similarity)를 결합(reciprocal rank fusion)하겠습니다.
- 사용자 피드백을 받겠습니다. "이 답변이 도움이 되었나요?"를 기록하지 않았는데, 이제는 청킹(chunking), Overlap 또는 프롬프트(prompt)를 미세 조정(fine-tune)할 수 있도록 기록해 두었기를 바랍니다.
- 더 작고 미세 조정된(fine-tuned) 모델을 고려하겠습니다. 매우 특정한 도메인(내부 API)의 경우, 범용 LLM보다 미세 조정된 7B 모델이 더 저렴하고 빠를 수 있습니다.
마치며
RAG는 마법이 아닙니다. 이는 청킹 (Chunking), 임베딩 (Embedding), 검색 (Retrieval), 그리고 프롬프트 디자인 (Prompt Design)의 세심한 엔지니어링 퍼즐입니다. 도구들 (FAISS, Pinecone 또는 관리형 API 등)은 단지 구현 세부 사항일 뿐입니다. 중요한 것은 여러분의 파이프라인이 어디에서 깨지는지를 이해하는 것입니다: 잘못된 청크 (Bad chunks) → 잘못된 검색 (Bad retrieval) → 잘못된 답변 (Bad answers).
우리는 이제 온보딩 질문의 약 80%를 정확하게 답변하는 작동 가능한 봇을 보유하게 되었습니다. 나머지 20%는 오래된 문서와 모호한 질문이 섞여 있습니다. 이는 "#general 채널에 물어보고 30분 동안 기다리기"보다 훨씬 큰 개선입니다.
코드 문서화를 위한 AI 어시스턴트를 구축하는 여러분의 접근 방식은 무엇인가요? 여러분도 유사한 청킹이나 검색 문제를 겪은 적이 있나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기