본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 14:10

에이전트가 스킬 본문은 무시하면서 시스템 프롬프트(System Prompt)는 따르는 이유

요약

에이전트 프레임워크에서 스킬 본문(Skill Body)을 수정해도 반영되지 않는 원인을 분석합니다. 이는 특정 스킬이 트리거될 때만 프롬프트가 로드되는 구조적 특성 때문이며, 해결을 위해 항상 켜져 있는 레이어(Always-on layer)를 활용해야 합니다.

핵심 포인트

  • 스킬 프롬프트는 해당 스킬이 트리거될 때만 로드될 수 있음
  • 모든 쿼리에 적용될 규칙은 Always-on layer에 위치해야 함
  • 프롬프트 수정 후에는 반드시 실제 요청 덤프(Request Dump)를 확인해야 함
  • Claude Agent Skills, Gemini CLI 등 많은 프레임워크가 유사한 구조를 가짐

행동을 수정하기 위해 스킬의 프롬프트(Prompt)를 편집합니다. 변경 사항이 런타임(Runtime)이 로드하는 파일에 반영되었음을 확인합니다. 재시작합니다. 하지만 에이전트는 여전히 예전 방식대로 행동합니다. 어쩌다 한 번이 아니라, 대부분의 쿼리(Query)에 대해 일관되게 말이죠.

본능적으로 프롬프트를 더 강하게 수정하고 싶어질 것입니다. 하지만 실제 문제는 당신이 편집한 프롬프트가 요청(Request)에 전혀 포함되지 않았다는 점입니다. 기본 시스템 프롬프트(Base System Prompt)에 조건부로 로드되는 스킬 본문(Skill Body)이 더해지는 스킬/에이전트 프레임워크(Skill/Agent Framework)에서는, 당신의 "프롬프트" 중 상당수가 스킬이 트리거(Trigger)될 때만 로드됩니다. 만약 해당 규칙이 모든 쿼리에 적용되어야 한다면, 그 규칙은 항상 켜져 있는 레이어(Always-on layer)에 존재해야 합니다. 동일한 버그의 후반부는 이렇습니다. 항상 켜져 있는 파일에는 배포(Deploy) 단계가 없어서, 실행 중인 복사본이 저장소(Repo)로부터 조용히 벗어나 있었습니다.

이는 프레임워크 전반에 걸친 문제입니다. 우리는 로컬 Hermes 에이전트에서 이 문제를 겪었지만, 현재 흔히 사용되는 '기본 프롬프트 + SKILL.md 분리' 구조를 가진 모든 것에 동일한 형태가 적용됩니다 — Claude Agent Skills, Gemini CLI skills, VS Code 에이전트 스킬 등이 그러합니다.

문제 (Problem)

우리의 에이전트 스택(Agent Stack)은 공유된 페르소나(SOUL.md)와 에이전트별 스킬(Navigator, Engineer, Logbook)을 가지고 있습니다. 페르소나는 선박의 이름을 지정하고 목소리를 설정하며, 각 스킬은 자신만의 직무를 계층적으로 쌓습니다. 우리는 Navigator 스킬 본문에서 선박 이름을 변경하고, 이를 배포했으며, 디스크상의 런타임 파일도 확인했습니다:

$ grep -n "VESSEL_NAME_HERE\|aboard" ~/.hermes/skills/naturali/navigator/SKILL.md
1:Navigator agent aboard s/v Naturali.

이름도 맞고, 파일도 맞고, 경로도 맞습니다. 재시작합니다. 그리고 일반적인 질문을 던져봅니다:

> how's our depth?
Aboard s/v Wrongboat, depth is 8.2 metres below the keel, Captain.

여전히 예전 이름입니다. 런타임이 로드하는 파일에는 올바른 내용이 적혀 있습니다. 하지만 에이전트는 틀린 내용을 말합니다. 스킬 본문을 몇 번을 다시 편집해도 이 쿼리에 대해서는 아무것도 변하지 않았습니다.

