튜토리얼: 프라이버시 고려 위임을 위한 GEPA

튜토리얼: 프라이버시 고려 위임을 위한 GEPA (GEPA for Privacy-Conscious Delegation)

이 튜토리얼에서는 PAPILLON 프로그램을 dspy.GEPA로 최적화할 거예요. GEPA는 LLM을 사용해 자신의 접근 방식과 실수를 돌아보고, 그 반성에 기반해 새로운 프롬프트를 제안하는 새롭고 독창적인 옵티마이저입니다.

PAPILLON은 프라이버시 보호 위임(privacy-preserving delegation) 시스템이에요. 작은 LM(보통 로컬 호스팅)이 더 강력하지만 프라이빗 데이터를 저장할 수 있는 더 큰 "신뢰할 수 없는(untrusted)" 외부 LLM을 사용해, 높은 품질과 프라이빗 채팅 사이의 균형을 맞춥니다.

단순함을 위해 작은 LM으로 "gpt-4.1-nano"를, 크고 "신뢰할 수 없는" LM으로 "gpt-4.1-mini"를 사용할 거예요.

출처: 문서

본문

PAPILLON 프로그램 (The PAPILLON Program)

class CraftRedactedRequest(dspy.Signature):
    """
    Given a private user query, create a privacy-preserving request for a powerful external LLM.
    The LLM may assist without learning private information about the user.
    """

    user_query = dspy.InputField()
    llm_request = dspy.OutputField()


class RespondToQuery(dspy.Signature):
    """
    Respond to a user query.
    For inspiration, we found a potentially related request to a powerful external LLM and its response.
    """

    related_llm_request = dspy.InputField()
    related_llm_response = dspy.InputField(desc="information from a powerful LLM responding to a related request")
    user_query = dspy.InputField(desc="the user's request you need to fulfill")
    response = dspy.OutputField(desc="your final response to the user's request")


class PAPILLON(dspy.Module):
    def __init__(self, untrusted_model):
        self.craft_redacted_request = dspy.ChainOfThought(CraftRedactedRequest)
        self.respond_to_query = dspy.Predict(RespondToQuery)
        self.untrusted_model = untrusted_model

    def forward(self, user_query):
        try:
            llm_request = self.craft_redacted_request(user_query=user_query).llm_request
            llm_response = self.untrusted_model(llm_request)[0]
            response = self.respond_to_query(
                related_llm_request=llm_request, related_llm_response=llm_response, user_query=user_query
            ).response
        except Exception:
            return dspy.Prediction(llm_request="", llm_response="", response="")

        return dspy.Prediction(llm_request=llm_request, llm_response=llm_response, response=response)

PUPA 데이터셋을 로드하고 학습/개발/테스트 세트를 만듭니다:

from datasets import load_dataset

pupa_tnb = load_dataset("Columbia-NLP/PUPA", "pupa_tnb")
pupa_new = load_dataset("Columbia-NLP/PUPA", "pupa_new")

examples = [
    dspy.Example(
        {"target_response": x["target_response"], "user_query": x["user_query"], "pii_str": x["pii_units"]}
    ).with_inputs("user_query")
    for x in pupa_new["train"]
]

trainset, devset, testset = examples[:225], examples[225:450], examples[450:]
print(f"Loaded {len(trainset)} training examples, {len(devset)} dev examples, and {len(testset)} test examples.")
Loaded 225 training examples, 225 dev examples, and 214 test examples.

이 판정자들을 사용해 평가용 지표를 정의할 수 있어요.

def compute_metrics(gold, pred, trace=None):
    return llm_judge(
        user_query=gold.user_query,
        new_resp=pred.response,
        og_resp=gold.target_response,
        updated_query=pred.llm_request,
        pii_str=gold.pii_str,
    )

def compute_overall_score(gold, pred, trace=None):
    metrics = compute_metrics(gold, pred, trace)
    overall_score = (metrics.quality + (1 - metrics.leakage)) / 2.0
    return overall_score

dspy.GEPA로 PAPILLON 최적화 (Optimize PAPILLON with dspy.GEPA)

GEPA는 반성적(reflective) 프롬프트 옵티마이저예요. 그 강점은 DSPy 프로그램의 실행 및 평가 파이프라인에서 텍스트 피드백을 볼 수 있다는 데 있어요. 이는 GEPA가 시스템이 왜 그 점수를 얻었는지에 대한 더 많은 가시성을 제공하고, GEPA가 점수를 어떻게 개선할지 식별하기 위해 내성적으로(introspect) 살펴볼 수 있게 해줍니다. 평가 지표를 GEPA에게 피드백을 제공할 수 있는 최적화 지표로 빠르게 수정해 볼게요!

