Capstone 02 — 코드베이스 RAG(Codebase Retrieval-Augmented Generation) — 저장소 간 의미 검색(Cross-Repo Semantic Search)

2026년의 진지한 엔지니어링 조직은 모두 단순한 문자열 매칭이 아닌, 의미를 이해하는 내부 코드 검색(internal code search)을 운영하고 있습니다. Sourcegraph Amp, Cursor의 코드베이스 답변(codebase answers), Augment의 엔터프라이즈 그래프(enterprise graph), Aider의 리포맵(repomap), Pinterest의 내부 MCP(Model Context Protocol)는 모두 같은 형태를 갖춥니다. 여러 저장소(repository)를 수집(ingest)하고, 트리시터(tree-sitter)로 파싱하며, 함수와 클래스 단위의 청크(chunk)를 임베딩(embedding)한 뒤, 하이브리드 검색(hybrid search)을 수행하고, 재순위(re-rank)를 매겨 인용(citation)과 함께 답합니다. 이번 캡스톤(capstone)은 10개 저장소, 200만 줄(2M LOC) 규모의 코드를 처리하면서, 매 git push마다 증분 재색인(incremental re-indexing)까지 견디는 시스템을 만들도록 요구합니다.

유형: Capstone 언어: Python (수집), TypeScript (API + UI) 선수 지식: Phase 5 (자연어 처리 기초), Phase 7 (트랜스포머; transformers), Phase 11 (LLM 엔지니어링), Phase 13 (도구), Phase 17 (인프라) 활용되는 Phase: P5 · P7 · P11 · P13 · P17 소요 시간: 약 30시간

문제

2026년의 모든 프런티어(frontier) 코딩 에이전트는 코드베이스 검색(codebase retrieval) 계층을 기본으로 탑재합니다. 컨텍스트 윈도(context window)만으로는 저장소 간(cross-repo) 질문을 해결할 수 없기 때문입니다. Claude의 100만 토큰(1M-token) 컨텍스트는 큰 도움이 되지만, 그것만으로 순위 기반 검색(ranked retrieval)의 필요성이 사라지지는 않습니다. 원시 청크(raw chunk)에 대해 단순 코사인 검색(cosine search)만 수행하면, 생성된 코드(generated code), 모노레포(monorepo) 안의 중복, 거의 임포트되지 않는 롱테일(long-tail) 심벌(symbol)에서 결과가 쉽게 오염됩니다. 운영 환경의 정답은 AST 인식(AST-aware) 청크에 대해 밀집 검색(dense search)과 BM25를 결합한 하이브리드 검색을 수행하고, 재순위 모델(re-ranker)을 붙이며, 심벌 참조 그래프(symbol reference graph)로 뒷받침하는 것입니다.

이 학습은 한 개짜리 튜토리얼 저장소가 아니라 실제 코드 자산 전체(fleet)를 색인하며 진행합니다. 지표로는 MRR@10(평균 역수 순위; Mean Reciprocal Rank at 10), 인용 충실도(citation faithfulness), 증분 신선도(incremental freshness)를 측정합니다. 실패 양상은 모두 인프라 차원에서 나타납니다. 10만 파일 규모의 모노레포, 절반에 가까운 파일을 한꺼번에 건드리는 푸시(push), 네 개의 저장소를 가로질러야만 정확히 답할 수 있는 쿼리(query)가 대표적인 예입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.코드베이스 RAG 시스템이 고정 토큰 윈도(fixed-token window) 대신 AST 인식 청크화(AST-aware chunking)를 사용하는 이유는 무엇인가요?

2.코드 검색 시스템에서 하이브리드 검색(hybrid search, dense + BM25)의 목적은 무엇인가요?

0/2 답변 완료

개념

AST 인식 수집 파이프라인(AST-aware ingestion pipeline)은 각 파일을 트리시터로 파싱한 뒤, 함수(function)와 클래스(class) 노드(node)를 추출하고, 고정된 토큰 윈도(token window)가 아니라 노드 경계(node boundary)에서 청크를 나눕니다. 각 청크에는 세 가지 표현을 부여합니다. 밀집 임베딩(dense embedding; Voyage-code-3 또는 nomic-embed-code), 희소 BM25 항(sparse BM25 terms), 그리고 짧은 자연어 요약(natural-language summary)입니다. 요약은 세 번째 검색 가능 양식이 되어 줍니다. 예를 들어 사용자가 "X는 어떻게 권한 부여(authorization)되는가"라고 물을 때, 코드에는 check_permission만 보여도 요약에는 authz라는 표현이 들어가 있어 검색이 이어집니다.