진단 (Diagnosis)

모델이 무엇을 "보아야 하는지"에 대해 이론만 세우지 말고, 모델이 실제로 무엇을 받았는지 확인하십시오. Hermes는 호출마다 전체 요청 덤프(request dump)를 작성합니다. 대부분의 프레임워크에도 이와 유사한 기능(디버그 로그, --print-prompt, 프록시 캡처 등)이 있습니다. 요청을 덤프하고 grep으로 검색해 보세요:

$ ls -t ~/.hermes/sessions/request_dump_*.json | head -1
/Users/me/.hermes/sessions/request_dump_1733270400.json

...

여기에 답이 있습니다. 단순한 "how's our depth?"라는 질문에 대해, 기본 페르소나(base persona)는 프롬프트에 포함되어 있었지만 Navigator 스킬 본문(skill body)은 포함되어 있지 않았습니다. 쿼리가 Navigator 스킬을 활성화하지 않았기 때문에, 해당 스킬의 SKILL.md 본문은 로드되지 않았습니다. 우리가 그토록 정성스럽게 편집해 온 선박 이름은 오직 그 본문 안에만 존재했습니다.

이것은 바로 이러한 프레임워크들이 구축된 점진적 공개 (progressive disclosure) 모델이며, Anthropic의 Agent Skills 문서에도 명확히 명시되어 있습니다:

Level 1: 메타데이터 (Metadata, 항상 로드됨) ... Claude는 시작 시 이 메타데이터를 로드하여 시스템 프롬프트(system prompt)에 포함합니다.
Level 2: 지침 (Instructions, 트리거 시 로드됨) ... 스킬의 설명(description)과 일치하는 요청을 하면, Claude는 파일 시스템에서 SKILL.md를 읽습니다. 그제서야 이 내용이 컨텍스트 윈도우(context window)에 들어옵니다.

따라서 스킬의 name/description은 항상 컨텍스트에 포함되어 있지만(이를 통해 모델은 스킬의 존재를 알 수 있습니다), 실제 지침인 **본문(body)**은 해당 스킬이 실행될 때만 로드됩니다. 반면, 기본 시스템 프롬프트(base system prompt)는 모든 요청에 포함됩니다. 만약 어떤 지침이 항상 적용되어야 한다면, 그것은 항상 켜져 있는 레이어(always-on layer)에 있어야 합니다. 선박의 정체성(Vessel identity)은 항상 적용되어야 하는 규칙입니다. 하지만 그것은 잘못된 레이어에 있었습니다.

잘못된 이름이 나타난 것은 이를 드러낸 _증상(symptom)_일 뿐이며, 이 원리는 모든 '항상 켜져 있는 불변값(always-on invariant)'에 일반화됩니다. 정체성, 시간/단위 규율, 환각 방지(anti-fabrication) 규칙 등은 모두 해당 규칙을 포함하고 있는 스킬을 트리거하지 않는 모든 쿼리에서 조용히 적용되지 않은 채 실패합니다.

우리가 시도한 것 (그리고 실패한 이유)

시도 1 — 스킬 본문을 편집하고 재배포

스킬 소스 내 이름을 수정하고 재배포함

$ scripts/deploy-navigator.sh
deploy-navigator: wrote ~/.hermes/skills/naturali/navigator/SKILL.md (vessel: Naturali)

> 현재 수심은 어느 정도인가요?
Aboard s/v Wrongboat, depth is 8.2 metres below the keel, Captain.

변화 없음. 파일은 정확하지만, 쿼리가 파일을 전혀 로드하지 못하고 있습니다. 조건부로 로드되는 본문(body)을 아무리 수정해 보아도, 해당 스킬을 트리거하지 않는 쿼리에는 결코 영향을 미칠 수 없습니다.

시도 2 — 파일이 배포되지 않았다고 가정하고 디스크 재확인

$ cat ~/.hermes/skills/naturali/navigator/SKILL.md | head -1
Navigator agent aboard s/v Naturali.

