# AI 에이전트

GitNexus를 쓰면 AI 에이전트가 코드를 놓치지 않는다는데, 과연 진짜일까?

@Hong2026-10-26

이 글에 담긴 코드와 결과는 직접 실행해 측정한 값을 바탕으로 작성했습니다. 실측은 Hong과 팀원들이 진행했고, 그 결과를 바탕으로 AI가 글을 쓰고 Hong이 검토합니다.

실습 코드 내려받기

질문
GitNexus 코드 그래프를 Claude Code에 붙이면 코드 탐색에 드는 토큰이 줄어들까?
답
기본 도구만 쓴 쪽과 정답은 비슷했고(자동 판정 12/12 대 11/12) 파일을 여는 호출도 줄었지만, 입력 토큰 중앙값은 50,988에서 94,520으로 늘었다. 불러온 도구 설명이 요청마다 다시 실렸다.
조건
2026-10-25, Claude Code 2.1.309 claude -p, Opus 5.5, effort medium, GitNexus 1.6.12, rich v15.0.0 질문 4개 × 2조건 × 3번

환경

측정일
2026-10-25
도구
GitNexus 1.6.12(PolyForm Noncommercial 1.0.0), 공식 Docker 이미지 ghcr.io/abhigyanpatwari/gitnexus:1.6.12, 임베딩 없는 기본 인덱스
Claude Code
2.1.309, --model opus(결과 JSON의 모델 ID는 claude-opus-5-5), --effort medium, 도구 Bash,Read,Grep,Glob,ToolSearch,Skill
저장소
Textualize/rich v15.0.0(커밋 6ac483c), 파일 553개 중 .py 213개
반복
질문 4개 × 조건 2개 × 3번, 모두 24번(조건별 워밍업 1번씩은 뺌)
비용
구독 로그인으로 호출, 금액은 Claude Code가 API 목록 단가로 환산한 추정치(실제 청구액 아님)
목차
  1. GitNexus가 하는 일
  2. 실습 준비
  3. 실습 1: 영향 범위 질문 하나를 두 조건으로
  4. 실습 2: 질문 4개를 세 번씩
  5. 정리

이번 시간에는 코드를 미리 그래프로 만들어 두는 GitNexus를 Claude Code에 붙였을 때, 에이전트가 덜 찾고 덜 읽어서 토큰이 줄어드는지를 claude -p로 측정하며 배워가는 시간입니다. claude -p는 대화 화면 없이 질문 하나를 보내고 결과를 받는 실행 방식이에요.

GitNexus를 붙인 조건과 기본 도구만 쓰는 조건에 같은 질문을 번갈아 보내 비교할게요. 다시 돌리면 숫자가 달라질 수 있고, --model opus가 가리키는 모델도 시간이 지나면 달라질 수 있어요.

GitNexus가 하는 일

gitnexus analyze는 저장소를 파싱해 함수·클래스와 그 사이의 호출·상속 관계, 실행 흐름을 그래프 DB에 저장해요. 에이전트는 이 그래프를 MCP 도구 17개(impact, context, trace, cypher 등)나 같은 이름의 CLI로 조회합니다.

README는 "so AI agents never miss code"라고 소개하고, 토큰 효율로 "no 10-query chains to understand one function"을 내세우며 조회 4번 이상 걸릴 답을 impact 1번으로 얻는 그림을 보여 줘요. 토큰을 몇 % 줄인다는 수치는 없어서, 정답과 도구 호출 수, 입력 토큰을 직접 재 볼게요. 라이선스는 PolyForm Noncommercial 1.0.0이라 개인 학습·연구에는 쓸 수 있지만, 상업적으로 쓰려면 따로 라이선스가 필요해요.

실습 준비

이 글의 명령은 모두 실습 코드의 gitnexus-code-graph/ 디렉터리에서 실행합니다.

  • lab.sh: 저장소 받기, 컨테이너 띄우기, 인덱스 만들기, claude -p 호출과 호출 순서를 담은 셸 함수 모음
  • summarize.py: 결과를 질문·조건별로 모으고 정답을 판정하는 스크립트
  • answers.json: 질문 4개와 정답 목록

따라 하시려면 Docker, Claude Code 2.1.309와 구독 로그인, git, jq, perl(macOS 기본)이 필요하고, Claude의 Bash 명령은 Mac에서 실행돼요. 셸에 ANTHROPIC_API_KEY나 ENABLE_TOOL_SEARCH가 있으면 과금 방식이나 도구 설명을 싣는 방식이 바뀌니 비우고 실행하세요.

