Jump to content

Agent Lightning 기반 프롬프트 자동 개선 파이프라인 — LLM 성능 끌어올리기⚡️


Recommended Posts

image.png.be86de18af2061b61bffc88b99272a68.png

LLM 기반 에이전트를 만들고 나면, 그다음 고민은 얼마나 잘 작동하느냐입니다.

초기 프롬프트가 단순한 데모 상황에서는 만족스러운 답변을 내놓더라도, 실제 서비스 환경에서는 응답 품질이 떨어지거나, 의도와 다른 행동을 보이거나, 특정 입력에 취약한 패턴이 드러날 수 있습니다. 이럴 때는 데이터를 보강하고, 프롬프트를 다듬고, 정책을 조정해 에이전트를 점진적으로 더 똑똑하고 안정적으로 만드는 과정이 필수적입니다.

이번 쿡북에서는 이러한 개선 과정을 손쉽게 반복 실행할 수 있도록 도와주는 프레임워크, Agent Lightning을 다룹니다.

특히 별도의 모델 튜닝 없이도 프롬프트를 자동으로 수정·검증해 주는 APO(Automatic Prompt Optimization)를 활용해, 최소한의 설정만으로 에이전트 개선 루프를 구성하는 방법을 소개합니다. 또한 기본적으로 영문 프롬프트 최적화에 맞춰 설계된 Agent Lightning의 APO를 한국어 환경에서도 안정적으로 활용할 수 있도록, POML(Prompt Optimization Markup Language) 템플릿을 한국어 기반으로 커스터마이징해 적용하는 방법도 함께 다룹니다.

이번 쿡북을 통해 CLOVA Studio에서 제공하는 모델을 더 안정적으로 다루고, 실제 서비스 품질을 높이는 프롬프트 개선 전략을 익히는 데 도움이 되길 바랍니다.

 

1. Agent Lightning 개요

마이크로소프트에서 공개한 Agent Lightning은 에이전트의 학습과 최적화를 체계적으로 수행할 수 있도록 설계된 프레임워크입니다. 이 프레임워크는 에이전트의 실행을 자동으로 추적하고, 그 결과로 얻은 보상(Reward)을 기반으로 프롬프트나 정책을 개선할 수 있게 해줍니다.

 

1-1. 핵심 개념

Agent Lightning에서 다루는 핵심 개념은 다음과 같습니다.

  • Task(태스크): 에이전트에게 주어지는 구체적인 입력 또는 임무입니다. 장소를 예약하거나 수학 문제를 풀어주는 것처럼, 에이전트가 해결해야 할 대상을 의미합니다.
  • Rollout(롤아웃): 하나의 태스크가 주어지고, 에이전트가 실행되어 도구 호출이나 LLM 호출 등을 거쳐 행동을 완료하고, 마지막에 보상(Reward) 을 받는 한 번의 전체 사이클을 말합니다.
  • Span(스팬): 롤아웃 내부의 작은 단위 실행입니다. LLM 호출 하나, 툴 실행 하나 등이 각각의 스팬이 될 수 있습니다.
  • Prompt Template(프롬프트 템플릿): 태스크를 해결하기 위해 에이전트가 사용하는 지시문 및 프롬프트의 구조입니다. 이 템플릿은 알고리즘에 의해 반복적으로 개선됩니다.

에이전트가 수행하는 모든 롤아웃은 보상 정보와 함께 기록되고, 이 데이터를 기반으로 프롬프트나 정책을 점진적으로 개선할 수 있습니다.

 

1-2. 구성 요소

Agent Lightning은 다음 세 가지 주요 구성 요소로 이루어집니다.

  • Agent(에이전트): 태스크를 입력받아 에이전트 로직을 수행하고 보상을 리턴합니다. 이를 통해 각 실행이 자동으로 롤아웃으로 기록됩니다.
  • Algorithm(알고리즘): 프롬프트나 정책을 개선하기 위한 알고리즘입니다. APO, VERL 등 다양한 알고리즘을 지원하며, 이번 쿡북에서는 프롬프트 최적화를 다루기 위해 APO를 사용합니다.
  • Trainer(트레이너): 에이전트와 알고리즘을 연결하고, 학습 루프를 제어하는 구성 요소입니다. 반복적인 실행과 평가를 통해 점진적인 개선을 수행합니다.

즉, 이미 만들어둔 에이전트 코드에 간단한 데코레이터(@rollout)를 추가하기만 하면, 각 실행의 입력, 출력, 보상 데이터를 자동으로 기록하고, 이를 기반으로 프롬프트나 정책을 개선하는 학습 가능한 에이전트 루프를 구성할 수 있습니다.

이러한 Agent Lightning을 활용하면 복잡한 학습 코드를 직접 작성하지 않아도, 에이전트의 실행 기록과 보상 정보를 바탕으로 다양한 프롬프트 버전을 자동으로 생성·평가할 수 있습니다. 그 과정에서 더 높은 보상을 주는 프롬프트가 자동으로 선택되고, 테스트셋 기준의 성능 비교와 기록까지 이루어져, 에이전트를 점진적으로 고도화하는 작업을 손쉽게 반복할 수 있습니다.

 

2. 환경 설정

2-1. CLOVA Studio API 준비

CLOVA Studio에서는 Chat Completions, 임베딩을 비롯한 주요 API에 대해 OpenAI API와의 호환성을 지원합니다. 본 예제에서는 OpenAI 호환 API 중 Chat Completions 엔드포인트(/chat/completions)를 활용하며, 상세 호환 정보는 OpenAI 호환성 가이드를 참고하시기 바랍니다. 

또한, 해당 API 호출하려면 CLOVA Studio에서 발급받은 API 키가 필요합니다. API 키 발급 방법은 CLOVA Studio API 가이드에서 확인할 수 있습니다.

 

2-2. 프로젝트 구성

프로젝트의 전체 파일 구조는 다음과 같습니다. Python은 3.10 이상을 사용하며, 3.13 버전을 권장합니다.

agent_lightning_cookbook/
├── .env
├── rollout.py
├── run_example.py
├── prompts/
│   ├── apply_edit_ko.poml
│   └── text_gradient_ko.poml
└── train_apo.py

 

2-3. 환경 변수 설정

루트 디렉토리에 .env 파일을 생성한 뒤, 앞서 발급받은 API Key를 다음과 같이 입력하고 저장합니다. 이때 따옴표 없이 값을 작성해야 하며, VS Code에서 실행할 경우 설정에서 Use Env File 옵션이 활성화되어 있는지 확인하세요.

