1 왜 레지스트리인가
C27 스킬 정의가 한 스킬의 정의를 표준화했다면, 150+개 스킬이 운영에 들어가기 시작하면 다음이 필요해진다.
| 운영 요구 | 레지스트리 없이의 결과 |
|---|---|
| LLM이 “어떤 스킬을 호출할지” 동적 선택 | 코드에 hardcode된 if/elif 폭증 |
| 새 스킬 도입 시 기존과 중복 검사 | 사람 review가 모든 PR마다 |
| 한 스킬을 사용하는 모든 에이전트 추적 | 변경 영향 분석 불가 |
| 스킬 버전 결정 (latest·active·pinned) | 런타임 에러·일관성 깨짐 |
| 스킬 사용 통계 (분기 audit) | 어느 스킬을 deprecate할지 결정 못 함 |
레지스트리는 단일 진실의 카탈로그다. 모든 스킬·관계·메타가 한 곳에서 조회 가능하고, 변경은 명시적 절차로만.
2 데이터 모델
# app/skills/registry/models.py
from datetime import datetime
from pydantic import BaseModel
from typing import Literal
class SkillRecord(BaseModel):
skill_id: str # "summarize_doc"
version: str # "1.3.0"
stage: Literal["draft", "canary", "staged", "active", "deprecated", "retired"]
traffic_pct: float # canary·staged 시 비중
owner: str
description: str
tags: list[str] # ["summarization", "multi-step"]
# 의존성
tools_required: list[str]
tools_optional: list[str]
skills_called: list[str] # composition (C29)
models_compatible: list[str] # ["gpt-4o", "claude-3.5"]
# 위치
git_path: str # "skills/summarize_doc/SKILL.md"
git_sha: str # 정확한 commit hash
# 메타
created_at: datetime
activated_at: datetime | None = None
deprecated_at: datetime | None = None
sunset_date: datetime | None = None
# 검증 산출물 (참조)
golden_eval_run_id: str | None = None
aa_test_run_id: str | None = None
# 검색 보조
embedding: list[float] | None = None # description + tags 임베딩 (검색용)C26 AgentRecord와 거의 동일한 패턴 — 스킬도 6단계 lifecycle. 차이는 tools_required·skills_called·models_compatible 같은 의존 필드.
3 등록 API
# app/skills/registry/store.py
class SkillRegistry:
def __init__(self, db, git_repo):
self.db = db
self.git = git_repo
def register(self, skill_dir: Path, owner: str) -> SkillRecord:
# 1. SKILL.md 파싱 + 검증
manifest = parse_manifest(skill_dir / "SKILL.md")
validate_schema(manifest)
# 2. 중복 검사
if self.db.skills.find_one(skill_id=manifest["id"], version=manifest["version"]):
raise ValueError(f"already exists: {manifest['id']}@{manifest['version']}")
# 3. 의존 도구 존재 확인
for tool in manifest["tools_required"]:
if not self.tools.exists(tool):
raise ValueError(f"required tool not registered: {tool}")
# 4. 호출하는 다른 스킬 존재 확인 (composition)
for called in manifest.get("skills_called", []):
if not self.skills.exists(called):
raise ValueError(f"called skill not registered: {called}")
# 5. 임베딩 생성 (검색용)
embedding = embed(manifest["description"] + " ".join(manifest["tags"]))
# 6. git sha 확보 + insert
record = SkillRecord(
**manifest,
stage="draft",
traffic_pct=0,
owner=owner,
git_path=str(skill_dir / "SKILL.md"),
git_sha=self.git.head_sha(),
embedding=embedding,
created_at=datetime.utcnow(),
)
self.db.skills.insert(record)
return record등록은 C26 promote 패턴과 같음 — draft 시작, 단계별 게이트 필수.
4 검색·Discovery
4.1 이름·태그 기반
def find_by_name(self, query: str) -> list[SkillRecord]:
"""exact·prefix·fuzzy."""
return self.db.skills.find(skill_id=query) \
or self.db.skills.find(skill_id={"$regex": f"^{query}"}) \
or self._fuzzy_search(query)
def find_by_tag(self, tag: str) -> list[SkillRecord]:
return self.db.skills.find(tags=tag, stage="active")4.2 임베딩 기반 — Semantic Search
LLM이 “이 작업에 적합한 스킬”을 찾을 때:
def find_by_intent(self, intent: str, top_k: int = 5) -> list[tuple[SkillRecord, float]]:
intent_emb = embed(intent)
candidates = self.db.skills.find(stage="active")
scored = [(s, cosine(intent_emb, s.embedding)) for s in candidates]
return sorted(scored, key=lambda x: -x[1])[:top_k]
# 예: LLM-driven routing
candidates = registry.find_by_intent("이 PDF를 한 페이지로 정리")
# → [summarize_doc 0.87, summarize_pdf 0.85, brief_generate 0.72]이 검색이 C29 동적 라우팅의 핵심 입력 — 다음 편에서 깊이 다룸.
4.3 Hybrid Search
이름·태그·임베딩을 결합한 점수:
def find_hybrid(self, query: str, top_k: int = 10):
name_hits = {s.skill_id: 1.0 for s in self.find_by_name(query)}
tag_hits = {s.skill_id: 0.6 for s in self.find_by_tag(query)}
semantic_hits = {s.skill_id: score * 0.4 for s, score in self.find_by_intent(query, 20)}
combined = defaultdict(float)
for hits in [name_hits, tag_hits, semantic_hits]:
for sid, score in hits.items():
combined[sid] = max(combined[sid], score)
return sorted(combined.items(), key=lambda x: -x[1])[:top_k]5 버전 Resolution
같은 스킬 ID에 여러 버전이 동시 존재 (canary·staged·active). 호출 시 어느 것을 줄지 정책 필요.
class VersionResolver:
"""호출자가 명시 안 했을 때의 정책."""
def resolve(self, skill_id: str, ctx: dict | None = None) -> SkillRecord:
# 1. 정확 버전 명시
if ctx and ctx.get("version"):
return self._exact(skill_id, ctx["version"])
# 2. 호출 사용자가 canary·staged 대상이면
if ctx and self._user_in_experiment(ctx["user_id"], skill_id):
return self._experimental(skill_id, ctx)
# 3. active stage 기본
return self._active(skill_id)
def _active(self, skill_id) -> SkillRecord:
rec = self.db.skills.find_one(skill_id=skill_id, stage="active")
if not rec:
raise NotFound(f"no active version of {skill_id}")
return rec
def _exact(self, skill_id, version) -> SkillRecord:
return self.db.skills.find_one(skill_id=skill_id, version=version)
def _experimental(self, skill_id, ctx) -> SkillRecord:
# sticky hash로 일관 할당
if sticky_hash(ctx["user_id"], skill_id) < canary_threshold(skill_id):
return self.db.skills.find_one(skill_id=skill_id, stage="canary")
return self._active(skill_id)호출자가 버전 결정의 부담을 갖지 않음 — registry가 정책 적용.
6 의존성 그래프
[Tool] ─── (used by) ──→ [Skill] ─── (used by) ──→ [Agent]
↓
(calls) (composition)
↓
[Skill]
이 관계를 그래프로 저장 — 영향 분석에 핵심.
# app/skills/registry/dependencies.py
from collections import defaultdict
import networkx as nx
class DependencyGraph:
def __init__(self):
self.G = nx.DiGraph()
def build(self, registry):
for skill in registry.all_active():
for tool in skill.tools_required:
self.G.add_edge(tool, skill.skill_id, kind="tool")
for sub in skill.skills_called:
self.G.add_edge(sub, skill.skill_id, kind="skill")
for agent in registry.all_active_agents():
for skill in agent.skills:
self.G.add_edge(skill, agent.agent_id, kind="agent")
def downstream(self, node: str) -> list[str]:
"""이 노드에 의존하는 모든 상위."""
return list(nx.descendants(self.G, node))
def upstream(self, node: str) -> list[str]:
"""이 노드가 의존하는 모든 하위."""
return list(nx.ancestors(self.G, node))
def detect_cycles(self) -> list[list[str]]:
return list(nx.simple_cycles(self.G))6.1 영향 분석
# tool API 변경 시 영향 받는 스킬·에이전트
affected = graph.downstream("github_search")
# → ["summarize_pr", "code_review", "qna_chatbot"]
# 한 스킬 deprecate 시 영향
to_notify = graph.downstream("summarize_doc_v1")C26 deprecate 알림이 이 그래프 사용.
6.2 Cycle Detection
스킬 A가 B를 호출하고 B가 A를 호출하면 무한 재귀. 등록 시점에 차단:
7 Storage 전략
7.1 Git + DB Hybrid
[Git] (단일 진실 — SKILL.md 파일)
↓ (commit hook)
[CI/CD] (validation·등록)
↓
[DB] (조회·검색·통계)
- Git: SKILL.md, prompt.j2, tests — 사람·도구가 직접 수정
- CI/CD: PR 머지 시 자동 검증 + DB 동기화
- DB: 빠른 조회·통계·embedding 인덱스
# .github/workflows/skill_register.yml
on:
push:
paths: ["skills/**/SKILL.md"]
jobs:
validate_and_register:
steps:
- uses: actions/checkout@v4
- run: python -m scripts.validate_skill --path skills/${{ env.SKILL_DIR }}
- run: python -m scripts.run_golden_eval
- run: python -m scripts.register_to_db --stage draft이 패턴이 07-1 GitHub Actions 위에 자연스럽게 얹힘.
8 Caching 전략
레지스트리 read는 매 query마다 발생. DB 직접 조회는 비효율.
# app/skills/registry/cache.py
import redis
class CachedRegistry:
def __init__(self, db, cache_ttl=300):
self.db = db
self.redis = redis.Redis()
self.ttl = cache_ttl
def resolve(self, skill_id: str, ctx: dict | None = None) -> SkillRecord:
key = f"skill:{skill_id}:{self._ctx_hash(ctx)}"
cached = self.redis.get(key)
if cached:
return SkillRecord.parse_raw(cached)
record = self.resolver.resolve(skill_id, ctx)
self.redis.setex(key, self.ttl, record.json())
return record8.1 Cache Invalidation
스킬 promote·deprecate 시 캐시 무효화 필수:
def on_skill_promote(skill_id: str):
# 패턴 매칭으로 일괄 삭제
for key in redis.scan_iter(f"skill:{skill_id}:*"):
redis.delete(key)또는 stale-while-revalidate — 캐시 hit 후 백그라운드로 갱신:
9 CI/CD 통합 — PR Workflow
1. 개발자가 skills/new_skill/SKILL.md 추가하는 PR 생성
2. CI가 자동 실행:
- SKILL.md schema 검증
- tools_required 존재 확인
- skills_called 존재 확인 + cycle 검사
- Golden eval (C30) 실행
- A/A test
3. 모두 통과 → PR에 자동 코멘트 (검증 결과)
4. 사람 reviewer 승인
5. 머지 → Webhook이 등록 API 호출 → DB에 draft 추가
6. 매뉴얼 또는 자동으로 promote (canary → ...)
코드와 동일 절차로 운영 — 스킬 변경의 안전성이 코드 변경 수준으로.
10 MINERVA 적용
app/skills/registry/
├── models.py # SkillRecord schema
├── store.py # DB 인터페이스
├── git_sync.py # SKILL.md ↔ DB 동기화
├── search.py # name/tag/embedding/hybrid
├── resolver.py # 버전 정책
├── dependencies.py # NetworkX 그래프
├── cache.py # Redis cache + invalidation
└── audit.py # 사용 통계·deprecated 탐지
scripts/
├── skill_register.py # CLI 등록
├── skill_search.py # CLI 검색
├── skill_audit.py # 사용 빈도·고아 스킬 탐지
└── skill_graph.py # 의존성 시각화
infra/
├── postgres # registry primary
├── redis # cache
└── pgvector # embedding search
C26 AgentRecord registry와 같은 인프라 — 통합 운영.
11 자주 발생하는 함정
11.1 Registry SPOF
레지스트리 down → 라우팅 결정 못 함 → 시스템 전체 down. C26 함정 동일.
해법: - read는 cache 우선 (Redis 5분 TTL) - registry down 시 last-known config로 degraded 모드 - write는 git이 primary (DB는 read replica)
11.2 Cache Invalidation
promote 후 일부 노드의 cache가 갱신 안 됨 → 옛 버전이 아직 호출됨.
해법: - promote 직후 invalidation 강제 (모든 cache 노드에 publish) - TTL을 짧게 (5분 이하) - stale-while-revalidate로 미세 차이 감수
11.3 Search Hit Miss
LLM이 의도와 다른 스킬을 찾음 — 임베딩이 description의 표현 차이에 민감.
해법: - 임베딩 + tags + name의 hybrid score - 사용자 피드백 (“이 스킬이 적합했나요?”) 수집 → embedding fine-tune - 분기마다 search 정확도 평가 (Top-1·Top-5)
11.4 Dependency Cycle
A → B → A 같은 사이클이 등록 시 차단됐어도, 별도 PR이 합쳐서 사이클이 만들어질 수 있음.
해법: - 등록 시점뿐 아니라 매 PR merge 후 graph audit - 사이클 발견 시 자동 PR comment + alert
11.5 Orphan Skills
호출되지 않는 스킬이 active 상태로 남아있음 — 코드 부채.
해법: - 분기 audit — 6개월 호출 0건이면 deprecation 검토 - 의존성 그래프에서 indegree 0인 active 스킬 자동 표시
11.6 Git ↔︎ DB Drift
git에는 새 SKILL.md, DB에는 old version. CI가 일시 실패한 후 손으로 안 맞춤.
해법: - git을 단일 진실로 — DB는 캐시·인덱스 역할 - 매일 reconciliation job (git → DB 일관성 점검) - 불일치 시 alert + 자동 재등록
11.7 Backward-incompatible 변경
tools_required에서 도구 1개 제거 → 호출 에이전트의 권한 매트릭스도 변경 필요. registry가 추적 안 하면 모름.
해법: - 의존성 graph 변경을 breaking change로 분류 (semver minor → major) - 영향 받는 모든 노드에 자동 알림 - C26 governance 게이트 강제
12 정리
| 영역 | 핵심 |
|---|---|
| 데이터 모델 | SkillRecord — id·version·stage·dependencies·embedding |
| 등록 | git → CI 검증 → DB draft 등록 → C26 promote |
| 검색 | name·tag·embedding hybrid, semantic 위주 |
| Resolution | exact·experimental(canary)·active 기본 |
| 의존성 | tool→skill→skill·agent 그래프, cycle 차단, 영향 분석 |
| Storage | git primary + DB cache, 매일 reconciliation |
| Cache | Redis + invalidation + stale-while-revalidate |
| CI/CD | PR 머지 → 자동 검증·등록·draft 진입 |
| 함정 | SPOF·cache invalidation·search miss·cycle·orphan·drift·breaking change |
13 응용 분야
| 시나리오 | 활용 |
|---|---|
| LLM이 적합한 스킬 동적 선택 | semantic search + hybrid score |
| 새 도구 도입 영향 분석 | dependency graph downstream |
| 사용 안 되는 스킬 정리 | orphan detection (indegree 0) |
| 분기 운영 보고서 | registry audit (사용 빈도·중복·deprecated) |
| 스킬 variant A/B 분배 | resolver의 experimental policy |
| 외부 모델 deprecation 대응 | models_compatible 필드로 영향 스킬 추출 |
14 관련 주제
선행 학습 (선수)
- C27 스킬 정의 — SKILL.md 표준 (등록 단위)
- C26 에이전트 생명주기 — 6단계 게이트 패턴
- C24 하네싱 — 권한·도구 매트릭스 통합
18-LangGraph 시리즈 cross-reference
- #16 SKILL.md 명세 — 본 편 SKILL.md 표준의 origin
- #14 스킬 설계와 생성 — 등록 워크플로 이론
후속 (Phase C-7)
- C29 스킬 조합·동적 선택 — registry semantic search 활용
- C30 스킬 테스트·품질 게이트 — CI 검증 단계 깊이
Cross-reference
- 07-1 CI/CD GitHub Actions — 자동 등록 워크플로
- 11-1 Reproducible Build — git_sha 영구 보관
- Engineering: Pydantic — SkillRecord schema
- Engineering: JSON Schema — SKILL.md schema 검증