공식 설치는 npm install -g gitnexus@1.6.12 뒤 저장소에서 gitnexus analyze와 gitnexus setup을 실행하는 거예요.

  • analyze: 인덱스(.gitnexus/), CLAUDE.md, AGENTS.md, 스킬 6개(.claude/skills/gitnexus-*)를 저장소에 씁니다.
  • setup: 홈의 ~/.claude.json에 MCP 서버를, ~/.claude/settings.json에 Grep·Glob·Bash 직전에 그래프 문맥을 붙이는 훅과 git 커밋 뒤 인덱스가 낡았는지 알리는 훅을 등록해요.

여기서는 Mac에 설치하지 않고 공식 Docker 이미지로 돌리며, 측정을 --setting-sources project로 하므로 setup의 MCP 서버·훅을 실습 저장소 안 설정으로 옮겼어요.

아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.

source ./lab.sh

py312() { docker run --rm --network none -v "$PWD":/w -w /w python:3.12-slim python -I "$@"; }

calls() { jq -r --arg p "$R/" 'select(.type == "assistant") | .message.content[] | select(.type == "tool_use") | "\(.name) \(.input | tostring | gsub($p; ""))"' "$1" | cut -c1-120; }

excerpt() { jq 'select(.type == "result") | {usage: (.usage | {input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens}), total_cost_usd, num_turns, duration_ms}' "$1"; }

steps() { jq -s -r '[.[] | select(.type == "assistant") | {id: .message.id, u: .message.usage}] | reduce .[] as $x ([]; if any(.[]; .id == $x.id) then . else . + [$x] end) | map(.u | .input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens) | join(" -> ")' "$1"; }

hooks() { jq -r 'select(.subtype == "hook_response") | "\(.hook_name) \(.output | .[0:120])"' "$1"; }

calls는 부른 도구를, excerpt는 결과 JSON의 주요 필드를, steps는 API 요청마다 실린 입력 토큰을, hooks는 훅 응답을 보여 줘요.

get은 rich v15.0.0을 runs/src/rich에 받아 커밋 해시를 찍고, up은 GitNexus 컨테이너를 띄워요. up의 핵심은 이렇습니다(발췌).

  docker run -d --name "$CTR" --network none --memory 3g --cpus 2.5 \
    -e GITNEXUS_HOME="$R/gnhome" -e NO_UPDATE_NOTIFIER=1 \
    -e GIT_CONFIG_COUNT=1 -e GIT_CONFIG_KEY_0=safe.directory -e GIT_CONFIG_VALUE_0='*' \
    -v "$R:$R" --entrypoint sleep "$IMG" infinity > /dev/null

컨테이너는 --network none이고, 훅과 MCP 서버가 저장소를 절대 경로로 찾아서 runs/를 Mac과 같은 경로로 마운트해요.

get
up

index는 기준 조건의 runs/a/rich와 같은 길이의 경로 runs/b/rich에 저장소를 복사해 컨테이너 안에서 gitnexus analyze를 옵션 없이 실행하고, 인덱스와 연결 설정을 runs/index.tar로 묶어 매번 똑같이 되살려요. 출력 앞부분은 이렇습니다(발췌).

index
  Repository indexed successfully (21.0s)
  4,090 nodes | 10,819 edges | 192 clusters | 326 flows
112M

인덱스는 21초, 112MB였고 네트워크가 막힌 컨테이너에서 만들어졌으니 코드가 밖으로 나가지 않아요. .gitnexus/는 실제로 쓸 때처럼 작업 트리 안에 있어서, Grep 도구는 건너뛰지만 Bash grep -r는 들어갈 수 있어요(이번 24번에는 그런 호출이 없었어요).

analyze가 쓴 CLAUDE.md는 3,556바이트이고, 호출·의존 관계를 읽기만 하는 질문에도 "Graph first; text search only for empty/UNKNOWN/literals."라고 그래프 도구를 먼저 쓰라고 지시해요. 이 문서는 고치지 않고 그대로 썼어요.

index가 부르는 connect는 GitNexus를 Claude Code에 잇는 파일 세 개를 써요.

  • runs/bin/gitnexus: GitNexus 조건에서만 PATH 앞에 두는 CLI예요. CLAUDE.md가 권하는 node .gitnexus/run.cjs도 이걸 씁니다.
  • runs/b/rich/.claude/settings.json: setup이 쓰는 훅 두 개를 matcher·timeout 그대로 옮기고, 명령만 docker exec -i gitnexus-lab node /app/gitnexus/hooks/claude/gitnexus-hook.cjs로 바꿨어요.
  • runs/mcp-b.json: docker exec -i -w <저장소> gitnexus-lab gitnexus mcp를 서버 이름 gitnexus로 등록해요. 기준 조건은 빈 설정 runs/mcp-a.json을 씁니다.

