Engineering · Knowledge Graph

XGEN MAKER: 코드 지식그래프 착지 정확도를 사람이 머지한 MR 2,827건으로 잰 개발 자동화 하네스

다중 저장소 플랫폼의 코드를 결정론적 정적 분석으로 지식그래프화하고, 자연어 요청을 파일·라인 단위로 착지시켜 구현·검증·MR 준비까지 수행하는 하네스입니다. 착지 품질은 사람이 실제로 머지한 MR 2,827건으로 측정했고, 표본을 넓히는 과정에서 작은 표본에 과적합된 순위 상수를 찾아 걷어냈습니다.

기간
2026.07 – 2026.08
소속
개인 프로젝트
역할
기획·설계·구현

Key Results

0.837
착지 R@10사람이 머지한 MR 2,827건, MR 제목을 질의로·변경 파일을 정답으로 사용, 출시 구성
0.385 → 0.459
착지 R@1같은 2,827건, 어휘 검색 단독 대비 의미 층·파일 중복 제거 적용
0 누락 · 0 초과
Python 정의 추출 정확도AST 기준 정의 2,628개 표본 대조

코딩 에이전트는 코드를 잘 쓰지만 고칠 위치를 잘 찾지 못합니다. 20여 개 저장소로 나뉜 플랫폼에서 "로그인 리다이렉트를 고쳐 달라"는 요청은 수천 개 파일 중 하나를 찾고, 그 파일에 의존하는 호출자에 회귀를 일으키지 않고, 팀의 브랜치·리뷰 규칙을 지키는 일입니다. 에이전트는 이 중 어느 것도 보지 못한 채 빈 컨텍스트에서 경로를 추측합니다.

XGEN MAKER는 에이전트 앞에 코드 지식그래프를, 뒤에 수렴 루프를 두는 개인 프로젝트입니다. 요청을 그래프에서 저장소:경로:줄 단위로 착지시키고, 에이전트가 구현하고, 테스트·샌드박스·LLM judge를 통과할 때까지 반복한 뒤 MR 준비에서 멈춥니다. 머지와 배포는 사람이 합니다.

이 글은 세 가지 판단을 다룹니다. 그래프 빌더로 LLM 기반 오픈소스 대신 결정론 추출기를 고른 이유, "전부 연결됐다"는 첫 결과를 의심해 크로스레포 매칭을 다시 설계한 과정, 그리고 검색 품질을 주장하는 대신 사람이 머지한 MR로 재기 시작하자 드러난 과적합입니다.

구조 지식이 사람 머릿속에만 있던 플랫폼

대상은 XGEN 2.0을 이루는 23개 저장소였습니다. 기획 단계에서 정리한 문제는 네 가지였습니다. 저장소를 가로지르는 구조·의존성·UI 흐름이 사람 머릿속에만 있는 지식의 비대칭, 분석부터 브랜치·구현·검증·MR까지 매번 수작업인 파이프라인, 변경이 UI에 미칠 영향을 머지 전에 확인하기 어려운 비가시성, 그리고 코드베이스를 이해한 결과가 자산으로 쌓이지 않고 휘발되는 문제입니다.

해법은 코드베이스 이해를 지식그래프라는 자산으로 물화하고, 그 위에서 하네스 루프가 개발 파이프라인을 실행하는 것이었습니다. 한 번의 실행은 아래 순서로 흐릅니다.

요청: "온톨로지 그래프가 재빌드 후 갱신되지 않는다, 고쳐 달라"
  ├─ 의도 분류          bug / feature / refactor / question
  ├─ 코드 어휘로 확장    한국어 요청 → 이 코드베이스가 쓰는 단어
  ├─ 그래프에 착지       어휘 + 의미 검색 융합 → 저장소:경로:줄
  ├─ 연결된 파일 확장    이 변경의 영향을 받는 곳
  ├─ 최신화 후 브랜치    네이밍 규칙 + 보호 브랜치 가드
  ├─ 에이전트 구현       그래프 컨텍스트 + 실제 소스 발췌
  ├─ 검증                구문 · 테스트 · 샌드박스 격리 · 회귀
  ├─ judge               품질 게이트, 실패 사유를 되먹여 재시도
  └─ MR 준비             ← 의도적으로 여기서 멈춤