CLOVA_STUDIO_API_KEY=YOUR_API_KEY

 

2-4. 패키지 설치

프로젝트에 필요한 패키지를 다음 코드를 실행하여 설치합니다.

pip install agentlightning openai python-dotenv poml

 

3. Rollout 구현

Agent Lightning의 롤아웃 구조를 단일 파일로 단순화해 구현해 봅니다. 

본 예제에서는 자연어 요청을 5개 카테고리(주행, 차량 상태, 차량 제어, 미디어, 생활정보)로 분류하는 에이전트를 구성합니다.

각 태스크는 @dataclass로 정의되며, CLOVA Studio의 HCX-005 모델을 사용해 분류를 수행합니다. 시스템 프롬프트에는 다섯 가지 카테고리의 정의와 출력 규칙이 포함되어 있으며, 모델은 입력 문장을 읽고 리스트 형태의 문자열로 응답합니다. 정답은 하나 또는 여러 개일 수 있으며, 어떤 카테고리에도 해당하지 않는 경우에는 빈 리스트([])를 반환하는 것이 올바른 출력입니다.

run_rollout() 함수는 한 번의 태스크 실행 단위를 나타내며, 태스크를 실행하고 그 결과를 기반으로 보상을 계산하는 역할을 합니다. 보상은 다음 규칙에 따라 계산되며, 이는 서비스 목적에 따라 자유롭게 커스터마이즈하고 확장할 수 있습니다.

  • 완전 일치(1.0): 모델 응답과 정답이 형식과 내용까지 모두 정확히 일치하는 경우
  • 부분 일치
    • 형식 불일치(0.9): 내용(레이블 집합)이 완전히 동일하지만, 따옴표, 공백, 대소문자 등의 형식이 다른 경우
    • 부분 문자열 일치(0.5): 레이블이 완전히 같지는 않지만 문자열이 부분적으로 겹치는 경우(예: '주행'과 '차량 주행')
    • 부분 레이블 일치(0.5): 여러 레이블 중 일부만 맞힌 경우(예: 2개 중 1개만 맞힌 경우)
  • 불일치(0.0): 리스트 형태로 파싱할 수 없거나, 파싱되더라도 정답과의 교집합이 전혀 없는 경우

이렇게 계산된 보상은 emit_reward()를 통해 Agent Lightning 내부에 기록되며, 이후 APO가 프롬프트를 개선할 때 신호로 활용될 수 있습니다. 

아래 코드는 태스크를 한 번 실행하고, 모델 응답을 평가해 보상을 기록하는 롤아웃 루프를 단일 파일로 구현한 예제입니다. 이는 프롬프트를 개선할 때 사용하는 핵심 루프 역할을 합니다. 아래 코드를 rollout.py로 저장하세요.

# rollout.py
import os
import re
import json
from dataclasses import dataclass
from typing import Optional, List

import asyncio
from dotenv import load_dotenv
from openai import AsyncOpenAI, RateLimitError
from agentlightning import emit_reward

load_dotenv()

# --- CLOVA Studio 설정 ---
BASE_URL = "https://clovastudio.stream.ntruss.com/v1/openai"
API_KEY = os.getenv("CLOVA_STUDIO_API_KEY")


# --- 태스크 정의 ---
@dataclass
class Task:
    """
    분류 태스크를 표현하는 데이터 구조입니다.

    - question: 분류 대상 문장
    - expected_labels: 정답으로 기대하는 레이블 리스트(예: ["주행", "미디어"])
    - task_id: (선택) 태스크 식별자
    - system_prompt: (선택) 기본 시스템 프롬프트를 덮어쓰고 싶을 때 사용
    """
    question: str
    expected_labels: List[str]
    task_id: Optional[str] = None
    system_prompt: Optional[str] = None


# --- 유틸리티 ---
def normalize_list(values: List[str]) -> List[str]:
    """리스트 값 정규화"""
    return [v.strip().lower() for v in values]


class RewardCalculator:
    """보상 계산 유틸리티"""

    @staticmethod
    def normalize(s: str) -> str:
        """문자열 정규화: 따옴표 제거, 공백 제거, 소문자 변환"""
        return s.strip().strip('\'"').lower()

    @staticmethod
    def is_partial_match(expected: str, actual: str) -> bool:
        """부분 문자열 일치 여부 확인"""
        e = RewardCalculator.normalize(expected)
        a = RewardCalculator.normalize(actual)
        return (e in a) or (a in e)


# --- LLM 클라이언트 ---
class ClovaClient:
    def __init__(self, model: str = "HCX-005", temperature: float = 0.0):
        self.client = AsyncOpenAI(
            base_url=BASE_URL,
            api_key=API_KEY,
        )
        self.model = model
        self.temperature = temperature

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # asyncio.run()이 루프를 닫기 전에 안전하게 정리
        try:
            self.client.close()
        except Exception:
            pass
        return False

    async def classify(self, task: Task) -> str:
        system_prompt = task.system_prompt or """
        당신은 분류기입니다. 입력 문장을 아래 5개 카테고리 중 해당되는 항목으로 분류하세요.

        카테고리 정의:
        - 주행: 주행 및 내비게이션 관련 요청
        - 차량 상태: 차량 진단/상태 확인
        - 차량 제어: 차량 기능 조작 요청
        - 미디어: 음악/라디오, 엔터테인먼트 요청
        - 개인 비서: 전화, 메시지, 일정 등 개인 비서 기능 요청

        출력 포맷:
        list 형태로 응답합니다. 해당되는 카테고리가 없다면 빈 배열로 응답하세요. 배열 내 문자열은 작은 따옴표로 감싸세요.
        """

        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": task.question},
        ]

        # LLM 호출(429 에러 시 재시도)
        max_retries = 5
        wait_time = 2

        for attempt in range(max_retries):
            try:
                resp = await self.client.chat.completions.create(
                    model=self.model,
                    messages=messages,
                    temperature=self.temperature,
                )
                return resp.choices[0].message.content.strip()
            except RateLimitError:
                if attempt < max_retries - 1:
                    # 지수 백오프: 2초, 4초, 8초, 16초, 32초
                    await asyncio.sleep(wait_time)
                    wait_time *= 2
                else:
                    raise
            except Exception:
                if attempt < max_retries - 1:
                    await asyncio.sleep(wait_time)
                    wait_time *= 2
                else:
                    raise