ask는 질문 하나를 claude -p로 보내요(발췌).

  fresh "$c"
  if [ "$c" = b ]; then path="$R/bin:$PATH"; allow="$allow mcp__gitnexus"; fi
  (cd "$R/$c/rich" && PATH="$path" perl -e 'alarm shift; exec @ARGV' 900 \
    claude -p "$prompt" --tools "$TOOLS" --allowedTools "$allow" \
      --model opus --effort medium \
      --setting-sources project --strict-mcp-config --mcp-config "$R/mcp-$c.json" \
      --no-session-persistence --include-hook-events --output-format stream-json --verbose \
      < /dev/null) > "$out" 2> "${out%.jsonl}.err"

fresh는 호출마다 저장소를 새로 복사하고 GitNexus 조건이면 index.tar를 풀어요. 두 조건의 도구 목록은 같고, GitNexus 조건에만 PATH의 CLI와 MCP 도구 허용(mcp__gitnexus)이 더해져요. ToolSearch가 있으면 Claude Code 기본 동작처럼 MCP 도구는 처음에 이름만 실리고, 고른 도구의 설명만 그다음 요청부터 실립니다.

실습 1: 영향 범위 질문 하나를 두 조건으로

GitNexus가 내세우는 impact(바꾸면 어디까지 영향받는지)에 맞는 질문으로 볼게요. 정답은 9개 모듈의 12항목이에요(안쪽 함수 하나는 바깥 함수와 함께 셈).

jq -r .q2.question answers.json
rich 저장소에서 `rich/_loop.py`의 `loop_last` 시그니처나 반환 형태가 바뀌면 직접 영향받는 모듈과 함수를 모두 찾아 줘. tests와 docs는 제외. 코드는 수정하지 마.

run은 질문·조건을 섞어 정해 둔 순서대로 26번 호출하고, 앞의 두 번은 조건별 워밍업이라 집계에서 빼요. 결과는 out/<순번>-<조건>-<질문>.jsonl에 남고, 조건은 a가 기준, b가 GitNexus예요.

run

6번째(기준)와 5번째(GitNexus) 호출이 부른 도구예요.