자동화 범위는 코드로 고정했습니다. 기본 모드(plan)는 저장소를 건드리지 않고, observe는 로컬 브랜치와 커밋까지, act만 push와 MR 생성을 합니다. 알 수 없는 모드 값은 쓰기 권한으로 승격하지 않고 거부합니다. 보호 브랜치에는 생성·커밋·push가 불가능하고, Dockerfile·CI 기술자·차트 같은 인프라 파일 변경은 거부됩니다. 초기에는 배포 트리거를 dry-run과 이중 인터록으로 설계했지만, 빌드와 배포는 사람이 하고 MAKER는 MR 준비까지만 한다는 경계를 정한 뒤 파이프라인에서 배포 트리거를 제거하고 배포 상태는 읽기 전용으로만 관측하게 했습니다.

그래프 빌더: LLM 파이프라인 대신 결정론 추출기

기획서 초안에서는 오픈소스 Understand-Anything(LLM 기반 코드 그래프 생성기)을 1차 빌더로 쓰고 크로스레포 연결만 직접 확장할 계획이었습니다. 판단을 실측으로 내리기 위해 같은 슬라이스(백엔드 저장소 컨트롤러 13개 파일의 사본)에서 둘을 비교했습니다.

항목 Understand-Anything 결정론 추출기
빌드 시간 약 19분 1초 미만
노드 / 엣지 85 / 132 146 / 226
엔드포인트 별도 종류 없음(45개가 일반 함수) 메서드·경로를 가진 노드 45개
함수 노드 66(비공개·소형 함수 누락) 81
설명문 문서화되지 않은 코드도 동작을 설명 docstring 원문 보존, 없으면 템플릿 요약

Understand-Anything이 분명히 나은 부분도 있었습니다. docstring이 없는 함수에서 결정론 요약은 "함수 x, y, z" 수준의 템플릿에 그쳤지만, Understand-Anything은 실제 동작을 설명하는 문장을 만들었습니다. 그러나 엔드포인트가 독립 노드로 나오지 않으면 프론트엔드의 API 호출과 백엔드 엔드포인트를 잇는 크로스레포 매칭의 입력이 없습니다. 비용도 맞지 않았습니다. 13개 파일에 19분이면, Python 파일 497개인 워크플로우 저장소 하나만 단순 외삽해도 12시간급입니다. 23개 저장소를 코드가 바뀔 때마다 다시 빌드하는 용도에는 구조적으로 부적합했습니다.

선택지 장점 단점 판단
Understand-Anything을 상시 빌더로 사용 미문서 코드도 설명하는 문장 품질 엔드포인트 종류 없음, 빌드 시간과 토큰 비용, 비결정 기각
결정론 추출기 단독 정밀한 구조, 1초 미만, 비용 0, 증분 갱신 가능 미문서 코드의 설명이 빈약 뼈대로 채택
결정론 뼈대 + 선택적 LLM 의미 층 구조 정밀도와 설명 품질을 분리해 확보 의미 층은 엔드포인트 가용성에 의존 최종 채택

그래서 2층 구조로 정했습니다. 뼈대(파일·클래스·함수·엔드포인트·라우트 노드와 imports·calls·크로스레포 resolves_to 엣지)는 AST와 정규식 기반 결정론 추출기가 만들고, 설명문은 docstring을 우선 쓰는 결정론 요약을 전 노드에 넣은 뒤 LLM 요약을 선택적으로 덧입히는 의미 층으로 분리했습니다. Understand-Anything은 일회성 온보딩 용도로만 남겼습니다.

결정론 추출기의 또 다른 이점은 증분 갱신이었습니다. 빌드할 때 저장소별 git HEAD와 소스 범위를 그래프 메타데이터에 기록해 두고, 동기화 시에는 기록된 HEAD부터 현재 HEAD까지의 diff와 워킹트리의 미커밋 변경만 다시 추출합니다. 삭제와 이름 변경도 처리하고, rebase로 기준점이 사라지면 조용히 틀린 그래프를 내는 대신 전체 재빌드가 필요하다고 보고합니다. 증분 동기화 결과가 반복 실행 후에도 전체 재빌드와 노드·엣지 단위로 같다는 것은 테스트로 고정했습니다.