# --- 롤아웃 함수 ---
async def run_rollout(task: Task, client: ClovaClient) -> tuple[str, float]:
    """
    단일 롤아웃 실행:
    1) 분류 실행
    2) 보상 계산
    3) 보상 emit
    """
    # LLM 호출
    predicted = await client.classify(task)

    # JSON 파싱을 위한 전처리
    predicted_normalized = predicted.replace("'", '"')

    try:
        parsed = json.loads(predicted_normalized)

        if isinstance(parsed, list):
            predicted_list = [str(x).strip() for x in parsed]

        elif isinstance(parsed, str):
            predicted_list = [parsed]

        elif isinstance(parsed, dict):
            # JSON 객체인 경우: categories 또는 labels 키 찾기
            if "categories" in parsed:
                categories = parsed["categories"]
                if isinstance(categories, list):
                    predicted_list = [str(x).strip() for x in categories]
                else:
                    predicted_list = [str(categories).strip()]
            elif "labels" in parsed:
                labels = parsed["labels"]
                if isinstance(labels, list):
                    predicted_list = [str(x).strip() for x in labels]
                else:
                    predicted_list = [str(labels).strip()]
            else:
                # 다른 구조의 dict -> 0.0
                reward = 0.0
                try:
                    emit_reward(reward)
                except RuntimeError:
                    pass
                return predicted, reward
        else:
            # 리스트도 문자열도 dict도 아님 -> 0.0
            reward = 0.0
            try:
                emit_reward(reward)
            except RuntimeError:
                pass
            return predicted, reward

    except Exception:
        # JSON 파싱 실패 -> 0.0
        reward = 0.0
        try:
            emit_reward(reward)
        except RuntimeError:
            pass
        return predicted, reward

    # 파싱 성공 시 내용 비교
    calculator = RewardCalculator()
    expected_norm = [calculator.normalize(x) for x in task.expected_labels]
    actual_norm = [calculator.normalize(x) for x in predicted_list]

    if sorted(expected_norm) == sorted(actual_norm):
        expected_json = str(task.expected_labels)
        if predicted.strip() == expected_json:
            reward = 1.0  # 완전 일치
        elif "'" in predicted:
            reward = 0.9  # 형식 불일치
        elif '"' in predicted:
            reward = 0.9  # 형식 불일치
        else:
            reward = 0.9  # 형식 불일치

    elif set(expected_norm) & set(actual_norm):
        # 일부 레이블만 일치
        reward = 0.5

    else:
        # 부분 문자열 일치 확인
        has_partial = False
        for e in expected_norm:
            for a in actual_norm:
                if calculator.is_partial_match(e, a):
                    has_partial = True
                    break
            if has_partial:
                break

        reward = 0.5 if has_partial else 0.0

    # Agent Lightning에 보상 emit
    try:
        emit_reward(reward)
    except RuntimeError:
        pass

    return predicted, reward

다음은 롤아웃 샘플 실행 코드입니다. 아래 코드를 실행하면 에이전트가 다섯 개의 샘플 태스크를 순차적으로 수행하며, 각 태스크에 대한 모델 응답을 평가하고 보상을 계산·기록하는 과정을 확인할 수 있습니다.

# run_example.py
import asyncio
from rollout import Task, ClovaClient, run_rollout


async def run_tests():
    # 샘플 태스크 정의
    tasks = [
        Task(
            question="회사까지 가장 빠른 길 안내 시작해줘",
            expected_labels=["주행"],
            task_id="task_01",
        ),
        Task(
            question="타이어 공기압 체크",
            expected_labels=["차량 상태"],
            task_id="task_02",
        ),
        Task(
            question="온열 시트 켜고 출근길에 듣기 좋은 노래 틀어줘",
            expected_labels=["차량 제어", "미디어"],
            task_id="task_03",
        ),
        Task(
            question="엄마한테 전화 좀 걸어줘",
            expected_labels=["개인 비서"],
            task_id="task_04",
        ),
        Task(
            question="1+1은?",
            expected_labels=[],
            task_id="task_05",
        ),
    ]

    client = ClovaClient()

    for i, task in enumerate(tasks, 1):
        print(f"[Task {i}/{len(tasks)}] {task.task_id}")
        print(f"질의: {task.question}")

        predicted, reward = await run_rollout(task, client)

        print(f"모델 응답: {predicted}")
        print(f"실제 정답: {task.expected_labels}")
        print(f"Reward: {reward:.2f}\n")


if __name__ == "__main__":
    asyncio.run(run_tests())

위 스크립트 실행 결과입니다. 결과를 보면, 일부 개선이 필요한 태스크를 확인할 수 있습니다. 이러한 부분은 APO를 활용한 프롬프트 자동 최적화를 통해 지침을 점진적으로 정교화함으로써 자연스럽게 개선될 수 있습니다.

[Task 1/5] task_01
질의: 회사까지 가장 빠른 길 안내 시작해줘
모델 응답: ['주행']
실제 정답: ['주행']
Reward: 1.00

[Task 2/5] task_02
질의: 타이어 공기압 체크
모델 응답: ['차량 상태']
실제 정답: ['차량 상태']
Reward: 1.00

[Task 3/5] task_03
질의: 온열 시트 켜고 출근길에 듣기 좋은 노래 틀어줘
모델 응답: ["차량 제어", "미디어"]
실제 정답: ['차량 제어', '미디어']
Reward: 0.90

[Task 4/5] task_04
질의: 엄마한테 전화 좀 걸어줘
모델 응답: ['개인 비서']
실제 정답: ['개인 비서']
Reward: 1.00

[Task 5/5] task_05
질의: 1+1은?
모델 응답: []
실제 정답: []
Reward: 1.00

 

Quote

🤖 도구를 사용하는 에이전트에서의 보상 설계 팁

도구 기반 에이전트를 만들 경우, 보상은 다음과 같이 두 단계를 조합해 설계할 수 있습니다.

  1. 답변 정확도 보상: 모델의 답변과 정답의 일치도를 검증합니다. exact_match(완전 일치), contains(부분 일치), fuzzy_match(유사도)의 세 가지 전략 중 하나를 선택할 수 있습니다.
  2. 도구 사용 보상: 도구가 필요한 상황에서 도구를 사용하면 보너스(+0.3), 사용하지 않으면 페널티(−0.5)를 적용합니다.

최종 보상은 이 두 값을 합산해 0~1 범위로 클리핑하여 계산하며, 이렇게 산출된 보상은 에이전트를 개선하는 학습 루프에 활용할 수 있습니다.

 