calls out/06-a-q2.jsonl
calls out/05-b-q2.jsonl
Grep {"pattern":"loop_last|_loop\\b|from \\._loop|from \\.\\._loop","path":"rich","output_mode":"content"}
Read {"file_path":"a/rich/rich/_loop.py"}
Bash {"command":"python3 -I - <<'EOF'\nimport ast,glob\nfor f in sorted(glob.glob(\"rich/*.py\")):\n    t=ast.parse(open
Grep {"pattern":"last, node|next\\(|levels|stack\\.","path":"rich/tree.py","output_mode":"content"}
ToolSearch {"query":"select:mcp__gitnexus__impact,mcp__gitnexus__context","max_results":2}
mcp__gitnexus__impact {"target":"loop_last","file_path":"rich/_loop.py","direction":"upstream","repo":"rich","maxDepth":
Grep {"pattern":"loop_last","path":"rich","output_mode":"content"}
Bash {"command":"cd rich && sed -n 105,115p tree.py; echo ---; sed -n 165,180p tree.py; echo ---; grep -n \"def \\|^clas

기준 조건은 Grep으로 호출 위치를 찾고, Mac의 python3로 소스를 파싱해 호출을 감싼 함수 이름을 뽑았어요. GitNexus 조건은 ToolSearch로 impact와 context 설명을 불러온 뒤 impact와 Grep을 한 요청에서 함께 불렀습니다. 둘 다 정답이었어요.

GitNexus 쪽 훅 응답이에요.

hooks out/05-b-q2.jsonl
PreToolUse:Grep {"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"[GitNexus] Local augment is unavailable (the gr
PreToolUse:Bash 
PostToolUse:Bash 

훅은 그래프 문맥 대신 "DB를 다른 GitNexus 프로세스가 잡고 있으니 MCP query 도구를 쓰라"는 안내 한 줄만 붙였습니다.

그래프 DB는 한 프로세스만 쓸 수 있어서, 훅은 MCP 서버가 DB를 열고 있으면 조회를 건너뛰고 이 안내를 저장소마다 10분에 한 번 보내도록 만들어져 있어요. 소스를 보면 Mac에 직접 설치해도 같은 판정을 합니다.

두 호출의 결과 JSON이에요(발췌).

excerpt out/06-a-q2.jsonl
excerpt out/05-b-q2.jsonl
{
  "usage": {
    "input_tokens": 8,
    "cache_creation_input_tokens": 3277,
    "cache_read_input_tokens": 45649,
    "output_tokens": 2508
  },
  "total_cost_usd": 0.0855378,
  "num_turns": 5,
  "duration_ms": 26423
}
{
  "usage": {
    "input_tokens": 8,
    "cache_creation_input_tokens": 7139,
    "cache_read_input_tokens": 91304,
    "output_tokens": 2405
  },
  "total_cost_usd": 0.1235048,
  "num_turns": 5,
  "duration_ms": 30790
}

num_turns는 둘 다 5인데, 이 측정에서는 늘 도구 호출 수에 1을 더한 값이었어요. 입력이 다시 실리는 단위는 API 요청이고, 한 요청에서 도구 여러 개를 함께 부르면 요청은 하나라 둘은 달라요.

이 글에서 "입력"은 usage의 세 필드(input_tokens, cache_creation_input_tokens, cache_read_input_tokens) 합이에요. 캐시에서 읽은 토큰도 요청마다 모델이 다시 받는 입력이라 넣었고, 기준 48,934, GitNexus 98,451이었어요. 요청마다 나눠 보면 이렇습니다.

steps out/06-a-q2.jsonl
steps out/05-b-q2.jsonl
10288 -> 12119 -> 12962 -> 13565
13322 -> 24210 -> 29570 -> 31349

요청은 둘 다 4번이었고, 첫 요청부터 13,322 대 10,288로 3,034토큰 더 실렸어요. 이 조건에만 실리는 것은 CLAUDE.md, GitNexus 스킬 6개의 설명, 설명을 아직 싣지 않은 MCP 도구 17개의 이름, git 상태에 보이는 미추적 파일 등이에요.

두 번째 요청에서는 차이가 12,091토큰으로 커졌어요. ToolSearch가 불러온 impact·context 설명(MCP 서버가 내놓는 설명과 입력 형식을 합쳐 20,881자와 7,527자)이 이때 10,888토큰으로 들어가 그 뒤 요청마다 다시 실렸고, 위험도·영향받는 실행 흐름까지 담은 impact 결과(10,742자)는 세 번째 요청부터 더해졌어요.

실습 2: 질문 4개를 세 번씩

나머지 질문은 이래요. 모두 tests와 docs를 빼고 묻고, 끝에 "코드는 수정하지 마."가 붙어요.

  • q1: _render_buffer가 정의된 곳(1곳)과 직접 호출하는 함수(4개)
  • q3: rich.print("[bold]hi[/bold]")에서 rich/markup.py의 _parse까지 호출 경로(6단계)
  • q4: Highlighter를 직접·간접 상속하는 클래스(9개)와 highlight 오버라이드 여부

summarize.py는 정답 목록의 항목이 모두 답에 있으면 정답으로 쳐요. 기본 규칙은 함수 이름이 있는 줄이나 그 위 40줄, 아래 3줄 안에 파일 이름이 있어야 하고, 느슨한 규칙은 답 어디에든 있으면 됩니다.

py312 summarize.py
q1 기준: 정답 3/3, MCP 쓴 회차 0, 입력 56,662, 요청 5번, num_turns 6, Read 2
q1 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 82,304, 요청 5번, num_turns 5, Read 0
q2 기준: 정답 3/3, MCP 쓴 회차 0, 입력 48,934, 요청 4번, num_turns 5, Read 1
q2 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 98,451, 요청 4번, num_turns 5, Read 0
q3 기준: 정답 3/3, MCP 쓴 회차 0, 입력 50,793, 요청 4번, num_turns 7, Read 4
q3 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 109,976, 요청 6번, num_turns 6, Read 0
q4 기준: 정답 2/3, MCP 쓴 회차 0, 입력 57,158, 요청 5번, num_turns 5, Read 0
q4 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 71,477, 요청 4번, num_turns 5, Read 0
전체 기준: 12회, 정답 11/12 (느슨한 규칙 11/12), MCP를 쓴 회차 0, CLI를 쓴 회차 0
  중앙값: 입력 50,988 (캐시 쓰기 4,364, 캐시 읽기 45,649), 출력 1,887, 요청 4번, num_turns 5, 23.4초, $0.0839
  도구 호출 중앙값: 전체 4, Bash 2, Grep 1, Glob 0, Read 1, 읽은 파일 1개, GitNexus MCP 0
전체 GitNexus: 12회, 정답 12/12 (느슨한 규칙 12/12), MCP를 쓴 회차 12, CLI를 쓴 회차 0
  중앙값: 입력 94,520 (캐시 쓰기 7,252, 캐시 읽기 88,677), 출력 1,896, 요청 5번, num_turns 5, 25.7초, $0.1213
  도구 호출 중앙값: 전체 4, Bash 0.5, Grep 1, Glob 0, Read 0, 읽은 파일 0개, GitNexus MCP 1

GitNexus 조건은 12번 모두 MCP 도구를 썼어요(q1 context, q2 impact, q3 trace, q4 cypher). CLI와 스킬은 한 번도 쓰지 않았고, 입력 중앙값은 네 질문 모두 GitNexus 쪽이 컸어요.

조회 여러 번 대신 한 번이면 된다는 README의 주장은 GitNexus MCP 호출 중앙값 1로는 맞았어요. 하지만 Claude가 그 결과를 Grep·Bash로 다시 확인해 도구 호출 중앙값은 4 대 4, 요청 수 중앙값은 4 대 5였어요. Read로 연 파일은 1개에서 0개로 줄었지만 두 조건 모두 Bash sed -n으로도 코드를 봤어요.

자동 판정에서 틀린 한 번은 기준 q4로, examples/log.py의 RequestHighlighter를 빠뜨렸어요. 답을 직접 읽을 때는 표의 호출 관계가 맞는지로 셌어요. 기준 q1 한 번(22번째)은 표에 호출 세 곳을 _write_buffer가 아닌 _check_buffer 안이라고 적어 오답으로 셌고, 그래서 기준은 10번이에요.

GitNexus q3 세 번은 trace가 rich.print에서 _parse까지 잇지 못해 소스를 읽어 경로를 이었는데, 요약 경로에 get_console을 한 단계로 넣었어요. 표에는 get_console이 돌려준 Console의 print를 호출한다고 맞게 적어서 정답으로 셌지만, 이것까지 오답으로 보면 10 대 9로 뒤집혀요.

도구 설명의 무게는 같은 q1에서 context만 불러온 14번째 호출과 context·impact를 함께 불러온 1번째 호출을 견주면 보여요.

steps out/14-b-q1.jsonl
steps out/01-b-q1.jsonl
13300 -> 16224 -> 17597
13301 -> 24189 -> 25060 -> 25581 -> 26006

두 호출 모두 실제로 부른 GitNexus 도구는 context 한 번이었어요. 쓰지 않은 impact 설명까지 불러온 쪽은 두 번째 요청이 7,965토큰 더 컸고, 요청도 5번 대 3번이라 입력 합계가 47,121 대 114,137로 갈렸어요.

정리

범위: Claude Code 2.1.309의 claude -p, Opus 5.5(claude-opus-5-5), effort medium, GitNexus 1.6.12 공식 Docker 이미지와 기본 인덱스, 도구 Bash,Read,Grep,Glob,ToolSearch,Skill, rich v15.0.0 질문 4개를 조건마다 3번씩 모두 24번.

  • 자동 판정 정답은 GitNexus 12번, 기준 11번이었고, 직접 읽으면 판정 기준에 따라 10 대 12에서 10 대 9까지 달라졌어요. "코드를 놓치지 않는다"는 차이는 3번씩으로는 가릴 수 없을 만큼 작았습니다.
  • GitNexus MCP는 12번 모두 쓰였고 Read로 연 파일은 줄었지만, 도구 호출 수는 같았고 입력 토큰 중앙값은 50,988에서 94,520으로 늘었어요. 읽은 파일 수보다 요청마다 다시 실리는 입력을 보세요.
  • 첫 요청에 3천 토큰 남짓이 더 실리고, ToolSearch로 불러온 설명은 그 뒤 요청마다 다시 실려요. impact 설명 하나가 요청마다 약 8천 토큰이니(q1 두 호출 비교) 요청이 많은 작업일수록 커집니다.
  • MCP 서버가 그래프 DB를 열고 있으면 훅은 그래프 문맥 대신 안내 한 줄만 보냈어요(12번 모두). 훅에 기대한다면 --include-hook-events로 실제로 붙는 내용을 확인하세요.
  • 인덱스는 21초, 112MB였고 코드가 밖으로 나가지 않았어요. trace는 객체 타입을 모르는 호출(write_console.print)을 잇지 못했으니, 호출 경로 답은 소스로 확인하세요.
내 링크 만들기