Aider의 레포 맵이 코드베이스를 실제로 읽는 방식 — 임베딩이 아닌 PageRank thumbnail
분석2026년 7월 2일개발자 도구

Aider의 레포 맵이 코드베이스를 실제로 읽는 방식 — 임베딩이 아닌 PageRank

Aider는 전체 코드베이스를 읽는 AI로 소개된다. v0.86.3.dev에서 읽은 레포 맵은 AI 문제가 아니라 순위 산정 문제다. tree-sitter가 심볼을 추출하고, 참조 그래프의 PageRank가 순위를 매기며, 이진 탐색이 상위 심볼을 토큰 예산에 맞춰 담는다. 임베딩도, 벡터 저장소도, 네트워크 호출도 없다.

ArcYou Editorial. 방법론 섹션에 고정된 저장소 상태를 기준으로 읽었습니다.

결정 요약

이 소스 리드가 분명하게 하는 것

실제로 무엇인가
심볼 그래프와 PageRank로 코드베이스를 압축하는 코딩 에이전트
잘 맞는 경우
예측 가능한 컨텍스트와 낮은 네트워크 의존이 중요한 저장소
피할 경우
워크플로가 의미 기반 자연어 검색이나 대화 중 동적 탐색에 놓여 있을 때
핵심 메커니즘
tree-sitter 심볼, 참조 그래프 PageRank, 토큰 예산 패킹

읽은 기준: aider-v0.86.3.dev @ v0.86.3.dev

Aider는 “전체 코드베이스를 읽는 AI”로 묘사된다. 나는 **v0.86.3.dev, 커밋 7a1bd15**에서 소스를 읽었고, 그 “읽기”는 AI 문제가 아니라 순위 산정 문제였다. 모델은 레포를 전혀 보지 않는다. 모델이 보는 것은 레포 맵이다. 코드의 시그니처로 만든 압축되고 순위가 매겨진 목차로, tree-sitter와 PageRank가 구축하며 그 경로 어디에도 임베딩, 벡터 데이터베이스, 네트워크 호출은 없다. 아래의 모든 주장은 코드의 해당 줄을 가리킨다.

이것은 무엇이고 어디에 놓일까?

레포 맵은 모델이 지금 편집하는 몇 안 되는 파일 밖에 무엇이 있는지 알 수 있도록 Aider가 앞에 붙이는 컨텍스트 블록이다. 코드베이스 전체는 컨텍스트 윈도에 들어가지 않으므로 Aider가 그대로 붙여 넣을 수 없다. 대신 예산에 맞춘 개요를 만든다. 가장 중요한 파일마다 약간의 주변 스코프를 포함한 class/def 헤더 몇 줄, 즉 시그니처 줄만 담고 나머지는 모두 제외한다.

이 하위 시스템 전체는 파일 하나인 aider/repomap.py(867줄)에 들어 있다. 여기서 마케팅의 역할을 하는 단어는 “중요한”이므로, 나는 그 의미를 읽으러 갔다. 알고 보니 아주 구체적이고 기계적인 뜻이었다.

짧게 말하면 Aider는 레포를 무엇이 무엇을 참조하는지 나타내는 그래프로 취급하고, 그 위에서 PageRank를 실행한 다음, 이진 탐색으로 순위가 가장 높은 심볼을 토큰 예산에 맞춰 담는다. 위로 올라오는 것은 최근에 편집한 것이나 질문과 의미상 “비슷한” 것이 아니다. 구조적으로 중심에 있는 것이며, 지금 대화 중인 파일과 이름 쪽으로 기울어진다.

맵은 실제로 어떻게 만들어질까?

순서대로 네 단계다.

단계 일어나는 일 위치
1. 추출 tree-sitter가 각 파일에서 정의와 참조를 추출하고 mtime 기준으로 캐시한다 get_tags_raw :279
2. 순위 산정 참조 그래프를 만들고 개인화 편향을 적용한 PageRank를 실행한다 get_ranked_tags :365
3. 패킹 토큰 예산에 들어갈 상위 태그 수를 이진 탐색한다 get_ranked_tags_map_uncached :629
4. 렌더링 선택한 시그니처를 스코프가 드러나는 트리로 출력한다 to_tree :748

1. 추출 — 이해가 아니라 tree-sitter