런타임 파일은 내내 올바른 상태였습니다. 이로써 배포 실패 가능성은 배제되었습니다. 이 시점은 "이 파일이 올바른가?"가 아니라, "이 파일이 아예 로드되고 있는가?"라는 질문으로 방향을 틀었어야 하는 순간이었습니다. 이 둘은 매우 다른 질문입니다. 우리는 잘못된 질문에 답하고 있었습니다.

시도 3 — 스킬 본문에 정체성 규칙을 더 강력하게 추가

우리는 최소한 로드된 스킬이 정체성을 유지할 수 있도록, 모든 스킬 본문에 이름/정체성 문구를 중복해서 넣는 방안을 고려했습니다. 이 방법은 어떤 스킬이라도 트리거하는 쿼리에는 "작동"하지만, (스킬을 전혀 트리거하지 않는) 순수/사회적/모호한 쿼리들은 여전히 잘못된 상태로 남겨둡니다. 또한 동기화해야 할 불변값(invariant)을 N개만큼 생성하게 되어, 전형적인 드리프트 함정(drift trap)에 빠지게 됩니다. 이는 증상만을 치료하는 방식입니다. 규칙은 스킬별로 적용되는 것이 아니라 항상 켜져 있어야(always-on) 합니다. 따라서 항상 켜져 있는 단 하나의 프롬프트에 위치해야 합니다.

해결책

항상 켜져 있어야 하는 불변값들을 항상 켜져 있는 레이어(기본 페르소나, base persona)로 옮기고, 선박 이름을 템플릿화하여 아무것도 하드코딩(hardcoded)되지 않도록 합니다:

<!-- SOUL.md — 모든 요청에서 로드되는 기본 페르소나 -->
You are the ship's computer aboard s/v {{VESSEL_NAME}}.
You address the user as "Captain."
...

그런 다음, 해당 파일을 한 번 수동으로 배치하는 대신, 단일 진실 공급원(source of truth)으로부터 이름을 치환하여 실제 배포 단계를 거치도록 합니다:

# scripts/deploy-soul.sh
vessel_name="$(resolve_vessel_name "$repo_root")"   # 활성 vessel 프로필을 읽음
sed "s|{{VESSEL_NAME}}|$vessel_name|g" "$repo_root/SOUL.md" > "$HERMES_HOME/SOUL.md"
> 현재 수심은 어느 정도인가요?
s/v Naturali호의 수심은 용골(keel) 아래로 8.2미터입니다, 선장님.

스킬(skill)을 트리거하지 않는 질의에 대해서도 올바른 이름을 답변합니다. 이는 정체성(identity)이 조건부적인 본체(body)가 아니라, 항상 켜져 있는 페르소나(persona)에 실려 있기 때문입니다.

스킬 본체(skill body)에는 진정으로 조건적인 것들, 즉 스킬이 실행될 때만 로드되어도 괜찮은 운영상의 힌트(operational hints)만을 유지합니다:

<!-- skills/navigator/body.md — Navigator가 트리거될 때만 로드됨 -->
"현재 수심은 어느 정도인가요?"라는 질문에는 belowTransducer가 아닌 environment.depth.belowKeel을 읽으십시오.

중요성 / 주의사항 (Why it matters / gotchas)

모든 프롬프트 규칙을 '항상 켜져 있는 것(always-on)'과 '조건적인 것(conditional)'으로 분류하고 그에 따라 배치하십시오. 각 규칙에 대해 한 가지 질문을 던지십시오: 이 규칙이 스킬을 트리거하지 않는 질의에서도 유지되어야 하는가? 만약 그렇다면, 베이스 프롬프트(base prompt)에 포함되어야 합니다 (또는 데이터 형태 규칙의 경우 결정론적 도구 계층(deterministic tool layer)에 포함되어야 하지만, 이는 다른 포스트에서 다룹니다).