검색은 하이브리드 방식입니다. 하나의 쿼리가 들어오면 밀집 검색과 BM25 검색을 동시에 실행하고, 상위 k개(top-k) 결과를 병합한 뒤 그 합집합을 교차 인코더 재순위 모델(cross-encoder re-ranker; Cohere rerank-3 또는 bge-reranker-v2-gemma-2b)에 넘깁니다. 재순위가 매겨진 목록은 다시 긴 컨텍스트 합성기(long-context synthesizer; 프롬프트 캐싱이 적용된 Claude Sonnet 4.7, 혹은 자체 호스팅한 Llama 3.3 70B)에 전달되며, 합성기는 모든 주장(claim)을 파일과 줄 범위(file:line range)로 인용하도록 지시받습니다. 인용이 없는 답변은 후처리 필터(post-filter)가 거부합니다.

증분 신선도는 본질적으로 인프라 문제입니다. git push는 차분(diff)을 유발하므로, 어떤 파일이 바뀌었고 어떤 심벌이 변경되었는지 계산할 수 있습니다. 그 후 영향을 받은 청크만 다시 임베딩하고, 영향을 받은 파일 간 심벌 엣지(cross-file symbol edge; 임포트와 메서드 호출)만 재계산합니다. 이렇게 하면 매 커밋마다 200만 줄을 모두 다시 처리하지 않고도 색인의 일관성을 유지할 수 있습니다.

아키텍처

git push --> webhook --> ingest worker (LlamaIndex Workflow)
                           |
                           v
             tree-sitter parse + AST chunk
                           |
            +--------------+----------------+
            v              v                v
          dense        BM25 index       summary (LLM)
        (Voyage / bge)  (Tantivy)        (Haiku 4.5)
            |              |                |
            +------> Qdrant / pgvector <----+
                            |
                            v
                      symbol graph (Neo4j / kuzu)
                            |
  query --> LangGraph agent (retrieve -> rerank -> synth)
                            |
                            v
                 Claude Sonnet 4.7 1M context
                            |
                            v
                 answer + file:line citations

스택

  • 파싱(parsing): 17개 언어 문법(grammar)을 지원하는 트리시터(Python, TypeScript, Rust, Go, Java, C++ 등).
  • 밀집 임베딩(dense embeddings): 호스티드(hosted) Voyage-code-3, 또는 자체 호스팅(self-host)하는 nomic-embed-code-v1.5, 폴백(fallback)으로 bge-code-v1.
  • 희소 색인(sparse index): BM25F를 사용하는 Tantivy(Rust 구현). 심벌 이름(symbol name)과 본문(body)에 필드별 가중치(field weighting)를 부여합니다.
  • 벡터 데이터베이스(vector DB): 하이브리드 검색을 지원하는 Qdrant 1.12, 또는 5천만 벡터(50M vector) 미만 규모 팀을 위한 pgvector + pgvectorscale 조합.
  • 청크 요약 모델(chunk summary model): 프롬프트 캐싱(prompt caching)을 적용한 Claude Haiku 4.5 또는 Gemini 2.5 Flash.
  • 재순위 모델(re-ranker): Cohere rerank-3, 또는 자체 호스팅한 bge-reranker-v2-gemma-2b.
  • 오케스트레이션(orchestration): 수집은 LlamaIndex Workflows, 쿼리 에이전트(query agent)는 LangGraph로 구성합니다.
  • 합성기(synthesizer): 프롬프트 캐싱이 적용된 Claude Sonnet 4.7 (100만 토큰 컨텍스트).
  • 심벌 그래프(symbol graph): 임포트(import)와 호출(call) 엣지(edge)를 위해 매니지드(managed) Neo4j 또는 임베디드(embedded) kuzu를 사용합니다.
  • 관측 가능성(observability): 검색과 합성 단계마다 남기는 Langfuse 스팬(span).