각 파일은 언어별 .scm 쿼리를 통해 tree-sitter로 파싱된다. 캡처된 노드는 캡처 이름을 순수하게 문자열로 검사한 결과에 따라 정의 또는 참조가 된다. name.definition.*def, name.reference.*ref다(:319-322). 타입 해석도 바인딩도 없다. “참조”는 문법이 참조라고 표시한 식별자 노드일 뿐이다. C와 C++를 비롯해 쿼리가 정의만 내보내는 언어의 경우 Aider는 pygments를 실행해 모든 Token.Name을 유지하는 방식으로 줄 번호 없는 참조를 보충한다(:338-363). 결과는 파일의 mtime을 키로 삼아 .aider.tags.cache.v{3,4} 아래의 SQLite diskcache에 캐시되므로(:43, :233-264), 바뀌지 않은 파일은 다시 파싱하지 않는다.

2. 순위 산정 — 참조 그래프 위의 PageRank

여기가 핵심이다. Aider는 이름을 참조하는 파일에서 그 이름을 정의하는 파일로 간선이 향하는 유향 멀티그래프를 만들고(:514), 그 위에서 networkx.pagerank를 실행한다(:525). 그런 다음 각 노드의 순위를 바깥으로 나가는 간선에 다시 분배해, 출력이 파일만이 아니라 (파일, 심볼) 정의의 순위를 나타내게 한다(:533-545).

두 가지 조절 장치가 그 순위를 사용자 쪽으로 기울인다. 첫째는 PageRank 개인화 벡터다. 채팅에 들어 있는 파일, 사용자가 이름을 언급한 파일, 경로가 사용자가 언급한 식별자와 일치하는 파일은 모두 가중치를 더 받는다(:422-445). 둘째는 그래프를 만들기 전에 적용하는 간선별 가중치 배수다.

신호 가중치 위치
메시지에서 사용자가 이름을 댄 식별자 ×10 :492
긴(8자 이상) 복합 식별자(snake/kebab/camel) ×10 :494
참조하는 파일이 채팅에서 열려 있음 ×50 :508
식별자가 _로 시작함(비공개) ×0.1 :496
식별자가 5개 넘는 파일에 정의됨(어디에나 있음) ×0.1 :498

참조 횟수에는 제곱근으로 감쇠를 적용하므로 100번 쓰인 심볼이 4번 쓰인 심볼을 압도하지 않는다(:512). 이 표는 우선순위로 읽으면 된다. 비공개 헬퍼의 가치는 공개 헬퍼의 십분의 일이고, 사용자가 입력한 이름은 열 배에서 쉰 배 더 가치 있다. 맵은 사용자가 방금 말한 내용을 중심으로 스스로 가중치를 다시 조정한다.

Aider가 정의, 참조, 사용자 언급, 채팅 파일을 PageRank 점수가 매겨진 레포 맵으로 바꾼다

Aider는 심볼을 추출하고 참조 그래프를 만든 뒤, 사용자 편향을 더한 PageRank로 점수를 매기고 순위가 가장 높은 시그니처를 레포 맵에 맞춰 담는다.

3. 패킹 — 토큰 예산을 기준으로 한 이진 탐색

