🌳
심화

개발 케이스스터디

프롬프트 17단계·50% 최적화·launchd 구조

목차

챕터 1: 프롬프트 17단계 세분화

도입

상담봇의 출력은 대단계 7개, 하위 단계 최대 17개로 쪼개져 있습니다. 원래는 7단계로 한 번에 호출했지만, 품질·맥락·건너뛰기 유연성 때문에 세분화했습니다.

이 챕터는 그 설계 결정을 다룹니다.

변경 전 · 변경 후

7단계 → 17단계 세분화

왜 세분화했는가

문제 1 — 한 호출이 너무 길었음

한 대단계에 "장점 + 단점 + 공감 멘트" 를 모두 요구하면, Claude가 균형 맞추느라 각 부분이 얇아짐. 특히 공감 멘트가 형식적으로 나왔음.

문제 2 — 교사가 세부 조절 불가

장점 해석은 충분한데 공감 멘트만 더 길게 받고 싶을 때, 대단계 통째로 다시 돌려야 했음.

문제 3 — 건너뛰기 단위가 너무 큼

공감 멘트만 건너뛰고 싶은데 대단계 전체가 나와야 했음.

현재 17단계 구조

출력 17단계 구조

맥락 전달 설계

각 하위 단계가 독립 호출이면 맥락 유실 위험

1-1 장점 해석에서 말한 내용을 1-2 단점 해석이 모르면, "앞서 말씀드린 장점이 뒤집히면..." 같은 연결 멘트가 안 나옵니다.

해결 — sub_responses 누적 전달

이전 하위 단계 응답의 마지막 2000자를 다음 프롬프트에 포함합니다.

맥락 전달 sub_responses

왜 2000자냐: 경험적으로 2000자 이상은 맥락 유지에 큰 차이 없고, 프롬프트 크기만 커져서 비용·속도 악화.

응답 포맷 자동 배분

단계 성격에 따라 3가지 포맷이 자동 배분됩니다:

포맷 길이 구조 할당 단계
FMT_EXPLAIN (설명형) 800~1200자 번호 + 굵은키워드 + 짧은문장 1-1, 1-2, 2-1, 2-2, 3-1, 3-2, 4-1, 4-2
FMT_SCRIPT (멘트형) 500~800자 교사 실전 대사, 큰따옴표 1-3, 2-3, 3-3, 4-3, 7-2
FMT_ANALYSIS (분석형) 800~1200자 데이터 항목별 분석 + 연결 3-1e, 5, 6, 7-1

코드상 구현

