핵심 프롬프트 템플릿은 해시로 핀됩니다. 템플릿이 한 글자라도 바뀌면 계산된 해시가 핀과 어긋나고 CI가 깨집니다. 프롬프트 변경을 막으려는 것이 아니라, 변경이 항상 의도된 diff로 드러나게 하려는 래칫입니다.
네 개의 핀
core/llm/prompts/__init__.py가 .md 템플릿 (router.md, commentary.md)을 로드해 SHA-256 앞 12자를 PROMPT_VERSIONS로 계산하고, 하드코딩된 _PINNED_HASHES와 비교합니다.
| 핀 | 출처 | 역할 |
|---|---|---|
ROUTER_SYSTEM | core/llm/prompts/router.md | AgenticLoop 시스템 프롬프트의 베이스 템플릿. |
AGENTIC_SUFFIX | core/llm/prompts/router.md | agentic 모드에서 덧붙는 suffix 절. |
COMMENTARY_SYSTEM | core/llm/prompts/commentary.md | 커멘터리 시스템 프롬프트. |
COMMENTARY_USER | core/llm/prompts/commentary.md | 커멘터리 사용자 템플릿. |
비교 함수는 verify_prompt_integrity입니다. 어긋난 핀의 목록을 반환하고, raise_on_drift=True면 첫 불일치에서 RuntimeError를 던집니다. CI 테스트가 이 검증을 게이트로 겁니다.
왜 빌드를 깨는가
시스템 프롬프트는 동작을 정의하는 코드입니다. 그런데 일반 코드와 달리 타입 체커도 테스트도 문구 변화를 잡지 못합니다. 머지 충돌 해소, 포매터, 선의의 한 줄 수정이 프롬프트를 조용히 바꾸면 에이전트 동작이 원인 불명으로 흔들립니다. 해시 핀은 그 모든 경로를 컴파일 오류와 같은 등급으로 끌어올립니다. 자기개선 루프가 wrapper 스캐폴드를 변이시키는 시스템에서는 더 중요합니다. 의도된 변이는 SoT 파일로, 의도되지 않은 drift는 빌드 실패로, 두 경로가 섞이지 않습니다.
의도된 변경: re-pin 절차
템플릿을 일부러 고쳤다면 새 해시를 계산해 핀을 갱신하고, 같은 커밋에 템플릿 diff와 핀 diff가 나란히 실리게 합니다.
python -c "from core.llm.prompts import PROMPT_VERSIONS as V; \ print(dict(sorted(V.items())))" # 출력을 _PINNED_HASHES에 반영
리뷰어는 핀 diff를 보고 "프롬프트가 의도적으로 바뀌었다"는 사실을 한 줄로 확인합니다.
경계
핀 대상은 정적 템플릿이지 렌더된 프롬프트가 아닙니다. 메모리 레이어, 날짜, wrapper override처럼 런타임에 합성되는 부분은 해시 범위 밖입니다. 렌더 결과의 재현성 감사가 필요하면hash_rendered_prompt가 같은 12자 해시를 렌더된 문자열에 적용합니다.
실패 모드
| 증상 | 원인 | 해법 |
|---|---|---|
| CI에서 "Prompt drift" 실패 | 템플릿 변경이 핀 갱신 없이 들어옴 | 변경이 의도라면 re-pin 절차를 따릅니다. 아니라면 diff를 되돌립니다. |
| 핀만 바뀌고 템플릿은 그대로인 PR | 이전 drift를 핀 갱신으로 덮으려는 시도 | 템플릿 diff 없는 핀 diff는 리뷰에서 거부합니다. |
다음
- 왜 ratchet 규율인가. 이 가드의 설계 철학.
- 프롬프트 조립. 핀된 템플릿이 소비되는 곳.