에이전트가 다루는 절차형 스킬이 100개를 넘어가면 무슨 일이 생길까요? 우선 스킬의 프론트매터를 적재하는 것만으로도 에이전트의 컨텍스트를 무겁게 만듭니다. 그뿐만이 아닙니다. 여러 스킬에 복제된 참고 자료나 운영 절차가 일부는 갱신되고 일부는 갱신되지 않으면서 일관성이 깨지기 시작합니다.

제가 일하고 있는 소울스트림에서는 이 문제를 어떻게 해결했을까요? 스킬 본문에는 절차를 실행하는 순서나 검증 게이트만을 남깁니다. 필요한 하위 스킬이나 레퍼런스는 해당 정보를 런타임에 찾아서 링크하라는 지시문으로 기입합니다. 그러면 스킬을 읽어들일 때 시스템이 필요한 하위 스킬과 레퍼런스를 조립해서 스킬 본문을 만들어줍니다. 마치 운영체제의 동적 링크 라이브러리(Dynamic Link Library, DLL)처럼 말이죠1.

제가 지금 작성 중인 이 원고부터 방금 개편을 마친 스킬 로더를 거쳐 만들어지고 있습니다. 인사이트 아티클 발행 스킬을 호출하자, 서버 컴파일러가 본문의 선언형 링크 블록을 해석해 블로그 분류 체계 카탈로그(blog-taxonomy)를 즉시 본문에 포함했습니다. 반대로 이미지 생성이나 문장 검수, 블로그 발행처럼 특정 단계에서만 사용하는 하위 스킬은 3줄짜리 호출 포인터로만 표시해 두었지요. 개편 전 127개 스킬(총 686KB)을 전수 조사해 132개 스킬과 11개 카탈로그의 동적 링크 아키텍처로 재편하면서, 운영체제의 링커 개념이 LLM 에이전트의 스킬을 관리하는 데도 유용하다는 걸 확인할 수 있었습니다.

원본 스킬 문서 (insight-article)1. 실행 순서 및 검증 게이트include: blog-taxonomy필수 레퍼런스 포함 지시문call: blog-publish하위 스킬 위임 지시문load_skill()동적 링커 조립런타임 심볼 결합조립된 스킬 본문 (컨텍스트 주입)1. 실행 순서 및 검증 게이트블로그 분류 체계 표 전체 삽입카테고리와 세부 주제 표가 펼쳐짐3줄 호출 포인터만 유지해당 단계 도달 시 하위 스킬 로드 원본 스킬 문서 (insight-article)1. 실행 순서 및 검증 게이트include: blog-taxonomy필수 레퍼런스 포함 지시문call: blog-publish하위 스킬 위임 지시문load_skill() 동적 링커런타임 심볼 결합 및 본문 조립조립된 스킬 본문 (컨텍스트 주입)1. 실행 순서 및 검증 게이트블로그 분류 체계 표 전체 삽입카테고리와 세부 주제 표가 본문에 펼쳐짐3줄 호출 포인터만 유지해당 단계 도달 시에만 하위 스킬 로드
스킬 로더(load_skill)의 런타임 조립 흐름. 당장 필요한 분류표(include)는 본문에 바로 펼쳐 넣고, 특정 단계에서만 쓰는 하위 스킬(call)은 3줄짜리 호출 안내문으로만 남겨 컨텍스트를 아낍니다.

1. 스킬이 100개를 넘었을 때 벌어진 일

처음 에이전트에게 일을 가르칠 때는 스킬 하나를 마크다운 문서 하나로 만들면 충분했습니다. 하지만 스킬이 하는 일이 복잡해지고 개수가 127개(총 686,236자)까지 늘어나자 두 가지 문제가 불거졌습니다.