STAGE_FORMAT = { "1-1": FMT_EXPLAIN, "1-3": FMT_SCRIPT, "7-1": FMT_ANALYSIS, # ... } def build_prompt(stage_id, context): fmt = STAGE_FORMAT[stage_id] return FORMAT_TEMPLATES[fmt].format(**context)

건너뛰기 UX

각 하위 단계 답변에 [다음 ▶] / [⏭️ 건너뛰기] 버튼:

사용자 클릭 다음 하위 ↓ 단계 실행 [⏭️ 건너뛰기] → 현재 하위 단계는 스킵 sub_responses에도 추가 안됨 → 다음 호출에서 이 단계 내용 없음

트레이드오프

측면 이전 (7 호출) 현재 (17 호출)
호출 횟수 적음 약 2.4배 증가
총 시간 짧음 (3~4분) 길어짐 (7~10분)
응답 품질 보통 높음
건너뛰기 유연성 낮음 높음
맥락 일관성 자동 sub_responses로 수동 관리
프롬프트 복잡도 낮음 높음

품질·유연성을 우선하는 결정이었음

요약

다음 챕터: Stage 3 프롬프트 50% 최적화 (203KB → 101KB)

이 내용이 궁금하신가요?

상담 교육 관련 질문을 편하게 보내주세요

심화 > 챕터 1 > 프롬프트 17단계 세분화

💬 질문하기

챕터 2: Stage 3 프롬프트 50% 최적화

도입

상담봇의 도형 해석 단계(Stage 3)는 가장 무거운 프롬프트였습니다. 전체 57개 도형 조합 전략 + 공통 해석 자료를 매번 함께 넣어서 203KB 까지 부풀었습니다.

이걸 101KB(50% 감소) 로 줄인 최적화 케이스입니다.

문제 상황

Stage 3 프롬프트 50% 최적화

왜 문제였나

  1. Claude 호출 시간 증가 — 입력 토큰 많으면 처리 시간 선형 증가
  2. 비용 증가 — 토큰 단위 과금 (나중 SDK 전환 시 직접 부담)
  3. 맥락 희석 — Claude가 관계없는 56개 조합까지 "고려" 하느라 핵심 놓침

최적화 전략 2가지

전략 1 — 필터링 (동적 선택)

섭외자의 에니어그램 번호에 해당하는 조합만 프롬프트에 포함.

이전: 현재: 57개 조합 전체 포함 섭외자 에니어 = 6번 (어떤 유형이 올지 모르니) ↓ 6번 관련 조합만 포함 (약 6~8개)

전략 2 — 중복 제거

57개 조합 중 모든 조합에 공통적으로 포함되는 대형 자료 3건(각 ~12KB)이 있었음. 매 조합마다 반복되니 프롬프트가 폭발.

해결: 공통 자료 3건을 프롬프트에서 제외하고, 필요 시 knowledge DB 조회로 별도 주입.

이전: 조합 A = 핵심해석(2KB) + 공통자료(38KB) 조합 B = 핵심해석(2KB) + 공통자료(38KB) ... (57번 반복 = 공통 38KB × 57) 현재: 조합 A = 핵심해석(2KB) 조합 B = 핵심해석(2KB) ... 공통 자료는 필요한 경우만 DB에서 조회

전략 3 — 도입부 제거

각 조합마다 "이 조합은 도형과 에니어의 관계를 설명합니다..." 같은 메타 도입부가 있었음. Claude에겐 불필요한 서술 → 모두 제거.

결과 비교

항목 이전 현재 감소율
프롬프트 크기 203KB 101KB -50%
포함 조합 수 57개 (고정) 6~8개 (동적) 동적
공통자료 중복 57회 0회 (DB 별도) -100%
평균 응답 시간 45~60초 20~30초 -50%

DB knowledge 도형 중복 제거 (별개 작업)

같은 시기에 knowledge DB의 도형 해석 항목도 중복 정리했습니다.

[이전] 92건 (유사한 조합 중복 다수) [현재] 57건 (중복 35건 제거) + 공통 대형 3건은 프롬프트에서 제외

knowledge DB 전체 변화: 186건 → 151건

교훈

✅ 효과적이었던 것

  1. 필터링은 사용자 컨텍스트 기반 — "누가 오는지" 를 먼저 파악
  2. 중복 제거 + DB 조회 분리 — 프롬프트는 얇게, DB는 풍부하게
  3. 메타 서술 제거 — Claude는 "왜 이 자료인지" 설명 없어도 활용 가능

⚠️ 놓치기 쉬웠던 것

  1. 중복을 처음엔 못 봤음 — 개별 조합 파일을 봐선 공통 자료의 반복이 안 보임. du 로 전체 크기 보고서야 감지
  2. 필터링 후 품질 검증 필요 — 관련 없는 유형이 혹시 영향 주고 있지 않았나 테스트케이스로 확인

코드 관점 — 동적 프롬프트 빌더

def build_stage3_prompt(user_ennea: int, context: dict) -> str: # 전략 1: 필터링 relevant_combos = db.get_combos_for_ennea(user_ennea) # 전략 3: 메타 도입부 제거, 핵심만 combo_blocks = [c["core_analysis"] for c in relevant_combos] prompt = STAGE3_TEMPLATE.format( combos="\n---\n".join(combo_blocks), user_data=context ) # 전략 2: 공통자료는 필요시만 별도 주입 if needs_common_knowledge(context): prompt += db.fetch_common_knowledge() return prompt

이 케이스에서 일반화 가능한 패턴

"모든 사용자에게 같은 대형 프롬프트" 는 거의 항상 비효율입니다. 확인할 것:

1. 이 프롬프트의 90%가 매번 필요한가? → 아니면 필터링 2. 같은 내용이 여러 섹션에 반복되는가? → 아니면 중복 제거 3. 메타 서술(설명의 설명)이 있는가? → 제거

요약

다음 챕터: launchd · TCC · claude CLI 구조 — 맥에서 봇 24시간 돌리기

이 내용이 궁금하신가요?

상담 교육 관련 질문을 편하게 보내주세요

심화 > 챕터 2 > Stage 3 프롬프트 50% 최적화

💬 질문하기

챕터 3: launchd · TCC · Claude CLI 구조

도입

봇을 24시간 자동 가동하려면 운영체제 수준의 구성이 필요합니다. 맥에서 겪은 3가지 이슈와 해결 방법을 정리합니다.

전체 구조

launchd 구조 4 레이어

이슈 1 — TCC (macOS 보안)

현상

~/Documents/ 경로에서 launchd로 실행하면 macOS가 파일 접근을 차단. TCC(Transparency, Consent, and Control) 정책 때문.

증상

해결

코드를 TCC 제외 경로로 복사 후 그곳에서 실행:

개발 원본: ~/Documents/consulting-bot/ (TCC 차단) ↓ deploy.sh로 동기화 실행 경로: ~/consulting-bot-run/ (TCC 허용)

deploy.sh 핵심 흐름

# 코드 동기화
for f in bot.py db.py handlers.py ...; do
    cp "$SRC/$f" "$DEST/$f"
done

# 데이터 rsync
rsync -au "$DATA_SRC/" "$DATA_DEST/"

# 봇 재시작
launchctl unload "$PLIST"
launchctl load -w "$PLIST"

이슈 2 — launchd PATH

현상

launchd로 실행된 프로세스는 셸 rc 파일(.zshrc)을 읽지 않음 → PATH가 최소한만 설정됨 → claude 같은 CLI 못 찾음.

증상

Errno 2 No such file or directory: 'claude'

shell에서는 claude 되는데, launchd 봇은 못 찾음.

해결 2가지

방법 A — plist에 PATH 명시

<key>EnvironmentVariables</key>
<dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>

방법 B — 코드에서 절대경로 사용 (더 안전)

import shutil

_NPM_CLAUDE = "/Users/.../.npm-global/bin/claude"
_CMUX_CLAUDE = "/Applications/cmux.app/.../claude"

if os.path.exists(_NPM_CLAUDE):
    CLAUDE_BIN = _NPM_CLAUDE
elif (_w := shutil.which("claude")) and _w != _CMUX_CLAUDE:
    CLAUDE_BIN = _w
else:
    CLAUDE_BIN = _CMUX_CLAUDE

권장: B (절대경로). A는 claude 설치 위치 바뀌면 또 고장.

이슈 3 — CLAUDECODE 중첩 세션

현상

봇이 Claude Code 환경 안에서 개발·테스트되는 경우, 환경변수 CLAUDECODE=1 이 상속됨. 이 상태에서 봇이 claude -p 를 호출하면:

Error: nested Claude Code session detected

해결

subprocess 호출 전에 환경변수 제거:

env = os.environ.copy()
env.pop("CLAUDECODE", None)  # 다른 CLAUDE* 변수는 유지

proc = await asyncio.create_subprocess_exec(
    CLAUDE_BIN, "-p",
    env=env,
    ...
)

CLAUDECODE 만 제거, CLAUDE_CONFIG_DIR 등 다른 CLAUDE* 변수는 유지해야 계정 인증 상태가 유지됨.

plist 파일 핵심 필드

<key>Label</key>
<string>com.consulting-bot</string>

<key>ProgramArguments</key>
<array>
    <string>/bin/bash</string>
    <string>/Users/.../consulting-bot-run/run_bot.sh</string>
</array>

<!-- 자동 재시작 (크래시 시) -->
<key>KeepAlive</key><true/>

<!-- 재시작 간격 (연속 크래시 방지) -->
<key>ThrottleInterval</key><integer>10</integer>

<!-- 로그인 시 자동 시작 -->
<key>RunAtLoad</key><true/>

<!-- 로그 -->
<key>StandardOutPath</key>
<string>/Users/.../logs/bot.log</string>
<key>StandardErrorPath</key>
<string>/Users/.../logs/bot.log</string>

단일 인스턴스 보장

문제

launchd 재시작 타이밍에 이전 프로세스가 완전히 죽기 전 새 프로세스가 올라오면, 두 봇이 동시에 텔레그램 폴링 → 409 Conflict.

해결 — PID 파일

import os import atexit PID_FILE = "/tmp/consulting-bot.pid" def ensure_single_instance(): if os.path.exists(PID_FILE): old_pid = int(open(PID_FILE).read()) if is_alive(old_pid): sys.exit("Already running") open(PID_FILE, "w").write(str(os.getpid())) atexit.register(lambda: os.remove(PID_FILE))

표 — launchctl 주요 명령

명령 역할
launchctl load -w 등록 + 활성화 (⚠️ 항상 -w)
launchctl unload 중지 + 등록 해제
`launchctl list \ grep name` 상태 확인 (PID, exit code)
launchctl kickstart -k 재시작

주의: load 할 때 -w 없으면 "Disabled" 상태로 남아 다음 부팅 시 안 뜰 수 있음.

상담봇 실제 구성 요약

plist 경로: ~/Library/LaunchAgents/com.consulting-bot.plist 개발 경로: ~/Documents/consulting-bot/telegram-claude-bot/ 실행 경로: ~/consulting-bot-run/ ← TCC 우회 └── .venv/ ← Python 3.13 venv └── data-extracted/ ← DB, knowledge 로그: ~/consulting-bot-run/logs/bot.log

요약

심화 레벨 마무리

세 챕터에서 다룬 것들:

  1. 프롬프트 17단계 세분화 — 품질·유연성 우선 설계
  2. Stage 3 50% 최적화 — 필터링 + 중복 제거 + 메타 제거
  3. launchd · TCC · CLI — 운영체제 레벨 상시 가동

이 패턴들은 다른 Claude 기반 봇에도 재사용 가능합니다. 상담봇은 기획부 업무봇, 교육 Q&A 봇 등 다른 봇들과 같은 구조를 공유합니다.

이 내용이 궁금하신가요?

상담 교육 관련 질문을 편하게 보내주세요

심화 > 챕터 3 > launchd · TCC · Claude CLI 구조

💬 질문하기
목차로 돌아가기