에이전트에게 코드베이스의 지도 제공하기 (에이전트 자신의 발자취를 일부 활용하여)
요약
코딩 에이전트가 새로운 저장소에 적응할 때 발생하는 비용을 줄이기 위해 '코드 맵(code map)'을 제안합니다. 구조적, 시간적, 행동적 신호를 결합하여 에이전트가 코드베이스의 맥락을 효율적으로 파악하도록 돕는 내비게이션 계층을 구축하는 방법론을 다룹니다.
핵심 포인트
- 에이전트의 세션별 컨텍스트 소실 문제를 해결하기 위한 코드 맵 제안
- tree-sitter를 활용한 정적 구조(Structure) 분석
- git 히스토리를 통한 시간적(Temporal) 변화 및 소유권 파악
- 에이전트의 실제 도구 호출 로그를 활용한 행동적(Behavioral) 신호 추출
- 정적 분석과 행동적 신호를 결합하여 실질적인 코드 결합도 파악
원문은 olund.dev에 게시되었습니다.
코딩 에이전트가 낯선 저장소(repository)에 착륙하는 것을 지켜보면, 신입 사원이 첫날 하는 행동을 하는 것을 볼 수 있습니다: 이름 검색(grep), 디렉토리 목록 나열, 파일 열기, 그리고 천천히 정신적 모델(mental model)을 구축하는 것입니다. 신입 사원과 달리, 에이전트는 이를 _매 세션(session)_마다 수행합니다. 에이전트가 구축한 모델은 컨텍스트 윈도우(context window)가 닫히면 폐기되며, 다음 세션은 토큰(tokens), 지연 시간(latency), 그리고 불완전한 그림이 만들어내는 잘못된 경로라는 비용을 다시 지불해야 합니다.
저의 해결책은 프로젝트별 **코드 맵(code map)**입니다. 이는 각 저장소가 보유하며 점진적으로 재구축되는 쿼리 가능한 내비게이션 계층으로, 새로운 세션이 단 하나의 파일을 읽기 전에 그 요약본을 전달받게 됩니다. 흥미로운 점은 이 맵이 존재한다는 사실 자체가 아니라(정적 코드 인텔리전스(static code intelligence)는 이미 성숙한 분야입니다), 이 맵이 어떤 세 가지 신호(signals)를 융합하느냐 하는 것입니다. 왜냐하면 세 번째 신호는 대부분의 도구가 전혀 볼 수 없는 것이기 때문입니다.
세 가지 신호, 하나의 지도
**구조(Structure)**는 전통적인 축입니다: 저장소가 혼합하여 사용하는 어떤 언어에 대해서도 tree-sitter를 통해 파싱된 심볼(symbols), 호출(calls), 그리고 임포트(imports)입니다. 이것은 의도적으로 얕은 수준의 정적 분석(static analysis) 버전입니다. 전용 도구들은 제가 하는 것보다 훨씬 더 깊은 타입 해석(type resolution)을 수행하며, 그들과 정면으로 경쟁하는 것은 제 시간을 낭비하는 일일 것입니다. 구문적 엣지(Syntactic edges)는 내비게이션에 충분히 유용하며, 엣지 테이블(edge table)은 각 엣지가 어디에서 왔는지 기록하므로, 더 깊은 언어별 분석기가 나중에 재설계 없이도 해석된 엣지를 추가할 수 있습니다.
시간적(Temporal) 신호는 git에서 옵니다: 변경량(churn), 소유권(ownership), 그리고 공동 변경(co-change) — 즉, 역사적으로 동일한 커밋(commits)에서 함께 변경되는 파일들입니다. 이는 일반적인 git log 출력에서 파싱되며, 라이브러리 의존성이 없고, 저장소의 전체 히스토리가 별다른 할 일 없이 그곳에 놓여 있습니다.
행동적 (Behavioral) 특성이 차별화 요소입니다. 저의 메모리 시스템은 에이전트가 수행하는 모든 도구 호출(tool call) — 즉, 프로젝트 및 세션 태그가 지정된 모든 읽기(read), 편집(edit), 쓰기(write) — 을 이미 기록하고 있습니다. 이 로그를 집계(rollup)하면 공동 작업 매트릭스(co-touch matrix)가 생성됩니다. 즉, 에이전트들이 실제 세션 내에서 함께 어떤 파일들을 작업했는지를 보여줍니다. 집계 과정에서 편집(editing)은 읽기(reading)보다 더 높은 가중치를 가집니다. 왜냐하면 두 파일을 함께 변경하는 것이 단순히 두 파일을 살펴보는 것보다 결합(coupling)에 대한 더 강력한 증거이기 때문입니다.
축(axes) 사이의 구분은 실무에서 매우 중요합니다. 호출 그래프(call graph)는 A가 B에 의존한다는 사실을 알려주지만, 이는 사실이면서도 때로는 무용지물일 수 있습니다. 코드베이스의 절반이 B에 의존할 수도 있기 때문입니다. 반면 행동적 신호(behavioral signal)는 지난 30번의 세션 동안 A가 변경될 때마다 B와 C도 함께 변경되었다는 사실을 알려줍니다. 이것들이 바로 A의 사실상 (de facto) 이웃들입니다. 즉, A를 건드릴 때 함께 열어두어야 할 파일 집합입니다. 정적 도구(Static tools)는 작업 과정을 전혀 볼 수 없기 때문에 이를 계산할 수 없습니다. Git의 공동 변경(co-change)은 이를 근사화하지만, 커밋(commit) 단위로 사후에만 가능하며 변경 사항에 대해서만 작동합니다. 다섯 번째 파일을 안전하게 편집하기 위해 네 개의 파일을 읽은 세션은 Git에 아무런 흔적도 남기지 않습니다. 에이전트 자신의 발자취만이 이 신호가 존재하는 유일한 곳입니다.
세션 시작 시에는 저렴하게, 필요할 때는 깊이 있게
지도는 두 가지 방식으로 소비되며, 이 분리는 의도적인 것입니다.
저렴한 경로는 모든 세션이 시작될 때 컨텍스트(context)에 주입되는 작게 렌더링된 요약본(digest)입니다. 여기에는 융합된 그래프(fused graph)에 대한 커뮤니티 탐지(community detection)를 통해 정렬된 서브시스템, 각 서브시스템별 몇 개의 핵심 파일, 그리고 한 줄의 목적이 포함됩니다. 현재 제가 진행 중인 주요 프로젝트의 경우, 913개의 파일이 33개의 커뮤니티로 접혀 있습니다. 한 가지 개선 사항은 매일 제 역할을 다하고 있습니다. 요약본의 섹션들이 브랜치의 현재 작업과 관련된 순서로 재정렬된다는 점인데, 세션 시작 메커니즘은 이전 포스트에서 설명한 포인터 파일로부터 이를 이미 알고 있습니다. 업로드 품질 관리(quality-control)에 관한 브랜치를 여는 세션은 알파벳 순서 목록이 아니라 QC 서브시스템을 가장 먼저 보게 됩니다.
Deep path는 에이전트가 세션 중간에 호출하는 일련의 쿼리 도구(query tools) 세트입니다:
neighborhood (파일의 구조적 및 행동적 이웃), hotspots (변경 빈도(churn) 가중치가 적용된 리스크), 그리고 why - 이것은 제가 가장 좋아하는 도구인데, 이전 포스트에서 다룬 문서 표준과 지도를 결합하기 때문입니다. 특정 파일이 왜 존재하는지 물으면, 그 파일의 호출자(callers)와 함께 해당 파일을 인용하는 아키텍처 결정 기록(architecture decision records, ADR)이 답변에 포함됩니다. 결정 경로(decision trail)와 호출 그래프(call graph)가 동일한 인덱스에 매달려 있으므로, "이것은 무엇인가"와 "왜 이렇게 되어 있는가"는 단 한 번의 쿼리 차이로 연결됩니다.
두 가지 경로가 존재하는 이유는 각각의 경제성(economics)이 다르기 때문입니다. 지형 파악(lay-of-the-land) 질문은 _주변적(ambient)_인 상태일 때 이득을 봅니다. 즉, 에이전트가 질문할 필요를 느끼기도 전에 쿼리 비용 없이 답변이 준비되어 있는 상태를 말합니다. 그래프 탐색(Graph traversals)은 폭발적인 데이터 증가 없이 산문(prose) 형태로 미리 렌더링될 수 없으므로, 도구의 형태로 유지됩니다. 순수 파일 시스템(pure-filesystem) 설계나 순수 쿼리(pure-query) 설계 모두 이러한 분리된 방식에 비해 효율이 떨어집니다.
데몬(daemon) 없이 최신 상태 유지하기
코드에서 벗어난 내비게이션 레이어는 아예 없는 것보다 못합니다. 오래된 호출 그래프(call graph)를 따라 경로를 잡는 에이전트는 확신을 가지고 잘못된 길로 안내하기 때문입니다. 제가 이제 어디에서나 재사용하는 패턴인 신선도 모델(freshness model)은 다음과 같습니다: 라이프사이클 이벤트(lifecycle events)가 일반적인 케이스를 포착하고, 예정된 스윕(scheduled sweep)이 수렴(convergence)을 보장합니다.
에이전트가 파일을 편집할 때, 편집 후 훅(post-edit hook)이 해당 파일만을 다시 파싱하여 행(rows)을 업데이트합니다. tree-sitter를 사용하면 1초 미만으로 완료되며, 이를 통해 다음에 쿼리될 가능성이 가장 높은 대상, 즉 에이전트가 방금 변경한 코드에 대해 인덱스를 정직하게 유지할 수 있습니다. 하지만 훅은 본질적으로 최선 노력(best-effort) 방식입니다. 세션 외부에서 이루어진 수동 편집, 삭제, 리베이스(rebase) 등을 놓칠 수 있습니다. 따라서 주기적인 전체 재인덱싱(full reindex)이 훅이 놓친 것들을 휩쓸어 담습니다. 어느 한 메커니즘만으로는 완벽하지 않습니다. 스윕이 없는 훅은 드리프트(drift, 편차)를 영원히 누적시키고, 훅이 없는 스윕은 현재 세션의 작업에 대해서만 지도가 틀리게 만듭니다. 이 둘이 결합되면 중요한 부분에서는 빠른 경로를 제공하면서 결과적 일관성(eventual consistency)을 달성합니다.
다이제스트(digest) 자체는 편집할 때마다가 아니라 느린 주기(slow cadence)로 다시 계산됩니다. 즉, 어떤 서브시스템(subsystems)이 존재하는지, 그리고 매주 무엇이 가장 중요한지는 안정적이며, (LLM이 커뮤니티를 명명하는) 비용이 많이 드는 한 단계를 키스트로크(keystroke)마다 수행할 가치는 없기 때문입니다.
의도적으로 구축하지 않은 것들
기능(features)보다 세 가지 거절 사항이 설계를 더 크게 형성했습니다:
- 언어 서버(language servers) 및 심층 타입 추론(deep type inference) 미구현. 감시 중인 모든 프로젝트에 걸쳐 N개의 언어 서버를 실행하는 것은 운영상 부담이 크며, 성숙한 도구들이 장악하고 있는 영역에서 그들과 경쟁하게 됩니다. 행동 신호(behavioral signal)가 해자(moat)이며, 구문 구조(syntactic structure)는 이를 위한 비계(scaffolding)일 뿐입니다. v2 경로(언어별 심층 어댑터)는 기록되고 출처 태그(provenance-tagged)가 지정되어 있지만, 아직 구축되지 않았습니다.
- 전역 프로젝트 간 그래프(global cross-project graph) 미구현. 각 프로젝트의 지도는 독립적이며 프로젝트 자체 데이터와 함께 존재합니다. 프로젝트 간 쿼리("어떤 리포지토리가 이 패턴을 사용하는가")는 아직 소비자가 없으며, 추측에 기반한 일반화는 모든 도구의 시그니처(signature)에 부담을 줄 것입니다. 실제 소비자가 나타나면, 프로젝트별 저장소 위에 얇은 애그리게이터(aggregator)를 추가할 수 있습니다.
- 별도의 행동 로그(behavioral log) 미구현. 공동 터치 매트릭스(co-touch matrix)는 메모리 시스템이 이미 유지하고 있는 이벤트 로그(event log)를 롤업(rollup)한 것입니다. 전용 추적 저장소를 만드는 것은 데이터를 중복시키고, 세션 결정(session decisions)과의 조인(join)을 유실시키며, 제가 편집 제거(redaction)를 깨끗하게 유지해야 하는 표면적(surface)을 두 배로 늘릴 것입니다.
전이되는 것들
- 에이전트의 작업 이력은 일급 코드 신호(first-class code signal)입니다. 리포지토리(repo)에서 에이전트를 실행하고 그들의 도구 호출(tool calls)을 기록한다면, 여러분은 정적 분석기(static analyzer)가 도출할 수 없는 결합 데이터(coupling data)를 보유하게 되는 것입니다. 이를 롤업(rollup)하는 것은 연구 프로젝트가 아니라 SQL 뷰(view)의 문제입니다.
- 상시 제공(ambient) 정보와 온디맨드(on-demand) 정보를 분리하세요. 방향 설정을 위해 항상 주입되는 작은 요약본(digest)과 깊이 있는 탐색을 위한 도구를 구분하십시오. 어느 하나가 다른 하나를 대체할 수는 없으며, 요약본은 무언가가 이를 재계산(recompute)할 때만 신뢰할 수 있습니다.
- 현재 작업에 따라 컨텍스트(context)의 순서를 정하세요. 관련 서브시스템(subsystem)이 먼저 나온다면 동일한 요약본이라도 두 배는 더 잘 읽힙니다. 여러분은 대개 이미 작업을 알고 있습니다. 이를 활용하십시오.
- 신선도(Freshness) = 빠른 최선 노력(best-effort) 경로 + 보장된 느린 스윕(sweep). 이벤트 후크(event hooks)로만 유지되는 모든 유도된 인덱스(derived index)는 조용히 틀린 정보를 제공하며, 배치(batch)로만 유지되는 모든 인덱스는 현재 시점에 대해 틀린 정보를 제공합니다.
- 성숙한 도구들을 통과하는 것이 아니라, 그 도구들을 우회하여 경로를 찾으세요. Tree-sitter-shallow와 고유한 신호를 결합하는 것이, 20년 된 타입 추론(type-inference) 엔지니어링의 방식에 맞서 경쟁하는 것보다 낫습니다.
이것은 동일한 작은 스택에 관한 네 번째 포스트이며, 패턴은 유지됩니다:
메모리 시스템은 발자취를 기록하고, 존재 레이어(presence layer)는 병렬 세션을 읽기 쉽게 유지하며, 문서 표준(docs standard)은 '이유(why)'를 보존하고, 코드 맵(code map)은 이 세 가지를 에이전트가 도착했을 때 실제로 필요로 하는 것, 즉 '장소에 대한 감각(sense of place)'으로 융합합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기