첫째는 절차와 참고 자료가 한 문서에 뒤섞여 몸집이 너무 커진 것입니다. HTML 보고서를 만들어 발행하는 스킬(html-publish)은 본문과 부속 자료를 합쳐 127.6KB까지 불어났습니다. 디자인 프리셋 수십 개와 색상 팔레트 표가 실행 절차 안에 통째로 들어 있었기 때문입니다. 캐릭터 삽화를 그리는 스킬에도 카메라 구도 32종, 표정 근육 조합표, 조명 배치 25종이 모두 한 문서에 적혀 있었습니다. 이번 작업에서는 구도 하나와 조명 하나만 쓰는데도, 스킬을 부를 때마다 거대한 백과사전을 매번 컨텍스트에 올리고 있었던 셈입니다.

둘째는 여러 스킬에 똑같이 복사해 넣은 운영 절차가 서로 어긋나는 드리프트 현상이었습니다. 프로그램으로 치면 공통 함수를 라이브러리로 분리하지 않고, 소스 파일 수십 개에 복사해서 붙여 넣은 상태와 같았습니다.

복제 대상발견 범위문제 현상
격리 워크트리 생성과 정리22개 스킬브랜치 슬러그와 삭제 조건 불일치
오브젝트 스토리지(R2) 업로드23개 스킬스크립트 인자와 경로 규칙 파편화
비밀값 저장소(Vault) 조회28개 스킬조회 명령과 주입 예시 중복
공유 호스트 고부하 잠금 규칙3개 발행 스킬2개 스킬이 구버전(8GB, flock) 잔류
문서 트리 UUID 하드코딩22개 문서 (48건)대상 카드 이동 시 호출부 즉시 파손

특히 서버 메모리를 8GB에서 16GB로 늘렸을 때 이 문제가 그대로 드러났습니다. 메모리가 늘어나면서 무거운 검증 작업을 실행하는 공식 규칙도 ‘가용 메모리 3000MiB 확인 및 전용 러너 스크립트 사용’으로 바뀌었습니다. 그런데 규칙을 본문에 직접 복사해 넣었던 두 핵심 발행 스킬(digest-post, insight-article)은 여전히 옛날 방식인 ‘8GB 기준 flock 명령’을 시키고 있었습니다. 규칙 본문을 베껴 적지 않고 스킬 이름으로만 참조를 걸어 두었던 목업 발행 스킬 하나만 최신 기준을 따르고 있었지요2.

양옆으로 똑같이 복제된 종이 두루마리 더미가 무너져 내리는 가운데 구버전의 작은 자물쇠와 하늘색으로 빛나는 신버전 자물쇠를 양손에 들고 당황하는 치비 서소영

2. 어떻게 런타임에 스킬을 조립하는가

이 문제를 풀려고 처음에는 여러 스킬을 묶어 주는 ‘파이프라인’이라는 상위 개념을 따로 만들까도 고민했습니다. 하지만 어차피 스킬 자체가 절차를 순서대로 적어 놓은 문서인데, 파이프라인과 스킬을 엄밀하게 구분할 근거가 없었습니다. 억지로 구분하면 결국 모든 스킬이 파이프라인이 되거나, 둘이 어정쩡하게 섞여서 유지보수만 어려워질 것이 뻔했습니다.

그래서 새로운 계층을 만드는 대신, 기존 마크다운 스킬 본문 안에 필요한 자료를 런타임에 불러오라는 선언형 링크 블록을 직접 적어 넣는 방식을 택했습니다. 링크 지시문은 역할에 따라 call, include, pick 세 가지로 구분됩니다.

<!-- 1. call: 특정 단계에서 부를 하위 스킬 이정표 (3줄 요약으로만 표시) -->
```link
call: blog-publish
when: 워크트리에서 글을 쓰고 작업 브랜치와 PR까지 낼 때 (section=posts)
```

<!-- 2. include: 항상 봐야 하는 필수 레퍼런스 삽입 -->
```link
include: blog-taxonomy
```