4. APO 트레이너

APO는 Agent Lightning에 내장된 자동 프롬프트 개선 알고리즘입니다. 에이전트가 여러 태스크를 수행하며 얻은 보상을 기반으로 프롬프트 템플릿을 반복적으로 수정해, 더 높은 성능의 프롬프트로 수렴시키는 방식으로 동작합니다.

APO의 최적화 과정은 다음 두 단계로 구성됩니다.

  • Gradient 단계: 무엇이 잘못되었고 어떻게 개선해야 하는지를 분석하는 단계입니다.
  • Apply-Edit 단계: Gradient 단계에서 도출된 개선 방향을 기반으로 실제 프롬프트를 재작성하는 단계입니다.

즉, 모델이 어떤 응답을 생성했고 어떤 보상을 받았는지 분석한 뒤, 그 피드백을 기반으로 더 나은 프롬프트 후보를 생성·실험하는 구조입니다.

 

4-1. POML 커스터마이징

Agent Lightning에서 사용하는 APO 기본 템플릿은 모두 영문 기반 POML 파일로 제공됩니다. 기본 지시문이 영어 프롬프트 최적화를 전제로 설계되어 있기 때문에, 실제 최적화 과정에서도 모델이 영어 중심의 프롬프트를 생성하는 경향이 있습니다.

따라서 본 문서에서는 Microsoft Agent Lighting 레퍼런스 코드를 참고해, 한국어 프롬프트 최적화에 적합한 커스텀 POML 파일을 직접 구성하고 import하는 방식을 사용합니다.

Gradient 단계

이 템플릿은 APO의 첫 번째 단계에서 사용되며, LLM이 프롬프트의 문제점을 찾고, 개선 방향을 생성하는 역할을 수행합니다.

다음은 한국어 기반으로 재작성한 POML 템플릿 예시로, 태스크의 요구사항에 따라 해당 내용도 커스터마이징이 가능합니다. 아래 내용을 그대로 prompts 디렉터리 하위의 text_gradient_ko.poml 파일로 저장하세요.

<poml>
  <p>주어진 프롬프트 템플릿이 낮은 보상을 받은 이유를 정확하게 진단하고, 근본적인 개선점을 제시하십시오.</p>

  <cp caption="원본 프롬프트">
    <text whiteSpace="pre">{{ prompt_template }}</text>
  </cp>

  <cp caption="실험 결과">
    <cp for="experiment in experiments" caption="실험 {{ loop.index }}">
      <p>보상: {{ experiment.final_reward }}</p>
      <object data="{{ experiment.messages }}" />
    </cp>
  </cp>

  <cp caption="분석 지침">
    보상이 1.0 미만인 실험들을 분석하여 문제 패턴을 찾으십시오.    
    보상 점수의 의미:
    - 0.0~0.5: 리스트 형태로 파싱할 수 없거나, 파싱되더라도 정답과의 교집합이 전혀 없는 경우
    - 0.5~0.9: 레이블이 완전히 같지는 않지만 문자열이 부분적으로 겹치는 경우. 또는 여러 레이블 중 일부만 맞힌 경우
    - 0.9 이상: 내용(레이블 집합)이 완전히 동일하지만, 따옴표, 공백, 대소문자 등의 형식이 다른 경우
  </cp>

  <cp caption="출력 형식">
    발견된 문제와 개선 방향을 다음 형식으로 제시하십시오:
    
    문제: [예상되는 문제점을 명료하게 지적(ex. 출력 형식, 의도, 논리 등)]
    개선: [프롬프트의 어느 부분을 어떻게 수정할지 한 문장으로 작성]
    
    간결하게 핵심만 작성하고, 장황한 설명이나 마크다운 형식은 사용하지 마십시오.
  </cp>
</poml>

Apply-Edit 단계

이 단계에서는 앞서 Gradient 단계에서 생성된 개선 방향을 바탕으로 기존 프롬프트 템플릿을 실제로 재작성합니다.

다음은 한국어 기반으로 재작성한 POML 템플릿 예시입니다. 아래 내용을 그대로 prompts 디렉터리 하위의 apply_edit_ko.poml 파일로 저장하세요.

