본문 바로가기

LLM

프롬프트 버전 관리 완전 가이드 — Git처럼 프롬프트를 관리하는 법

반응형

프롬프트를 수정했더니 응답 품질이 떨어졌습니다. 언제 바꿨는지, 뭘 바꿨는지 모릅니다. 되돌릴 수도 없습니다. 코드는 Git으로 관리하면서 프롬프트는 왜 노션에 복붙하고 있습니까.

[핵심 요약]
→ 문제: 프롬프트 변경 이력 없음 → 품질 저하 원인 추적 불가
→ 해결: 프롬프트를 코드처럼 버전 관리
→ 방법: Git 기반 파일 관리 + 메타데이터 + 자동 평가
→ 도구: YAML 파일 + Git + LangSmith / PromptLayer / 자체 구축
→ 원칙: 프롬프트 = 코드 → 같은 방식으로 관리
→ 효과: A/B 테스트, 롤백, 팀 협업, 품질 추적 가능

왜 프롬프트 버전 관리가 필요한가

코드 버전 관리 (당연하게 함):
git commit -m "로그인 버그 수정"
git revert HEAD  # 되돌리기
git diff         # 변경사항 확인
git blame        # 누가 언제 바꿨나

프롬프트 버전 관리 (대부분 안 함):
# 노션 페이지에 최신 프롬프트 복붙
# 슬랙에 "프롬프트 업데이트함" 메시지
# 3주 후: "왜 응답이 이상해졌지?"
# 원인 추적: 불가능
[프롬프트 버전 관리가 없을 때 발생하는 문제]

① 품질 저하 추적 불가
→ "지난주부터 응답이 이상한데 뭘 바꿨지?"
→ 변경 이력 없음 → 원인 불명

② 팀 협업 충돌
→ A가 프롬프트 수정 → B가 다른 버전 사용
→ 같은 기능인데 응답이 다름

③ A/B 테스트 불가
→ "이 프롬프트가 더 좋은지 확인하고 싶은데..."
→ 어떤 버전이 배포 중인지 모름

④ 롤백 불가
→ 새 프롬프트 배포 → 품질 저하
→ 이전 버전으로 되돌리고 싶은데 기록 없음

실전 1 — YAML 기반 프롬프트 파일 구조

# prompts/customer_support/v2.3.yaml

metadata:
  name: customer_support
  version: "2.3"
  created_at: "2026-04-28"
  author: "cell"
  description: "고객 지원 챗봇 메인 프롬프트"
  changelog: |
    v2.3: 환불 정책 섹션 추가, 톤 조정 (더 친근하게)
    v2.2: 다국어 지원 추가
    v2.1: 응답 길이 제한 추가
  tags: [customer-support, production]
  model: claude-sonnet-4-6
  temperature: 0.7

variables:
  - name: company_name
    required: true
    description: "회사 이름"
  - name: product_name
    required: false
    description: "제품 이름 (없으면 일반 지원)"

system_prompt: |
  당신은 {{company_name}}의 친절한 고객 지원 전문가입니다.

  ## 응답 원칙
  - 항상 공감으로 시작하세요
  - 구체적인 해결책을 제시하세요
  - 해결 불가 시 에스컬레이션 안내

  ## 환불 정책
  - 구매 후 7일 이내: 전액 환불
  - 7~30일: 50% 환불
  - 30일 초과: 환불 불가

  ## 응답 길이
  - 일반 답변: 200자 이내
  - 복잡한 문제: 500자 이내

  {% if product_name %}
  현재 지원 제품: {{product_name}}
  {% endif %}

evaluation:
  golden_set_path: "tests/prompts/customer_support_golden.json"
  min_quality_score: 0.85
  metrics:
    - accuracy
    - tone_consistency
    - resolution_rate
# 프롬프트 로더 구현
import yaml
from pathlib import Path
from string import Template

