RAG 파이프라인 구축: SmartQueue가 알려준 리트리벌 교훈
출처: Dev.to
When I set out to add an AI assistant to SmartQueue, a distributed task queue I’ d already built in Go for handling IT support tickets, the obvious move was to bolt on an LLM and call it done. Type a question, get an answer.
하지만 일반적인 LLM은 회사의 비밀번호 재설정 절차, P1 장애 대응 매뉴얼, 환불이 $500을 초과할 경우 관리자 승인이 필요하다는 사실 등을 알 수 없습니다.
이것은 실제 내부 지식을 기반으로 grounding(근거)을 제공하는 것이 필요했으며, RAG(Retrieval‑Augmented Generation)는 이를 위해 설계되었습니다. 먼저 자체 문서에서 관련 사실을 추출하고, 그 정보를 모델에 컨텍스트로 전달하여 비즈니스에 대한 이해를 스스로 기대하지 않게 합니다.
이 포스트에서는 해당 파이프라인이 실제로 어떻게 작동하는지, 중간过程中에 바뀐 아키텍처적 선택과 그 이유, retrieval depth와 온도 같은 요소에 대한 제가 선택한 수치, 그리고 이것이 진정한 “실제” RAG인지에 대한 솔직한 의견을 다룹니다.
SmartQueue Bot은 대시보드의 Queue Health와 AI Bot 탭에 위치해 있습니다. 에이전트가 티켓을 선택하고 “데이터베이스 장애 발생 시 즉시 수행해야 할 단계가 무엇인가요?” 같은 질문을 던지면, 봇은 자체 내부 지식 베이스인 IT 매뉴얼을 기반으로 토큰 단위로 스트리밍하며 답변합니다.
요청 흐름
agent question → 에이전트 질문
세 가지 일이 모델에 텍스트가 도달하기 전에는 발생합니다: 사용자 메시지는 프롬프트 주입 시도를 확인하고, 해당 메시지를 지식 베이스에 대한 쿼리로 사용하며, 상위 일치 항목이 티켓의 카테고리, 우선순위, 설명과 함께 시스템 프롬프트에 편입됩니다. 모델은 프레임 없이 원본 문서를 직접 보지 않습니다. 구조화된 브리ーフ(간략한 개요)를 보게 됩니다.
반전시킨 결정: ChromaDB → BM25
지식 베이스의 최초의 버전은 기본 ONNX 임베딩 함수인 ChromaDB를 사용했습니다. 이는 올바른 벡터 검색을 제공하고 토치 의존성을 없으며, 이벤트 루프를 차단하지 않는 스레드 풀을 통해 쿼리됩니다. 이것이 교과서적인 RAG 설정이며 로컬에서는 정상적으로 작동했습니다.
하지만 전체 스택을 Hugging Face Spaces에 단일 컨테이너로 배포하려는 순간 문제가 발생했습니다. 배포는 supervisord를 이용해 Redis, Go API, 두 개의 Go 워커 복제본, FastAPI AI 서비스까지 모두 하나의 컨테이너 안에서 실행하도록 구성했으며, 원래는 별도의 ChromaDB 프로세스도 포함되었습니다. 이는 메모리와 CPU가 제한된 무료 티어 컨테이너 내에서 5개의 장시간 실행 중인 프로세스가 경쟁하게 되는 상황이며, supervisord는 올바른 순서로 시작하고 생명을 유지하도록 관리합니다.
ChromaDB는 시작 경합과 조용한 실패를 계속 일으켰습니다. 충분한 커밋 메시지(예: “fix: remove ChromaDB from supervisord” 및 “fix: replace ChromaDB with in‑memory BM25 search”)가 쌓이면서 완전히 제거하기로 결정했습니다.
대체는 약 50줄의 순수 Python 코드로, 임베딩 모델이나 외부 프로세스, 네트워크 호출 없이 동작합니다:
def _bm25_score(query_tokens, doc_tokens, k1=1.5, b=0.75):
avg_dl = sum(len(d) for d in _CORPUS) / len(_CORPUS)
tf = Counter(doc_tokens)
score = 0.0
for term in query_tokens:
if term not in tf:
continue
idf = _idf(term, _CORPUS)
dl = len(doc_tokens)
score += idf * (tf[term] * (k1 + 1)) / (tf[term] + k1 * (1 - b + b * dl / avg_dl))
return score
이것은 표준 Okapi BM25 공식이며, 매 쿼리 시 메모리 내 런북 코퍼스에 대해 즉시 계산됩니다. 인덱스를 구축할 필요도 없고, 데몬을 유지할 필요도 없으며, 캐시 시작 시 임베딩 지연도 없습니다.
트레이드오프
BM25는 용어 중복에만 매칭하므로, 런북의 표현과 매우 다른 문장(동의어, 재표현)으로 구성된 쿼리는 점수가 잘 나오지 않습니다. 하지만 10개의 짧은 키워드가 풍부한 IT 매뉴얼로 구성된 고정된 집합에서 사용자는 일반적으로 “VPN”, “password reset”, “outage”와 같은 런북과 동일한 어휘를 사용하기 때문에 이 약점은 실제로는 거의 나타나지 않습니다.
이 규모에서는 검색 품질보다 서비스가 매번 신뢰성 있게 시작되는 것이 더 중요했습니다.
이 파이프라인 내 상수는 제가 손을 대지 않은 기본값이 아니라 의도적인 튜닝 결정이었습니다. 이는 정밀/재현/신뢰도 점수를 이용한 RAGAS 스타일 평가가 아니며, eval harness 도 없습니다. 단지 제가 작업 중인 제약 조건(무료 LLM 제공업체, 단일 데모 컨테이너, 변하지 않는 지식 베이스)에 기반한 시스템 수준 튜닝만 있을 뿐입니다.
| Constant | Value | Why |
|---|---|---|
| Retrieved docs (k) | 4 | 보통 정답을 커버하기에 충분한 런북 컨텍스트이며, 프롬프트 과부하 없이 800 토큰 응답 예산 내 유지 |
| BM25 k1 / b | 1.5 / 0.75 | 단 10개의 문서밖에 없으므로 의미 있게 튜닝할 신호가 부족해 로버트슨 기본값을 그대로 사용 |
| Bot temperature | 0.2 | 문제 해결 답변은 창의적이지 않고 직설적이며 반복 가능해야 함 |
| Classifier temperature | 0.1 | 출력은 JSON으로 파싱되며, 근사적인 일관성을 유지해 오류가 적은 응답을 생성 |
| Recommender temperature | 0.3 | 큐 상태를 추론하는 데 약간의 여유가 필요해, 단순히 필드만 추출하는 것보다 |
| Bot max_tokens | 800 | 다단계 문제 해결 가이드에 충분히 길며, 스트리밍을 빠르게 유지 |
| Classifier max_tokens | 250 | 스키마가 작아 8개의 짧은 필드와 문장이 없어서 |
| Session history window | last 10 turns, capped at 20 stored, 1‑hour TTL in Redis | 실제 문제 해결 대화에 충분한 연속성 유지하면서 메모리가 무한히 성장하지 않음 |
| Rate limit | 30 requests / minute per session | 단일 제어不能 클라이언트에 의해 무제한 사용되는 무료 Groq 할당량을 보호 |
| LLM client retries | 0, with a 10s timeout | 각 호출자는 이미 자체 폴백(키워드 분류기, 규칙 기반 추천기, 사전 준비된 봇 응답)을 가지고 있어, 동일한 실패에 재시도하면 latency만 추가되고 바로 fallback 됩니다. |
마지막 하나는 주목할 만합니다. 이 시스템의 모든 AI 백엔드 엔드포인트에는 비‑LLM 폴백 경로가 있습니다. Groq가 제한되거나 다운될 경우, 분류기는 키워드 매칭으로, 추천기는 큐 깊이에 대한阈值 기반 규칙으로, 봇은 동일한 검색된 런북 문단으로부터 템플릿화된 응답을 fallback 합니다.
시스템은 실패하지 않고 저하되도록 설계되었으며, 이는 유료 SLA가 적용된 환경보다 무료 API 티어에서 더 중요합니다.
정확히 말해, yes:它在生成之前 검색하고, 생성은 검색된 내용에 의존합니다. 하지만 이는 RAG가 의미하는 것을 나타내는 좁은 부분일 뿐입니다. 청크링(각 런북이 평탄한 문서로 임베드됨), 재랭킹 단계, 하이브리드 검색, 그리고 특정 질문에 대해 올바른 런북이 실제로 표시되었는지 판단하는 평가 루프가 없습니다.
이는 문제 규모에 맞춰 적절히 RAG를 구성한 것입니다: 소규모이며 정적인 키워드 친화적 지식 베이스이며, 보다 복잡한 구조물을 구축하는 비용이 이득을 초과했을 것입니다.
“BM25 over ChromaDB”가 더 나은지는 최적화 목표에 따라 다릅니다. 보다 크고 다양한 코퍼스에서 검색 품질을 고려할 때, 임베딩 기반 접근이 승리합니다. BM25는 문서 고유 어휘를 재사용하지 않는 질문에서는 성능이 떨어집니다.
이 배포 환경에서, 지식 베이스 규모와 호스팅 제약 조건을 고려할 때 벡터 스토어를 제거하는 결정은 명백히 올바른 선택이었습니다. 이는 전체 클래스의 배포 실패를 없애고, 10개의 짧은 문서가 임베딩으로 해결할 필요가 없다는 문제를 더 이상 의존하지 않게 했습니다.
재구축 대신 확장한다면, 다음 실제 업그레이드는 기본 검색 평가(예: “라벨링된 테스트 질문 집합에서 올바른 런북이 상위 4개에 들어갔는지”), 더 긴 런북을 작은 청크로 나누어 모델이 각 검색된 구간에 더 관련 있는 텍스트를 받을 수 있게 하고, 지식 베이스 규모가 약 50문서를 넘어서면 하이브리드 접근법을 도입하는 것이 될 것입니다.
그 정도 규모에서는 순수 키워드 중복만으로는 재표현된 쿼리를 벡터 검색이 무료로 잡을 수 있는 것이 충분하지 않게 됩니다.
가 별도 프로젝트인 AskMyDoc에서 저는 BM25와 ChromaDB를 결합하여 하이브리드 리…