첫 빌드는 백엔드 저장소 2개 전체와 프론트엔드 일부에서 노드 11,113개, 엣지 15,146개(엔드포인트 848, UI 라우트 41, 프론트엔드 API 호출 137)였습니다. pnpm 워크스페이스 이름과 tsconfig 경로 alias를 해석하자 프론트엔드 앱의 imports가 6개에서 148개로 늘었고, feature 패키지 172개를 포함한 재빌드는 노드 13,943개, 엣지 22,367개가 됐습니다.

"137건 전부 연결"을 의심한 이유

크로스레포 연결은 프론트엔드가 호출하는 API URL을 백엔드 엔드포인트에 잇는 작업입니다. 초판 매칭은 137건 중 137건, 100%가 연결됐다고 나왔습니다. 동적 URL이 섞여 있고 아직 스캔하지 않은 저장소도 있는 상황에서 100%는 오히려 의심해야 할 숫자였습니다. 결과를 열어 보니 경로 파라미터 같은 와일드카드 세그먼트끼리 서로 교차 매칭되면서 실제로는 다른 엔드포인트가 짝지어진 오탐이었습니다.

매칭 규칙을 리터럴 앵커링으로 다시 설계했습니다. 리터럴 세그먼트를 최소 1개 공유해야 하고, 와일드카드와 리터럴의 교차 매칭은 금지하며, 후보가 여럿이면 (리터럴 수, 길이)가 가장 큰 것을 고릅니다. 연결은 60건으로 줄었고 표본 12건은 모두 정상이었습니다. 연결되지 않은 77건은 대부분 스캔하지 않은 저장소의 엔드포인트와 동적 URL이었고, 문서 서비스와 MCP 서비스 저장소를 스캔 대상에 추가하자 7개 소스에서 노드 15,802개, 엣지 24,918개가 됐고 크로스레포 연결은 63건에서 107건으로 늘었습니다.

이 경험 이후 그래프 품질은 가정하지 않고 저장소 자체와 대조해 감사했습니다. BOM이 있는 파일이 그래프에서 통째로 빠지고 동기화 후 끊긴 엣지 45개가 남던 결함을 고치면서, 동기화 무결성을 스스로 복구하는 기능도 더했습니다(끊긴 엣지 45개에서 0개).

점검 결과
디스크 파일 중 그래프에 있는 비율 99.5%
Python 정의 vs AST 정답 표본 2,628개, 누락 0 · 초과 0
imports 엣지에 실제 import가 있는 비율 표본 300건, 100%
calls 엣지에 실제 호출이 있는 비율 99.2%, 이후 주석·문자열은 스캔하지 않도록 수정

착지에서 MR까지

착지는 세 층을 융합합니다. 어휘 층은 그래프 노드의 이름·경로·docstring·참조 식별자·설정 키와 값·UI에 노출되는 문구를 BM25로 검색합니다. 의미 층은 요청과 코드가 단어를 공유하지 않는 경우를 위한 밀집 벡터 검색입니다. 앵커 층은 요청이 직접 지목한 경로나 심볼입니다. 세 순위를 상호 순위 융합(RRF)으로 합치고, 착지점과 그래프 1홉으로 연결된 파일까지 함께 넘겨 에이전트가 자기 변경의 영향 범위를 알게 합니다. 초기 착지 순위는 손으로 정한 상수와 임계값에 의존했는데, 이를 BM25 기반으로 다시 썼습니다.

의미 층은 선택 사항이고 기본값은 꺼짐입니다. 임베딩 엔드포인트가 없으면 어휘 검색으로 내려가지만 조용히 내려가지는 않습니다. "설정되지 않음"과 "설정됐지만 도달 불가"를 다른 상태로 구분해 실행 로그와 자가 진단에 표시합니다. 말없이 사라질 수 있는 층은 나쁜 답을 받고 나서야 부재를 알게 되기 때문입니다.

