사내 자료를 밖으로 못 내보낸다 — 회사 GPU 위에 RAG를 세운 TextRAG 구축기
외부 LLM에 사내 코드·문서를 보낼 수 없다는 제약에서 출발해, 회사 GPU 위에 임베더+리랭커+생성 스택을 직접 올리고 골든셋으로 recall@5 87%까지 튜닝한 뒤 한 엔진을 사내검색·MCP·MR 리뷰봇이 공유하게 만든 설계 기록.
제약이 먼저 설계를 정했다
보통 RAG 글은 “어떤 임베더가 좋은가”부터 시작한다. 나는 “세 모델을 어떻게 한 장비에 욱여넣는가”부터 풀어야 했다. 보안 제품을 만드는 회사라 제약이 먼저 정해졌고, 그 제약이 설계를 거의 다 결정했다.
제약은 두 겹이다. 첫째, 사내 코드와 내부 문서는 외부로 올리지 않는다. 그래서 RAG를 붙이려는 순간 가장 흔한 선택지부터 막힌다 — 외부 임베딩 API도, 외부 생성 API도 못 쓴다.
질의 한 줄에 코드 조각이나 문서 본문이 실려 나가는 구조 자체가 안 된다. 검색부터 생성까지 모든 처리가 사내에서 끝나야 한다.
둘째, 서빙 장비의 RAM이 작다. 임베딩·재정렬·생성 세 모델을 동시에 상주시킬 수 없다. 외부로 한 바이트도 못 내보내면서, 한 장비에 다 띄울 수도 없다는 뜻이다.
원래는 취미로 만들어 쓰던 로컬 RAG 도구였다. 흩어진 사내 자료와 이슈 히스토리를 빠르게 찾을 필요를 직접 느껴 만든 것이다. 그걸 사내 인프라로 끌어와 본체로 키운 게 TextRAG다.
메모리 예산: 장비를 둘로 가른다
생성은 사내에 상주하는 로컬 LLM으로 받고, 임베딩과 재정렬도 전부 사내 모델로 돌렸다. 핵심은 세 모델을 한 장비에 같이 못 띄운다는 제약을 푸는 일이었다.
해법은 장비를 둘로 나누는 것이었다. 무거운 인덱싱(임베딩 일괄 생성)은 인덱싱 전용 장비에서 돌린다. 사용자 질의를 받는 서빙 장비는 검색·생성에만 집중한다.
그래도 서빙 쪽 메모리가 빠듯해서, 세 가지 노브로 예산을 깎았다.
- 모델별 상주 토글 — 모델이 메모리에 머무는 시간(keep_alive)을 모델마다 따로 둔다.
- 재정렬 선택적 off — 재정렬 모델을 필요 없는 시나리오에선 내린다.
- 생성 KV cache 축소 — 생성 모델의 KV cache를 줄여 점유를 낮춘다.
그다음 “이 시나리오에서는 어떤 모델이 얼마를 점유하는가”를 표로 짜 놓았다. 동시 상주가 필요 없는 조합을 골라 예산 안에 맞췄다.
색인 단위에서도 한 번 더 갈렸다. 코드는 심볼 청크와 패시지 청크로 이중 분할했다.
같은 코드베이스를 함수·심볼 단위 코드 검색과 서술형 답변 양쪽에서 재사용하기 위해서다. 덕분에 여러 제품 코드베이스와 사내 문서 수천 건을 한 코퍼스 체계로 색인해 상시 검색 가능한 상태로 둘 수 있었다.
골든셋으로 recall@5 87%까지
“잘 된다”는 감으로는 튜닝을 못 한다. 대표 질의 30문항으로 골든셋을 직접 만들고, 회귀 스크립트를 붙였다.
스크립트가 산출하는 지표는 세 가지다.
recall@k— 정답 문서가 상위 k개 안에 들어왔는가.MRR— 정답이 평균 몇 번째에 떴는가.- 무관 출처 혼입률 — 엉뚱한 출처가 섞여 들어온 비율.
청크 크기나 재정렬 on/off를 바꾸면 기준선 대비 회귀를 바로 비교한다.
이 과정에서 가장 정직하게 남겨야 할 부분은, 측정 방식 자체에 허점이 두 군데 있었다는 점이다. 평가 코드가 틀려 있으면 그 위에서 모델을 아무리 튜닝해도 잘못된 방향으로 간다.
두 허점을 찾아 바로잡고 나서야 수치를 믿을 수 있게 됐다. 최종은 recall@5 87% · recall@10 90% · MRR 0.76, 코드 검색은 거의 100%다. 약한 구간은 덮지 않고 원인을 짚어 개선안으로 남겼다.
지금은 검색·답변 1건마다 지연·결과 수·점수 분포·호출원을 append-only로 기록하는 계측 레이어가 붙어 있다. 검색 파이프라인은 그대로 두고, 쿼리 원문은 해시로만 남겼다. 로깅이 실패해도 서비스는 멈추지 않게 격리한 결과다.
엔진 하나를 셋이 공유한다
여기까지 만들고 나니, 같은 검색 엔진을 쓰고 싶어 하는 자리가 셋이었다.
- 웹 사내 검색
- 개발 도구 — Claude Code에 MCP로 붙는 검색 도구
- GitLab MR 리뷰봇
각자 따로 색인을 들고 가면 코퍼스가 셋으로 갈라지고 품질도 따로 논다. 그래서 단일 서버 엔진을 공통 인프라로 두고, 그 위에 소비자별 어댑터만 얇게 얹는 구조로 갔다.
색인과 검색 품질은 한 곳에서 관리되고, 소비자만 셋이다. 접근 제어는 구글 계정 로그인 위에 그룹↔코퍼스 매핑(그룹 하나에 N개 코퍼스)으로 붙여, 검색 범위와 권한을 같은 축에서 제어한다.
하지만 장비를 둘로 가르는 선택이나 평가 허점을 의심한 판단은 내 몫이었다. 제안부터 설계·구현·서버 구축·패키징, iOS 팀 도입 교육까지 한 사이클을 직접 돌렸다.
외부로 한 바이트도 안 내보낸다는 제약이 출발점이었다. 그런데 결과적으로는 그 제약 덕분에 엔진을 공유 자원으로 설계하게 됐다. 제약이 좁은 길을 강제했고, 그 좁은 길이 재사용 구조로 이어졌다.