<!-- 3. pick: 작업 의도(intent)에 맞는 도메인 카탈로그 골라 넣기 -->
```link
pick: catalog
axis: [camera.shot-size, subject.expression, light.setup]
top: 3
optional: false
```
  • call (하위 스킬 호출): 작업 도중 특정 조건(when)이 되었을 때 다른 스킬을 불러 실행하라는 안내문입니다. 하위 스킬의 긴 본문을 미리 다 합쳐 두지 않고 3줄짜리 호출 포인터로만 남겨 두므로 컨텍스트를 아낄 수 있습니다.
  • include (필수 자료 삽입): 블로그 카테고리 체계처럼 그 스킬을 실행할 때마다 예외 없이 봐야 하는 참고 문서나 특정 절(section)을 선언된 위치에 그대로 삽입합니다.
  • pick (맞춤 카탈로그 선택): 지정한 선택 차원(axis)의 참고 자료 후보들 가운데, 지금 요청(intent)에 가장 잘 맞는 상위 top개를 골라 결합합니다. 후보가 하나뿐인 필수 항목은 시맨틱 검색을 거치지 않고 0초 만에 바로 결합합니다3.

대화가 길어져 컨텍스트가 압축되면 어떻게 될까요?

에이전트가 오랫동안 작업을 이어 가다 보면, 컨텍스트 윈도우가 가득 차서 이전 대화를 요약하는 압축(Compaction)이 일어납니다. 압축이 끝난 뒤 에이전트가 쓰던 스킬을 다시 불러올 때, pick이 처음과 다른 카탈로그를 골라 버리면 작업 도중에 화풍이나 디자인 규칙이 바뀌어 버립니다.

이를 막기 위해 스킬 로더는 처음 스킬을 조립할 때 골랐던 카탈로그 이름 목록을 pins라는 1차원 배열(예: ["color-palette", "lighting-setup", "seosoyoung-canon"])로 함께 돌려줍니다. 압축 뒤에 스킬을 다시 열 때는 작업 의도(intent) 없이 이 pins 배열만 그대로 넘기면 됩니다. 그러면 시맨틱 검색을 아예 실행하지 않고 0.04초에서 0.07초 만에 처음과 똑같은 카탈로그 조합을 그대로 복원합니다. 만약 캐릭터 설정 하나만 다른 것으로 바꾸고 싶다면 pins에 바꿀 이름만 넣어서 다시 부를 수도 있습니다.

3. 절차와 선택지의 분리: 직교(Orthogonal) 카탈로그와 번들(Bundle) 카탈로그

런타임 조립 지시문을 만든 뒤, 기존 127개 스킬을 전수 조사해 절차 안에 섞여 있던 참고 자료들을 11개의 도메인 카탈로그로 분리했습니다. 여러 스킬에 중복으로 적혀 있던 블로그 발행이나 이미지 처리 절차도 5개의 공용 하위 스킬(blog-publish, blog-images 등)로 묶어 냈습니다.

이렇게 분리한 카탈로그는 조합하는 방식에 따라 두 가지로 구분됩니다.

  • 직교(orthogonal) 카탈로그: 서로 주제가 겹치지 않아 한 작업에서 여러 개를 동시에 결합할 수 있는 참고 자료입니다. 캐릭터 삽화를 그릴 때 쓰는 카메라 구도 32종(camera-framing), 표정 근육 12종과 고개 각도 12종(character-acting), 조명 배치 25종(lighting-setup)이 대표적입니다. 구도와 표정, 조명은 서로 충돌하지 않으므로 한 번에 함께 불러와 조합합니다.
  • 번들(bundle) 카탈로그: 같은 주제의 여러 대안 가운데 이번 작업에 쓸 하나만 골라야 하는 묶음입니다. HTML 보고서를 만들 때 브루탈리즘, 스위스 그리드, 에디토리얼 매거진 중 하나의 디자인 테마를 고르거나, 등장인물 중 누구의 외형 설정을 쓸지 고르는 캐릭터 설정 번들(subject.identity)이 여기에 해당합니다.

이젤에 세워진 두루마리 보드에 카메라, 웃는 얼굴, 조명 아이콘 타일 세 개를 하늘색 핀으로 꽂아 고정하는 치비 서소영

