
모두가 에이전트에게 기술을 부여합니다. 저는 기술이 스스로 에이전트를 부화시키도록 만들었습니다.
요약
에이전트의 기술(skills)을 단순한 프롬프트 산문이 아닌, 컴파일 가능한 소스 코드로 다뤄야 한다는 아키텍처적 관점을 제시합니다. 현재의 프롬프트 기반 방식은 컨텍스트 오염과 아키텍처 부패 문제를 야기하므로, 결정론적 실행을 위한 컴파일 단계가 필요함을 강조합니다.
핵심 포인트
- 단순 프롬프트 방식은 기술 간 간섭과 아키텍처 부패를 유발함
- 기술(skill)은 프롬프트의 부속품이 아닌 에이전트의 소스 코드여야 함
- 컴파일 단계를 통해 타입 체크와 모호성을 사전에 제거해야 함
- 에이전트 기술의 목표는 더 잘 쓰는 것이 아니라 에이전트로 컴파일하는 것
저는 직장에서 사용하는 내부 에이전트 스택을 위해 2주 동안 SKILL.md를 작성하는 데 시간을 보냈습니다. 제가 3개월 넘게 피땀 흘려 만든 모든 API 엔드포인트, 모든 MCP 설정, 모든 토큰 예산(token-budget) 규칙을 깔끔한 마크다운(markdown) 형식으로 기록했습니다. 저는 이것을 결과물로 제출했습니다.
그러고 나서 Claude Code가 그것을 실행하는 것을 지켜보았습니다.
SKILL.md 파일은 산문(prose)입니다. 사람이 읽기 위해 사람이 작성한 산문이죠. 그런 다음 그것을 시스템 프롬프트(system prompt)에 붙여넣고, LLM이 매 턴(turn) 실행 시점에 당신이 의도한 바를 파악하도록 요청합니다.
기술 하나라면 괜찮습니다. 세 개라면 관리할 만합니다. 다섯 개 이상인가요? 상황이 망가지기 시작합니다.
| 발생 현상 | 이유 |
|---|---|
| 기술들이 서로 간섭함 (Skills leak into each other) | 모든 기술이 하나의 컨텍스트 윈도우(context window)를 공유하기 때문입니다. 파일 정리 기술과 git-ops 기술이 서로 오염됩니다. 에이전트가 한 기술의 로직을 다른 기술에 적용해 버립니다. |
| ... |
이것은 Anthropic의 버그도, OpenAI의 버그도 아닙니다. 당신의 기술이 나쁘기 때문도 아닙니다. 이것은 아키텍처의 부패(architectural decay)입니다. 당신은 하나의 LLM에게 격리(isolation)가 전혀 되지 않은 일곱 개의 산문 조각을 동일한 컨텍스트 윈도우 내에서 해석하도록 요구하고 있으며, 그 해석은 실행할 때마다 달라집니다.
이렇게 생각해 보세요. 누군가에게 일곱 권의 운영 매뉴얼을 건네주고 질문을 던지는 것과 같습니다. 그 사람은 매번 일곱 권의 책을 모두 넘겨가며 답변을 짜 맞춰야 합니다. 그 사람이라면 정신이 나갈 것입니다. LLM도 마찬가지입니다. 다만 조용히 나갈 뿐이죠.
핵심 문제는 SKILL.md가 소프트웨어 공학(software engineering)이 아니라 프롬프트 엔지니어링(prompt engineering)이라는 점입니다. 컴파일 단계(compile step)도 없고, 타입 체크(type checking)도 없으며, 당신과 모델 사이의 계약(contract)도 없습니다.
논지: 기술은 소스 코드이고, 에이전트는 바이너리이다
나머지 부분을 건너뛰더라도, 이 부분만큼은 실제로 깊이 고민해 보셨으면 합니다.
현재 모든 이들이 기술(skills)을 작성하고 있습니다. Claude Code를 위해, Codex CLI를 위해, OpenClaw를 위해, 그리고 다음에 무엇이 오든 그 기술들을 위해 말이죠. 그리고 이 모든 경우에 기술의 역할은 동일합니다. 시스템 프롬프트에 채워 넣어 실행 시점에 호스트 에이전트(host agent)에 의해 해석되는 산문일 뿐입니다. 기술은 프롬프트의 부속품(accessory)입니다. 스스로 존재할 수 없습니다.
그것은 잘못된 패러다임입니다.
기술은 프롬프트의 부속품이 되어서는 안 됩니다. 기술은 에이전트의 소스 코드(source code)가 되어야 합니다.
Java는 JVM을 위한 바이트코드(bytecode)로 컴파일됩니다. TypeScript는 브라우저를 위한 JavaScript로 컴파일됩니다. 컴파일 단계가 존재하는 이유는 런타임(runtime) 이전에 인간의 표현을 기계가 결정론적(deterministically)으로 실행할 수 있는 형식으로 변환하기 때문입니다. 오타, 타입 오류(type errors), 모호성(ambiguity) 등 모든 것이 에이전트가 실행되기도 전인 컴파일 타임(compile time)에 포착됩니다.
SKILL.md에는 컴파일 단계가 없습니다. 매 턴마다, 영원히, LLM에 가공되지 않은 산문(prose)을 전달하고 최선의 결과가 나오기를 바랄 뿐입니다.
따라서 기술(skill)의 최종 목표는 "더 잘 쓰는 것"이 아닙니다. "에이전트로 컴파일하는 것"입니다. 기술은 에이전트를 위한 부화(hatching) 입력값이 되어야 합니다. 당신이 기술을 작성하면, 컴파일러가 이를 자체적인 도구(tools), 자체적인 상태 머신(state machine), 자체적인 설정(config)을 갖춘 독립적인 런타임(runtime)으로 변환합니다. 기술은 더 이상 프롬프트의 파편이 아니라 하나의 프로그램이 됩니다.
그것이 바로 agenthatch가 하는 일입니다. 당신은 여전히 마크다운(markdown)으로 기술을 작성하며, 그 부분은 변하지 않습니다. 생성된 에이전트는 호스트(host) 없이도 독립적으로 실행됩니다. agenthatch는 그 사이의 단계, 즉 컴파일러입니다. .java에 javac가 있다면, SKILL.md에는 agenthatch가 있습니다.
이 개념을 내재화하고 나면, 여러 가지 사항들이 딱 맞아떨어집니다:
- 기술이 시스템 프롬프트(system prompt)에서 토큰을 낭비하는 일이 중단됩니다. 컴파일된 에이전트는 약 150바이트의 런타임 설정(runtime config)만을 가집니다.
- 각 기술은 격리된 에이전트가 됩니다. 더 이상의 교차 오염(cross-contamination)은 없습니다.
- 스키마 검증(Schema validation)이 컴파일 타임에 이루어집니다. 오타와 모호성은 런타임 이전에 사라집니다.
- 출력물은 실제 Python 패키지입니다.
pip install,import를 통해 어디서든 실행할 수 있으며, 호스트가 필요하지 않습니다.
파이프라인: 3단계, 6개의 하네스(harnesses)
여기에 아키텍처가 있습니다. 제가 언급하는 모든 파일 경로는 실제이며, 리포지토리(repo)에서 직접 열어볼 수 있습니다.
SKILL.md → Parse → 6-Harness LLM Pipeline → Code Generation → Runnable Agent
(input) (Phase 1) (Phase 2: AI inference) (Phase 3: Jinja2) (output)
Phase 1: 결정론적 파싱(deterministic parse), AI 미사용
Phase 1은 AI를 사용하지 않습니다. SKILL.md를 읽고, 프론트매터 (frontmatter), 본문, 그리고 스킬 디렉토리 내의 모든 파일을 추출합니다. 순수한 파일 시스템 작업 (filesystem operations)입니다. 진입점은 parser.py의 assemble_context()입니다:
def assemble_context(skill_path: str | Path) -> ContextPack:
skill_dir = _resolve_skill_directory(Path(skill_path))
dir_name = skill_dir.name
...
여기서 핵심적인 설계 결정은 다음과 같습니다: Phase 1은 어떠한 의미론적 판단 (semantic judgment)도 하지 않습니다. 특정 파일이 스크립트인지, 문서인지, 아니면 설정 파일인지 추측하려 하지 않습니다. 그것은 Phase 2의 역할입니다. Phase 1은 그저 바이트 (bytes)를 읽고, SHA-256 해시 (hashes)를 계산하며, YAML 파싱 (parsing)을 수행할 뿐입니다.
파일 리더에서 제가 좋아하는 작은 디테일이 하나 있습니다. PNG, JPEG, PDF, ZIP 등을 건너뛰기 위해 바이너리 매직 넘버 (binary magic numbers)를 확인합니다:
_BIN_SIGS: list[bytes] = [
b"\x89PNG\r\n\x1a\n",
b"\xff\xd8\xff",
...
1MB를 초과하는 파일은 건너뜁니다. 헤더에 널 바이트 (null bytes)가 포함된 파일도 건너뜁니다. 결정론적 (deterministically)으로 처리할 수 있는 것이 있다면, LLM에게 묻지 마십시오. 이 원칙은 이 코드베이스 전반에 걸쳐 반복해서 나타납니다.
Phase 1.5: AST 시그니처 추출 (AST signature extraction)
이 기능은 v0.8에 추가되었으며, 저는 이것이 프로젝트 전체에서 가장 과소평가된 부분이라고 생각합니다.
Phase 1.5는 Python의 내장 ast 모듈을 사용하여 Python 스크립트를 파싱하고, 정규 표현식 (regex)을 사용하여 쉘 스크립트를 파싱함으로써 함수 시그니처 (function signatures)를 추출합니다. 결정론적이며, LLM을 전혀 사용하지 않습니다. 이는 정확한 인터페이스 추론 (interface inference)을 위한 Harness C로 전달됩니다.
def extract_python_signatures(file_path: Path) -> list[ToolSchema]:
"""AST-parse a Python script, extract public function signatures.
Deterministic, zero LLM. Uses Python's built-in ast module.
...
왜 굳이 이렇게 할까요? Harness C가 도구 시그니처(tool signatures)를 설계해야 하기 때문입니다. 만약 원본 스크립트 내용을 그대로 읽는다면, 토큰을 낭비하고 환각 (hallucination)을 일으킵니다. 대신 AST를 통해 추출된 1KB 크기의 압축된 시그니처 요약을 전달하면 추론 (inference) 품질이 급격히 향상됩니다. 이것이 컴파일러적 사고방식입니다. 결정론적 (deterministic)으로 추출할 수 있는 것은 무엇이든 추출하고, 진정으로 모호한 부분만 LLM에게 남겨두는 것입니다.
2단계: 6개의 AI 하네스 (harnesses)
이것이 핵심입니다. 6개의 특화된 하네스가 기술을 처리하며, 각 하네스는 고유한 페르소나와 온도 (temperature) 설정을 가집니다. 설정은 engine.py에 하드코딩되어 있습니다:
HARNESS_CONFIG: dict[str, dict[str, Any]] = {
"A": {"thinking": True, "temperature": 0.1,
"reason": "Identity extraction is deterministic — low temp for consistency"},
...
모든 온도 설정에는 이유가 있습니다. 정체성 추출 (Identity extraction)은 결정론적이므로 온도를 0.1로 낮춥니다. 의도 추론 (Intent inference)은 롱테일 (long-tail) 트리거를 포괄해야 하므로 창의성을 위해 0.5를 할당합니다. 조립 검증 (Assembly validation)은 구조화되어 있으므로 0.2를 사용합니다.
| 하네스 (Harness) | 역할 (Job) | 모델 계층 (Model tier) | 온도 (Temp) |
|---|---|---|---|
| A — Identity | 프론트매터 (frontmatter)에서 이름, 버전, 설명을 추출 | small | 0.1 |
| ... |
왜 6개일까요? 모든 것을 수행하는 하나의 거대한 프롬프트를 시도해 보았으나, 결과가 복권처럼 운에 맡겨야 했기 때문입니다. 각 하네스가 한 가지 일만 하도록 분리했더니 품질이 크게 향상되었습니다. 컴파일러가 프론트엔드를 어휘 분석기 (lexer), 구문 분석기 (parser), 의미 분석 (semantic analysis)으로 나누는 것과 같은 이유입니다. 단일 책임 원칙 (Single responsibility)입니다.
각 하네스는 최대 2회의 내부 재시도 (retries)를 포함하는 분석 (Analyze), 추론 (Infer), 자체 검증 (Self-Validate), 수정 (Correct) 루프를 실행합니다. 모든 하네스는 고유한 validate_output()을 가집니다. 하네스 A는 identity.id가 케밥 케이스 (kebab-case)인지 확인합니다:
def validate_output(self, result: dict[str, Any]) -> tuple[bool, str]:
identity = result.get("identity", {})
identity_id = identity.get("id", "")
...
Harness B는 트리거(triggers) 개수가 5에서 15 사이인지, 만족(satisfies) 항목이 3에서 8 사이인지, 그리고 요약(summary)이 최소 20자 이상인지를 확인합니다. 이러한 제약 조건은 LLM이 결정하는 것이 아닙니다. 코드로 강제됩니다. 만약 LLM이 규정에 맞지 않는 출력을 생성하면, 다시 수행하도록 되돌려 보내집니다.
Harness E는 매우 중요한 단계입니다. 이는 나머지 5개의 Harness를 교차 검증(cross-validate)하고 통합된 AHSSPEC (Agent Hatch Standard Specification)을 생성합니다. 또한 E는 구조적 신뢰도 점수(structural confidence score)를 계산하는데, 여기서 중요한 점은 이것이 LLM의 자기 평가(self-assessment)가 아니라 코드가 필드(fields)를 직접 세는 방식이라는 것입니다:
def _compute_structural_confidence(self, ahs_dict: dict[str, Any]) -> float:
"""LLM의 자기 평가가 아닌, 구조적 검사를 기반으로 신뢰도를 계산합니다."""
checks = 0
...
저는 LLM이 스스로 보고하는 신뢰도를 믿지 않습니다. 모델은 필수 필드 3개를 누락하면서도 즐겁게 "신뢰도 0.95"라고 말할 것입니다. 필드를 세는 코드는 거짓말을 하지 않습니다.
또한 기술 유형(skill type)별로 모델 계층(model tiers)을 선택하는 사전 비행 분류기(pre-flight classifier)도 있습니다. 순수 지시(pure-instruction) 기술은 Harness D를 완전히 건너뜁니다 (감지할 베이스 클래스가 없음). API 호출 및 스크립트가 포함된 통합(integration) 기술은 모든 것을 대형 모델(large models)로 업그레이드합니다. 모든 기술에 비싼 모델이 필요한 것은 아닙니다. 이 분류기는 실제 비용을 절감해 줍니다.
MODEL_TIER_MAP: dict[str, dict[str, str]] = {
"pure_instruction": {
"A": "small", "B": "small", "C": "large", "D": "skip", "E": "small", "F": "small",
...
Phase 3: 코드 생성 (code generation)
Phase 3는 Jinja2 템플릿을 통해 AHSSPEC을 완전한 Python 패키지로 렌더링합니다. 엔진은 generate/engine.py에 있는 GenerateEngine입니다:
TEMPLATE_MAP: dict[str, str] = {
"pyproject.toml.j2": "pyproject.toml",
"agent.py.j2": "src/{package_name}/agent.py",
...
하지만 3단계(Phase 3)는 단순히 템플릿을 렌더링하는 것만이 아닙니다. 여기에는 AI 코드 생성(AI code generation) 단계가 포함됩니다. _ai_generate_tool_impls()는 전체 기술 디렉토리 컨텍스트(SKILL.md, 참조 파일, 스크립트 파일, 템플릿)를 읽고, LLM이 각 도구에 대한 실제 Python 함수 본문을 생성하도록 합니다. 단순한 스텁(stub)이 아닙니다.
핵심적인 부분은 검증(validation)입니다. AI가 생성한 코드는 디스크에 직접 기록되지 않습니다. 먼저 compile()을 통해 체크됩니다. 만약 실패하면, 엔진은 들여쓰기(indentation)를 자동으로 수정하려고 시도합니다.
wrapper = "def _validate():\n" + indented + "\n"
try:
compile(wrapper, f"<tool:{func_name}>", "exec")
...
이것이 컴파일러의 태도입니다. 생성된 코드는 반드시 컴파일되어야 합니다. 컴파일되지 않는다면 수정하십시오. 수정할 수 없다면 템플릿 스텁(template stub)으로 되돌아갑니다(fall back). 구문적으로 깨진 코드가 런타임(runtime)에 도달하게 해서는 절대 안 됩니다.
또한 생성된 모든 .py 파일을 스캔하여 유출된 JavaScript 키워드(null, undefined, true, false)가 있는지 확인하는 _validate_generated_python() 패스(pass)가 있습니다. LLM이 Python을 TypeScript처럼 작성하려고 시도하는 것을 너무 여러 번 목격한 후, v0.7.15 버전에서 추가되었습니다.
결과물
출력물은 실제 Python 패키지입니다:
hatched-agent/
├── pyproject.toml # pip 설치 가능
├── runtime.toml # LLM 제공자, 모델, API 키
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기