1 문제 정의: Copilot의 한계
기존 Copilot(GitHub Copilot, Cursor 등)은 단일 함수·단일 클래스 수준의 질문에는 비교적 정확하지만, 포괄적 질문(크로스 모듈 관계, 변경 영향도, 설정값 전파 등)에 대해서는 코드 요약이나 일반적 답변, 심하면 환각을 내뱉는다.
| Copilot의 한계 | 이 agent가 해야 하는 것 |
|---|---|
| 단일 파일만 봄 | 모듈 간 관계를 봄 |
| 코드 텍스트만 읽음 | 구조 메타데이터(AST 기반)를 읽음 |
| “이 함수가 뭐 하는지”는 잘 답함 | “이 함수를 바꾸면 어디가 깨지는지”를 답함 |
| 포괄적 질문 → 요약/환각 | 포괄적 질문 → 구조 기반 추론 |
핵심 차이는 agent가 코드를 읽기 전에 “어디를 읽어야 하는지”를 먼저 아는 것이다. 이를 가능하게 하는 것이 AST 기반으로 사전 생성된 메타데이터이고, 시스템 프롬프트는 agent에게 “이 메타데이터를 어떻게 읽고 조합해서 답변하라”를 알려주는 것이다.
Codebase Analyzer Agent: Architecture 비교에서 Copilot 기반과 GraphRAG 기반의 아키텍처를 비교했다면, 이 포스트는 그중 시스템 프롬프트 기반 접근의 상세 스킬 설계를 다룬다.
2 접근 전략: GraphRAG 없이 메타데이터 + 시스템 프롬프트
GraphRAG을 구축하기엔 투입 자원과 비용이 과다하므로, 다음 전략을 취한다:
- AST를 기반으로 코드 구조 정보를 추출한다 (모듈 의존성, 함수 호출 관계, 클래스 계층, 설정값 참조)
- 추출된 구조 정보를 메타데이터 파일로 저장한다 (JSON, MD)
- 추가로 코드의 docstring, 주요 인터페이스의 type hint, 도메인 전문 용어 사전을 메타데이터로 관리한다
- 시스템 프롬프트가 agent에게 “질문 유형에 따라 어떤 메타데이터를 어떤 순서로 읽고 조합하라”를 지시한다
이 구조에서 agent의 우위는: Copilot이 열린 파일의 코드 텍스트만 보는 반면, 이 agent는 코드를 읽기 전에 구조 메타데이터로 “어디를 봐야 하는지”를 먼저 파악하고, 필요한 코드만 정밀하게 읽는다는 점이다.
3 프롬프트 유형 분류: A+B+D 혼합
시스템 프롬프트 유형 분류와 혼합 설계의 분류 기준으로 이 agent는 A+B+D가 섞여 있다:
- 유형 A (멀티태스크): 영향도 분석, 관계 분석, 코드 Q&A, 설정값 추적 — 독립적인 분석 태스크가 여러 개
- 유형 B (파이프라인): 질문 수신 → 진입점 식별 → 메타데이터 조립 → 분석 → 응답 — 모든 태스크가 동일한 전처리 파이프라인을 거침
- 유형 D (도구 중심): 메타데이터 JSON 읽기, AST 파서, 파일 탐색 — 외부 데이터 소스 사용법이 중요
혼합 유형 설계 원칙에 따라: 유형별로 레이어를 나누고, 파이프라인(B)은 태스크 내부 Step으로 흡수하고, 도구 사용법(D)은 별도 가이드로 분리한다.
4 전체 아키텍처
사전 처리(agent 밖, CI/CD 또는 커밋 훅으로 자동 실행)에서 AST 파서 스크립트가 코드베이스의 메타데이터 파일을 생성하고, 시스템 프롬프트가 이를 읽어 답변한다.
[사전 처리 — agent 밖]
AST 파서 스크립트가 코드베이스에서 메타데이터 파일 생성
↓
[메타데이터 파일] metadata/
├── module-graph.json 모듈 간 import/의존성
├── call-graph.json 함수 → 함수 호출 관계
├── class-hierarchy.json 상속, 구성, 구현 관계
├── config-impact.json 설정값 → 영향받는 코드 매핑
├── domain-glossary.md 도메인 전문 용어 정의
└── interface-types.md 주요 인터페이스 시그니처 + 타입
↓
[시스템 프롬프트 — agent 안]
코어 + 스킬이 메타데이터를 읽고 조합하여 답변
5 스킬 레이어 설계
이 구조는 블로그 관리 시스템에서 검증한 skill-based 패턴을 코드 분석 도메인에 적용한 것이다.
[코어] agent-guide.md — 항상 로드
├── 메타데이터 스키마 레지스트리 (어떤 파일이 어디에 있고 무엇을 담는가)
├── 1차 라우팅 테이블 (질문 유형 → 태스크 스킬 매핑)
└── 공통 규칙 (응답 형식, 정확도 기준, 환각 방지 규칙)
[컨텍스트 조립] context/
├── context-assembly.md "이 질문에 답하려면 어떤 메타데이터를 읽어야 하는가"
└── scope-resolution.md "질문의 범위를 코드 단위로 좁히는 절차"
[태스크 스킬] tasks/
├── impact-analysis.md 변경 영향도 분석
├── relationship-analysis.md 관계 분석 (함수-함수, 모듈-모듈, 크로스)
├── code-qa.md 포괄적 코드 질문 응답
└── config-trace.md 설정값 변경 → 결과 예측
[메타데이터 읽기 가이드] meta-guides/
├── reading-module-graph.md module-graph.json 읽는 법 + 쿼리 패턴
├── reading-call-graph.md call-graph.json 읽는 법
├── reading-class-hierarchy.md
└── reading-config-impact.md
각 레이어의 역할:
- 코어: 매 대화마다 로드한다. “어떤 메타데이터가 존재하는가”와 “어떤 질문이면 어떤 스킬을 로드하는가”만 담는다
- 컨텍스트 조립: 모든 태스크 실행 전에 반드시 거치는 전처리이다. “코드를 읽기 전에 메타데이터로 범위를 좁히는” 절차를 정의한다. 이것이 Copilot과의 차별점을 만드는 핵심 레이어이다
- 태스크 스킬: 실제 분석 로직이다. 컨텍스트 조립이 “어디를 읽을지” 결정한 후, 태스크 스킬이 “읽은 것으로 무엇을 할지” 결정한다
- 메타데이터 읽기 가이드: 각 메타데이터 파일의 스키마, 읽는 법, 전형적인 쿼리 패턴을 담는다. 태스크 스킬이 필요할 때 참조한다
6 메타데이터 스키마 설계
각 메타데이터 파일의 구체적 구조를 정의한다. 이 스키마가 agent가 읽는 “외부 지식”의 형태를 결정하므로, 스키마 설계는 곧 agent의 능력 범위를 결정하는 것이다.
6.1 module-graph.json
모듈 간 import/의존성 관계를 나타낸다. 노드가 모듈, 엣지가 import 관계이다.
{
"nodes": {
"auth_service": {
"path": "src/auth/service.py",
"type": "module",
"description": "인증 토큰 검증 및 세션 관리",
"exports": ["validate_token", "create_session", "revoke_session"],
"lines": 245
},
"api_router": {
"path": "src/api/router.py",
"type": "module",
"description": "REST API 엔드포인트 라우팅",
"exports": ["login", "refresh", "logout"],
"lines": 180
}
},
"edges": [
{
"from": "api_router",
"to": "auth_service",
"imports": ["validate_token", "create_session"],
"type": "direct"
}
]
}exports와 imports 필드가 핵심이다. agent가 “auth_service를 누가 쓰는가?”라는 질문에 edges를 탐색하여 답할 수 있다. description은 scope-resolution에서 추상적 질문(“인증 관련”)을 구체적 모듈로 매핑할 때 사용한다.
6.2 call-graph.json
함수 단위의 호출 관계이다. module-graph보다 세밀한 그래프이다.
{
"functions": {
"auth_service.validate_token": {
"signature": "(token: str) -> bool",
"callers": [
"api_router.login",
"api_router.refresh_token",
"middleware.check_permission"
],
"callees": [
"token_store.get",
"crypto.verify_signature"
],
"line_range": [45, 72]
}
}
}callers와 callees가 impact-analysis의 핵심 입력이다. signature는 타입 변경 시 영향을 판단하는 데 쓰인다. line_range는 agent가 실제 코드를 읽을 때 전체 파일이 아니라 해당 범위만 읽도록 안내한다.
6.3 config-impact.json
설정값과 그것을 참조하는 코드의 매핑이다.
{
"config_keys": {
"AUTH_TOKEN_TTL": {
"default": 3600,
"type": "int",
"readers": [
{"function": "auth_service.validate_token", "usage": "토큰 만료 판단 기준"},
{"function": "session_manager.cleanup", "usage": "만료 세션 정리 기준"}
],
"writers": [
{"source": "env", "key": "AUTH_TOKEN_TTL"},
{"source": "config.yaml", "path": "auth.token_ttl"}
]
}
}
}readers의 usage 필드가 중요하다. agent가 “이 설정을 바꾸면?”에 답할 때, 단순히 “이 함수가 읽는다”가 아니라 “이 함수에서 토큰 만료 판단 기준으로 사용한다”까지 알 수 있다.
6.4 스키마 설계 원칙
| 원칙 | 설명 | 예시 |
|---|---|---|
| 관계를 양방향으로 저장 | caller→callee뿐 아니라 callee→caller도 | validate_token의 callers 필드 |
| 설명을 짧게 포함 | agent의 라우팅 판단에 필요한 최소 설명 | module의 description, config의 usage |
| 코드 위치를 포함 | agent가 실제 코드를 읽을 때 범위를 좁히도록 | line_range, path |
| 타입 정보를 포함 | 변경 영향 판단에 타입이 필수 | signature, config의 type |
7 핵심 스킬: context-assembly.md 상세
context-assembly는 이 agent의 가장 중요한 스킬이다. Copilot은 열린 파일을 그대로 읽지만, 이 agent는 질문을 분석하여 “어떤 메타데이터를 어떤 순서로 어떤 깊이까지 읽을지”를 먼저 결정한다.
7.1 Step 1: 진입점(entry point) 식별
사용자가 언급한 구체적 코드 단위를 식별한다 — 함수명, 클래스명, 모듈명, 설정 키 등. “인증 로직”처럼 추상적이면 domain-glossary.md에서 구체적 코드 단위로 매핑한다.
7.2 Step 2: 질문의 방향(direction) 판별
| 방향 | 설명 | 읽어야 할 메타데이터 |
|---|---|---|
| 상향 (upstream) | “이 함수를 호출하는 것은?” | call-graph → callers |
| 하향 (downstream) | “이 함수가 호출하는 것은?” | call-graph → callees |
| 횡단 (lateral) | “이 모듈과 같은 레벨에서 상호작용하는 것은?” | module-graph → neighbors |
| 영향 (impact) | “이것을 바꾸면 뭐가 깨지나?” | call-graph + module-graph + config-impact (복합) |
| 설정 (config) | “이 설정값을 바꾸면?” | config-impact → 직접 매핑 |
7.3 Step 3: 탐색 깊이(depth) 결정
| 질문 범위 | 깊이 | 읽는 양 |
|---|---|---|
| 단일 함수 | 1-hop | 직접 caller/callee만 |
| 단일 모듈 | 2-hop | 모듈 내 함수들 + 직접 의존 모듈 |
| 크로스 모듈 | 3-hop | 간접 의존까지 |
| 시스템 전체 | 전체 | module-graph 전체 요약 + 핵심 경로만 상세 |
7.4 Step 4: 메타데이터 로드 순서
- 항상 먼저: domain-glossary.md (용어 해석)
- 진입점의 위치 확인: module-graph.json에서 해당 모듈 찾기
- 방향에 따른 그래프 탐색: call-graph 또는 class-hierarchy
- 필요시 추가: config-impact.json, interface-types.md
- 마지막: 실제 코드 파일 읽기 (메타데이터로 좁힌 범위만)
핵심 원칙: 코드를 먼저 읽지 않는다. 메타데이터로 “어디를 읽어야 하는지”를 확정한 후, 해당 코드만 읽는다. 이것이 Copilot과의 결정적 차이이다.
7.5 오류 및 모호성 처리
context-assembly는 사용자 질문을 메타데이터 쿼리로 변환하는 과정이므로, 변환이 실패하거나 모호한 경우가 반드시 발생한다.
진입점을 찾을 수 없는 경우:
사용자: "결제 관련 코드를 분석해줘"
↓
Step 1: 진입점 식별 → "결제"는 함수명/클래스명/모듈명이 아님
↓
domain-glossary.md 검색 → "결제" = payment_service, billing_module, ...
↓
매칭 결과:
0개 → "결제 관련 용어가 도메인 용어 사전에 없습니다. 구체적 함수명이나 모듈명을 알려주세요"
1개 → 그대로 진행
2개+ → "결제와 관련된 모듈이 여러 개입니다: payment_service(결제 처리), billing_module(청구서 생성). 어느 것을 분석할까요?"
방향 판별이 모호한 경우:
사용자: "auth_service에 대해 알려줘"
↓
Step 2: 방향 판별 → 상향? 하향? 횡단? — 명확하지 않음
↓
기본 동작: "전체 개요" 모드로 전환
1. module-graph에서 auth_service의 위치 (의존하는 모듈, 의존받는 모듈)
2. call-graph에서 exports 함수 목록 + 각각의 caller 수
3. 요약 응답 후 "더 구체적으로 알고 싶은 부분이 있나요?" 유도
탐색 깊이 초과 경고:
3-hop 이상 탐색이 필요한 질문에 대해서는 결과의 신뢰도가 떨어짐을 명시한다.
"3-hop까지의 직접/간접 영향을 분석했습니다.
그 이상의 간접 영향은 정적 분석만으로 정확히 판단하기 어렵습니다.
추가 확인이 필요한 경로: [module_x → module_y → ...]"
8 태스크 스킬 예시: impact-analysis.md
context-assembly가 “어디를 읽을지”를 결정했으므로, 이제 태스크 스킬이 “읽은 것으로 무엇을 할지”를 정의한다. 영향도 분석(impact-analysis)을 예시로 보면:
선행: context-assembly.md 실행 (방향: impact)
Step 1: 직접 영향 (1-hop) call-graph에서 변경 대상의 직접 caller를 모두 나열한다. 각 caller에 대해: 호출 시 전달하는 인자의 타입(interface-types.md 참조), 반환값을 어떻게 사용하는지를 확인한다.
Step 2: 간접 영향 (2-hop+) 1-hop caller 각각에 대해 다시 caller를 추적한다. 전이적 추적은 3-hop까지로 제한한다. 3-hop 이상은 “간접 영향 가능성 있음”으로 표시만 한다.
Step 3: 설정 경로 영향 변경 대상이 config 값을 읽는 경우, config-impact.json에서 같은 config를 읽는 다른 코드를 찾는다. 설정값 변경이 이 코드들에 미치는 영향을 추론한다.
Step 4: 영향 보고서 형식
| 영향 수준 | 대상 | 관계 | 영향 내용 |
|---|---|---|---|
| 직접 (1-hop) | module_b.func_x | calls → 변경 대상 | 반환 타입 변경 시 TypeError |
| 간접 (2-hop) | module_c.func_y | calls → func_x → 변경 대상 | func_x 실패 시 연쇄 실패 |
| 설정 경로 | module_d.func_z | reads CONFIG_KEY | 값 변경 시 분기 로직 변경 |
Step 5: 위험도 판정 직접 영향 코드 수 × 타입 불일치 가능성으로 위험도를 산정한다. 테스트 커버리지 정보가 있으면 “테스트 없는 직접 영향” 코드를 최우선 경고한다.
9 나머지 태스크 스킬 상세
9.1 relationship-analysis.md — 관계 분석
두 코드 단위 간의 관계를 구조적으로 설명한다. “모듈 A와 모듈 B는 어떤 관계인가?”류의 질문에 대응한다.
선행: context-assembly.md 실행 (방향: lateral 또는 upstream/downstream)
Step 1: 직접 관계 확인 module-graph.json에서 두 모듈 간 직접 import 관계가 있는지 확인한다. 있으면 방향(누가 누구를 import하는지)과 import하는 심볼 목록을 나열한다.
Step 2: 간접 관계 확인 직접 관계가 없으면 module-graph에서 최단 경로를 탐색한다. “A → C → B” 같은 간접 경로를 제시한다. 3-hop 이내에 경로가 없으면 “두 모듈은 독립적이다”로 판단한다.
Step 3: 함수 수준 관계 call-graph.json에서 두 모듈 사이에 실제로 오가는 함수 호출을 나열한다. 방향, 호출 빈도(callers 수), 시그니처를 포함한다.
Step 4: 관계 보고서 형식
[A → B 방향]
A.login() → B.validate_token(token: str) → bool
A.logout() → B.revoke_session(session_id: str) → None
[B → A 방향]
없음 (단방향 의존)
[관계 요약]
api_router는 auth_service에 단방향으로 의존한다.
인증 관련 2개 함수를 사용하며, auth_service는 api_router를 알지 못한다.
9.2 code-qa.md — 포괄적 코드 질문 응답
impact-analysis, relationship-analysis, config-trace에 매핑되지 않는 일반적 코드 질문을 처리하는 폴백 스킬이다.
선행: context-assembly.md 실행
Step 1: 질문 분류 질문이 다른 전문 태스크 스킬로 라우팅되어야 하는지 재확인한다. “바꾸면 어디가 깨지나” → impact-analysis로 재라우팅. 어디에도 해당하지 않으면 이 스킬에서 처리한다.
Step 2: 메타데이터 기반 답변 시도 context-assembly가 준비한 메타데이터만으로 답변 가능한지 판단한다. 가능하면 메타데이터 기반으로 답변한다.
Step 3: 코드 읽기 보충 메타데이터만으로 부족하면 context-assembly가 좁힌 범위의 코드를 읽어 보충한다. 이때도 전체 파일이 아니라 call-graph의 line_range를 참조하여 필요한 범위만 읽는다.
Step 4: 메타데이터 한계 명시 코드를 직접 읽어 답변한 부분은 “이 부분은 코드에서 직접 확인한 내용입니다”로 표시하고, 메타데이터 기반 부분과 구분한다.
9.3 config-trace.md — 설정값 추적
설정값 변경이 코드에 미치는 영향을 추적한다.
선행: context-assembly.md 실행 (방향: config)
Step 1: 설정값 식별 사용자가 언급한 설정값을 config-impact.json에서 찾는다. 정확한 키 이름이 아니면 domain-glossary.md와 config-impact.json의 키 목록에서 매칭한다.
Step 2: 영향 범위 매핑 config-impact.json의 readers 필드에서 해당 설정값을 읽는 모든 함수를 나열한다. 각 함수의 usage 필드로 “이 설정이 어떤 목적으로 사용되는지”를 함께 제시한다.
Step 3: 값 변경 시나리오 분석 설정값의 type과 default를 기반으로, 값이 바뀔 때 각 reader 함수에서 어떤 행동 변화가 예상되는지 추론한다.
설정: AUTH_TOKEN_TTL (현재: 3600초)
→ 값을 줄이면 (예: 600초):
- validate_token: 토큰이 더 자주 만료 → 사용자 재인증 빈도 증가
- session_manager.cleanup: 정리 주기는 동일하나 만료 세션 수 증가
→ 값을 늘리면 (예: 86400초):
- validate_token: 토큰 유효 기간 증가 → 보안 위험 증가
- 성능 영향 없음 (만료 체크 로직 자체는 동일)
Step 4: 설정 소스 확인 config-impact.json의 writers 필드에서 이 설정이 어디서 오는지(환경변수, config 파일, DB 등)를 확인하고, 변경 방법을 안내한다.
10 실행 흐름 예시
전체 스킬이 어떻게 연결되어 동작하는지, 구체적 질문으로 확인한다.
사용자: "auth_service의 validate_token을 async로 바꾸면 영향이 어디까지야?"
↓
[코어] 1차 라우팅: "변경 영향" → impact-analysis.md 로드
↓
[컨텍스트 조립] context-assembly.md 실행
Step 1: 진입점 = auth_service.validate_token
Step 2: 방향 = impact (상향 + 설정 경로)
Step 3: 깊이 = 2-hop (단일 모듈 변경이므로)
Step 4: 로드 순서 결정
↓
[메타데이터 읽기]
1. domain-glossary.md → "auth_service"는 인증 모듈
2. module-graph.json → auth_service를 import하는 모듈: [api_router, middleware, test_auth]
3. call-graph.json → validate_token의 caller: [login(), refresh_token(), check_permission()]
4. interface-types.md → validate_token(token: str) -> bool
5. 실제 코드: 위에서 식별된 3개 caller 함수만 읽기
↓
[태스크 실행] impact-analysis.md Step 1~5
직접 영향: login(), refresh_token(), check_permission()이 sync 호출 → await 필요
간접 영향: login()을 호출하는 api_router의 엔드포인트들
위험도: 높음 (3개 직접 caller 모두 sync context)
↓
[응답] 영향 보고서 출력
Copilot은 이 질문을 받으면 validate_token이 있는 파일을 읽고 그 파일 안에서 추론한다. 이 agent는 validate_token의 caller 3개를 메타데이터에서 먼저 찾고, 그 3개 함수의 코드만 정밀하게 읽는다. 같은 컨텍스트 윈도우 크기에서 정보 밀도가 다르다.
11 블로그 프로젝트 스킬 구조와의 대응
이 설계는 블로그 관리 시스템의 스킬 구조를 코드 분석 도메인에 이식한 것이다. 각 레이어가 어떻게 대응하는지 비교하면:
| 블로그 프로젝트 | 코드베이스 분석 agent | 역할 |
|---|---|---|
| AGENT_GUIDE.md | agent-guide.md | 코어 + 라우팅 |
| info-search.md | context-assembly.md | 공통 전처리 (무엇을 읽을지 결정) |
| write-post.md, convert-tbd.md | impact-analysis.md, relationship-analysis.md | 태스크 스킬 |
| Category/GUIDE.md | meta-guides/reading-*.md | 도메인별 읽기 규칙 |
| audit.md | (응답 검증 스킬) | 후처리 |
| docs/book/ (교재 파일) | metadata/ (메타데이터 파일) | 사전 생성된 지식 소스 |
마지막 행이 핵심이다. 블로그에서 교재 PDF→MD가 agent의 외부 지식이듯, 코드베이스에서는 AST→메타데이터 JSON/MD가 agent의 외부 지식이다. 시스템 프롬프트는 이 외부 지식을 “어떤 순서로, 어떤 범위만큼 읽어서, 어떻게 조합하여 답변하라”는 절차를 정의하는 것이다. 결국 두 프로젝트 모두 동일한 문제를 풀고 있다: agent의 컨텍스트 윈도우에 지금 필요한 정보만 밀도 있게 채우는 것.
12 컨텍스트 밀도 관점에서의 설계 정당성
이 스킬 구조가 Copilot보다 나은 이유를 컨텍스트 밀도로 설명할 수 있다:
| 접근 | 컨텍스트에 올라가는 것 | 밀도 |
|---|---|---|
| Copilot | 열린 파일 전체 + 인접 파일 일부 | 낮음 — 대부분 현재 질문과 무관한 코드 |
| 단순 RAG | 키워드 매칭된 코드 조각들 | 중간 — 관련은 있지만 구조적 맥락 부재 |
| 이 agent | 메타데이터로 좁힌 관련 코드 + 구조 관계 정보 | 높음 — “어디가 연결되어 있는지”와 “그 코드가 뭘 하는지”가 함께 |
13 대규모 코드베이스로의 확장: 20만줄 × 35개 멀티 레포
위 설계는 소규모(단일 레포, 1~2만 줄) 기준이다. 20만 줄 × 35개 멀티 레포 규모에서는 핵심 원칙은 동일하지만, 메타데이터 자체가 컨텍스트에 못 올라가는 새로운 문제가 생긴다.
13.1 규모에서 깨지는 것
소규모에서는 call-graph.json이 수백 KB면 agent가 전체를 읽을 수 있다. 대규모에서는:
- 함수 수: 수천~만 개
- 모듈 수: 수백 개
- 레포 간 의존성: 35×35 매트릭스
- call-graph.json: 수 MB ~ 수십 MB → 전체 로드 불가능
코드를 못 읽어서 메타데이터를 만들었는데, 이제 메타데이터도 못 읽는 상황이 된다.
13.2 해법: 메타데이터의 계층화 (3-Layer)
블로그 프로젝트의 교재 참조 체계가 이미 이 문제를 풀고 있다:
| 블로그 교재 참조 | 대규모 코드베이스 메타데이터 |
|---|---|
| Summary MD (목차+키워드) → 항상 먼저 읽음 → “어느 챕터를 볼지” 결정 | Layer 0: 시스템 지도 → 항상 먼저 읽음 → “어느 레포/모듈을 볼지” 결정 |
| Full MD (챕터 본문) → 필요한 구간만 선택적 읽기 | Layer 1~2: 요약→상세 → 해당 레포/모듈 범위만 읽기 |
메타데이터를 3층으로 계층화한다:
Layer 0: 시스템 지도 (항상 로드 가능, 수 KB)
├── 35개 레포 목록 + 각 레포의 1줄 설명 + 핵심 도메인
├── 레포 간 의존성 그래프 (레포 단위, 35×35)
└── 도메인 용어 사전 (domain-glossary.md)
Layer 1: 레포별 요약 (라우팅 후 해당 레포만 로드, 수~수십 KB)
├── repo-A/module-summary.json ← 모듈 목록 + 모듈 간 의존성 + 모듈별 1줄 설명
├── repo-A/public-api.md ← 외부 노출 인터페이스 요약
└── repo-A/config-keys.json ← 이 레포가 읽는/쓰는 설정값 목록
Layer 2: 모듈별 상세 (특정 모듈 분석 시만 로드, 수~수십 KB)
├── repo-A/modules/auth/call-graph.json
├── repo-A/modules/auth/class-hierarchy.json
├── repo-A/modules/auth/interface-types.md
└── repo-A/modules/auth/docstrings.md
Layer 3: 실제 코드 (최종 확인 시만 읽기)
└── repo-A/src/auth/validate_token.py
질문 한 번에 agent가 보는 메타데이터 경로:
시스템 지도 (전체) → 레포 2~3개 요약 → 모듈 3~5개 상세 → 코드 파일 5~10개
수 KB 수십 KB 수십 KB 수십 KB
전체 합: 컨텍스트 윈도우 안에 들어옴
13.3 context-assembly가 2단계로 분리된다
소규모에서는 context-assembly.md 하나로 “진입점 → 방향 → 깊이 → 로드”를 처리했다. 대규모에서는 그 앞에 scope-resolution이 추가된다:
소규모:
질문 → [context-assembly] → 메타데이터 읽기 → 분석 → 응답
1단계 조립
대규모:
질문 → [scope-resolution] → [context-assembly] → 메타데이터 읽기 → 분석 → 응답
Layer 0~1로 Layer 2로
레포/모듈 특정 모듈 내 상세 조립
"35개 중 어느 레포?" "그 안에서 어디?"
scope-resolution.md의 역할:
- Step 1: 진입점 식별 + domain-glossary.md로 용어 해석
- Step 2: 시스템 지도(Layer 0)에서 관련 레포 특정 (35개 → 2~3개)
- Step 3: 해당 레포의 요약(Layer 1)에서 관련 모듈 특정 (수십 개 → 3~5개)
- Step 4: 크로스 레포 관계 확인 — 이 모듈이 다른 레포의 뭘 호출하는가?
이후 context-assembly.md가 특정된 모듈 범위 안에서 기존과 동일하게 작동한다.
13.4 대규모에서 추가되는 문제와 대응
크로스 레포 의존성 단일 레포에서는 import 관계가 모듈 단위였지만, 멀티 레포에서는 레포 간 인터페이스가 새로운 관계 유형이 된다. Layer 0의 system-map.json에 레포 간 exposes/consumes 관계를 명시한다:
{
"repo-auth": {
"exposes": ["validate_token", "create_session"],
"consumes": ["repo-config/get_setting", "repo-db/user_store"],
"protocol": "gRPC"
},
"repo-api": {
"exposes": ["POST /login", "POST /refresh"],
"consumes": ["repo-auth/validate_token", "repo-rate-limit/check"],
"protocol": "REST"
}
}레포 수 × 인터페이스 수이므로 크기가 작고 항상 로드 가능하다.
같은 이름, 다른 맥락 35개 레포에 utils.py, config.py, models.py가 각각 있을 수 있다. scope-resolution의 Step 2에서 처리한다. 사용자 질문에 레포 힌트가 없으면 system-map.json에서 해당 함수를 expose하는 레포를 찾는다. 모호하면 사용자에게 확인한다.
메타데이터 갱신 비용 35개 레포 전체의 AST를 매 커밋마다 파싱하면 CI 부담이 크다. 계층별 갱신 주기를 다르게 가져간다:
| Layer | 갱신 주기 | 이유 |
|---|---|---|
| Layer 0 (시스템 지도) | 레포 추가/삭제 시, 공개 API 변경 시 | 레포 간 인터페이스는 자주 안 바뀜 |
| Layer 1 (레포별 요약) | 해당 레포에 PR 머지 시 | 모듈 구조 변경은 중간 빈도 |
| Layer 2 (모듈별 상세) | 해당 모듈 파일 변경 시 | 함수 호출 관계는 자주 바뀜 |
변경된 레포/모듈만 재생성하는 incremental update 방식이다.
도메인 지식의 분산 35개 레포는 보통 여러 팀이 관리한다. 도메인 용어가 팀마다 다를 수 있다. domain-glossary.md를 계층화한다:
metadata/
├── domain-glossary.md ← 전사 공통 용어 (Layer 0, 항상 로드)
├── repo-auth/domain-glossary.md ← 인증 도메인 용어 (Layer 1, 해당 레포 분석 시)
├── repo-ml-pipeline/domain-glossary.md ← ML 도메인 용어
13.5 소규모 vs 대규모 비교 요약
| 항목 | 소규모 (단일 레포) | 대규모 (20만줄 × 35레포) |
|---|---|---|
| 메타데이터 계층 | 1층 (전체 로드 가능) | 3층 (지도 → 요약 → 상세) |
| 컨텍스트 조립 | context-assembly 1단계 | scope-resolution + context-assembly 2단계 |
| 메타데이터 로드 | 전체 로드 | 계층별 선택적 로드 |
| 메타데이터 갱신 | 전체 재생성 | incremental update (변경분만) |
| 도메인 용어 사전 | 1개 | 전사 공통 + 레포별 |
| 크로스 경계 분석 | 모듈 간 | 레포 간 + 모듈 간 |
본질은 같다: agent가 한 번에 보는 것을 줄이고, 필요한 것만 단계적으로 좁혀가는 것. 소규모에서는 “코드 → 메타데이터 → 코드”로 1단계 간접 참조였다면, 대규모에서는 “코드 → 메타데이터 지도 → 메타데이터 요약 → 메타데이터 상세 → 코드”로 3단계 간접 참조가 되는 것이다.
14 실행 전략: GraphRAG 없이 Copilot을 이기는 경로
14.1 왜 온톨로지 + GraphDB + GraphRAG을 4개월에 1명이 못하는가
단순히 시간 부족이 아니라 각 단계가 순차 의존성을 가진다:
온톨로지 설계 (도메인 분석 → 클래스/관계 정의 → 역량 질문 검증)
↓ 이게 확정되어야
GraphDB 스키마 설계 + 데이터 적재 (AST 파싱 → 매핑 → 적재 → 검증)
↓ 이게 돌아가야
GraphRAG 구현 (쿼리 생성 → 서브그래프 추출 → 프롬프트 조립 → 평가)
↓ 이게 작동해야
실제 사용 가능한 agent
각 단계에서 “해보니 설계가 틀렸다”는 피드백 루프가 반드시 생긴다. 온톨로지를 끝냈는데 GraphDB에 적재해보니 관계가 빠져있고, GraphRAG을 돌려보니 쿼리가 원하는 서브그래프를 못 뽑는다. 이 반복을 1명이 4개월에 수렴시키기는 20만줄 × 35레포 규모에서 거의 불가능하다.
14.2 대안: Skill-Based System Prompt가 GraphRAG 구현 비용 5%로 80%를 커버한다
| GraphRAG이 하는 일 | System Prompt로 대체하는 방법 | 커버리지 |
|---|---|---|
| 구조화된 지식 저장 | AST → 메타데이터 JSON/MD (스크립트) | 90% — 그래프 쿼리 없이 파일 읽기로 대체 |
| 관계 탐색 (N-hop) | 메타데이터에서 caller/callee 추적 | 70% — 3-hop까지는 충분, 그 이상은 드물게 필요 |
| 의미 기반 검색 | domain-glossary + scope-resolution | 60% — 벡터 검색 없이 용어 매핑으로 대체 |
| 자동 추론 | agent의 LLM 추론 + 명시적 규칙 | 50% — 전이적 추론은 prompt 규칙으로 지시 |
구축 비용이 “AST 파싱 스크립트 + 시스템 프롬프트 작성”으로 줄어든다. 1명이 2~4주면 할 수 있다.
14.3 실험 로드맵
Phase 1 (2~3주): 메타데이터 생성 기반 구축
├── AST 파싱 스크립트 작성 (Python ast 모듈 또는 tree-sitter)
├── 메타데이터 파일 생성 (module-graph, call-graph, config-impact 등)
├── domain-glossary 수동 작성 (핵심 도메인 용어 50~100개)
└── 시스템 프롬프트 + 스킬 파일 작성
Phase 2 (1~2주): 실험 A — Copilot/Cursor에 붙이기
├── .cursorrules 또는 Copilot system prompt에 스킬 구조 적용
├── 메타데이터를 프로젝트 내 파일로 배치
├── 테스트 질문 10~20개로 Copilot 기본 vs 스킬 적용 비교
└── 판정: "Copilot보다 나은가?"
Phase 3 (1~2주): 실험 B — 1M context window에 붙이기
├── Claude/Gemini 1M context에 메타데이터 + 코드 일부를 직접 주입
├── 동일 테스트 질문으로 비교
└── 판정: "Phase 2보다 나은가?"
Phase 4 (판단 시점): 여기서 멈출지 결정
├── 성능이 충분하다 → 여기서 멈춤. 메타데이터 갱신 자동화만 추가
├── 특정 유형의 질문에서 부족하다 → 그 유형만 보강
└── 전반적으로 부족하다 → GraphRAG 검토 (이 시점에선 도메인 이해가 축적되어 설계 수월)
이 순서가 합리적인 이유: Phase 1~3까지의 산출물(AST 파싱 스크립트, 메타데이터 구조, 도메인 용어 사전)은 나중에 GraphRAG으로 가더라도 그대로 활용된다. 버리는 작업이 아니라 GraphRAG의 요구사항 분석을 실험으로 대신하는 셈이다.
14.4 성공 기준
| 질문 유형 | Copilot 예상 수준 | 이 agent 목표 수준 |
|---|---|---|
| 단일 함수 설명 | 잘함 | 동등 (이길 필요 없음) |
| “이 함수를 바꾸면 어디가 깨지나?” | 같은 파일 내만 답함 / 환각 | 직접 caller 3-hop까지 정확히 나열 |
| “모듈 A와 모듈 B의 관계는?” | 요약 수준 / 환각 | 의존성 방향 + 인터페이스 시그니처 제시 |
| “설정값 X를 바꾸면?” | 모름 / 환각 | config-impact에서 영향받는 코드 나열 |
| “인증 흐름 전체를 설명해줘” | 부분적 / 환각 혼재 | 메타데이터 기반으로 모듈 경계 넘어 추적 |
단일 함수 질문에서 동등, 크로스 모듈 질문에서 우위가 현실적 목표이다.
15 왜 Copilot을 이길 수 있는가: 정보 획득 경로의 차이
Copilot과 이 agent는 정보 획득 경로 자체가 다르다:
Copilot:
코드 텍스트 → [LLM이 구조를 추론] → 답변
확률적 추론으로 구조 파악
"이 import를 보니 아마 저 모듈을 쓰는 것 같다"
이 agent:
코드 → [AST가 구조를 추출] → 메타데이터 → [LLM이 의미를 추론] → 답변
결정론적 추출 확률적 추론은 의미 해석에만
"이 함수는 정확히 이 3개 함수에서 호출된다" (사실)
Copilot은 “코드 구조 파악”과 “의미 해석” 둘 다를 LLM에게 맡긴다. LLM은 구조 파악에서 환각을 낸다 — import를 보고 “사용하는 것 같다”고 추론하지만 실제로는 dead import일 수 있고, dynamic dispatch로 호출되는 함수를 코드 텍스트만 보고는 발견할 수 없다.
이 agent는 구조 파악을 AST(결정론적)에게 맡기고, LLM에게는 “이 구조 정보를 바탕으로 영향을 해석하라”는 의미 추론만 시킨다. LLM이 잘하는 것(의미 해석)에만 LLM을 쓰고, LLM이 못하는 것(구조 추출)은 AST가 한다. 각 도구를 강점 영역에서만 쓰는 것이다.
| 영역 | Copilot | 이 agent | 환각 차이 |
|---|---|---|---|
| “이 함수가 뭘 하는가” (단일 함수) | LLM이 코드 읽고 해석 | LLM이 코드 읽고 해석 | 동등 |
| “이 함수를 누가 호출하는가” (구조) | LLM이 추론 (환각 가능) | AST가 추출 (사실) | agent 압도적 우위 |
| “바꾸면 뭐가 깨지는가” (영향 분석) | LLM이 구조+의미 동시 추론 | AST가 구조 제공 + LLM이 의미만 추론 | agent 우위 |
16 개발자 숙련도별 사고 방식과 agent 확장 경로
현재 설계가 모델링하는 것은 “시니어 수준”이다. 그 위 수준으로 확장하려면 어떤 메타데이터와 스킬이 추가되어야 하는지를 정리한다.
16.1 수준별 사고 방식 차이
주니어 시니어 풀스택 아키텍트/마스터
───────────────── ───────────────── ───────────────── ─────────────────
"이 함수가 뭘 하지?" "이걸 바꾸면 어디가 "이 변경이 다른 레이어 "이 구조가 왜 이렇고,
깨지지?" 에서 어떤 부작용을 어떤 트레이드오프를
일으키나?" 감수하고 있는가?"
코드를 읽는다 구조를 탐색한다 레이어를 넘어 추적한다 설계 의도를 추론한다
↓ ↓ ↓ ↓
텍스트 이해 관계 추적 (what) 레이어 간 영향 (impact) 원칙 기반 판단 (why)
“validate_token을 async로 바꾸자”에 대한 각 수준의 반응:
| 수준 | 반응 |
|---|---|
| 주니어 | “함수 시그니처를 바꾸고 await를 붙이면 된다” |
| 시니어 | “caller 3개(login, refresh_token, check_permission)를 고쳐야 한다” |
| 풀스택 | “API 응답 패턴이 바뀌니 프론트도 수정. 성능은 나아지지만 전환 중 혼재 위험” |
| 아키텍트 | “동기 유지 결정은 세션 정합성 때문이었다(ADR-001). async 전환은 인증 모듈만이 아니라 전체 미들웨어 패턴을 바꾸는 시작점이다. 분산 락 구현이 선행되어야 한다” |
| 마스터 | “async 전환을 지금 하면 팀 B의 릴리즈 일정과 충돌한다. 기술 결정이 아니라 리소스 배분 결정이다” |
16.2 현재 agent의 위치와 확장 가능 범위
주니어 시니어 풀스택 아키텍트 마스터
├──────────┤
Copilot 이 agent
(현재) (Phase 1~3)
├─────────┤
메타데이터 종류 추가로
확장 가능 (Phase 5)
├──────────┤
ADR 등 사람이 작성하는
메타데이터로 부분 가능 (Phase 6)
├──────────┤
조직/비즈니스 맥락은
메타데이터로 표현 불가
16.3 Phase 5: 풀스택 수준 확장
시니어 → 풀스택의 핵심 차이: “같은 레이어 내 추적”에서 “레이어를 넘는 추적”으로 확장해야 한다.
추가 메타데이터 4종:
- layer-map.json — 코드가 어떤 레이어(frontend/api/backend/data/infra)에 속하고, 레이어 간 어떻게 연결되는가
- api-contract.md — API 엔드포인트 ↔︎ 백엔드 함수 ↔︎ 프론트 호출 매핑, response schema, SLA, failure mode
- infra-dependency.json — 코드 → 인프라 리소스(redis, postgres 등) 의존성
- performance-profile.json — 함수/엔드포인트별 성능 특성과 병목
추가 스킬: cross-layer-impact.md
Step 1: impact-analysis.md 실행 (백엔드 내 영향)
Step 2: api-contract.md에서 영향받는 엔드포인트 식별
Step 3: 해당 엔드포인트를 호출하는 프론트 코드 식별
Step 4: infra-dependency.json에서 인프라 영향 확인
Step 5: performance-profile.json에서 성능 영향 예측
16.4 Phase 6: 아키텍트 수준 확장
풀스택 → 아키텍트의 핵심 차이: “what/where”에서 “why/trade-off”로 전환해야 한다.
추가 메타데이터 5종:
- decisions/ (ADR) — 설계 결정의 맥락, 선택지, 결정, 트레이드오프, 재검토 조건
- trade-off-map.json — 현재 구조가 의도적으로 감수하고 있는 트레이드오프 목록
- tech-debt-registry.json — 알려진 기술 부채와 우선순위, 차단 관계
- failure-history.md — 과거 장애와 그로 인한 구조 변경 이력
- constraint-registry.md — 코드 구조에 영향을 주는 외부 제약 (규제, 팀 구조, 배포 주기)
추가 스킬: design-reasoning.md (“왜 이렇게 되어 있는가?”에 답변), trade-off-analysis.md (변경 제안에 대한 트레이드오프 평가)
16.5 확장 비용 구조
| Phase | 추가 메타데이터 | 생성 방식 | 소요 기간 (1명) |
|---|---|---|---|
| 1~3 (시니어) | call-graph, module-graph, config-impact, glossary, interface-types | AST 자동 생성 + 용어 수동 | 2~4주 |
| 5 (풀스택) | layer-map, api-contract, infra-dependency, performance-profile | 반자동 (API 스펙 파싱 + 수동 보완) | 1~2주 |
| 6 (아키텍트) | ADR, trade-off-map, tech-debt, failure-history, constraints | 대부분 사람이 작성 | 지속적 (자동화 불가) |
시니어→풀스택은 메타데이터 종류만 추가하면 되므로 스킬 아키텍처 변경 없이 점진 확장이 가능하다. 풀스택→아키텍트는 사람이 작성해야 하는 메타데이터(ADR, 장애 히스토리)가 필요하므로 비용 구조가 근본적으로 달라진다. 마스터 수준(조직 맥락, 비즈니스 판단)은 메타데이터로 표현할 수 없는 영역이므로 시스템 프롬프트 기반 agent의 한계이다.
17 시니어 수준 agent의 예상 정확도 분석
17.1 질문 유형별 예상 정확도
| 질문 유형 | 예상 정확도 | 근거 |
|---|---|---|
| “이 함수를 누가 호출하는가?” | 95%+ | AST가 정적 호출을 100% 잡고, LLM은 결과를 보여주기만 하면 된다. 환각 개입 여지 거의 없음 |
| “이 모듈이 의존하는 모듈 목록” | 95%+ | import 관계는 AST가 결정론적으로 추출. 동일 논리 |
| “이 함수를 바꾸면 어디가 깨지나?” (직접 영향) | 85~90% | 1-hop caller는 정확. “깨진다”의 판단은 LLM이 타입/시그니처를 보고 추론해야 하므로 약간의 오차 |
| “이 함수를 바꾸면 어디가 깨지나?” (간접 영향) | 70~80% | 2-3 hop까지는 메타데이터로 추적 가능하지만 “실제로 영향받는가”의 판단은 LLM 추론 의존도 상승 |
| “설정값 X를 바꾸면?” | 80~90% | config-impact가 잘 만들어져 있으면 높음. 런타임에 동적으로 읽는 설정은 AST로 못 잡을 수 있음 |
| “모듈 A와 B의 관계를 설명해” | 85~90% | 구조 관계(import, 호출)는 정확. “왜 이런 관계인가”의 해석은 LLM 품질에 의존 |
| “인증 흐름 전체를 설명해” | 70~80% | 여러 모듈을 관통하는 설명은 메타데이터 조합 + LLM 서술 능력 모두 필요 |
| “이 코드를 리팩토링하려면?” | 60~70% | 구조적 제안은 가능하지만 “좋은 리팩토링”의 판단은 메타데이터 밖의 설계 감각 영역 |
17.2 정확도를 결정하는 3가지 변수
변수 1: 메타데이터 품질 (가장 큰 변수) AST 파싱 스크립트의 품질이 전체 시스템의 천장을 결정한다. 메타데이터가 정확하면 agent 정확도가 높고, 부정확하면 agent가 틀린 사실을 자신있게 말한다(환각보다 위험).
| AST가 잘 잡는 것 | AST가 못 잡는 것 |
|---|---|
from A import B |
importlib.import_module(name) |
A.method() 직접 호출 |
getattr(obj, method_name)() |
클래스 상속 class A(B) |
믹스인, 메타클래스 동적 생성 |
config["KEY"] 직접 참조 |
config.get(os.environ["KEY_NAME"]) |
일반적인 엔터프라이즈 코드베이스에서 정적 호출이 85~90%이므로 메타데이터 커버리지도 그 수준이다.
변수 2: 질문의 구조 의존도
| 구조 의존도 | 질문 예시 | 예상 정확도 | 비고 |
|---|---|---|---|
| 100% 구조 | “함수 A의 caller 목록” | 95%+ | AST가 답을 결정론적으로 제공 |
| 70% 구조 + 30% 해석 | “함수 A를 바꾸면 어디가 깨지나” | 85% | 구조는 정확, 해석에서 약간의 오차 |
| 30% 구조 + 70% 해석 | “이 모듈의 설계가 적절한가” | 65% | LLM 판단 비중이 높아짐 |
| 0% 구조 | “이 변수명이 적절한가” | Copilot과 동등 | 메타데이터 우위 없음 |
이 agent의 우위 영역은 구조 의존도가 50% 이상인 질문이다. 그 아래로 가면 Copilot과 차이가 줄어든다.
변수 3: 컨텍스트 윈도우 내 정보 밀도 메타데이터가 정확하고 질문이 구조적이어도 context-assembly가 잘못된 범위를 읽으면 정확도가 떨어진다:
- scope-resolution이 잘못된 레포를 특정 → 이후 모든 분석이 틀림
- context-assembly가 너무 넓게 읽음 → 컨텍스트에 잡음 증가 → LLM 판단 흐려짐
- context-assembly가 너무 좁게 읽음 → 필요한 관계 누락 → 불완전한 답변
이것은 스킬 설계(시스템 프롬프트)의 품질로 조절할 수 있다. Phase 2~3 실험에서 반복 튜닝하면 개선된다.
17.3 총평: 전체 70~85% 정확도 예상
- 구조적 질문(caller 추적, 의존성 나열): 90%+
- 영향도 분석: 75~85%
- 포괄적 설명/판단: 65~75%
Copilot의 크로스 모듈 질문 정확도가 체감 30~50%(환각 포함)인 것을 감안하면, 가장 약한 영역에서도 Copilot보다 낫고 강한 영역에서는 압도적이다.
17.4 Copilot 대비 비교
| 질문 유형 | Copilot 예상 | 이 agent 예상 | 차이 |
|---|---|---|---|
| 단일 함수 이해 | 85% | 85% | 동등 |
| caller/callee 추적 | 40~50% (환각 혼재) | 95%+ | agent 압도 |
| 변경 영향도 (직접) | 30~40% (같은 파일만) | 85~90% | agent 압도 |
| 변경 영향도 (간접) | 20~30% (환각) | 70~80% | agent 대폭 우위 |
| 설정값 영향 | 20% (대부분 모름) | 80~90% | agent 압도 |
| 크로스 모듈 설명 | 40~50% (표면적 요약) | 70~80% | agent 우위 |
18 솔직한 약점과 “정확하게 틀리는” 위험
이 agent가 못하는 영역:
- 런타임 동적 행위:
getattr(module, func_name)()같은 동적 호출, 설정 파일에서 클래스명을 문자열로 읽어 인스턴스를 만드는 패턴은 AST로 잡을 수 없다 - 암묵적 의존성: 이벤트 버스, 메시지 큐, 공유 DB 테이블을 통한 간접 커플링은 코드에 직접적 호출 관계가 없으므로 call-graph에 나타나지 않는다
- 비즈니스 로직 심층 이해: “이 할인 로직이 왜 이렇게 구현되었는가”는 메타데이터가 아니라 도메인 지식의 영역이다
그런데 이 약점들은 Copilot도 똑같이 못한다. 차이는 이 agent는 “모르는 것을 모른다고 말할 수 있다”는 점이다 — 메타데이터에 없으면 “이 경로는 정적 분석으로 추적할 수 없다”고 답할 수 있지만, Copilot은 모르는 것을 환각으로 채운다.
가장 걱정되는 것은 메타데이터에 동적 호출이 빠져 있을 때이다. agent는 “이 함수를 호출하는 곳은 3개입니다”라고 자신있게 답하지만, 실제로는 동적 호출하는 곳이 하나 더 있다. Copilot의 환각과 달리 근거가 있는 오답이므로 사용자가 잘못을 발견하기 더 어렵다.
보완책: 시스템 프롬프트에 “메타데이터의 한계를 응답에 명시하라”는 규칙을 포함해야 한다.
"이 분석은 정적 분석(AST) 기반입니다.
동적 호출(getattr, importlib, 이벤트 버스, 메시지 큐 등)을 통한
간접 의존성은 포함되지 않을 수 있습니다."
이 면책 조항이 아키텍트 수준의 “모르는 것을 모른다고 말할 수 있다”는 능력과도 연결된다.
19 어텐션 희석이 이 agent에서 나타나는 방식
어텐션 희석과 2-Pass 워크플로우에서 다룬 문제가 이 agent에서도 동일하게 발생한다. 블로그 시스템에서는 “형식 규칙 vs 콘텐츠 규칙”의 경쟁이었다면, 코드 분석 agent에서는 “메타데이터 읽기 절차 vs 분석 품질 규칙”의 경쟁이다.
19.1 경쟁 구도
[어텐션 경쟁]
높은 가중치 (잘 따름):
- "call-graph.json에서 callers를 찾아라" → 기계적, 패턴 명확
- "module-graph.json에서 import 관계를 확인하라" → 기계적
낮은 가중치 (자주 무시됨):
- "3-hop 이상은 신뢰도를 명시하라" → 판단 필요, 실행 비용 높음
- "동적 호출 가능성을 경고하라" → 메타데이터에 없는 것에 대한 추론
- "영향도를 과대/과소 평가하지 마라" → 균형 잡기, 추상적
메타데이터 읽기 절차는 형식 규칙과 같은 특성을 가진다 — 패턴이 명확하고 기계적이라 가중치 경쟁에서 이긴다. 분석 품질 규칙(신뢰도 명시, 한계 경고, 균형 잡기)은 콘텐츠 규칙과 같은 특성이다 — 판단이 필요하고 비용이 높아서 밀린다.
19.2 이 agent에서의 대응 전략
블로그 시스템의 3가지 축(공간적·시간적·가중치 분리)을 그대로 적용한다.
공간적 분리 — 이미 스킬 구조로 적용됨: 코어, 컨텍스트 조립, 태스크 스킬, 메타데이터 읽기 가이드가 별도 파일로 분리되어 있다.
시간적 분리 — 분석 + 검증 2-Pass:
1차 Pass: context-assembly + 태스크 스킬 실행 → 분석 결과 생성
이 단계에서는 메타데이터 읽기 + 기본 분석에 집중
활성 규칙: 메타데이터 쿼리 절차, 분석 로직
2차 Pass: 분석 품질 검증
이 단계에서는 1차 결과를 검토
활성 규칙: 신뢰도 적절성, 한계 경고 누락 여부, 과대/과소 평가 검증
가중치 분리 — 핵심 품질 규칙만 태스크 스킬에 반정규화: 각 태스크 스킬의 마지막 Step에 “응답 전 필수 체크” 2~3개만 넣는다.
## 응답 전 필수 체크 (impact-analysis.md 끝)
1. **동적 호출 경고를 포함했는가** — 정적 분석 한계를 명시
2. **hop별 신뢰도를 구분했는가** — 1-hop 95%, 2-hop 80%, 3-hop 70%12개의 품질 규칙을 전부 넣는 것이 아니라, 이 태스크에서 가장 빈번하게 위반되는 2개만 넣는다. 나머지는 2차 Pass(검증)에서 잡는다.
20 메타데이터 동기화: Stale 메타데이터의 위험
이 agent의 가장 큰 운영 위험은 코드는 변경됐는데 메타데이터가 갱신되지 않은 상태이다. Copilot의 환각과 달리, 이 agent는 “근거 있는 오답”을 내놓기 때문에 사용자가 틀림을 발견하기 어렵다.
20.1 위험 시나리오
1. 개발자가 validate_token에 새 caller(api_v2.check_auth)를 추가한다
2. CI가 실패하거나, incremental update가 아직 안 돌았다
3. 사용자가 "validate_token을 바꾸면 영향이 어디까지?"라고 묻는다
4. agent가 call-graph.json(구버전)을 읽고 "caller가 3개입니다"라고 답한다
5. 실제로는 4개 — api_v2.check_auth가 빠져 있다
6. 사용자는 agent의 답변을 신뢰하고 3개만 수정한다
→ api_v2.check_auth가 깨진다
환각보다 위험한 이유: agent가 “call-graph.json에서 확인한 결과”라고 근거를 제시하므로, 사용자가 의심하지 않는다.
20.2 대응 전략
예방 — 갱신 자동화:
| 트리거 | 갱신 범위 | 방법 |
|---|---|---|
| PR 머지 시 | 변경된 파일의 모듈/함수 메타데이터 | CI 파이프라인에 AST 파싱 스크립트 연동 |
| 일일 배치 | 전체 메타데이터 정합성 검증 | 전체 재생성 후 diff 확인 |
| 레포 추가/삭제 시 | Layer 0 시스템 지도 | 수동 트리거 또는 레포 관리 훅 |
탐지 — 메타데이터 타임스탬프:
각 메타데이터 파일에 생성 시점을 포함한다.
{
"_metadata": {
"generated_at": "2026-03-24T14:30:00Z",
"source_commit": "abc1234",
"generator_version": "1.2.0"
},
"functions": { ... }
}agent가 메타데이터를 읽을 때 generated_at과 현재 시점의 차이가 크면(예: 7일 이상) 경고를 포함한다.
"주의: 이 분석에 사용된 메타데이터는 7일 전(2026-03-18) 기준입니다.
이후 코드 변경이 반영되지 않았을 수 있습니다.
최신 결과가 필요하면 메타데이터 갱신을 먼저 실행해주세요."
완화 — “메타데이터 + 코드” 교차 검증:
높은 정확도가 필요한 질문(변경 영향도 등)에서는 메타데이터 결과를 코드로 교차 검증한다.
Step 5 (교차 검증):
call-graph.json에서 찾은 caller 목록을 실제 코드에서 grep으로 확인한다
메타데이터에 없는 호출이 발견되면:
1. 응답에 "메타데이터에 없는 추가 호출이 발견되었습니다" 경고 포함
2. 추가 caller를 영향 분석에 포함
3. 메타데이터 갱신을 권장
이 교차 검증은 비용(코드를 직접 읽어야 함)이 들지만, 영향도 분석처럼 오답의 결과가 치명적인 태스크에서는 필수이다.
21 본질: “시니어 개발자의 사고 과정”을 모델링한 것
시니어 개발자가 “이 함수 바꾸면 어디가 깨져?”라는 질문을 받으면:
- 머릿속에서 모듈 구조를 떠올린다 (= Layer 0, 시스템 지도)
- “이건 auth 모듈이니까 api_router랑 middleware가 영향받겠다” (= Layer 1, 모듈 요약)
- 구체적으로 어떤 함수들이 호출하는지 기억하거나 IDE에서 찾는다 (= Layer 2, call-graph)
- 해당 코드를 열어서 실제 사용 패턴을 확인한다 (= Layer 3, 코드 읽기)
이 agent의 실행 흐름이 이것과 정확히 동일한 사고 경로를 따른다. 시니어 개발자의 구조 지식을 메타데이터로 외재화하고, 사고 절차를 시스템 프롬프트로 명시한 것이다. Copilot은 이런 사고 경로 없이 코드를 보고 즉석에서 추론한다. 코드베이스를 처음 보는 주니어와 같다. 이 agent는 코드베이스를 이미 아는 시니어의 구조 지식을 가지고 추론한다. 같은 LLM이라도 입력되는 맥락의 질이 다르면 출력의 질이 다르다.
22 관련 주제
선행 지식
- Skill-based Agent Pattern — 스킬 기반 패턴의 기초. 이 포스트의 스킬 레이어 구조의 원형
- 시스템 프롬프트 분해 6단계 — 분해 방법론. 코어/스킬/도메인 분리의 이론적 기반
- 시스템 프롬프트 유형 분류와 혼합 설계 — A+B+D 혼합 유형의 레이어 설계 원칙
- 프롬프트 설계와 SW 설계의 구조적 대응 — 반정규화 판단 기준. “메타데이터 한계 경고”의 반정규화 근거
- 어텐션 희석과 2-Pass 워크플로우 — 이 agent의 분석 품질 규칙이 무시되는 문제와 대응 전략
- Codebase Analyzer Agent: Architecture 비교 — Copilot vs GraphRAG 아키텍처 비교. 이 포스트는 그중 System Prompt 접근의 상세 설계
관련 포스트
- 스킬 패턴의 실전 적용: 블로그 지식 관리 시스템 — 이 설계의 원형이 된 블로그 프로젝트의 스킬 구조