class PromptRegistry:
    """YAML 기반 프롬프트 레지스트리"""

    def __init__(self, prompts_dir: str = "prompts"):
        self.prompts_dir = Path(prompts_dir)
        self._cache = {}

    def load(
        self,
        name: str,
        version: str = "latest",
        variables: dict = None
    ) -> dict:
        """프롬프트 로드 + 변수 치환"""

        if version == "latest":
            prompt_file = self._find_latest(name)
        else:
            prompt_file = self.prompts_dir / name / f"v{version}.yaml"

        if not prompt_file.exists():
            raise FileNotFoundError(f"프롬프트 없음: {name} v{version}")

        with open(prompt_file) as f:
            prompt_data = yaml.safe_load(f)

        # 변수 치환
        if variables:
            system = prompt_data["system_prompt"]
            for key, value in variables.items():
                system = system.replace(f"{{{{{key}}}}}", str(value))
            prompt_data["system_prompt"] = system

        return prompt_data

    def _find_latest(self, name: str) -> Path:
        """가장 최신 버전 찾기"""
        prompt_dir = self.prompts_dir / name
        versions = sorted(
            prompt_dir.glob("v*.yaml"),
            key=lambda p: [int(x) for x in p.stem[1:].split(".")]
        )
        return versions[-1] if versions else None


# 사용
registry = PromptRegistry("prompts")

prompt = registry.load(
    name="customer_support",
    version="2.3",
    variables={"company_name": "셀테크", "product_name": "셀 앱"}
)

print(prompt["system_prompt"])
print(f"버전: {prompt['metadata']['version']}")

실전 2 — Git 워크플로우 통합

# 프롬프트 디렉토리 구조
prompts/
├── customer_support/
│   ├── v1.0.yaml
│   ├── v2.0.yaml
│   └── v2.3.yaml  ← 현재 프로덕션
├── code_review/
│   ├── v1.0.yaml
│   └── v1.2.yaml
└── summarizer/
    └── v1.0.yaml

# .gitignore에서 prompts/ 제외 (추적 대상)
# .gitattributes로 YAML diff 설정
echo "prompts/**/*.yaml diff=yaml" >> .gitattributes
# 프롬프트 변경 워크플로우

# 1. 새 버전 브랜치 생성
git checkout -b prompt/customer-support-v2.4

# 2. 프롬프트 수정
cp prompts/customer_support/v2.3.yaml \
   prompts/customer_support/v2.4.yaml
# v2.4.yaml 수정...

# 3. 자동 평가 실행 (CI/CD)
python scripts/evaluate_prompt.py \
    --prompt customer_support \
    --version 2.4

# 4. 평가 통과 시 PR 생성
git add prompts/customer_support/v2.4.yaml
git commit -m "feat(prompt): customer_support v2.4 - 환불 정책 업데이트"
git push origin prompt/customer-support-v2.4

# 5. PR 머지 → 프로덕션 배포
# GitHub Actions로 자동 평가
# .github/workflows/prompt-eval.yml
"""
name: Prompt Evaluation

on:
  pull_request:
    paths:
      - 'prompts/**/*.yaml'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Detect changed prompts
        id: changed
        run: |
          CHANGED=$(git diff --name-only origin/main | grep 'prompts/.*\.yaml')
          echo "files=$CHANGED" >> $GITHUB_OUTPUT
      - name: Evaluate changed prompts
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python scripts/evaluate_prompt.py \
            --files "${{ steps.changed.outputs.files }}"
"""

실전 3 — 프롬프트 평가 자동화

import anthropic
import json
from pathlib import Path

