발단
RSS 피드를 등록해서 원하는 기술 블로그들의 새 글을 하루에 한번씩 알림으로 받고, 글에 관해서 클로드와 이야기한 다음에 마크다운 노트로 남길 수 있는 articles-os라는 플러그인을 만들었습니다.
요런 식으로 나름 잘 쓰고 있었는데요. (조 쿨은 제가 설정한 봇 이름이랍니다)
어느 정도 원하는 기능이 잘 돌아가는 것 같다고 느껴지니 문득 "이거 생각보다 토큰을 많이 쓰고 있으면 어떡하지?"하는 걱정이 들었습니다. 원하는 동작이 제대로 기능하는 것에만 집중했기 때문에 비용 관련해서는 전혀 고려하지 않고 있었거든요.
안 그래도 부족한 클로드 토큰을 이렇게 상대적으로(?) 가벼운 곳에 낭비하고 있었다면 매우 큰일이기 때문에 당장 최적화 할 수 있는 부분이 있는지 찾아보기로 했습니다.
우선 현재 상황 파악하기
스킬 하나가 세션에 더하는 토큰을 콕 집어 보여주는 전용 command는 아직 없는 것으로 알고 있습니다. 대신 /context command로 스킬 로드 전/후·사용 전/후의 컨텍스트 사용량을 비교하거나, count_tokens API에 해당 스킬의 name·description을 직접 넣어 재는 방법이 있는데요.
제가 측정하고자 하는 것은 "플러그인"의 소비 토큰이고, 다행스럽게도 Claude에서는 플러그인의 예상 소비 토큰을 확인할 수 있는 claude plugin details 라는 CLI 명령어를 제공하고 있었습니다. 👏
plugin details로 articles-os의 소비 토큰을 측정한 결과는 아래와 같았습니다.