범용 카탈로그와 특정 캐릭터 번들을 따로 떼어 놓으면 둘을 이어 주는 연결 고리도 필요합니다. 예를 들어 범용 표정 카탈로그(character-acting)에는 어떤 캐릭터에나 쓸 수 있는 안면 근육 기호(AU1+AU4 등)만 적혀 있습니다. 하지만 실제로 그림을 그릴 때는 글자로 된 기호만으로는 부족하고, 해당 캐릭터의 표정 12가지를 미리 그려 놓은 4×3 레퍼런스 시트 이미지에서 몇 행 몇 열(#r1c1부터 #r3c4까지)을 참고할지 함께 알려주어야 일관된 인상을 유지할 수 있습니다.

# 캐릭터 설정 번들(seosoyoung-canon) 메타데이터의 오버레이 바인딩 선언
axis: [subject.identity]
composition: bundle
bindings:
  - target_catalog: character-acting
    target_axis: subject.expression
    sheet_template: references/facs/{style}-facs-12.png
    grid: 4x3
  - target_catalog: lighting-setup
    target_axis: light.setup
    sheet_template: references/lighting/{style}-lighting-12.png
    grid: 4x3

그래서 캐릭터 번들 메타데이터에 bindings라는 연결 규칙을 넣었습니다. 스킬 로더가 서소영 캐릭터 번들과 범용 표정 카탈로그를 함께 고르면, 서소영 번들에 적힌 4×3 표정 시트 경로가 자동으로 연결됩니다. 또한 분량이 큰 카탈로그는 요약본(L1)과 세부 표(L2 샤드)로 분리해, 처음에는 가벼운 목차만 보여 주고 필요한 세부 표만 골라 읽게 해 캐릭터 번들의 기본 주입량을 32% 줄였습니다.

4. 정적 분석기(skill_lint)로 찾은 실전 버그들

스킬과 카탈로그를 이름으로 연결하기 시작하면, 오타 하나로 링크가 깨지거나 필요한 문서가 누락될 수 있습니다. 일반 프로그램은 라이브러리를 찾지 못하면 즉시 에러를 내며 멈추지만, LLM 에이전트는 참고 문서가 빠진 채로 로드되어도 멈추지 않고 자기 상식으로 그럴듯하게 절차를 지어내어 실행합니다.

이런 조용한 오류를 막기 위해 전체 132개 스킬과 11개 카탈로그의 링크 블록 78개를 배포 전에 검사하는 정적 분석기(skill_lint.py)를 만들었습니다. 코드 문법이 틀렸거나(E006), 존재하지 않는 스킬이나 절 제목을 가리키거나(E008), 한 스킬의 여러 pick 블록에 같은 카탈로그가 중복 등록된 경우(W109)를 자동으로 잡아냅니다.

하늘색 돋보기와 정밀 집게를 들고 두 개의 작은 두루마리 카드 사이에서 끊어진 하늘색 연결 실을 정교하게 이어 붙이는 치비 서소영

린터와 런타임 로더를 대조해 검증하자마자, 그냥 두었으면 운영 중에 원인을 찾기 힘들었을 실제 버그 두 건을 바로 잡아냈습니다.

첫째는 출력 글자 수 제한에 걸려 새로 만든 스킬 6개가 감쪽같이 잘려 나간 버그였습니다. 런타임 스킬 로더가 전체 스킬 목록을 가져올 때 기본 출력 제한인 50,000자 설정이 그대로 걸려 있었습니다. 스킬이 132개로 늘어나면서 목록 길이가 50,000자를 넘겼고, 목록 맨 끝에 있던 blog-publish 등 6개 신규 스킬이 조용히 잘려 나갔습니다. 개편 직후 다이제스트 발행 스킬을 시험 로드하자마자 로더가 “blog-publish 대상을 찾을 수 없다"는 경고를 띄운 덕분에 즉시 발견해 글자 수 제한을 해제했습니다.

둘째는 Python 린터와 TypeScript 스킬 로더의 YAML 해석 차이였습니다. Python의 PyYAML은 옛 YAML 1.1 규격에 따라 on, off, yes, no를 불리언 참과 거짓으로 읽는 반면, TypeScript의 yaml 패키지는 YAML 1.2 규격에 따라 일반 문자열로 읽어 두 환경의 판정이 어긋났습니다. 두 언어에 규칙을 따로 적는 대신, 정상과 오류 사례 37건을 담은 공통 테스트 파일(link_vectors.json)을 만들어 양쪽 테스트가 똑같은 결과를 내도록 통일했습니다4.

5. 개편 후 무엇이 좋아졌는가

정적 모놀리스였던 스킬 트리를 동적 링크와 카탈로그 구조로 재편한 전후 지표는 다음과 같습니다.

정적 분석 및 아키텍처 지표개편 전개편 후
스킬 / 도메인 카탈로그 수127개 / 0개132개 / 11개 (공용 스킬 5종 분리)
링크 문법 및 참조 오류 (E006-E010)측정 불가 (암묵적 결합)0건 (78개 링크 블록 전수 통과)
내부 UUID 하드코딩 (W101)48건0건 (심볼 이름 참조로 통일)
전체 린트 경고 (W101-W109)67건5건 (통째로 읽는 문서 5개 예외)
최대 스킬 크기 (html-publish)127,633자11,226자 (컨텍스트 부하 91% 감소)

이번 개편으로 체감한 가장 큰 변화는 세 가지입니다.

  1. 에이전트의 컨텍스트가 훨씬 가벼워졌습니다. 가장 거대했던 html-publish 본문이 127.6KB에서 11.2KB로 91% 줄었고, skill-creator(-65%)와 digest-post(-61%)도 절반 이하로 가벼워졌습니다. 당장 쓰지 않을 선택지 사전을 매번 읽느라 컨텍스트를 낭비하는 일이 사라졌습니다.
  2. 공통 규칙을 한 곳에서만 고치면 됩니다. 20여 개 스킬에 흩어져 있던 워크트리 생성, 이미지 업로드, 블로그 발행 절차를 공용 하위 스킬과 카탈로그로 묶어 연결했기 때문에, 서버 환경이나 운영 규칙이 바뀔 때 기준 문서 하나만 수정하면 이를 링크한 모든 스킬에 즉시 반영됩니다.
  3. 프롬프트 문서도 코드처럼 배포 전에 검증할 수 있게 되었습니다. 문서 안에 적혀 있던 물리적 ID(node_id) 하드코딩 48건을 모두 지우고 심볼 이름으로만 링크한 뒤 정적 린터(skill_lint)를 돌리자, 문서를 이동하거나 수정할 때 깨지는 참조를 1초 만에 확인할 수 있는 체계가 완성되었습니다.

  1. 본문의 치비 서소영 커버와 삽화는 「느낌적인 느낌을 숫자로 옮기는 일」의 치비 서소영 라인아트를 참조하여 gpt-image-2.5-flare image-to-image로 생성했습니다. ↩︎

  2. 당시 공유 호스트 잠금 규칙을 참조하던 스킬은 세 개였습니다. 이 가운데 전용 러너 스크립트를 이름으로 간접 참조한 목업 발행 스킬 하나만 16GB 호스트, 가용 메모리 3000MiB라는 최신 기준을 적용했고, 절차를 본문에 직접 복사해 둔 digest-post와 insight-article은 옛 규칙에 머물러 있었습니다. ↩︎

  3. 치비 서소영 삽화 생성 스킬처럼 여러 카탈로그를 쓰는 작업에서도 카메라 구도나 표정 근육처럼 후보가 하나뿐인 필수 차원은 리랭커 비용 없이 즉시 결합되고, 여러 후보가 경쟁하는 캐릭터 정체성 차원이나 선택적 블록만 단 1회의 배치 평가로 처리됩니다. ↩︎

  4. link_vectors.json은 블록 파싱 테스트 32건(정상 12건, E006 오류 20건)과 마크다운 절 추출 테스트 5건(정상 3건, E008 오류 2건)으로 구성되며, Python의 pytest와 TypeScript의 vitest에서 동시에 실행됩니다. ↩︎