만약 규칙이 특정 태스크 내부에서만 유효하다면, 스킬 본체(skill body)가 토큰 비용을 아낄 수 있는 적절한 위치입니다. 정체성(identity), 안전성(safety), 단위(units), 그리고 환각 방지(anti-fabrication) 규칙은 거의 항상 전자에 해당합니다.

프롬프트를 수정하기 전에 요청 덤프(request dump)를 먼저 확인하십시오. "프롬프트에는 X라고 되어 있는데 에이전트가 Y를 한다"는 상황은 외부에서 보기에 동일해 보이는 두 가지 실패 모드가 있습니다: 프롬프트가 틀렸거나, 프롬프트가 로드되지 않았거나. 오직 요청 덤프만이 이 둘을 구분할 수 있습니다. 우리는 요청에 포함되지도 않은 파일을 편집하느라 디버깅 사이클을 두 번이나 허비했습니다. 덤프된 프롬프트를 grep 한 번만 해봤어도 즉시 잡아낼 수 있었을 것입니다.

모든 런타임 프롬프트 아티팩트 (runtime prompt artifact)는 배포 단계가 필요하며, 그렇지 않으면 부패합니다. 이 버그의 두 번째 원인은 다음과 같습니다. 우리의 ~/.hermes/SOUL.md 파일은 몇 달 전 한 번 수동으로 배치되었으며, 그 사이 이름이 변경된 보트(boat)의 이름을 그대로 가지고 있었습니다. 오직 스킬 (skill) 에만 배포 스크립트가 있었기에, 스킬만이 리포지토리 (repo)를 추적하고 있었습니다. 수동으로 복사하여 배치한 프롬프트 파일은 내용이 달라졌음을 알려주는 메커니즘이 없으며, 그대로 오래된 상태(stale)로 조용히 실행됩니다. 우리는 SOUL.md에 스킬이 이미 가지고 있던 것과 동일한 처리를 적용하여 이 문제를 해결했습니다.

# .git/hooks/pre-commit — 변경된 소스가 무엇이든 재배포
if echo "$changed" | grep -Eq '^(SOUL\.md$|scripts/(deploy-soul|vessel-name)\.sh$)'; then
  scripts/deploy-soul.sh    # 관련 커밋마다 기본 페르소나 (base persona) 재배포
...

이제 두 소스 중 하나라도 편집하는 커밋이 발생하면 이를 재배포합니다. 런타임 (runtime)은 해당 내용을 알리는 커밋 없이는 리포지토리로부터 벗어날(drift) 수 없습니다.

공유된 값들을 위한 단일 진실 공급원 (One source of truth). 보트 이름은 활성 보트 프로필 (active vessel profile)로부터 결정됩니다. 이 프로필은 데이터 레이어 (data-layer)의 기본 값들을 생성하는 것과 동일한 프로필이므로, 데이터 모델 (data model)과 페르소나 (persona) 내의 보트 이름이 동일하게 유지되며, 보트를 전환할 때 프롬프트 수정이 전혀 필요하지 않습니다. 두 레이어에 나타나는 값을 하드코딩 (hardcode) 하지 마세요. 하나의 리졸버 (resolver)로부터 두 곳 모두를 템플릿화 (template) 하십시오. 두 곳에 하드코딩된 값은 곧 발생할 드리프트 (drift)를 기다리는 것과 같습니다.

결론 (Close)

이 사례는 전동 차터 카타마란 (all-electric charter catamaran)을 위한 AI ops 레이어를 구축하는 과정에서 나왔습니다. 이는 해양 데이터 버스 (marine data bus) 상에서 공유된 페르소나와 작업별 스킬을 가진 로컬 LLM (local LLM) 환경이었으며, 여기서 에이전트 (agent)는 항해 관련 쿼리뿐만 아니라 모든 쿼리에서 보트를 실제 이름으로 호출해야 했습니다. 에이전트 스킬과 배포 스크립트는 오픈 소스로 공개되어 있습니다: github.com/sailingnaturali/naturali-agents.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0