구현 이후는 수렴 루프입니다. 처음 MAKER는 각 단계를 한 번씩 실행하고 실패하면 멈췄습니다. XGEN 하네스 엔진(PyPI 패키지)의 파이프라인을 통째로 쓰는 방안도 검토했지만, 엔진의 루프는 LLM 호출 자체가 에이전트인 구조이고 MAKER의 에이전트는 실제 파일을 고치는 코딩 에이전트 CLI라 맞지 않았습니다. 그래서 엔진의 수렴 계약(continue, retry, stop)과 샌드박스 실행만 선택적으로 가져왔습니다. 구현, 샌드박스 격리 검증과 테스트, judge, 결정을 반복하고, retry면 실패 사유를 다음 프롬프트에 실어 다시 구현합니다. 의존성 미설치나 테스트 수집 실패 같은 환경 문제는 실패가 아니라 사유를 붙인 skipped로 분류합니다. 스텁 에이전트가 1회차에 구문 오류를 내면 샌드박스가 실패를 잡고 2회차에 스스로 고쳐 수렴하는 경로를 테스트로 고정했습니다.

실제 버그 한 건으로 전체 경로를 완주했습니다. 온톨로지 재빌드 후 그래프 뷰가 갱신되지 않던 알려진 버그였습니다. 루프는 그래프 뷰 컴포넌트에 착지했고(점수 127.5), 에이전트는 컴포넌트를 key 교체로 다시 마운트하던 방식을 reload 토큰 기반의 제자리 재조회로 바꿔 카메라·테마·대화 상태를 보존했습니다. 휴리스틱 judge 0.9(임계 0.7)를 통과해 2개 파일(+100/-11) 커밋과 MR 초안이 만들어졌고, 이어 act 모드로 실제 MR을 생성했으며 머지는 사람이 했습니다.

검색 품질을 주장하지 않고 재기로 하다

착지가 맞았는지는 사람이 이미 머지한 MR로 쟀습니다. MR 제목을 요청으로 쓰고, 그 MR이 바꾼 파일을 정답으로 삼습니다. MAKER가 팀이 실제로 작업한 곳에 착지하면 검색이 맞은 것입니다.

수집한 MR은 4,155건이었습니다. 이 중 그래프가 색인하지 않는 파일만 바꾼 667건, 같은 제목에 정답이 다른 343건, 릴리즈·브랜치 병합 217건, 요청이라 부르기엔 제목이 짧은 101건을 제외해 2,827건을 확정했습니다. 제외 기준은 검색기가 아니라 데이터의 성질로 정의했고, 정의가 흔들리지 않도록 각 규칙마다 회귀 테스트를 두었습니다.

지표 어휘 검색만 • 의미 층 출시 구성
R@1 (착지가 정확히 맞음) 0.385 0.459 0.459
R@10 (에이전트 근거 목록에 정답 포함) 0.782 0.819 0.837
MRR 0.516 0.577 0.587

세 열은 같은 2,827건에서 잰 값이라 서로 비교할 수 있습니다. 마지막 열의 차이는 순위 조정이 아니었습니다. 파일 노드와 그 안의 함수 노드는 서로 다른 노드라 상위 목록에 나란히 들어왔고, 상위 10개 중 평균 2.1개가 이미 나온 파일의 중복이었습니다. 에이전트는 그만큼의 후보를 보지 못하고 있었습니다. 파일마다 가장 높은 항목 하나만 남기자(함수가 파일보다 높으면 줄 번호와 함께 함수가 남습니다) 1,204건 대응 비교에서 놓치던 정답 24건을 찾았고 잃은 것은 없었습니다(McNemar p < 10⁻⁶). 목록 맨 위에는 원래 제거할 중복이 없었으므로 R@1은 그대로입니다.

표본을 넓히자 드러난 과적합