순위 목록에는 항목이 수천 개나 있을 수 있지만 예산은 작다(기본값 map_tokens=1024, :49). 그래서 Aider는 포함할 상위 태그의 개수를 이진 탐색한다(:666-706). 밀도 추정값인 min(max_map_tokens // 25, num_tags), 즉 태그당 약 25토큰으로 초기 추측을 정하고(:676), 그 접두 구간을 렌더링해 토큰을 센 다음 범위를 양분한다. 예산에 들어오는 가장 큰 렌더링을 유지하고, 목표의 15% 이내에 들어오면 일찍 멈춘다(ok_err = 0.15, :689-696). 약 200자를 넘는 것의 토큰 수는 그 자체가 추정값이다. 전체 줄의 약 1/100을 표본으로 뽑아 외삽하는데(:89-101), 바로 이 때문에 15%의 여유가 있다. 몇몇 “중요한” 파일(README, 매니페스트 등)은 순위와 관계없이 항상 앞에 붙는다(:657-662).

4. 렌더링 — 본문이 아니라 시그니처

선정된 항목은 파일별로 묶이고 path: 뒤에 약간의 스코프 컨텍스트를 포함한 관련 시그니처 줄을 붙이는 형식으로 출력된다. grep-ast의 TreeContext를 사용하며, 줄 번호도 전체 본문도 없다. 미니파이된 파일 하나가 예산을 터뜨리지 않도록 100자가 넘는 줄은 모두 잘린다(:748-784).

여기에 없는

“코드베이스를 읽는 AI”로 마케팅되는 기능치고 맵에 AI가 거의 없다는 점이 인상적이다. 임포트하는 것은 os, math, sqlite3, tree-sitter, pygments, diskcache, tqdm, 그리고 순위 함수 안에서 불러오는 networkx가 전부다(:1-27, :368). 그 밖에는 아무것도 없다. 파일에서 embed, openai, requests, vector, faiss, numpy를 grep하면 유일하게 걸리는 것은 주석 속 networkx 문서 URL이다. 맵을 만드는 경로에는 임베딩 모델도, 벡터 저장소도, 유사도 검색도, 네트워크나 LLM 호출도 없다. 모델에 닿는 유일한 곳은 token_count인데, 이것은 예산 크기를 정하는 데 쓰는 로컬 토크나이저일 뿐 순위를 매기거나 검색하는 데 쓰이지 않는다.

그 한 가지 사실이 이야기의 전부다. Aider의 관련성 신호는 그래프 중심성에 사용자의 명시적 언급을 더한 것이며, 오프라인에서 결정론적으로 계산된다. 의미 유사도가 아니다.

그렇다면 왜 이런 식으로 동작할까?

메커니즘을 보고 나면 특이한 동작도 더는 수수께끼가 아니다.

  • 채팅에 파일을 추가하면 맵은 좁아지고, 하나도 추가하지 않으면 부푼다. 채팅에 파일이 없으면 예산에 8(map_mul_no_files)을 곱하되 context_window − 4096까지만 늘린다(:123-132). Aider가 아무것도 모를 때는 큰 방향 탐색용 맵을 만들고, 사용자가 무언가를 가리키면 좁은 주변 지도를 만든다.
  • 이름을 대면 강하게 방향을 틀 수 있다. ×10/×50 배수 때문에 프롬프트에서 함수나 파일을 언급하면 그 주변이 맨 위로 확 끌려온다. Aider가 무언가를 “보지 못한다면” 의도된 조절 수단은 더 큰 예산이 아니라 그 이름을 대는 것이다.
  • 최근 코드가 아니라 중심 코드를 위로 올린다. 순위 산정 어디에서도 git의 최신성이나 편집 시각은 보지 않는다. 한 시간 전에 건드린 파일도 사용자가 언급하지 않는 한 참조 그래프에서 차지한 위치에 따라 순위가 정해진다.
  • 큰 레포는 한 번 비용을 치르고 나면 저렴하다. 첫 스캔은 “can happen only once”라고 경고한다(:391-395). mtime 캐시 덕분이다. 하지만 Python의 재귀 한도를 넘을 만큼 큰 레포에서는 “git repo too large?“라는 메시지와 함께 맵을 완전히 비활성화한다(:143-146).
  • 같은 레포면 같은 맵이 나온다. PageRank와 결정론적 동률 처리 때문에 출력은 표본 추출이나 흐릿한 검색이 아니라 코드와 그 mtime의 함수다.

더 큰 지도에서 어디에 놓일까?

내가 어떤 코딩 에이전트든 지도 위에 놓을 때 사용할 축은 **모델이 어떤 코드를 보게 할지 어떻게 결정하는가?**다. 진영은 둘이다.

한쪽은 정적 분석 우선이다. 코드를 파싱하고, 심볼 그래프나 인덱스를 만든 뒤, 구조를 기준으로 고른다. Aider가 깔끔한 예다. 참조 그래프 위에서 PageRank를 실행한다. 다른 쪽은 임베딩 우선이다. 레포를 청크로 나누고, 청크를 임베딩한 다음, 프롬프트와의 벡터 유사도로 컨텍스트를 가져온다. 이 한 가지 선택의 영향은 연쇄적으로 퍼진다. 정적 분석 우선 방식은 결정론적이고 오프라인에서 작동하며 다시 실행하기 저렴하고 인덱스 서버가 필요 없다. 하지만 tree-sitter 태그가 있는 언어 밖에서는 보지 못하고, 코드를 이름과 중심성으로 찾으므로 사용자가 이름을 대야 한다. 임베딩 우선 방식은 이름을 모를 때도 “날짜를 파싱하는 함수”를 찾을 수 있지만 인덱스와 임베딩 모델이 필요하고, 코드가 바뀌면 어긋난다.

Aider는 확실히 첫 번째 진영에 있으며, 위의 모든 동작은 여기서 나온다. 관련성을 결정하는 숨은 모델 없이 손으로도 다시 계산할 수 있는 순위만 있으므로, 비교를 논리적으로 따져보기에도 더 깔끔한 쪽이다.

언제 이 방식을 선택할까?

컨텍스트 선택이 결정론적이고 오프라인에서 작동하며 벡터 인덱스 서비스가 필요 없기를 바라고, 코드가 tree-sitter의 태그가 있는 언어로 작성됐다면 Aider의 방식은 정말 잘 맞는다. 이 방식은 한 가지 습관에 보상한다. 중요한 파일과 심볼의 이름을 대라. 언급은 전체 순위에서 가장 강한 조절 수단이기 때문이다. 경계는 알고 시작해야 한다. 레포가 거대하거나, 지원되지 않는 언어로 작성됐거나, 이름을 대지 않아도 에이전트가 코드를 의미로 떠올리길 기대한다면 정적 참조 그래프 맵은 그 코드를 찾아주지 못한다. 그것은 임베딩 진영의 일이며, 트레이드오프도 다르다.

방법론과 범위

나는 **Aider v0.86.3.dev의 커밋 7a1bd15**를 읽었고, aider/repomap.pybase_coder.py, models.py, args.py의 호출 지점에 초점을 맞췄다. 핵심 주장 세 가지, 즉 개인화와 배수 편향을 적용한 PageRank 순위 산정, 임베딩·벡터·네트워크 경로가 전혀 없다는 점, 이진 탐색을 통한 토큰 패킹은 각각 이 커밋을 대상으로 적대적 주장 검증을 거쳐 확인됐다. 줄 번호는 해당 커밋에서 정확하며, 코드 인용은 한두 줄로 제한했다.

범위의 주의점을 명확히 밝힌다.

  • 렌더링된 트리의 시각적 형태, 즉 각 시그니처가 얼마나 많은 스코프 컨텍스트를 얻는지는 외부 의존성인 grep-ast의 TreeContext가 만든다. 나는 이 의존성을 한 줄씩 읽지 않았다. 내 주장은 Aider가 여기에 무엇을 전달하는지에 관한 것이지 내부 포매팅에 관한 것이 아니다.
  • 언어 지원 범위는 어떤 tree-sitter 태그 쿼리가 함께 제공되는지에 달려 있다(여기서는 약 30개 언어). “이름을 대야 한다”는 주의점은 pygments 참조 경로로 폴백하는 언어에서 가장 강하게 적용된다.1
  • 나는 런타임을 벤치마크하지 않았다. “한 번 스캔한 뒤에는 저렴하다”는 주장은 스톱워치가 아니라 mtime 캐시 코드에서 나온 것이다.

Footnotes

  1. pygments 폴백(:338-363)은 줄 번호와 중복 제거 없이 모든 이름 토큰에 대해 참조를 내보내므로, 정의만 제공하는 언어에서는 실제 참조 캡처가 있는 언어보다 그래프 간선이 거칠다. 순위는 여전히 매기지만 무엇이 무엇을 사용하는지 바라보는 시야가 더 뭉툭할 뿐이다.

방법론

이 글은 아래의 고정된 저장소 상태를 기준으로 읽었습니다. 디렉터리 이름은 편의용 키이고, 실제 고정 지점은 commit SHA입니다.

저장소 키버전커밋원본
aider-v0.86.3.devv0.86.3.dev7a1bd15f0c78github.com/Aider-AI/aider

관련 분석

결정 축은 컨텍스트 소유권이다. 코드를 미리 압축하느냐, 로컬 인덱스에서 검색하느냐, 모델이 직접 파일을 읽게 하느냐가 토큰 비용과 예측 가능성을 바꾼다.