이 경우 평가 지표가 "quality" 점수와 "leakage" 점수라는 두 개의 서로 다른 점수의 집계이므로, 피드백 지표는 quality와 leakage 점수가 무엇인지 보여주는 것처럼 간단할 수 있어요. 그러면 GEPA가 무엇을 개선해야 하는지 반성할 수 있죠!

def compute_overall_score_with_feedback(gold, pred, trace=None, pred_name=None, pred_trace=None):
    metrics = compute_metrics(gold, pred, trace)
    overall_score = (metrics.quality + (1 - metrics.leakage)) / 2.0
    feedback_text = f"The overall score is {overall_score:.2f}, which is the arithmetic mean of the quality score ({metrics.quality:.2f}) and the leakage score ({1 - metrics.leakage:.2f}). Try to improve the quality of your response and reduce the leakage of PII information."
    return dspy.Prediction(
        score=overall_score,
        feedback=feedback_text,
    )

PAPILLON에 GEPA를 사용해 볼게요. 우리는 보통 사용자에게 최적화에 auto="high" 예산을 사용할 것을 권장하지만, GEPA의 샘플 효율성을 시연하기 위해 예산을 단 1번의 전체 평가로 제한할 거예요!

from dspy import GEPA

papillon = PAPILLON(untrusted_model=large_lm)
papillon.set_lm(local_lm)

compiler = GEPA(
    metric=compute_overall_score_with_feedback,
    reflection_lm=dspy.LM(model="openai/gpt-4.1", api_key=api_key),
    num_threads=16,
    track_stats=True,
    track_best_outputs=True,

    # Set the budget. GEPA accepts any one of "auto" or "max_full_evals" arguments.
    # GEPA scales with higher budget. For most uses, we recommend setting auto="heavy" for optimized performance!
    # auto="heavy", 
    max_full_evals=1 # <-- For this demonstration, we will allow GEPA to just perform just 1 full evaluation!
)

optimized_papillon = compiler.compile(
    student=papillon,
    trainset=trainset,
    valset=devset,
)

실행 로그는 다음과 같이 진행됩니다:

2025/08/12 18:38:42 INFO dspy.teleprompt.gepa.gepa: Running GEPA for approx 450 metric calls of the program. This amounts to 1.00 full evals on the train+val set.
2025/08/12 18:38:42 INFO dspy.teleprompt.gepa.gepa: Using 225 examples for tracking Pareto scores. You can consider using a smaller sample of the valset to allow GEPA to explore more diverse solutions within the same budget.
2025/08/12 18:38:58 INFO dspy.evaluate.evaluate: Average Metric: 179.6126262626263 / 225 (79.8%)
2025/08/12 18:38:58 INFO dspy.teleprompt.gepa.gepa: Iteration 0: Base program full valset score: 0.7982783389450057
2025/08/12 18:38:58 INFO dspy.teleprompt.gepa.gepa: Iteration 1: Selected program 0 score: 0.7982783389450057

GEPA가 craft_redacted_request.predict 예측기에 대한 새롭고 더 나은 텍스트를 제안합니다. 이 개선된 프롬프트는 사용자 쿼리를 분석하고, 추론 단계를 거치며, 개인 정보를 보호하면서도 유용한 요청을 외부 LLM에 구성하도록 지시합니다.

2025/08/12 18:39:13 INFO dspy.teleprompt.gepa.gepa: Iteration 1: Proposed new text for craft_redacted_request.predict: You are given a private user query as input. Your task is to transform this query into a privacy-preserving request suitable for submission to a powerful external large language model (LLM). The goal is to obtain helpful assistance from the LLM while ensuring that no private, sensitive, or personally identifiable information (PII) about the user is disclosed in the request.

Follow these steps:
1. **Analyze the User Query:** ... 
2. **Reasoning Step:** ... 
3. **Construct the LLM Request:** ... 
4. **Output Format:** ...

최적화가 완료되면 GEPA는 새 프로그램이 선형 파레토 프론트에 있음을 보고해요:

2025/08/12 18:45:52 INFO dspy.evaluate.evaluate: Average Metric: 189.65 / 225 (84.3%)
2025/08/12 18:45:52 INFO dspy.teleprompt.gepa.gepa: Iteration 1: New program is on the linear pareto front
2025/08/12 18:45:52 INFO dspy.teleprompt.gepa.gepa: Iteration 1: Full valset score for new program: 0.8428888888888889
2025/08/12 18:45:52 INFO dspy.teleprompt.gepa.gepa: Iteration 1: Best score on valset: 0.8428888888888889

여기서 GEPA가 단 1개의 새 후보만 제안한 후에도 PAPILLON 프로그램의 점수를 77%에서 86%로 최적화한 것을 볼 수 있어요!

더 알아보기 (Learn more)