class PromptEvaluator:
    """프롬프트 자동 평가"""

    def __init__(self):
        self.client = anthropic.Anthropic()

    def evaluate(
        self,
        prompt_data: dict,
        golden_set_path: str
    ) -> dict:
        """골든셋으로 프롬프트 품질 평가"""

        with open(golden_set_path) as f:
            golden_set = json.load(f)

        results = []

        for case in golden_set:
            # 프롬프트 실행
            response = self.client.messages.create(
                model=prompt_data["metadata"]["model"],
                max_tokens=1024,
                system=prompt_data["system_prompt"],
                messages=[{"role": "user", "content": case["input"]}]
            )
            actual = response.content[0].text

            # LLM-as-Judge로 품질 평가
            score = self._judge(
                input=case["input"],
                expected=case["expected"],
                actual=actual,
                criteria=case.get("criteria", [])
            )

            results.append({
                "case_id": case["id"],
                "score": score,
                "passed": score >= prompt_data["evaluation"]["min_quality_score"]
            })

        avg_score = sum(r["score"] for r in results) / len(results)
        passed    = sum(1 for r in results if r["passed"])

        return {
            "version":   prompt_data["metadata"]["version"],
            "avg_score": avg_score,
            "passed":    passed,
            "total":     len(results),
            "pass_rate": passed / len(results),
            "details":   results
        }

    def _judge(
        self, input: str, expected: str,
        actual: str, criteria: list
    ) -> float:
        """LLM-as-Judge 품질 평가"""
        criteria_text = "\n".join(f"- {c}" for c in criteria)

        response = self.client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=200,
            messages=[{
                "role": "user",
                "content": f"""다음 AI 응답을 0~1 사이 점수로 평가해주세요.

입력: {input}
기대 응답: {expected}
실제 응답: {actual}

평가 기준:
{criteria_text}

JSON으로만 응답: {{"score": 0.85, "reason": "이유"}}"""
            }]
        )

        result = json.loads(response.content[0].text)
        return result["score"]


# A/B 테스트
def ab_test(name: str, version_a: str, version_b: str):
    """두 프롬프트 버전 비교"""
    registry  = PromptRegistry()
    evaluator = PromptEvaluator()

    prompt_a = registry.load(name, version_a)
    prompt_b = registry.load(name, version_b)

    golden_set = prompt_a["evaluation"]["golden_set_path"]

    result_a = evaluator.evaluate(prompt_a, golden_set)
    result_b = evaluator.evaluate(prompt_b, golden_set)

    print(f"\n=== A/B 테스트 결과 ===")
    print(f"v{version_a}: {result_a['avg_score']:.3f} ({result_a['pass_rate']*100:.1f}% 통과)")
    print(f"v{version_b}: {result_b['avg_score']:.3f} ({result_b['pass_rate']*100:.1f}% 통과)")

    winner = version_b if result_b["avg_score"] > result_a["avg_score"] else version_a
    print(f"승자: v{winner}")

    return winner


# 실행
winner = ab_test("customer_support", "2.3", "2.4")

실전 4 — 프롬프트 배포 파이프라인

import json
from datetime import datetime

class PromptDeployment:
    """프롬프트 배포 및 롤백 관리"""

    def __init__(self, config_path: str = "prompt_config.json"):
        self.config_path = config_path
        self._load_config()

    def _load_config(self):
        try:
            with open(self.config_path) as f:
                self.config = json.load(f)
        except FileNotFoundError:
            self.config = {"active": {}, "history": []}

    def _save_config(self):
        with open(self.config_path, "w") as f:
            json.dump(self.config, f, ensure_ascii=False, indent=2)

    def deploy(self, prompt_name: str, version: str) -> None:
        """새 버전 배포"""
        # 현재 버전 히스토리에 저장
        if prompt_name in self.config["active"]:
            self.config["history"].append({
                "prompt":     prompt_name,
                "version":    self.config["active"][prompt_name],
                "retired_at": datetime.now().isoformat()
            })

        # 새 버전 활성화
        self.config["active"][prompt_name] = version
        self._save_config()

        print(f"배포 완료: {prompt_name} v{version}")

    def rollback(self, prompt_name: str) -> str:
        """이전 버전으로 롤백"""
        # 히스토리에서 직전 버전 찾기
        history = [
            h for h in self.config["history"]
            if h["prompt"] == prompt_name
        ]

        if not history:
            raise ValueError(f"롤백할 이전 버전 없음: {prompt_name}")

        prev_version = history[-1]["version"]
        self.deploy(prompt_name, prev_version)
        print(f"롤백 완료: {prompt_name} → v{prev_version}")
        return prev_version

    def get_active_version(self, prompt_name: str) -> str:
        return self.config["active"].get(prompt_name)

    def print_status(self):
        print("\n=== 프롬프트 배포 현황 ===")
        for name, version in self.config["active"].items():
            print(f"{name}: v{version} (활성)")