플러그인이 소비하는 토큰을 always-on과 on-invoke 두가지로 분류해서 출력해주는데요.
- always-on : 세션 시작시에 자동으로 주입되는 목록 텍스트들이 소비하는 토큰입니다. 스킬이나 에이전트의 frontmatter에서 name과 description이 포함되고, 클로드는 description을 읽고 상황에 맞는 도구들을 호출하게 됩니다.
- on-invoke : 스킬이 실제로 트리거되었을 때 로드되는 SKILL.md가 소비되는 토큰입니다. plugin details가 보여주는 on-invoke는 SKILL.md 크기를 기반으로 한 추정치라서, 실행 중에 스킬이 읽어들이는 참조 파일이나 스크립트 출력은 이 수치에 잡히지 않고 그때그때 실제비용으로 더해집니다.
마침 plugin details가 이 둘을 나눠서 보여주니, 저도 이 두 축을 작업 방향으로 잡고 하나씩 살펴보기로 했습니다.
always-on 줄이기 : description 다이어트
먼저 always-on부터 봤습니다. 컴포넌트별 값을 보니 유독 큰 것들이 있었는데, 전부 서브에이전트들이었습니다. qa-research가 ~350, fetch-source가 ~300으로, 일반 스킬들(~100–170)보다 훨씬 높았습니다.
이 파일들을 열어보니 description 안에 <example> 블록이 길게 들어가 있었습니다.
---description: |Use this agent when the qa skill needs external research beyond the article text —up-to-date facts, referenced concepts, counterpoints, or claims to verify. Spawnedconditionally by the qa skill; not typically user-facing.<example>Context: 아티클 Q&A 중 본문에 없는 최신 정보가 필요함user: "이 글에서 말한 API, 지금도 deprecated 상태야?"assistant: "본문엔 없는 현재 상태라 qa-research 에이전트로 확인할게요."<commentary>웹 fan-out은 메인 Q&A 대화에서 격리 — 조사 결과만 받아 통합한다.</commentary></example>---
이 부분은 예전에 Claude에게 서브에이전트 문서 초안을 맡겼을 때, "이 서브에이전트를 언제 어떻게 호출하면 되는지"를 판단하기 위해서 예시로 적어둔 것이었습니다.
하지만 이 "언제 호출하는가"는 호출되는 서브 에이전트가 아니라 호출하는 쪽이 알아야 하는 정보입니다. 그리고 이 서브 에이전트는 유저가 직접 호출할 에이전트가 아니라서, 이 예시까지 always-on으로 매번 실릴 이유는 없었습니다. 이미 해당 에이전트들을 호출하는 쪽 SKILL.md에 어떻게 해당 에이전트를 호출하는지에 대한 설명을 적어두었기 때문에 description에서 아예 example 블록을 제거하기로 했습니다. 그 김에 "누가 호출하는게 아닌지" 같은 부연 설명들을 모두 제거하고 꼭 필요한 내용만 명료한 한국어로 수정했습니다.
---description: >아티클 본문만으로 부족할 때 외부 조사를 맡는 에이전트. qa 스킬이 명시적으로 소환한다.---
훨씬 날씬해졌죠?
on-invoke 줄이기 : 스킬 문서는 지시서다
다음은 on-invoke, 즉 SKILL.md 본문 차례였는데요. 여기선 구구절절 적힌 설명들이 가장 먼저 눈에 띄었습니다.
저는 스킬이나 에이전트를 만들 때 Claude에게 초안을 부탁하고 다듬는 방식을 자주 쓰는데, (사람마다 자기 Claude 성향이 다르겠지만) 제 Claude는 자꾸 "왜 이렇게 해야 하는지", "이 상황은 무엇인지"를 산문으로 구구절절 적어두는 버릇이 있었습니다.
본문 확보(세션 한정 지연 fetch)와 메모 저장(`save_note.py` 호출)을 메인 스레드에서 직접 수행한다— 회상 인터뷰가 끝난 직후 같은 턴에서 저장 결과를 바로 알려줘야 하는 흐름이라 서브에이전트로 위임하면 왕복만 늘고 이득이 없다.
이런 식으로 왜 서브에이전트로 위임하지 않고 직접 수행하는지 이유를 파일에다 남겨두거나
실행 결과가 `ok: false`면 `error`를 보고 갈라진다:- `article not found for url: ...` : 아티클 지정이 어긋난 것이다. 1단계로 돌아가 URL을 다시 확정한 뒤 재시도한다.- `stdin body is empty` : 본문 조립이 비었다. 다시 조립해 재시도한다.- 그 밖(`notes_path not configured`, `articles.json not found`, 쓰기 실패 등) : 재시도로 풀리지 않는다. `error`를 그대로 보여주고 중단한다.
이렇게 상황을 설명하는 것이죠.
그런데 스킬 문서는 사실 지시서입니다. 해야 할 일을 명료하게 적고, 판단이 필요한 지점은 판단 기준을 명료하게 정리해주는 것만으로도 충분합니다.
사실 이렇게 구구절절 모든 것을 문서로 남기는 것은 처음부터 거슬려서 아예 conciseness라는 간결성 규칙을 rule에 만들었지만 메인 컨텍스트의 권한을 최소화하기 위해서 만들어둔 least-privilege이라는 다른 규칙과 충돌하면서 실제 작업에는 불필요한 설명들이 남게 된 것이었습니다... 그래서 이번에 충돌하던 규칙도 수정하고, SKILL.md에 남아 있던 근거, 설득, 설명 문장들을 싹 걷어냈습니다.
`ok: false`면 `error`에 따라:- `article not found for url: ...` : 1단계로 돌아가 URL 재확정 후 재시도- `stdin body is empty` : 본문 다시 조립해 재시도- 그 밖 : 재시도로 풀리지 않는다. `error`를 그대로 보여주고 중단
이렇게 Claude가 해당 상황에서 해야 할 일들만 명확히 적어두었습니다.
비슷한 맥락으로, 스킬 8개마다 똑같이 복붙돼 있던 초기화 확인 블록도 걷어냈습니다. 원래는 스킬마다 이런 bash 블록과 안내 3줄이 통째로 반복됐는데요.
## 0. 스킬 실행 조건 확인```bashpython3 "${CLAUDE_PLUGIN_ROOT}/scripts/paths.py"```- `error` 키가 있는 경우 : ...- `initialized: false` 인 경우 : "먼저 `/articles-os:setup`을 실행하세요" ...- `initialized: true` 인 경우 : ...
스크립트 실행 결과로 AI가 하던 게 단순 분기였기 때문에, 그 판정을 스크립트가 직접 하도록 옮겼습니다.
## 0. 데이터 경로 확인`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/paths.py" --require-initialized` 실행.`ok: false`면 `error`를 그대로 보여주고 끝낸다.
매 실행마다 스킬 8곳에서 반복되던 지시를 한 줄로 줄일 수 있었습니다!
근본 처방 : 에이전트 개수 자체를 줄이기
마지막은 좀 더 근본적인 처방이었습니다. description을 아무리 깎아도 스킬/에이전트 하나가 존재하는 이상 최소한의 always-on은 남습니다. 그러니 제일 확실한 방법은 애초에 스킬/에이전트 개수를 줄이는 것이라고 생각합니다.
우리가 skill이나 agent를 쓰는 이유는 결국 AI의 판단이 필요한 비결정형 작업을 시키기 위함입니다. 그렇다면 반대로, AI의 판단이 필요 없는 결정형 작업은 굳이 AI에게 맡길 필요가 없습니다. 그냥 스크립트로 쓰면 되고, 오히려 AI의 불확실성이 끼어들지 않으니 더 안정적으로 동작하기도 합니다.
이 기준으로 보니 에이전트로 빼둔 것 중 두 개가 걸렸습니다. RSS 피드에서 글을 가져오는 fetch-source와, 피드 유효성을 검증하는 source-healthcheck였는데요. 둘 다 정해진 입력에 정해진 출력을 내는 결정형 작업이라, AI의 판단이 개입할 이유가 없었습니다.
이 둘을 에이전트로 분리했던 이유는 솔직히 에이전트라는 것을 써보고 싶기도 했지만 (ㅋㅋ) 메인 컨텍스트와 병렬로 돌릴 수 있는 작업이라고 생각했기 때문입니다. 그런데 병렬 실행하는 방법에 에이전트만 있는 게 아니더라고요. 스크립트에서 스레드를 띄우면 됩니다.
게다가 fetch-source는 원래 RSS 피드 하나당 에이전트 하나를 띄워 한꺼번에 병렬로 가져오는 그림으로 만든 에이전트였는데, 이렇게 되면 각 에이전트의 출력을 다시 메인 오케스트레이터 스킬이 받아서 합치는 과정이 필요해집니다. 이렇게 합치는 과정 역시 비용이 많이 드는 작업이고요.
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/manage_config.py" sources list \| python3 "${CLAUDE_PLUGIN_ROOT}/scripts/fetch_feed.py" \| python3 "${CLAUDE_PLUGIN_ROOT}/scripts/apply_collection_results.py" "<RUN_TS>"
그래서 스크립트가 피드를 스레드로 병렬 fetch해서 하나의 배열로 묶어 출력한 뒤에 다음 스크립트로 곧장 파이프하는 식으로 수정했습니다.
이렇게 수정하니 아티클 수집에서부터 알림까지 거의 5분이 걸려서 남겨둔 이슈까지 해결되는 1석 2조의 효과가 있어서 좋았습니다 😙🎶
개선 결과