처음 벤치마크는 머지된 MR 294건이었습니다. 이 표본으로 결함을 하나씩 고치며 커밋마다 R@10을 0.796에서 0.906까지 끌어올렸습니다. 그런데 표본을 4,155건으로 넓히자 순위 상수 세 개의 최적점이 옮겨 갔습니다. 294건 표본은 정답이 작은 저장소에 몰려 있었습니다. 그래프 노드의 1.0%와 3.6%를 차지하는 저장소에 정답의 14%와 13%가 있었습니다. 상수들은 그 치우침에 맞춰져 있었고, 그중 저장소 크기 보정 상수는 대표성 있는 표본에서는 보정할 치우침 자체가 없어 통째로 제거했습니다. 294건 시절의 0.9대 수치와 최종 0.837은 측정 기준이 달라 비교할 수 없는 값입니다.

전체 표본으로 매번 튜닝하면 느리기 때문에, 저장소와 월 분포를 보존한 600건 층화 표본을 만들어 4배 빠르게 반복했습니다. 다만 그 표본은 후보 값들의 순위를 전체 표본과 같은 순서로 매길 때만 신뢰했고, 그 일치 여부 자체를 확인했습니다.

잴 수 있는 이득이라도 모두 취하지는 않았습니다. 테스트 파일 감점을 풀면 R@1이 9포인트 올랐습니다. 그러나 벤치마크는 MR이 바꾼 파일 중 아무것이나 맞히면 정답으로 치고, 테스트 파일은 찾기 쉽습니다. 구현 파일만 채점하면 같은 변경이 오히려 나빴습니다. 감점은 그대로 두고 그 함정을 상수 옆에 적었습니다. 심볼이 소속 파일의 UI 문구를 물려받게 하는 변경도 같은 이유로 기각했습니다. 심볼 착지가 33%에서 43%로 올라 설계가 통한 것처럼 보였지만 R@10이 떨어졌고, 나머지 변화는 표본 표준오차 안이었습니다. 그 설정은 누군가 근거 없이 다시 켜지 않도록 관련 코드와 함께 제거했습니다. 살아남은 상수 옆에는 그 값을 정당화한 측정을 적었고, 일부 테스트는 상수가 그 측정 기록을 여전히 지니고 있는지를 검사합니다. 이후 수정하는 사람은 값을 다시 추측하는 대신 다시 측정해야 합니다.

결과와 남은 과제

지표 이전 이후 측정 조건
착지 R@1 0.385 0.459 머지된 MR 2,827건, 어휘 검색만 대비 출시 구성
착지 R@10 0.782 0.837 같은 2,827건
MRR 0.516 0.587 같은 2,827건
파일 중복 제거 - 정답 +24, 손실 0 1,204건 대응 비교, McNemar p < 10⁻⁶
크로스레포 연결 137건(오탐 포함) 60건, 표본 12건 전부 정상 리터럴 앵커링 규칙 도입 전후
그래프 빌드 시간 약 19분 1초 미만 같은 13개 파일 슬라이스, Understand-Anything 대비 결정론 추출기

그래프는 오늘의 코드로 만들기 때문에, 오래된 MR일수록 그 MR이 가리키던 자리가 이미 옮겨 갔고 점수도 그 거리를 따라 낮아집니다. 최근 약 3개월에 머지된 MR만 보면 R@10은 0.886입니다. MR이 바꾼 파일을 전부 덮는 비율은 47.3%(평균 66.2%)로, 여러 파일을 함께 바꾸는 MR의 커버리지는 아직 과제입니다. 의미 층은 설정한 임베딩 엔드포인트에만 코드를 보내고, 설정하지 않으면 어휘 검색만으로 동작합니다.

이 프로젝트에서 가장 크게 바뀐 것은 측정을 대하는 태도였습니다. 100% 연결, 0.9대 R@10처럼 좋아 보이는 숫자는 대개 측정 대상이 아니라 측정 장치의 성질을 보여 주고 있었습니다. 순위 상수마다 근거 측정을 옆에 적고, 표본의 대표성을 먼저 확인한 뒤에 수치를 믿는 방식으로 정리했습니다. 테스트는 586개이고, 대부분 실제로 일어난 버그에 대한 회귀 테스트입니다.