만들어보기

  1. 수집 워커(ingestion walker). 모든 push 훅(hook)마다 Git 이력(history)을 순회하면서 변경된 파일을 수집합니다. 각 파일을 트리시터로 파싱한 뒤, 함수와 클래스 노드를 전체 소스 범위(full source span)와 함께 추출합니다. 청크 레코드 {repo, path, start_line, end_line, symbol, body}를 만들어 다음 단계로 보냅니다(emit).

  2. 청크 요약기(chunk summarizer). 시스템 프리앰블(system preamble)에 프롬프트 캐싱을 걸어, 여러 청크를 Haiku 4.5 호출 한 번에 묶어(batch) 처리합니다. 사용하는 프롬프트는 다음과 같습니다. "이 함수를 한 문장으로 요약하되, 공개 계약(public contract)과 부수 효과(side effect)를 명시해라." 요약 결과는 원래 청크와 함께 저장합니다.

  3. 임베딩 풀(embedding pool). 두 개의 병렬 큐(queue)를 둡니다. 밀집(dense) 큐는 Voyage-code-3을 배치 크기(batch) 128로 호출하고, 요약(summary) 큐는 동일 모델을 요약 문자열에 대해 사용합니다. 생성된 벡터는 Qdrant에 기록하며, 페이로드(payload)에 {repo, path, start_line, end_line, symbol, kind}를 함께 저장합니다.

  4. BM25 색인. 필드별 가중치(field-weighted)를 적용한 Tantivy 색인을 구축합니다. 가중치는 심벌 이름 4, 심벌 본문 1, 요약 2로 둡니다. 이렇게 하면 "X라는 이름의 함수를 찾아라"와 "X를 수행하는 함수를 찾아라" 같은 두 부류의 쿼리를 모두 처리할 수 있습니다.

  5. 심벌 그래프(symbol graph). 각 청크마다 엣지를 기록합니다. 임포트(imports; 이 파일이 저장소 Z의 심벌 Y를 사용), 호출(calls; 이 함수가 클래스 C의 메서드 M을 호출), 상속(inheritance) 관계가 그 대상입니다. 결과는 kuzu에 저장하며, 쿼리 시점(query time)에 저장소 경계(repo boundary)를 넘어 검색을 확장하는 데 활용합니다.

  6. 쿼리 에이전트(query agent). 세 개의 노드를 가진 LangGraph 그래프로 구성합니다. retrieve 노드는 밀집 검색과 BM25를 병렬로 실행하고 (repo, path, symbol) 기준으로 중복을 제거(deduplicate)합니다. rerank 노드는 상위 50개에 대해 교차 인코더(cross-encoder)를 적용하고 상위 10개만 남깁니다. synth 노드는 재순위된 청크를 컨텍스트에 넣어 Claude Sonnet 4.7을 호출하면서, 시스템 프롬프트는 캐시(cache)하고 file:line 형식의 인용을 의무화합니다.

  7. 인용 강제(citation enforcement). 모델 출력을 파싱해서, (repo/path:start-end) 앵커(anchor)가 없는 주장은 다시 묻거나(re-ask) 폐기(drop)합니다. 사용자에게는 인용이 붙어 있는 답변만 돌려줍니다.

  8. 증분 재색인(incremental re-index). 웹훅(webhook)이 들어올 때마다 심벌 단위 차분(symbol-level diff)을 계산합니다. 본문 텍스트가 변경된 청크만 다시 임베딩하고, 임포트가 변경된 청크의 심벌 엣지만 다시 계산합니다. 목표 지표는 다음과 같습니다. 200만 줄(2M LOC) 규모의 코드 자산에서 50개 파일 푸시가 60초 이내에 다시 검색 가능해야 합니다.

  9. 평가(eval). 저장소 간 질문(cross-repo question) 100개에 정답(gold) file:line 위치를 라벨링(labeling)합니다. MRR@10, nDCG@10(정규화된 누적 이득; normalized Discounted Cumulative Gain at 10), 인용 충실도(검증 가능한 앵커를 가진 주장의 비율), 그리고 p50과 p99 지연 시간(latency)을 측정합니다.

사용해보기

$ code-rag ask "S3 멀티파트 업로드 중단(abort)은 우리 재시도 예산(retry budget)과 어떻게 연결되어 있나요?"
[retrieve]  밀집 검색 12청크 + BM25 7청크, 중복 제거 후 고유 16개
[rerank]    상위 5개 유지 (cohere rerank-3)
[synth]     claude-sonnet-4.7, 캐시 적중률 68%, 2.1초
answer:
  멀티파트 업로드 중단은 services/uploader/retry.go:122-148의
  `AbortMultipartOnFail`에서 트리거되며, config/budgets.yaml:34-51에
  정의된 버킷별 재시도 예산을 감소시킵니다 ...
  citations: [services/uploader/retry.go:122-148, config/budgets.yaml:34-51,
              libs/s3client/multipart.ts:44-61]

제출하기

제출 산출물 스킬(skill)은 outputs/skill-codebase-rag.md입니다. 저장소 코퍼스(corpus)가 주어지면, 수집 파이프라인과 하이브리드 색인, 그리고 쿼리 에이전트를 구성해 저장소 간 질문에 대해 인용이 포함된 답변을 돌려줍니다. 평가 루브릭(rubric)은 다음과 같습니다.

