AI 비디오 아키타입(Archetype) 선택 방식을 산문 형태에서 코드 기반의 일일 지침으로 전환한 방법
요약
LLM 루틴이 긴 산문 형태의 지침을 무시하는 문제를 해결하기 위해, 전략적 결정을 코드 기반의 짧고 명확한 명령형 지침(Directive)으로 전환하는 방법을 다룹니다. 데이터 분석 스크립트가 직접 실행 가능한 지침을 생성하여 LLM의 실행력을 높이는 구조적 개선 사례를 소개합니다.
핵심 포인트
- 긴 산문 형태의 지침은 LLM에게 제약이 아닌 제안으로 인식될 위험이 있음
- 전략적 결정은 산문이 아닌 코드 기반의 명령형 지침으로 분리해야 함
- 분석 스크립트가 결정을 소유하고 LLM은 이를 실행만 하는 구조가 효과적임
- 단일 화면 분량의 짧고 명확한 지침이 LLM의 컨텍스트 유지에 유리함
문제점: 루틴이 훑어보고 지나쳐 버리는 500줄짜리 지식 저장소
이 실험을 위한 YouTube 분석 파이프라인을 처음 구축했을 때, 나는 발견한 내용들을 읽기 쉬운 지식 저장소 형태인 docs/yt-knowhow-bank-en.md로 구조화했습니다. 아이디어는 매일의 숏폼 비디오 스크립트를 생성하는 LLM 루틴이 이 파일을 읽고, 발견 사항을 흡수하여 그에 따라 출력을 조정하는 것이었습니다.
실제로 루틴은 지식 저장소를 읽기는 했지만, 그 내용은 무시했습니다.
가장 명확한 증상은 build_in_public 비디오였습니다. 지식 저장소에는 build_in_public 아키타입(Archetype)의 조회수 중앙값이 34회에서 8회로 급감했다는 점이 명시되어 있었고, 약 200줄 정도 아래에 "build_in_public을 제작하지 마시오"라는 지침이 포함되어 있었습니다. 하지만 루틴은 계속해서 이를 제작했습니다. 매일 그런 것은 아니었지만, run.py에 의해 수집된 조회수 데이터에 따르면 채널 중앙값보다 약 20배 높은 성과를 내고 있는 product_findindiegame 형식에 할당되었어야 할 게시 슬롯을 동일한 죽은 형식이 계속 잡아먹을 정도로 빈번했습니다.
문제는 LLM이 지침을 이해하지 못한 것이 아니었습니다. 전략적 결정이 맥락, 주의 사항, 역사적 관찰 및 서식 노트가 뒤섞인 긴 산문(Prose) 문서 안에 매몰되어 있었던 것이 문제였습니다. 200줄 안에 하나의 금지 사항이 묻혀 있는 500줄짜리 파일은 제약 시스템(Constraint system)이 아니라 제안함(Suggestion box)에 불과합니다.
나는 이전 포스트에서 초기 분류기 설계를 기록했고, 루틴이 계속 이를 무시할 때 진행한 재구축에 대해서도 기록했습니다. 그 중 어느 것도 근본적인 문제를 완전히 해결하지는 못했습니다. 이번 방법이 바로 그 문제를 해결한 방법입니다.
해결책: 레포지토리에 커밋된 단일 화면의 명령형 지침
해결책은 scripts/yt-analytics/run.py에 있습니다. 이제 매일의 분석 실행 시 docs/yt-today-directive.md에 파일을 작성합니다. 이는 루틴이 무언가를 생성하기 전에 반드시 가장 먼저 읽어야 하는, 단일 화면 분량의 명령형 지침(Imperative directive)입니다.
DIRECTIVE_PATH = REPO_ROOT / "docs" / "yt-today-directive.md"
해당 라인의 코드 주석은 다음과 같습니다: "전략적 결정(어떤 아키타입을 만들 것인가)은 루틴의 산문적 판단이 아닌, 우리가 제어할 수 있는 코드 내에 존재합니다. 기존의 산문적 판단은 무시되고 있었습니다."
생성된 지침(directive)은 정확히 하나의 아키타입 대상과 두 가지 금지 사항을 명시합니다:
## SHORT (daily)
- **archetype = product_findindiegame** ← 오늘 이것을 생성할 것 (최종 선택)
- **numeric** 훅(hook) 패턴으로 시작할 것
...
루틴은 다른 어떤 컨텍스트보다 먼저 이 파일을 읽도록 지시받습니다. 이 파일은 단 하나의 실행 가능한 결정만을 담고 있으며 주변에 부연 설명(prose)이 없는 짧은 파일이기 때문에, 대충 훑어보고 지나칠 만한 내용이 없습니다.
지식 뱅크(knowledge bank)는 여전히 존재하며 관찰 내용과 과거 데이터를 축적합니다. 지침(directive)은 이와 분리되어 있습니다. Python 분석 스크립트가 결정을 소유하고, LLM 루틴은 이를 실행합니다.
구현: DEAD_ARCHETYPES와 공유 편향(bias) 함수
run.py 내의 두 곳은 어떤 아키타입이 실행 가능한지에 대해 일치해야 합니다: 지식 뱅크 섹션(yt-knowhow-bank-en.md에 추가됨)과 지침(yt-today-directive.md에 작성됨)입니다. 두 곳이 서로 모순되는 것을 방지하기 위해, 양쪽 모두 동일한 archetype_bias() 함수를 호출합니다:
DEAD_ARCHETYPES = frozenset({"build_in_public", "meta", "curated", "technical"})
def archetype_bias(videos, high):
...
"unknown"을 제외하는 로직을 올바르게 구현하는 데 시간이 좀 걸렸습니다. 스크립트의 초기 버전은 분류되지 않은 비디오가 분포의 어느 쪽에 위치하느냐에 따라 "unknown"을 최선의 아키타입 또는 최악의 아키타입으로 표시하곤 했습니다. yt-publish 실패로 인해 최근 비디오에 매칭할 업로드된 메타데이터(uploaded-metadata) 파일이 남지 않게 되면, 업로드 실패 사례들이 모여 "unknown"이 마치 그 주의 최고 성과자인 것처럼 보이게 만듭니다. 이를 제외한다는 것은 순위가 내가 실제로 제어할 수 있는 아키타입만을 반영함을 의미합니다.
DEAD_ARCHETYPES frozenset은 의도적으로 성과 순위(performance ranking)와 분리되어 있습니다. 이 아키타입(archetypes)들은 단순히 성과가 낮은 것이 아니라, 경험적 근거에 따라 범주적으로 제외된 것들입니다: build_in_public은 중앙값 조회수(median views)가 약 34회에서 8회로 급감했습니다. meta, curated, technical은 추구할 가치가 있는 반복적인 신호(repeat signal)를 생성하지 못했습니다. 이를 이름이 지정된 상수(named constant)로 유지한다는 것은, 실시간 순위가 어떻게 변하든 상관없이 지침(directive)이 실수로 이를 오늘의 타겟으로 지정하는 일이 결코 발생하지 않음을 의미합니다.
| 접근 방식 | 리스크 | 결과 |
|---|---|---|
| 지식 저장소(knowledge bank) 내 산문 형태의 금지 명령 | LLM이 문서를 훑어보거나 문서 중간에 문맥을 놓침 | 죽은 아키타입(dead archetypes)이 계속 나타남 |
| ... |
3회 연속 방지 장치 (The 3-in-a-row guard)
지침이 승리하는 아키타입을 안정적으로 지정하기 시작하자, 매일이 product_findindiegame의 날이 되었습니다. 성과 관점에서는 문제가 없습니다. 검증된 승자에게 집중하는 것이 옳기 때문입니다. 하지만 이는 두 가지 실질적인 문제를 야기합니다: 동일한 오디언스가 변주 없는 동일한 포맷을 매일 보게 된다는 점과, (특정 게임 타이틀을 짝지어주는) 매치업 큐(matchup queue)가 보충되는 속도보다 더 빠르게 소진된다는 점입니다.
방지 장치는 가장 최근의 업로드 2개를 확인합니다:
recent = recent_uploaded_archetypes(2)
if len(recent) == 2 and recent[0] == recent[1] == target:
alt = next(
...
핵심 제약 조건: 대체재인 alt는 죽은 아키타입(dead archetype)이어서는 안 됩니다. 이는 2026-07-07에 발생한 구체적인 버그의 원인이었습니다. 당시 지침은 build_in_public을 오늘의 타겟으로 지정했으나, 동일한 파일 내에는 "절대 제작 금지: build_in_public"이라고 명시되어 있었습니다. 방지 장치는 연속 기록을 올바르게 감지하여 전환을 시도했지만, 죽은 아키타입으로 전환하는 것이 허용되었던 것입니다. 해결책은 DEAD_ARCHETYPES에 속하지도 않고, 플래그가 지정된 avoid_arch도 아닌 대안만을 수락하는 것이었습니다.
방지 장치 이후에는 최종 안전 점검 단계가 있습니다:
if target in DEAD_ARCHETYPES:
target = DEFAULT_TARGET_ARCHETYPE
switched_from = None
심층 방어 (Defense in depth). 만약 가드 (guard)가 실행되기 전에 랭킹 데이터가 어떤 방식으로든 죽은 아키타입 (dead archetype)을 승자로 생성하더라도, 이것이 이를 잡아냅니다. 가드와 최종 확인 (final check)은 독립적이며, 어느 쪽도 상대방이 충분할 것이라고 가정하지 않습니다.
가드가 하지 않는 한 가지는 다음과 같습니다: 다음 날에 다시 승자(winner)로 전환하는 것. 루틴은 전환이 이미 발생했는지 여부가 아니라, 마지막 두 번의 업로드 (uploads)를 추적합니다. 이는 승자가 다시 재개될 수 있기 전에 대안 아키타입 (alternative archetype)이 단 한 번만 실행됨을 의미합니다. 더 긴 이력을 추적하는 것도 고려했지만, 두 번의 연속된 업로드는 과도하게 교정하지 않으면서도 단조로움을 깨기에 충분한 신호였습니다.
DEFAULT_TARGET_ARCHETYPE 폴백 (fallback)
채널 초기에는 아키타입을 신뢰성 있게 순위 매길 만큼 분류된 비디오 (classified videos)가 충분하지 않았습니다. "데이터 없음, 결정 없음"으로 폴백 (fallback)되는 라우팅 함수 (routing function)는 쓸모가 없습니다.
DEFAULT_TARGET_ARCHETYPE = "product_findindiegame"
이것은 pref_arch == "—" (아직 분류된 데이터가 없음)일 때와 죽은 아키타입 (dead-archetype) 확인 후의 최종 안전망 (safety net)으로 사용됩니다. 이것은 임의적인 것이 아닙니다. 초기 추론 (inference)은 신뢰할 수 없었기 때문에, 불충분한 데이터로부터 추론하는 대신 코드에 명시적으로 설정된, 경험적으로 알려진 승자입니다.
전체 지침 (directive) 흐름:
YouTube 통계 가져오기 (fetch YouTube stats)
↓
attach_archetype() — 제목 단어 중첩에 의해 매칭 (≥4개 단어)
...
지침은 yt-analytics.yml에 의해 자동으로 리포지토리 (repo)에 커밋됩니다. 하루 중 나중에 비디오 생성 루틴 (video generation routine)이 실행될 때쯤이면, 지침은 이미 main에 들어와 이를 기다리고 있습니다.
내가 다르게 했을 점
지침을 마크다운 (markdown)이 아닌 구조화된 JSON으로 발행할 것. 현재 형식은 인간과 LLM 모두 읽을 수 있지만, 프로그래밍 방식으로 파싱 (parse)해야 하는 작업에는 불편합니다. JSON 스키마 (JSON schema) — { "target": "product_findindiegame", "hook": "numeric", "avoid": ["build_in_public"] } — 를 사용하면 다른 스크립트들이 지침을 검증하고 의심스러운 상태에 대해 경고를 보낼 수 있습니다. 마크다운 형식은 저항이 가장 적은 경로(path of least resistance)였으나, LLM 루틴 이외의 소비자 (consumers)를 추가하기 전에 교체되어야 합니다.
조회수(view count) 대신 시청 시간(watch time)을 사용하세요. 분류기(classifier)는 YouTube Data API v3 videos.list 엔드포인트가 추가 인증 없이 viewCount를 반환하기 때문에 이를 사용합니다. 시청 시간(시청된 분 단위)은 아키타입(archetype)별 시청자 유지율(audience retention)을 파악하는 데 더 강력한 신호입니다. OAuth를 사용하는 YouTube Analytics API를 사용하면 이를 활용할 수 있지만, 이는 서로 다른 할당량 단위(quota units)를 가진 별도의 API 표면(API surface)입니다. 아직 이를 연결하지는 않았으나, 현재의 분류기는 방향성 측면에서 충분히 정확합니다.
3회 연속(3-in-a-row) 가드(guard)를 추출하고 단위 테스트(unit-test)하세요. 가드 로직은 render_directive()에 내장되어 있어, 이를 테스트하려면 전체 분석 파이프라인(analytics pipeline)을 모킹(mocking)해야 합니다. apply_streak_guard(target, ranked, recent, dead)를 독립적인 함수로 추출하면 가짜 히스토리(fake history)를 사용하여 테스트할 수 있습니다. 여기에는 'dead'가 아닌 모든 아키타입이 avoid_arch인 엣지 케이스(edge case)도 포함됩니다(현재는 승자를 유지하는 것으로 처리되어 결과는 맞지만, 해당 경로는 테스트되지 않은 상태입니다).
48-Shorts 회고록에는 이러한 아키타입 결정의 근거가 되는 실증적 데이터가 기록되어 있습니다. 롱폼 파이프라인 큐 로직(longform pipeline queue logic)과 2인 호스트 스펙 형식(two-host spec format)은 동일한 아키타입 시스템의 하위 소비자(downstream consumers)입니다. 할당량 및 cron 작업(quota and cron work)은 GitHub Actions 스케줄링 측면을 다룹니다. 분석 워크플로(analytics workflow)와 생성 워크플로(generation workflow)는 설계상 서로 다른 시간에 실행됩니다.
FAQ
왜 금지 사항을 시스템 프롬프트(system prompt)에 내장하지 않나요?
시스템 프롬프트(system prompt)는 여러 루틴(routine)에서 공유됩니다. 그곳에 아키타입(archetype)별 금지 사항을 내장하면, 승리한 포맷이 바뀔 때마다 프롬프트를 변경해야 합니다. 지침 파일(directive file)을 사용하면 프롬프트를 건드리지 않고도 분석 스크립트가 결정을 업데이트할 수 있습니다.
루틴은 지침을 먼저 읽어야 한다는 것을 어떻게 알 수 있나요?
루틴의 작업 설명(task description)에는 다른 어떤 컨텍스트보다도 docs/yt-today-directive.md를 읽는 것이 명시적인 첫 번째 단계로 나열되어 있습니다. 작업 설명 내에서의 위치가 중요합니다. 제약 사항을 앞부분에 배치(front-loading)하면 이를 건너뛰기가 더 어려워집니다.
YouTube API를 사용할 수 없으면 어떻게 되나요?
분석 스크립트는 우아하게 실패(fails gracefully)합니다. 데이터가 불충분함을 알리는 최소한의 보고서를 작성하고 지침을 업데이트하지 않습니다. 전날의 지침이 그대로 유지됩니다. 일시적인 API 오류가 루틴을 의식적으로 선택한 적 없는 기본값(default)으로 리셋시키지는 않습니다.
DEAD_ARCHETYPES를 하드코딩하는 것이 유지보수 부담을 주지 않나요?
frozenset은 6주 동안 두 번 변경되었습니다. 이는 산문 형태의 문서보다 낮은 비율입니다. 산문 문서는 원래의 금지 사항이 희석될 때까지 계속해서 단서(qualification)가 추가되는 경향이 있습니다. 코드로 관리되는 상수(constant)는 감사(audit)하기가 더 쉽습니다. grep -r DEAD_ARCHETYPES를 실행하면 해당 상수가 사용되고 강제되는 모든 위치를 확인할 수 있습니다.
list나 set 대신 왜 frozenset을 사용하나요?
멤버십 테스트(target in DEAD_ARCHETYPES)의 경우, list의 O(n)에 비해 frozenset은 O(1)입니다. 요소가 4개일 때는 실행 시간 차이가 미미하지만, frozenset은 실행 도중 의도치 않은 변이(mutation)를 방지합니다. set을 사용하면 실행 중에 .add()를 통해 제약 사항이 조용히 변경될 수 있습니다.
세 개의 AI 큐레이션 디렉토리 사이트를 운영하는 6개월간의 지속적인 실험 중 일부입니다. 여기에 기술된 주장들은 실제이며, 이 기사는 AI의 도움을 받았습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기