always-on 기준 ~1,808 토큰에서 ~1,411 토큰으로, 매 세션 무조건 나가던 값을 약 22% 덜어냈습니다. 자리값이 제일 비쌌던 qa-research는 ~350에서 ~60까지 내려갔고, fetch-source와 source-healthcheck는 아예 목록에서 제거되었습니다. on-invoke 역시도 전반적으로 줄어들었네요!
몇몇 컴포넌트는 오히려 always-on이 늘었는데요. 정리한 description을 테스트하던 중에 몇몇 스킬은 라우팅 정확도가 떨어지는 바람에 description을 좀 더 또렷하게 다듬었고, 그 과정에서 토큰이 조금 늘었습니다. 중요한 것은 의도한 상황에서 의도한 스킬이 잘 나오는 것이기 때문에 이 부분은 감수해야 할 트레이드 오프라고 생각합니다 🥹
끝!
AI를 쓰면서 경험적으로 (사실 생존용으로) 토큰을 아끼기 위해 컨텍스트를 자주 비운다거나 하는 갖은 노력을 하고 있었는데, 이렇게 before/after를 실제로 비교해보면서 줄여 본 것은 처음이라 너무 재밌었네요
그리고 단순히 스킬 문서의 분량을 줄이는 것 보다는 "이 일을 정말 AI가 해야 하나?"와 "이 문장이 정말 AI에게 필요한가?"를 계속 고민하고 답을 찾아보는 작업이었어서 이 과정에서 얻은 것들이 앞으로 좀 더 똘똘하게 AI를 사용하는데 도움이 될 것 같습니다 😁😁
관련 글