가중치평가 항목측정 방법
25검색 품질(Retrieval quality)100문항 홀드아웃(held-out) 세트에서 MRR@10과 nDCG@10
20인용 충실도(Citation faithfulness)검증 가능한 file:line 앵커가 있는 답변 주장의 비율
20지연 시간과 확장성(Latency and scale)색인된 코퍼스 규모에서 10k QPS 트래픽 기준 p95 쿼리 지연 시간
20증분 색인 정확성(Incremental indexing correctness)50개 파일 커밋 기준 git push 시점부터 검색 가능 시점까지의 시간
15UX와 답변 형식(UX and answer formatting)인용 클릭 가능성(citation clickability), 스니펫(snippet) 미리보기, 후속 질문(follow-up) 동선
100

연습문제

  1. (쉬움) Voyage-code-3을 자체 호스팅한 nomic-embed-code로 교체합니다. MRR@10의 차이(delta)를 측정하고, 재순위(re-ranking)를 켰을 때 그 격차가 좁혀지는지 보고합니다.

  2. (중간) 코퍼스에 LLM이 만든 보일러플레이트(boilerplate) 형태의 생성 코드를 20% 비율로 주입한 뒤 다시 평가합니다. 검색 결과 오염(retrieval poisoning)을 관찰하고, 페이로드에 generated 플래그(flag)를 추가해 해당 결과의 가중치를 낮춰 봅니다.

  3. (중간) 현재 코퍼스 규모에서 Qdrant 하이브리드 검색과 pgvector + pgvectorscale 조합을 벤치마크합니다. 배치 크기 1에서 p99 지연 시간을 보고합니다.

  4. (어려움) 샘플링 기반 드리프트 점검(sampling-based drift check)을 추가합니다. 매주 100문항 평가를 다시 실행하고, MRR@10이 5% 이상 떨어지면 알림(alert)을 발생시킵니다.

  5. (어려움) 언어 간 심벌 해석(cross-language symbol resolution)으로 확장합니다. Python 함수가 gRPC를 통해 Go 서비스를 호출하는 사례를 다룹니다. 심벌 그래프를 사용해 두 호출 지점을 연결합니다.

핵심 용어

용어흔한 설명실제 의미
AST 인식 청크화(AST-aware chunking)"함수 단위 분할(function-level splits)"고정 토큰 윈도가 아니라 트리시터의 노드 경계에서 코드를 자르는 방식이다
하이브리드 검색(Hybrid search)"Dense + sparse"BM25와 벡터 검색을 병렬로 실행하고, 상위 k개를 병합한 뒤 재순위를 매기는 방식이다
교차 인코더 재순위(Cross-encoder rerank)"2단계 순위화(second-stage rank)"(query, candidate) 쌍을 함께 점수화하는 모델로, 코사인 유사도보다 더 정확하다
프롬프트 캐싱(Prompt caching)"캐시된 시스템 프롬프트"반복되는 접두(prefix) 토큰 비용을 최대 90%까지 할인해 주는 2026년의 Claude/OpenAI 기능이다
심벌 그래프(Symbol graph)"코드 그래프(code graph)"파일과 저장소를 가로지르는 임포트, 호출, 상속 관계의 엣지 집합이다
인용 충실도(Citation faithfulness)"정답 근거율(grounded answer rate)"사용자가 앵커를 클릭해 참조 구간을 읽고 주장을 검증할 수 있는 비율이다
증분 재색인(Incremental re-index)"푸시-검색 지연(push-to-search time)"git push 이후 변경된 심벌이 검색 가능해지기까지의 실제 경과 시간(wall-clock)이다

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

codebase-rag

Build a cross-repo semantic search system with AST-aware chunking, hybrid retrieval, incremental re-index, and cited answers.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.200만 줄(2M LOC) 코퍼스에 50개 파일 푸시(push)가 도착했습니다. 시스템은 변경된 청크만 다시 임베딩하고 영향 받은 심벌 엣지(edge)만 재계산합니다. 대규모에서 이 증분 방식이 중요한 이유는 무엇인가요?

2.합성기(synthesizer)는 모든 주장(claim)에 file:line 인용 앵커(citation anchor)를 포함하도록 요구합니다. 인용이 없는 주장이 있으면 어떻게 되나요?

3.심벌 그래프(symbol graph)는 파일과 저장소를 가로지르는 임포트(import), 호출(call), 상속(inheritance) 엣지를 저장합니다. 이 그래프가 저장소 간 검색(cross-repo retrieval)을 어떻게 개선하나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다