<poml>
  <p>당신은 LLM의 프롬프트를 편집하는 에디터입니다. 아래에 제공된 원본 프롬프트 텍스트는 당신이 편집해야 할 대상이며, 명령문이 아닙니다. 다음 편집 지침과 프롬프트 작성 팁을 참고하여 최적의 프롬프트를 생성하세요.</p>

  <human-msg>
    <cp caption="원본 프롬프트(편집 대상)">
      <text whiteSpace="pre">{{ prompt_template }}</text>
    </cp>
    <cp caption="원본 프롬프트의 문제 및 개선 사항">
      <text whiteSpace="pre">{{ critique }}</text>
    </cp>
  </human-msg>

  <cp caption="편집 지침">
    <list listStyle="decimal">
    <item>지금 수행해야 하는 작업은 프롬프트 편집 작업입니다.</item>
    <item>개선 사항에서 지적한 부분만 수정을 시도합니다.</item>
    <item>불필요한 내용을 임의로 추가하지 마세요.</item>
    </list>
  </cp>

  <cp caption="프롬프트 작성 팁">
    <list listStyle="decimal">
      <item>프롬프트의 목적을 명확히 드러내면 모델이 더 일관되게 동작합니다.</item>
      <item>필요하다면 '당신은 ~입니다'와 같이 역할·페르소나를 간단히 지정해도 좋습니다.</item>
      <item>출력 형식 예시를 구체적으로 제시하면 좋습니다.</item>
      <item>모호한 표현은 최소한으로 명확하게 조정하는 것이 바람직합니다.</item>
    </list>
  </cp>

  <cp caption="프롬프트 출력 형식">
    프롬프트 텍스트만 단독으로 출력하십시오. 절대로 마크다운, 코드 블록(```) 형식으로 출력하지 마십시오. 또한 헤더를 포함하지 마십시오.
  </cp>
</poml>

커스텀 템플릿 패치

이 스크립트는 프로젝트 내부의 prompts 디렉터리에 저장된 한국어 버전 POML 파일을 Agent Ligntning의 APO 디렉터리에 복사하여, 프롬프트 템플릿을 한국어 버전으로 패치합니다. 즉, 기존 영문 템플릿을 한국어 템플릿으로 덮어쓰도록 설정하여, 최적화 과정 전반이 한국어 기준으로 수행되도록 합니다.

아래 내용을 그대로 루트 디렉터리의 apo_ko_setup.py 파일로 저장하세요. 이후 apo_ko_setup 모듈을 import하는 것만으로, 앞서 정의한 한국어 템플릿이 APO 내부에 자동으로 적용됩니다.

# apo_ko_setup.py
import shutil
from pathlib import Path
import agentlightning.algorithm.apo as apo_mod


def patch_apo_for_korean():
    """APO 라이브러리의 영어 프롬프트를 한국어 프롬프트로 교체"""
    prompts_dir = Path(__file__).parent / "prompts"
    apo_base_dir = Path(apo_mod.__file__).parent
    apo_prompts_dir = apo_base_dir / "prompts"
    
    files = {
        "text_gradient_ko.poml": "text_gradient_variant01.poml",
        "apply_edit_ko.poml": "apply_edit_variant01.poml",
    }
    
    if not apo_prompts_dir.exists():
        print(f"APO 프롬프트 디렉터리를 찾을 수 없습니다: {apo_prompts_dir}")
        return
    
    for ko_file, apo_file in files.items():
        ko_path = prompts_dir / ko_file
        apo_path = apo_prompts_dir / apo_file
        
        if ko_path.exists():
            shutil.copy(ko_path, apo_path)
        else:
            print(f"{ko_file} 없음")


try:
    patch_apo_for_korean()
except Exception as e:
    print(f"APO 패치 실패: {e}")

 

4-2. 데이터셋 준비

분류 태스크의 학습 및 평가에 사용할 데이터를 준비합니다.

Quote
# dataset.py
from typing import List
from rollout import Task

def create_classification_dataset() -> List[Task]:
    return [
        # 주행(단일 카테고리) - 15개
        Task(task_id='task_01', question='집까지 가장 빠른 길 안내해줘', expected_labels=['주행']),
        Task(task_id='task_02', question='근처 주유소 가는 경로 안내 시작', expected_labels=['주행']),
        Task(task_id='task_03', question='고속도로 교통상황 알려줘', expected_labels=['주행']),
        Task(task_id='task_04', question='서울역으로 내비게이션 설정해줘', expected_labels=['주행']),
        Task(task_id='task_05', question='회사까지 최단거리로 안내', expected_labels=['주행']),
        Task(task_id='task_06', question='강남역 가는 길 알려줘', expected_labels=['주행']),
        Task(task_id='task_07', question='톨게이트 요금 얼마나 나올까?', expected_labels=['주행']),
        Task(task_id='task_08', question='우회도로 경로 찾아줘', expected_labels=['주행']),
        Task(task_id='task_09', question='목적지까지 남은 시간 알려줘', expected_labels=['주행']),
        Task(task_id='task_10', question='근처 휴게소 어디 있어?', expected_labels=['주행']),
        Task(task_id='task_11', question='인천공항까지 가는 최적의 경로 찾아줘', expected_labels=['주행']),
        Task(task_id='task_12', question='트래픽 없는 우회 경로 안내해줘', expected_labels=['주행']),
        Task(task_id='task_13', question='가장 가까운 전기차 충전소 찾아줘', expected_labels=['주행']),
        Task(task_id='task_14', question='출발지에서 목적지까지 거리 몇 km야?', expected_labels=['주행']),
        Task(task_id='task_15', question='지금 현재 위치에서 강서구청까지 가는 길 보여줘', expected_labels=['주행']),
        
        # 차량 상태(단일 카테고리) - 15개
        Task(task_id='task_16', question='엔진오일 교체가 필요한지 확인해줘', expected_labels=['차량 상태']),
        Task(task_id='task_17', question='지금 타이어 압력 괜찮아?', expected_labels=['차량 상태']),
        Task(task_id='task_18', question='배터리 잔량 알려줘', expected_labels=['차량 상태']),
        Task(task_id='task_19', question='경고등이 켜졌는데 뭐가 문제야?', expected_labels=['차량 상태']),
        Task(task_id='task_20', question='냉각수 부족한지 체크해줘', expected_labels=['차량 상태']),
        Task(task_id='task_21', question='브레이크 패드 상태 확인', expected_labels=['차량 상태']),
        Task(task_id='task_22', question='차량 점검 필요한 항목 알려줘', expected_labels=['차량 상태']),
        Task(task_id='task_23', question='연료 얼마나 남았어?', expected_labels=['차량 상태']),
        Task(task_id='task_24', question='엔진 온도가 정상인지 확인', expected_labels=['차량 상태']),
        Task(task_id='task_25', question='차량 진단 결과 보여줘', expected_labels=['차량 상태']),
        Task(task_id='task_26', question='타이어 마모도는 어느 정도야?', expected_labels=['차량 상태']),
        Task(task_id='task_27', question='엔진 점검 필요한데 언제 해야 돼?', expected_labels=['차량 상태']),
        Task(task_id='task_28', question='배터리 확인해줄래?', expected_labels=['차량 상태']),
        Task(task_id='task_29', question='휠 얼라인먼트 체크 필요해', expected_labels=['차량 상태']),
        Task(task_id='task_30', question='현재 주행거리 몇 km야?', expected_labels=['차량 상태']),
        
        # 차량 제어(단일 카테고리) - 15개
        Task(task_id='task_31', question='운전석 창문 조금 내려줘', expected_labels=['차량 제어']),
        Task(task_id='task_32', question='에어컨을 22도로 맞춰줘', expected_labels=['차량 제어']),
        Task(task_id='task_33', question='시동 꺼줘', expected_labels=['차량 제어']),
        Task(task_id='task_34', question='문 잠금 해제해줘', expected_labels=['차량 제어']),
        Task(task_id='task_35', question='선루프 열어줘', expected_labels=['차량 제어']),
        Task(task_id='task_36', question='뒷좌석 열선 켜줘', expected_labels=['차량 제어']),
        Task(task_id='task_37', question='외부 순환 모드로 바꿔', expected_labels=['차량 제어']),
        Task(task_id='task_38', question='와이퍼 작동시켜줘', expected_labels=['차량 제어']),
        Task(task_id='task_39', question='헤드라이트 켜줘', expected_labels=['차량 제어']),
        Task(task_id='task_40', question='실내등 켜줘', expected_labels=['차량 제어']),
        Task(task_id='task_41', question='뒷유리 열선 켜', expected_labels=['차량 제어']),
        Task(task_id='task_42', question='주차 센서 활성화해줘', expected_labels=['차량 제어']),
        Task(task_id='task_43', question='크루즈 컨트롤 속도 70으로 설정', expected_labels=['차량 제어']),
        Task(task_id='task_44', question='사이드 미러 자동 접기 실행', expected_labels=['차량 제어']),
        Task(task_id='task_45', question='배기구 매연 필터 리셋해줘', expected_labels=['차량 제어']),
        
        # 미디어(단일 카테고리) - 15개
        Task(task_id='task_46', question='라디오 켜', expected_labels=['미디어']),
        Task(task_id='task_47', question='볼륨을 10으로 낮춰봐', expected_labels=['미디어']),
        Task(task_id='task_48', question='아이유 노래 틀어줘', expected_labels=['미디어']),
        Task(task_id='task_49', question='음악 일시정지', expected_labels=['미디어']),
        Task(task_id='task_50', question='다음 곡으로 넘겨', expected_labels=['미디어']),
        Task(task_id='task_51', question='재즈 음악 재생해줘', expected_labels=['미디어']),
        Task(task_id='task_52', question='블루투스 연결해줘', expected_labels=['미디어']),
        Task(task_id='task_53', question='FM 95.1 주파수로 맞춰', expected_labels=['미디어']),
        Task(task_id='task_54', question='이전 곡으로 돌아가', expected_labels=['미디어']),
        Task(task_id='task_55', question='플레이리스트 셔플해줘', expected_labels=['미디어']),
        Task(task_id='task_56', question='방탄소년단 앨범 재생', expected_labels=['미디어']),
        Task(task_id='task_57', question='팟캐스트 틀어줘', expected_labels=['미디어']),
        Task(task_id='task_58', question='클래식 채널로 변경해줘', expected_labels=['미디어']),
        Task(task_id='task_59', question='밸런스 왼쪽으로 조정해', expected_labels=['미디어']),
        Task(task_id='task_60', question='음악 반복 모드 설정해줄래?', expected_labels=['미디어']),
        
        # 개인 비서(단일 카테고리) - 15개
        Task(task_id='task_61', question='엄마한테 전화 걸어줘', expected_labels=['개인 비서']),
        Task(task_id='task_62', question='김철수한테 3시 도착 예정으로 문자 하나 보내줘', expected_labels=['개인 비서']),
        Task(task_id='task_63', question='오늘 일정 알려줘', expected_labels=['개인 비서']),
        Task(task_id='task_64', question='지금 안읽은 메시지 쭉 읽어줘', expected_labels=['개인 비서']),
        Task(task_id='task_65', question='캘린더에 내일 오전 10시 회의 일정 하나 추가해놔', expected_labels=['개인 비서']),
        Task(task_id='task_66', question='지난 3일 동안 메일 온 것들 브리핑 해줘', expected_labels=['개인 비서']),
        Task(task_id='task_67', question='메신저 알림 좀 1시간 동안 잠깐 꺼줘', expected_labels=['개인 비서']),
        Task(task_id='task_68', question='오후 3시에 알람 설정해줘', expected_labels=['개인 비서']),
        Task(task_id='task_69', question='내 핸드폰 배터리 확인하고 50퍼 이하면 저전력 모드 켜주라', expected_labels=['개인 비서']),
        Task(task_id='task_70', question='음성 메모 녹음 시작해줘', expected_labels=['개인 비서']),
        Task(task_id='task_71', question='아버지한테 "곧 도착하겠습니다" 보내줄래?', expected_labels=['개인 비서']),
        Task(task_id='task_72', question='내일 아침 기상 알림 켜놔줘', expected_labels=['개인 비서']),
        Task(task_id='task_73', question='부재중 찍힌거 누구야?', expected_labels=['개인 비서']),
        Task(task_id='task_74', question='최근 스캔한 명함 정보 말해줘', expected_labels=['개인 비서']),
        Task(task_id='task_75', question='내 휴대폰 방해금지 모드로 해줘', expected_labels=['개인 비서']),
        
        # 멀티레이블 - 15개
        Task(task_id='task_76', question='창문 닫고 아이유 신곡 재생', expected_labels=['차량 제어', '미디어']),
        Task(task_id='task_77', question='엉따 2단계 해주고 퇴근길 경로 찾아줘', expected_labels=['차량 제어', '주행']),
        Task(task_id='task_78', question='음악 잠깐 정지하고 엄마한테 전화 걸어줘', expected_labels=['미디어', '개인 비서']),
        Task(task_id='task_79', question='재생목록 틀어주고 실내 공기 순환 모드 켜줘', expected_labels=['미디어', '차량 제어']),
        Task(task_id='task_80', question='타이어 공기압 확인하고 근처 정비소 안내해줘', expected_labels=['차량 상태', '주행']),
        Task(task_id='task_81', question='배터리 상태 보고하고 문자 앱 좀 열어줘', expected_labels=['차량 상태', '개인 비서']),
        Task(task_id='task_82', question='히터 켜고 음악 틀어줘', expected_labels=['차량 제어', '미디어']),
        Task(task_id='task_83', question='연료 잔량 확인하고 근처 주유소 안내', expected_labels=['차량 상태', '주행']),
        Task(task_id='task_84', question='라디오 끄고 오늘 일정 알려줘', expected_labels=['미디어', '개인 비서']),
        Task(task_id='task_85', question='선루프 열고 집 가는 길 안내', expected_labels=['차량 제어', '주행']),
        Task(task_id='task_86', question='에어컨 20도로 맞추고 클래식 재생해줘', expected_labels=['차량 제어', '미디어']),
        Task(task_id='task_87', question='엔진 진단하고 스팸 메시지 삭제해', expected_labels=['차량 상태', '개인 비서']),
        Task(task_id='task_88', question='휠 상태 확인하고 가장 빠른 경로로 안내', expected_labels=['차량 상태', '주행']),
        Task(task_id='task_89', question='시트 히터 켜고 좋아하는 팟캐스트 틀어', expected_labels=['차량 제어', '미디어']),
        Task(task_id='task_90', question='배터리 잔량 확인 후 동료한테 연락해', expected_labels=['차량 상태', '개인 비서']),
        
        # 범위 외 - 10개
        Task(task_id='task_91', question='가벼운 점심 메뉴 뭐가 좋을까?', expected_labels=[]),
        Task(task_id='task_92', question='하얀 푸들 강아지 이름 추천해줘', expected_labels=[]),
        Task(task_id='task_93', question='고양이는 왜 낮잠을 많이 자?', expected_labels=[]),
        Task(task_id='task_94', question='"내비"는 콩글리쉬야?', expected_labels=[]),
        Task(task_id='task_95', question='야간 운전할 때 눈이 덜 피곤하게 하는 팁 같은 거 있어?', expected_labels=[]),
        Task(task_id='task_96', question='파이썬 공부하기 좋은 책 추천', expected_labels=[]),
        Task(task_id='task_97', question='김치찌개 맛있게 끓이는 방법', expected_labels=[]),
        Task(task_id='task_98', question='지하철은 몇 호선이 제일 오래됐어?', expected_labels=[]),
        Task(task_id='task_99', question='세계에서 가장 높은 산은?', expected_labels=[]),
        Task(task_id='task_100', question='재미있는 영화 추천해줘', expected_labels=[])
    ]

 

4-3. 실행 및 결과

아래 코드는 APO 트레이너를 구성하고, 한국어 POML 템플릿을 사용해 프롬프트 최적화 루프를 실행하는 예제입니다.

CLOVA Studio의 HCX-005 모델을 기반으로 APO 알고리즘을 초기화하고, 분류 작업에 맞는 초기 프롬프트 템플릿을 initial_resources에 직접 지정하여 학습을 시작합니다. @agl.rollout 데코레이터로 정의된 에이전트는 각 태스크를 실행하면서 LLM 응답을 생성하고, run_rollout에서 계산된 보상 값을 반환합니다. 이 보상 값은 APO가 다음 프롬프트를 수정하고 개선하는 데 핵심적인 학습 신호로 활용됩니다. 트레이너는 초기 프롬프트 템플릿을 기준으로 학습·검증 데이터셋을 반복 실행하며, 보상을 최대화하는 방향으로 프롬프트를 자동으로 수정하고 버전(v0, v1, v2…) 단위로 관리합니다.

학습이 완료되면, trainer.store에 저장된 프롬프트 버전들을 모두 불러와 테스트셋으로 다시 평가합니다. 이 중 가장 높은 성능을 기록한 프롬프트가 최종 버전으로 선택되며, 모든 버전의 프롬프트 내용과 테스트셋 점수는 prompt_history.txt 파일에 저장됩니다.

# train_apo.py
import os
import random
import asyncio
import logging
from copy import deepcopy

from dotenv import load_dotenv
import agentlightning as agl
from openai import AsyncOpenAI

from rollout import Task, run_rollout, ClovaClient
from dataset import create_classification_dataset
import apo_ko_setup  # 한국어 POML 패치용

logging.getLogger("agentlightning").setLevel(logging.CRITICAL)
load_dotenv()

# --- 설정 ---
BASE_URL = "https://clovastudio.stream.ntruss.com/v1/openai"
API_KEY = os.getenv("CLOVA_STUDIO_API_KEY")
MODEL_NAME = "HCX-005"
RANDOM_SEED = 42
BEAM_ROUNDS = 1
BEAM_WIDTH = 1

# --- 전역 변수 ---
task_counter = 0


@agl.rollout
async def classification_agent(task: dict, prompt_template: agl.PromptTemplate) -> float:
    """
    APO에서 호출되는 분류 에이전트.

    - task: 데이터셋에서 전달된 태스크(dict)
    - prompt_template: APO가 현재 시점에 사용 중인 시스템 프롬프트 템플릿
    """
    global task_counter

    try:
        task_obj = Task(**task)

        # APO가 최적화한 프롬프트를 system_prompt로 주입
        if hasattr(prompt_template, "template"):
            prompt_str = prompt_template.template
        else:
            prompt_str = str(prompt_template)
        task_obj.system_prompt = prompt_str

        # ClovaClient는 컨텍스트 매니저로 사용하여 연결 정리
        async with ClovaClient(model=MODEL_NAME) as client:
            _, reward = await run_rollout(task_obj, client)

        task_counter += 1
        print(f"\r학습 중... (진행: {task_counter})", end="", flush=True)

        return reward

    except Exception as e:
        print(f"\nTask 오류: {e}")
        return 0.0


async def evaluate_prompt_on_dataset(prompt_template, dataset_tasks):
    """
    주어진 프롬프트 템플릿으로 데이터셋 평가.
    APO가 만든 프롬프트(각 버전)에 대해 test셋에서 평균 reward 계산.
    """
    if hasattr(prompt_template, "template"):
        prompt_str = prompt_template.template
    else:
        prompt_str = str(prompt_template)

    rewards = []

    async with ClovaClient(model=MODEL_NAME) as client:
        for task_item in dataset_tasks:
            try:
                task_obj = deepcopy(task_item) if isinstance(task_item, Task) else Task(**task_item)
                task_obj.system_prompt = prompt_str
                _, reward = await run_rollout(task_obj, client)
                rewards.append(reward)
            except Exception:
                rewards.append(0.0)

    return sum(rewards) / len(rewards) if rewards else 0.0


def extract_version_info(trainer_store):
    """
    Trainer.store 내부에서 버전별 프롬프트를 추출.
    InMemoryLightningStore의 _resources를 직접 읽어서
    v0, v1, v2 ... 버전별 prompt_template를 모은다.
    """
    resources_dict = trainer_store._resources if hasattr(trainer_store, "_resources") else {}
    initial_prompt = None
    resources_prompts = {}

    for version in sorted(resources_dict.keys(), key=lambda v: int(v[1:])):
        resources_update = resources_dict[version]
        if not (hasattr(resources_update, "resources") and "prompt_template" in resources_update.resources):
            continue

        prompt = resources_update.resources["prompt_template"]
        resources_prompts[version] = prompt

        if version == "v0":
            initial_prompt = prompt

    return {
        "resources_dict": resources_dict,
        "resources_prompts": resources_prompts,
        "initial_prompt": initial_prompt,
    }


def main():
    # --- 데이터셋 분할 ---
    all_tasks = create_classification_dataset()
    random.seed(RANDOM_SEED)
    random.shuffle(all_tasks)

    total = len(all_tasks)
    train_tasks = all_tasks[: int(total * 0.6)]
    val_tasks = all_tasks[int(total * 0.6) : int(total * 0.8)]
    test_tasks = all_tasks[int(total * 0.8) :]

    # --- APO 설정 ---
    try:
        client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY)
        algorithm = agl.APO(
            client,
            gradient_model=MODEL_NAME,
            apply_edit_model=MODEL_NAME,
            beam_rounds=BEAM_ROUNDS,
            beam_width=BEAM_WIDTH,
        )
    except Exception as e:
        print(f"오류: {e}")
        return

    trainer = agl.Trainer(
        algorithm=algorithm,
        strategy=agl.SharedMemoryExecutionStrategy(main_thread="algorithm"),
        tracer=agl.OtelTracer(),
        initial_resources={
            "prompt_template": agl.PromptTemplate(
                template="""
당신은 분류기입니다.
입력 문장을 아래 5개 카테고리 중 해당되는 항목으로 분류하세요.

카테고리 정의:
- 주행: 주행 및 내비게이션 관련 요청
- 차량 상태: 차량 진단/상태 확인
- 차량 제어: 차량 기능 조작 요청
- 미디어: 음악/라디오, 엔터테인먼트 요청
- 개인 비서: 전화, 메시지, 일정 등 개인 비서 기능 요청

출력 포맷:
list 형태로 응답합니다. 해당되는 카테고리가 없다면 빈 배열로 응답하세요. 배열 내 문자열은 작은 따옴표로 감싸세요.
                """,
                engine="f-string",
            )
        },
        adapter=agl.TraceToMessages(),
    )

    # --- 학습 실행 ---
    trainer.fit(agent=classification_agent, train_dataset=train_tasks, val_dataset=val_tasks)

    # --- 버전별 프롬프트 추출 ---
    if not (hasattr(trainer.store, "_resources") and trainer.store._resources):
        print("리소스 없음")
        return

    info = extract_version_info(trainer.store)
    if not info["initial_prompt"] or not info["resources_prompts"]:
        print("프롬프트 추출 실패")
        return

    # --- 테스트셋 평가 ---
    async def run_evaluation():
        version_test_results = {}

        for version in sorted(info["resources_prompts"].keys(), key=lambda v: int(v[1:])):
            prompt = info["resources_prompts"][version]
            score = await evaluate_prompt_on_dataset(prompt, test_tasks)
            version_test_results[version] = score

        return version_test_results

    try:
        version_test_results = asyncio.run(run_evaluation())

        # 최적 버전 선택
        best_version = max(version_test_results.keys(), key=lambda v: version_test_results[v])
        best_score = version_test_results[best_version]
        initial_test_score = version_test_results.get("v0", 0.0)

        print("\n" + "=" * 60)
        print("최종 평가 결과")
        print("=" * 60)
        print(f"  초기 프롬프트(v0): {initial_test_score:.3f}")
        print(f"  수정된 프롬프트({best_version}): {best_score:.3f}\n")

        # --- 프롬프트 히스토리 저장 ---
        with open("prompt_history.txt", "w", encoding="utf-8") as f:
            f.write("=" * 80 + "\n프롬프트 최적화 이력\n" + "=" * 80 + "\n\n")
            for version in sorted(info["resources_prompts"].keys(), key=lambda v: int(v[1:])):
                prompt = info["resources_prompts"][version]
                prompt_str = prompt.template if hasattr(prompt, "template") else str(prompt)
                score = version_test_results[version]
                f.write(f"[{version}] 테스트셋 점수: {score:.3f}\n")
                f.write("-" * 80 + "\n")
                f.write(f"{prompt_str}\n")
                f.write("=" * 80 + "\n\n")

        print("✓ prompt_history.txt 저장\n")

    except Exception as e:
        print(f"평가 중 오류: {e}")
        import traceback

        traceback.print_exc()


if __name__ == "__main__":
    main()

다음은 위 코드를 실행했을 때 출력된 결과입니다. 동일한 테스트셋을 기반으로 비교한 결과, 수정된 프롬프트(v4)가 기존 프롬프트 대비 더 높은 분류 성능을 보여줌을 확인할 수 있습니다.

============================================================
최종 평가 결과
============================================================
  초기 프롬프트(v0): 0.835
  수정된 프롬프트(v4): 0.945

✓ prompt_history.txt 저장

다음은 APO 알고리즘을 통해 자동으로 개선된 프롬프트 v4의 원본입니다. 이후 필요에 따라 학습 파라미터를 조정해 추가적인 최적화 실험을 진행할 수도 있습니다.

문장 분류기를 위한 분류 작업을 수행해주세요. 분류할 카테고리는 다음과 같습니다:

- 주행: 주행 및 내비게이션과 관련된 내용
- 차량 상태: 차량 진단이나 상태에 대한 정보 요청
- 차량 제어: 차랑 기능 조작 요청
- 미디어: 음악 또는 라디오 등의 엔터테인먼트 요청
- 개인 비서: 전화걸기, 메시지 보내기, 일정 등록 등과 같은 개인 비서 업무 요청

제공된 문장을 위의 5가지 카테고리 중 가장 적합하다고 생각하는 곳으로 분류하고 그 결과를 list 형태로 제시해 주세요. 예를 들어 아래와 같이 나타낼 수 있습니다:

```plaintext
['주행', '차량 제어']
```

위 예시처럼 해당하는 카테고리를 작은따옴표 안에 넣어서 list로 구성해주시면 됩니다. 만약 문장이 어떠한 카테고리에도 속하지 않는다면 빈 배열 `[]`을 반환하셔도 됩니다.

 

5. 맺음말

이번 쿡북에서는 CLOVA Studio 모델을 기반으로 롤아웃을 구성하고, APO를 통해 프롬프트를 자동으로 개선하는 전체 흐름을 살펴보았습니다.

단순한 분류 태스크도 보상 구조만 잘 설계하면 원하는 방향으로 모델을 안정적으로 유도할 수 있고, APO는 이 보상 신호를 활용해 프롬프트를 점진적으로 더 좋은 형태로 다듬어 줍니다.

이 구조는 다른 도메인의 에이전트에도 그대로 확장할 수 있는데요. 서비스 요구사항에 맞게 태스크, 보상 체계 등을 커스터마이즈하면, 보다 복잡한 워크플로우나 실제 서비스 환경에서도 안정적인 프롬프트를 자동으로 구축할 수 있습니다. 특히 프롬프트 성능이 곧 모델 품질로 이어지는 LLM 기반 시스템에서는, 이러한 자동 최적화가 품질을 빠르게 끌어올리는 데 효과적입니다.

이제 여러분의 서비스 맥락에 맞는 태스크를 넣어 보며 프롬프트가 어떻게 진화하는지 확인해 보세요. 🧐

 

 

image.png.b9da587841f5ad3f1b694d81ce098866.png

 

링크 복사
다른 사이트에 공유하기

게시글 및 댓글을 작성하려면 로그인 해주세요.



로그인
×
×
  • Create New...