
skill 평가 리포지토리를 만들려다 만들지 않은 이야기 (107개의 lint 실측 포함)
요약
Claude Code의 skill과 harness를 평가하기 위한 리포지토리 구축 시도와 그 과정에서 얻은 설계적 통찰을 다룹니다. 정적 lint, 실행 평가(execution eval), harness 평가라는 세 가지 관점을 분석하고, 비용 효율적인 정적 lint collector 도입 결정을 설명합니다.
핵심 포인트
- skill 평가는 정적 lint, 실행 평가, harness 평가의 세 영역으로 구분됨
- 정적 lint는 skill 파일의 구문 및 필수 필드 오류를 빠르게 검사함
- 실행 평가는 sub-agent를 활용한 채점 및 A/B 테스트 방식을 사용함
- harness 평가는 agent framework 자체의 성능 차이를 측정함
- 모든 평가 요소를 통합하기보다 목적에 맞는 비용 효율적 접근이 중요함
Claude Code의 skill과 harness를 평가하는 전용 리포지토리를 구축하려고, 우선 기존 사례 조사부터 시작했습니다. 결론부터 말씀드리면, 리포지토리는 만들지 않았습니다. 만든 것은 수중에 있는 건전성 harness에 추가한 정적 lint collector 1개뿐입니다.
이 기사는 "만들지 않기로 한" 조사 지도와 설계 판단의 기록입니다. 직접 움직인 부분보다, 조사하며 알게 된 사실과 버린 선택지들이 아마 더 가치가 있을 것입니다. 반나절의 조사로 새로운 리포지토리 1개와 LLM 과금을 절약할 수 있었으므로, 같은 구상을 가진 분들에게 지도가 되기를 바랍니다.
skill 평가는 이미 3가지 질문으로 나뉘어 있었다
"skill을 평가하고 싶다"라고 뭉뚱그려 생각했지만, 조사해 보니 세상의 선행 사례들은 사실 서로 다른 3가지 질문에 답하고 있었습니다.
질문 1: skill이 망가지지 않았는가. 즉, 정적 lint (static lint)의 영역입니다.
선행 사례는 pulser입니다. SKILL.md의 frontmatter 구문, 필수 필드, 참조 끊김 등 5가지 체크를 정적 분석만으로 수행합니다. 40개의 skill 검사가 200ms 이하로 끝나는 속도입니다. GitHub Action으로 CI에 삽입하는 구성도 공개되어 있으며, skill을 전혀 실행하지 않습니다. skill 파일에 대한 eslint입니다. 저자는 214개의 skill을 감사하여 73%가 망가져 있었다는 보고도 작성했습니다. 이 수치는 나중에 다시 다루겠습니다.
질문 2: skill이 효과가 있는가. 이쪽은 실행 eval (execution eval)의 영역입니다.
Anthropic 공식의 skill-creator는 skill 생성뿐만 아니라 평가 파이프라인을 내장하고 있습니다. 테스트 프롬프트를 evals.json에 저장하고, skill이 있는 경우와 없는 경우의 2가지 조건으로 서브 에이전트 (sub-agent)를 병렬 실행합니다. 결과의 채점과 집계도 grader / comparator / analyzer라는 전용 서브 에이전트의 몫입니다. description의 자동 최적화에서는 eval 세트를 훈련용 60%, 검증용 40%로 나누는 단계까지 수행합니다. 머신러닝 (Machine Learning)의 방식이 그대로 skill 개발에 도입된 형태입니다.
StackHawk의 eval harness는 운영 측면의 좋은 예시입니다. skill 이외의 조건을 모두 고정하고, 신구 2개 버전의 skill을 여러 실제 리포지토리에서 실행합니다. 채점하는 judge에게는 어느 버전인지 알려주지 않습니다. skill 변경의 before/after를 측정하는 블라인드 A/B 테스트입니다.
연구 측면에서는 OpenSkillEval이 600개 이상의 태스크와 30개의 공개 skill로 계통적인 평가를 하고 있습니다. 제시된 결과는 skill이 이용 가능하더라도 실제로 사용된다는 보장은 없다는 것이었습니다. 효과 또한 모델과 agent framework의 조합에 강하게 의존했습니다. "넣으면 작동한다"를 전제로 할 수 없는, 냉정한 실측 결과입니다.
질문 3: harness 자체는 어떠한가. skill의 외부를 측정하는 영역입니다.
terminal-bench와 그 실행 기반인 Harbor는 skill이 아니라 agent harness 자체를 피험자로 삼습니다. 동일한 태스크 군에 대해 Claude Code나 Codex CLI 같은 harness를 교체하며 실행하여, harness 간의 차이를 측정하는 구조입니다.
지도를 그려보니 알게 된 것은, 질문이 다르면 비용 구조도 채점 방식도 완전히 다르다는 것이었습니다. 그리고 제가 처음에 구상했던 "skill 평가 리포지토리"는 이 세 가지를 무의식적으로 모두 담으려 하고 있었습니다.
왜 정적 lint만을 채택했는가
저는 이전 기사에서 쓴 code-health-ops라는 개인적인 harness로 제 리포지토리 군을 주간 단위로 A-F 채점하고 있습니다. 설계 원칙은 세 가지입니다. 측정에 LLM을 사용하지 않는다, 코드 본문을 저장하지 않는다, 임계값(threshold)은 절대값으로 한다.
skill 평가를 여기에 넣으려 했을 때, 실행 eval (질문 2)은 이 원칙들과 세 가지 지점에서 충돌했습니다.
비용 모델이 다릅니다. 결정적인 lint라면 재측정은 무료이며 몇 초면 끝납니다. 따라서 일간 cron에 올릴 수 있고, 마음이 내킬 때 모든 리포지토리를 다시 측정할 수 있습니다. 실행 eval은 1회의 관측이 과금 이벤트이며, 게다가 몇 분이 소요됩니다. "일단 매일 돌린다"는 경제적으로 성립하지 않습니다.
채점 방식이 다릅니다. LLM-as-judge는 비결정적(non-deterministic)입니다. 동일한 skill에 대해 매주 같은 점수가 나오지 않으면, 시계열적 변동이 "skill이 악화된 것"인지 "채점자의 기분이 변한 것"인지 구분할 수 없습니다. 절대 임계값으로 시계열을 쌓는다는 운영 전제가 채점자 측으로부터 무너집니다.
측정 대상이 다르다. 정적 lint (Static lint)가 보는 것은 코드로서의 건전성입니다. 실행 eval (Execution eval)이 보는 것은 에이전트의 거동, 즉 description을 바꿨을 때 기동률이 올라가는가와 같은 이야기입니다. 건강검진 종이에 영업 성적을 적어 넣는 것과 같아서, 이를 함께 두면 양쪽의 해석이 모호해집니다.
그래서 두 층으로 나누었습니다. 정적 lint 층은 기존 하네스 (Harness)의 한 가지 관점으로서 즉시 구현합니다. 실행 eval 층은 향후 별도의 기반으로 분리하며, 구현하더라도 서머리 (Summary)의 grade만 이쪽 DB에 다시 쓰는 설계입니다. 실행 eval을 버린 것이 아니라, 자리를 나눈 것뿐입니다. 같은 "skill 평가"라도, 결정적이고 무료인 관측과 비결정적이며 과금되는 실험은 같은 스코어카드 (Scorecard)에 앉혀서는 안 됩니다. 이것이 이번 설계 판단의 핵심입니다.
6가지 체크와 107개의 실측
구현한 정적 체크는 6종류입니다. pulser의 5가지 체크와 거의 같은 발상에, symlink 운용 고유의 1가지를 추가했습니다.
| 체크 | 내용 |
|---|---|
| fm_missing | frontmatter를 읽을 수 없음 |
| ... |
symlink 체크만 보충하겠습니다. 제 환경에서는 skill 본체를 정의 원본 리포지토리 (Repository)에 하나만 둡니다. 사용하는 쪽의 각 리포지토리에는 symlink로 배포하는 방식입니다. 이 운용 방식에서 symlink가 끊어졌을 때, 에러는 발생하지 않습니다. skill 목록에서 해당 skill이 조용히 사라질 뿐입니다. 호출되지 않는 skill은 실패 로그조차 남기지 않기 때문에, 이 고장 모드 (Failure mode)는 정적 lint 이외에는 포착할 수 없습니다. 6번째 체크는 바로 이 "조용히 사라지는" 현상에 대한 대책입니다.
임계값은 "위반 수/skill 수"이며, 0이면 100점, 1.0이면 0점입니다. 수중에 있는 8개 리포지토리, 107개의 SKILL.md를 측정한 결과, 위반은 18건이었습니다. 내역은 frontmatter 누락이 2건, 500행 초과가 16건입니다. 스코어 최하위는 skill 57개를 보유한 가장 오래된 리포지토리인 C(72점)였습니다. 그다음으로 오래된 2개는 B(86점)와 A(92점), 나머지 5개 리포지토리는 A(100점)입니다. skill이 가장 많고 오래된 곳이 가장 낮다는, 직관과 일치하는 분포입니다. 직관과 일치하는 숫자가 나왔을 때만, 척도는 신뢰할 수 있습니다.
500행 초과 16건의 정체는, 투고 절차나 리뷰 절차를 통째로 떠안은 오케스트레이션 (Orchestration) 계열의 skill이었습니다. 500행이라는 상한선은 공식 베스트 프랙티스 (Best practice)에서 유래했지만, 실질적인 해악도 명확하여 긴 SKILL.md는 읽힐 때마다 컨텍스트 (Context)를 소비합니다. 대책의 정석은 본체를 탐지 로직만 담은 얇은 버전으로 만들고, 상세한 수정 예시는 위반이 있을 때만 읽는 별도 파일로 분리하는 것입니다. 이른바 progressive disclosure (점진적 공개)입니다. 이 분리를 미리 마쳐두었던 AI 문장 체크 계열의 skill 그룹들은 이번 lint를 무사히 통과했습니다. 과거의 나의 설계 판단이 다른 계측기에 의해 재확인되는 것은 꽤 기분 좋은 일입니다.
가장 큰 배움은 오검출의 교정이었다
구현 그 자체보다, 첫 실행 후의 뒷정리가 본론이었습니다. 첫 실행에서는 위반이 67건 발생했습니다. 수작업으로 전건 확인해보니, 49건이 오검출 (False positive)이었습니다. 오검출률 73%의 linter가 탄생한 것입니다.
오검출은 두 클래스로 나뉘었습니다.
클래스 1: 엄격한 YAML 파싱으로 가동 중인 21개를 고장 난 것으로 판정. 수중의 skill frontmatter에는 다음과 같은 행이 자주 등장합니다.
---
name: research
argument-hint: [검색 쿼리] or [--full <id>]
...
엄격한 YAML 파서 (Parser)는 이 argument-hint 값을 flow sequence로 해석하여 파싱에 실패합니다. 그 결과, 매일 문제없이 작동하던 skill 21개가 frontmatter 오류로 판정되었습니다. 하지만 Claude Code 본체는 이것들을 문제없이 읽어 들입니다. 즉, lint의 정답 사양은 "YAML 사양"이 아니라 "런타임 (Runtime)의 관용성"이었습니다. 엄격한 파서를 버리고, 런타임과 비슷한 수준으로 관용적인 라인 기반 파서로 교체했습니다. 교훈을 일반화하자면, lint를 만들 때의 정답 사양은 런타임의 구현에 두어야 합니다. 사양서 (Specification)를 정답으로 삼으면, 사양보다 관용적인 런타임 위에서 활발히 돌아가고 있는 실물을 고장 난 것으로 취급하게 됩니다.
클래스 2: 플레이스홀더를 링크 끊김으로 판정. 문서 중의 [search](url)와 같은 설명용 플레이스홀더를 broken_refs로 계산하고 있었습니다. 링크 대상에 . 또는 /를 포함하는 것만 검사하도록 조건을 설정하여 제외했습니다.
두 클래스 모두 회귀 테스트 (regression test)로 고정되어 있습니다. 교정 후에 남은 18건은 전건 실물을 열어 확인했으며, 오탐 (false positive)은 제로였습니다.
여기서 앞서 언급한 pulser의 "214개 중 73%가 깨져 있었다"는 내용으로 돌아가겠습니다. 저의 첫 실행 시에도 107개 중 63%가 위반이었으나, 교정 후에는 17%까지 떨어졌습니다. linter의 정답 사양 (specification)이 런타임 (runtime)보다 엄격하면, 감사 (audit) 수치는 그만큼 부풀려집니다. pulser의 수치가 그렇다는 말을 하고 싶은 것이 아닙니다. 다만, skill 감사 결과에서 큰 비율이 나타난다면, 우선 "그 linter가 런타임보다 엄격하지 않은가"를 의심해 볼 가치가 있습니다. 이것이 저의 실측을 통해 얻을 수 있는 시사점입니다. 깨져 있는 것이 skill인가, 아니면 척도 (yardstick)인가. 이번에는 21개 분량만큼 척도 쪽이 문제였습니다.
결국, eval로서 무엇이 우수한가
조사와 구현을 통해 얻은 현시점의 답은 "질문(question)에 따라 선택한다"입니다. 방법론 그 자체에는 우열이 없습니다. 질문과 비용의 대응을 잘못 맞추면, 우수한 방법론도 무용지물이 됩니다.
- 매일 모든 skill에 돌리는 회귀 체크 (regression check)가 필요하다 → 정적 lint (static lint). 결정적이며, 무료이고, 몇 초 만에 끝납니다. 단, "효과가 있는가"는 원리적으로 보이지 않습니다.
- skill을 수정했을 때의 효과를 알고 싶다 → StackHawk형의 블라인드 A/B 테스트. 릴리스 판단 시에만 실행하는 유료 이벤트로 취급합니다.
- 어떤 skill을 도입해야 할지 선정하고 싶다 → OpenSkillEval형의 skill 유무 비교. "넣으면 효과가 있다"는 점을 의심하는 것부터 시작합니다.
- harness 자체의 교체를 판단하고 싶다 → terminal-bench형. skill이 아니라 harness를 교체하여 측정합니다.
교환 포맷(exchange format)으로는 skill-creator의 evals.json 형식이 현재 가장 유력한 후보라고 보고 있습니다. 테스트 프롬프트 (test prompt)의 축적은 어떤 방법론에서도 자산이 되므로, 향후 실행 eval 계층은 이것과 호환되도록 만들 계획입니다.
요약
새 리포지토리를 만들기 전 반나절 동안 조사한 결과, 구상의 절반은 이미 세상에 존재하며, 나머지 절반은 기존 harness에 collector 하나를 추가하는 것으로 충분하다는 것을 알게 되었습니다. 현재 107개의 SKILL.md가 일일 cron에 의해 매일 검사되고 있습니다. 만들지 않기로 결정한 리포지토리만큼, 유지보수 대상은 늘어나지 않았습니다.
평가 기반을 구축할 때 가장 효과적이었던 것은, 체크 내용보다 앞서 있었던 "이 질문에 실행 (execution)이 필요한가"라는 분류였습니다. lint로 해결될 질문을 LLM에게 묻는 것은, 체온계로 끝날 진찰을 MRI로 시작하는 것과 같기 때문입니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기