사내에서 "우리 코드에서 결제 실패 재시도 어떻게 처리해?" 같은 질문에 매번 직접 레포 뒤지거나 사람한테 물어봐야 하는 상황이 반복됐다.
그래서 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초로 잡아야 했음
: 임베딩 모델과 생성 모델은 분리 관리 — 나중에 모델 교체 시 영향 범위가 줄어듦
'PROJECT > AI' 카테고리의 다른 글
| [Search API] SSE 스트리밍으로 설계한 코드 검색 API와 레포 필터 구조 (0) | 2026.05.22 |
|---|---|
| [GitHub App] GitHub App + Webhook으로 배운 자동 인덱싱 파이프라인 (0) | 2026.05.19 |