본문 바로가기
PROJECT/AI

[RAG] 사내 코드 검색 시스템을 만들며 배운 RAG 파이프라인 정리

by 놀고 쉬고 싶은 개발자 2026. 5. 15.

사내에서 "우리 코드에서 결제 실패 재시도 어떻게 처리해?" 같은 질문에 매번 직접 레포 뒤지거나 사람한테 물어봐야 하는 상황이 반복됐다.

그래서 GitHub Org 전체 코드를 대상으로 자연어로 검색할 수 있는 내부 도구를 만들었고, 그 핵심인 RAG 파이프라인을 구축하면서 정리한 내용을 공유하려고 한다 📚

 


RAG

RAG(Retrieval-Augmented Generation)란? 

: 외부 지식 베이스에서 관련 정보를 먼저 검색(Retrieval)한 뒤, 그 결과를 컨텍스트로 넣어 LLM이 답변을 생성(Generation)하게 하는 방식

: 모델 자체가 모든 걸 암기하는 방식이 아니라, "필요한 정보를 찾아서 그걸 보고 답변한다"는 개념

 

RAG가 필요한 이유

: 사내 코드는 지속적으로 변함 → LLM 파인튜닝으로는 실시간 반영 불가

: 코드베이스가 여러 레포에 분산되어 있어 컨텍스트 전체를 넣는 건 불가능

: "이 로직이 어느 파일에 있지?"에 대한 답을 코드 근거와 함께 줘야 함


전체 구조

두 개의 파이프라인

: RAG 시스템은 크게 인덱싱질의, 두 흐름으로 나뉨

: 인덱싱 파이프라인 — 코드를 쪼개서 벡터로 저장해두는 사전 작업

: 질의 파이프라인 — 사용자 질문이 들어올 때 관련 코드를 찾고 답변을 생성하는 흐름


인덱싱 파이프 라인

전체 흐름
GitHub Org 레포
→ GitHub App으로 파일 트리 조회
→ 파일 필터링
→ Tree-sitter AST 청킹
→ 시크릿 마스킹
→ Ollama 임베딩 생성 (nomic-embed-text)
→ Chroma upsert + SQLite 상태 기록

 

1. 파일 필터링

: 불필요하거나 민감한 파일을 먼저 걸러냄

제외 대상 이유
`.env`, `*.key`, 시크릿 파일 민감 정보 보호
`build/`, `dist/`, `target/`, `.next/` 생성물 제외
`node_modules/`, `.venv/`, `Pods/` 의존성 제외
`*.lock` 대량 노이즈
500KB 초과 파일 토큰/메모리 보호

 

2. Tree-sitter AST 청킹

: 단순히 "몇 줄씩 잘라내기"를 하면 함수가 중간에 잘리거나, 관련 없는 코드가 한 청크에 섞이는 문제가 생김

언어 청킹 단위
Java / Kotlin 클래스, 메서드, 인터페이스, 어노테이션 블록
TypeScript 함수, React 컴포넌트, 커스텀 훅, 타입/인터페이스 선언
Swift 클래스, struct, extension, 메서드

: 이렇게 하면 하나의 청크 = 하나의 의미 단위 → 검색 정확도가 훨씬 높아짐

 

3. 임베딩 생성 및 저장

: Ollama `nomic-embed-text` 모델로 각 청크를 벡터로 변환

: Chroma에 벡터와 원문을 함께 저장 (upsert)

: SQLite에는 인덱싱 잡 상태, 청크 메타데이터 관리 → 재인덱싱 이력 추적 가능


질의 파이프라인

전체 흐름
사용자 질문
→ 질문 임베딩 생성
→ Dense 벡터 검색 (Chroma)
→ BM25 키워드 검색
→ RRF로 결과 병합 → 상위 K개 청크 선택
→ 프롬프트 구성 (코드 컨텍스트 + 질문)
→ Ollama LLM 생성 (qwen2.5-coder:7b)
→ SSE 스트리밍 응답
→ Web이 파일 경로 / 라인 번호 / 코드 근거 표시

 

Hybrid Search: Dense + BM25를 같이 쓰는 이유

: Dense 검색만 쓰면 — "결제 실패 재시도"처럼 의미 기반 질문엔 강하지만, `RetryHandler`, `PaymentException` 같은 정확한 클래스명엔 약함

: BM25만 쓰면 — 키워드가 정확히 일치해야만 찾을 수 있고, "결제 오류 처리"처럼 표현이 다르면 놓침

: 두 방식을 병행하고 RRF(Reciprocal Rank Fusion)으로 순위를 합산하면 두 단점을 상호 보완할 수 있음

 

방식 원리 잘 찾는 케이스
BM25 단어 빈도 기반 `retry`, `PaymentException` 등 정확한 키워드
Dense 벡터 유사도 결제 실패 처리"처럼 의미상 가까운 코드
Hybrid RRF 병합 두 방식 모두 상위권인 청크 우선 노출

 

프롬프트 구성 및 LLM 생성

: 검색으로 뽑힌 코드 청크들을 컨텍스트로 조합해 프롬프트를 만들고

: `qwen2.5-coder:7b` 모델에 질문과 함께 전달 → 원래는 'qwen3.5:9b' 이었으나 용량 및 성능 이슈로 다운그레이드

: 응답은 SSE(Server-Sent Events)로 스트리밍해서 Web에서 타이핑 효과로 렌더링


저장소 역할 분리

SQLite vs Chroma

: 처음엔 벡터 DB 하나로 다 관리하려 했는데, 운영 상태 관리용 메타와 검색용 벡터는 성격이 달라서 분리함

 

구분 SQLite Chroma
저장 내용 레포 목록, 인덱싱 잡 이력, 쿼리 로그 코드 청크 원문 + 임베딩 벡터
쿼리 방식 SQL (WHERE, JOIN) 벡터 유사도 + BM25
용도 운영 상태 관리, 어드민 통계 RAG 검색

느낀 점

: 청킹이 생각보다 제일 중요했음 — 라인 단위로 자르면 검색 품질이 눈에 띄게 떨어짐

: Dense 검색 혼자선 부족함 — Hybrid Search로 바꾸고 나서 정확도가 크게 올라감

: 모델 응답 시간이 변수 — CPU 추론 환경이라 타임아웃을 넉넉히 300초로 잡아야 했음

: 임베딩 모델과 생성 모델은 분리 관리 — 나중에 모델 교체 시 영향 범위가 줄어듦