# 전체 워크플로우
registry   = PromptRegistry()
evaluator  = PromptEvaluator()
deployment = PromptDeployment()

# 현재 배포 버전 확인
deployment.print_status()
# customer_support: v2.3 (활성)

# 새 버전 평가
prompt_new = registry.load("customer_support", "2.4")
result     = evaluator.evaluate(
    prompt_new,
    prompt_new["evaluation"]["golden_set_path"]
)

if result["avg_score"] >= 0.85:
    deployment.deploy("customer_support", "2.4")
else:
    print(f"배포 취소: 품질 미달 ({result['avg_score']:.3f})")

# 품질 이슈 발생 시 롤백
deployment.rollback("customer_support")
# 롤백 완료: customer_support → v2.3

실전 5 — LangSmith / PromptLayer 연동 (선택)

자체 구축이 부담스러우면 기존 도구를 활용합니다.

# LangSmith로 프롬프트 허브 활용
from langsmith import Client

ls_client = Client()

# 프롬프트 저장
ls_client.push_prompt(
    "customer-support",
    object=ChatPromptTemplate.from_messages([
        ("system", SYSTEM_PROMPT),
        ("user", "{input}")
    ])
)

# 특정 버전 불러오기
prompt = ls_client.pull_prompt("customer-support:v2.3")

# 커밋 해시로 버전 고정 (프로덕션 안전)
prompt = ls_client.pull_prompt(
    "customer-support",
    commit_hash="abc123def"  # 정확한 버전 고정
)
[자체 구축 vs 외부 도구]

자체 구축 (이 가이드 방식):
→ 비용: 무료 (Git + YAML)
→ 유연성: 완전한 커스터마이징
→ 통합: CI/CD 파이프라인 자유롭게 구성
→ 단점: 초기 구축 시간 필요

LangSmith:
→ 비용: 무료 플랜 있음, 팀은 유료
→ 장점: 트레이싱, 평가, 허브 통합
→ 단점: 벤더 의존

PromptLayer:
→ 비용: 유료 ($0.002/요청)
→ 장점: 빠른 설정, 분석 내장
→ 단점: 비용, 외부 의존

마무리

✅ 프롬프트 버전 관리 써야 할 때
→ 팀에서 여러 명이 프롬프트를 수정하는 경우
→ 프로덕션에 여러 프롬프트가 배포된 경우
→ A/B 테스트로 프롬프트 성능을 비교하고 싶을 때
→ 품질 저하 발생 시 원인을 추적해야 할 때
→ 프롬프트 변경이 잦은 서비스 (주 1회 이상)

❌ 과한 경우
→ 1인 개발자, 프롬프트 1~2개
→ 프로토타입/실험 단계
→ 프롬프트가 거의 바뀌지 않는 서비스

[최소 구현 (오늘 당장 시작)]
1. prompts/ 디렉토리 생성
2. 현재 프롬프트를 v1.0.yaml로 저장
3. git add prompts/ && git commit
4. 이후 변경 시 버전 올리고 changelog 작성
→ 이것만 해도 추적 가능해짐

관련 글:

https://cell-devlog.tistory.com/42

 

컨텍스트 엔지니어링 — 프롬프트 엔지니어링의 다음 단계

2025년 6월, Andrej Karpathy(전 OpenAI, Tesla AI 디렉터)가 X에 짧은 글 하나를 올렸어요."프롬프트 엔지니어링이라는 말은 우리가 실제로 하는 일을 너무 사소하게 만든다. 더 정확한 표현은 컨텍스트 엔

cell-devlog.tistory.com

https://cell-devlog.tistory.com/151

 

AI 에이전트 상태 관리 완전 가이드 — 장기 실행 에이전트에서 상태를 잃지 않는 법

에이전트가 30분 작업 중 20분에 크래시났습니다. 처음부터 다시 시작합니다. 이 문제를 구조적으로 해결하는 법을 정리했습니다.[핵심 요약]→ 문제: LLM 컨텍스트는 세션 종료 시 사라짐 → 장기

cell-devlog.tistory.com

 

반응형