LLM에 지시할 프롬프트를 일일히 작성하기가 쉽지가 않습니다.
귀찮기도 하고 SLM 같은 경우 말을 못알아 먹기도 하고요. 특정 일부 값 때문에 지시사항을 조금 고쳐서 다시 돌리고 조금 고쳐서 다시 올리고 하는 과정이 꽤 많습니다.
LLM이 대신 프롬프트를 자동으로 생성해주고, 실행해주고, 평가해주고, 개선해주는 DSPy + GEPA를 해봤읍니다.
1. 개요
DSPy는 프롬프트를 사람이 직접 문자열로 짜지 않고, 입력과 출력만 선언하면 그 사이를 채우는 프롬프트와 예시를 옵티마이저가 자동으로 찾아주는 프레임워크입니다. 스탠퍼드 NLP 연구실에서 만들었고, 2023년 말 공개 이후 깃허브 스타 28,000개를 넘기면서 꾸준히 커왔습니다.
GEPA는 그 안에 들어가는 옵티마이저 중 하나인데, 그냥 프롬프트를 무작위로 바꿔보는 게 아니라 현재 프롬프트로 실제 문제를 풀어보고 틀린 사례와 그 이유를 리플렉션 모델이 읽은 다음 어디를 어떻게 고칠지 스스로 판단해서 다음 버전을 만들어냅니다.
테스트로 실행해본 태스크는 RAG 질의응답이었습니다.
문맥(context)과 질문(question)을 주면 그 안에서 답을 뽑아 {"answer": "..."} 형태의 JSON으로만 출력하게 만드는 게 목표였고, 검증셋은 회사 매출, 산 높이, 노트북 스펙, 역사 연도 같은 걸 섞어서 한국어와 영어로 16개를 직접 만들었습니다.
시작 프롬프트는 DSPy가 자동으로 만들어주는 가장 기본적인 한 줄이었는데, 이 상태에서는 모델이 JSON이 아니라 그냥 설명하듯 답을 내놔서 검증 점수가 정확히 0점이었습니다.
GEPA를 돌리고 나서는 총 398번의 metric 호출과 5번의 전체 검증셋 평가를 거쳐 프롬프트 버전이 5개 나왔고, 그중 4번 버전이 최종으로 채택됐습니다. 중간에 3번 버전은 오히려 점수가 떨어졌는데, GEPA는 이걸 그냥 버리지 않고 점수가 잘 나왔던 2번 버전에서 다시 갈라져 나온 별개 시도로 남겨뒀습니다. 제가 실제로 손댄 건 시드 프롬프트 한 줄이랑 채점 기준, 검증 데이터 준비뿐이었고, JSON 형식을 강제하는 규칙이나 문맥 속 수식어를 그대로 보존하라는 규칙 같은 건 전부 GEPA가 오답을 읽으면서 스스로 찾아낸 것들이었습니다.
타겟 모델 vs 리플렉션 모델
GEPA를 쓸 때는 모델이 두 개 필요합니다.
하나는 실제로 문제를 푸는 타겟 모델이고, 다른 하나는 그 타겟 모델이 틀린 걸 보고 프롬프트를 고쳐주는 리플렉션 모델입니다.
이번에는 타겟 모델로 gemma4-E4B-it을 썼고, 리플렉션 모델로는 Qwen3.6-27B를 썼습니다. 타겟 모델은 가볍고 빠른 쪽으로, 리플렉션 모델은 상대적으로 더 크고 추론에 강한 쪽으로 나눠서 붙인 셈입니다.
두 모델의 역할이 완전히 다릅니다. 타겟 모델은 매 rollout마다 context와 question을 받아서 answer를 뽑아내는 실무자 역할이고, 리플렉션 모델은 그 답이 왜 틀렸는지 채점 결과와 피드백을 읽고 "지시문을 이렇게 고치면 된다"는 판단을 내리는 역할입니다. 리플렉션 모델은 실제로 데이터를 보고 답을 내는 게 아니라, 프롬프트라는 텍스트 자체를 편집하는 일을 합니다.
DSPy + GEPA 동작 순서
전체 흐름은 이렇게 돌아갑니다.
- 타겟 모델이 현재 프롬프트로 검증셋 문제를 풉니다.
- metric 함수가 각 답을 채점하고, 틀린 경우 왜 틀렸는지 텍스트 피드백까지 같이 만듭니다.
- 리플렉션 모델이 그 오답과 피드백을 읽고 새 프롬프트 후보를 제안합니다.
- 새 후보로 다시 검증셋을 돌려서 점수가 오르면 채택하고, 떨어지면 후보 풀에만 남겨둡니다.
- 이 과정을 정해진 rollout 횟수만큼 반복하면서 점점 더 나은 버전을 쌓아갑니다.
이번 실행에서는 이 사이클이 총 398번의 metric 호출과 5번의 전체 검증셋 평가를 거치며 돌았고, 그 결과로 나온 게 5개의 프롬프트 버전입니다.
실전 1 — 데이터셋과 태스크 세팅
먼저 태스크를 Signature로 선언했습니다. context와 question을 받아서 answer를 뽑아내는 형태인데, context, question -> answer처럼 문자열 한 줄이면 충분했습니다. JSON 형식을 강제하는 로직은 따로 코드에 넣지 않았고, 그건 리플렉션 모델(Qwen3.6-27B)이 프롬프트 안에서 스스로 규칙을 만들도록 그대로 뒀습니다.
검증셋(rag_qa.jsonl)은 16개 샘플로 직접 만들었습니다. 회사 매출, 산 높이, 노트북 스펙, 마라톤 코스 거리, 영화 상영시간처럼 도메인을 일부러 다양하게 섞었고, 한국어와 영어 문맥을 절반씩 넣었습니다. 실제로 넣은 데이터는 이런 식입니다.
{"context": "에베레스트산의 높이는 8,849m로 세계에서 가장 높은 산이다. 두 번째로 높은 산은 K2로 8,611m이며, 안나푸르나는 8,091m이다. 에베레스트는 네팔과 중국 국경에 위치한다.", "question": "K2의 높이는?", "answer": "{\"answer\": \"8,611m\"}"}
{"context": "The library has 5 floors and holds over 2 million books. It opens at 9 AM and closes at 9 PM on weekdays. Membership costs $30 per year.", "question": "How many books does the library hold?", "answer": "{\"answer\": \"over 2 million\"}"}
정답은 전부 이렇게 {"answer": "8,611m"} 형태의 JSON 문자열로 저장해뒀는데, 이건 나중에 리플렉션 모델이 "형식이 틀렸다"는 피드백을 만들 때 기준이 되는 값입니다.
시작 프롬프트, 그러니까 버전 #0은 DSPy가 자동으로 만들어주는 가장 기본적인 문장 하나였습니다.
Given the fields `context`, `question`, produce the fields `answer`.
이 상태로 검증셋을 돌려보면 타겟 모델(gemma4-E4B-it)이 JSON이 아니라 그냥 "8,611m입니다" 같은 일반 문장으로 답을 내놨습니다. 채점 로직은 정확히 {"answer": "..."} 형태만 정답으로 인정하기 때문에 형식부터 아예 안 맞았고, 검증 점수는 0.0000점이었습니다. 이 실패한 답변들이 리플렉션 모델이 프롬프트를 고쳐나가는 첫 번째 재료가 됐습니다.
실전 2 — 세대별 진화 과정
여기서부터는 타겟 모델 (Gemma4-E4B-it)가 실행한 결과를
리플렉션 모델(Qwen3.6-27B)이 각 세대마다 실제로 뭐라고 판단했고, 프롬프트가 어떤 문장으로 바뀌었는지 그대로 옮겨봤습니다.

#1 — JSON 형식부터 강제
리플렉션 모델의 판단은 이랬습니다.
문제 진단: 기존 프롬프트는 모델이 순수 JSON 형식이 아닌 일반 텍스트나 마크다운/코드 블록을 포함하여 출력하도록 유도했습니다. 추가한 규칙: 출력이 반드시 유효한 JSON 객체여야 한다는 핵심 규칙이 추가되었습니다. "answer"라는 단일 키를 포함해야 하며, 마크다운 코드 블록이나 JSON 외부의 어떠한 텍스트도 포함해서는 안 된다는 제약이 명시되었습니다.
실제로 바뀐 프롬프트는 이렇습니다.
You are a precise question-answering assistant. Given the `context` and `question`, extract the exact answer from the context.
Output Format Requirements:
1. Output must be a valid JSON object containing a single key: "answer".
2. The value of "answer" should be the extracted answer string.
3. STRICT CONSTRAINTS:
- Output ONLY the raw JSON string.
- Do NOT use markdown code blocks
규칙은 생겼지만 검증 점수는 여전히 0점이었습니다. 코드펜스나 접두어가 실제로는 완전히 사라지지 않았기 때문입니다.
#2 — 한정사까지 정확히
문제 진단: 코드펜스(```), 설명, 접두어 등 JSON 외의 다른 텍스트가 포함되어 있었습니다. 예시 검증: Example 2와 3에서 발생한 문제(순수 JSON 미준수)를 해결하기 위해, 답변 시 문맥에 나타난 수식어(예: "about")까지 정확하게 포함하도록 지침을 보강했습니다.
You are a precise reading comprehension assistant. Given the `context` and `question`, extract the exact answer from the context.
Output Requirements:
1. Output must be a valid JSON object.
2. The JSON object must contain a single key: "answer", with the value being the extracted answer string.
3. STRICT CONSTRAINT: Output ONLY the raw JSON string.
이 버전에서 처음으로 검증 점수가 0.5000점까지 올랐습니다.
#3 — 조였는데 오히려 후퇴
문제 진단: 답변 필드에 "1446년"과 같은 텍스트가 포함되었을 때 JSON 유효성 검사에 실패하는 문제가 지적되었습니다. 최종 결정: "오직 JSON 문자열만 출력하라"는 강력한 제약을 부여하고, 컨텍스트에 나타난 단위·숫자·한정사를 정확하게 보존하도록 지시했습니다.
You are a precise reading comprehension assistant. Given the `context` and `question`, extract the answer from the context.
Output Requirements:
1. Format: Output must be a valid JSON object with the structure {"answer": "<extracted_answer>"}.
2. Strict Constraints:
- Output ONLY the JSON string.
내용상으로는 #2보다 더 엄격해졌는데, 검증 점수는 오히려 0.2500점으로 떨어졌습니다. 그래서 이 버전은 채택되지 않았고, GEPA는 점수가 더 좋았던 #2로 돌아가 거기서 다시 다른 방향으로 시도했습니다.
#4 — 구조를 아예 확정
문제 진단: 답변이 순수한 JSON 객체가 아닌 단순 문자열('25만원')로 출력되는 오류가 있었습니다. 최종 결정: '정확한 읽기 이해 보조원' 역할을 부여하고, 출력 형식을 '순수 JSON 문자열'로 고정하는 것을 확정했습니다. 이는 형식적 오류를 해결하고, 내용적 정확성을 유지하며, 불필요한 텍스트 생성을 원천 차단하기 위함입니다.
You are a precise reading comprehension assistant. Your task is to extract the answer to the `question` from the provided `context`.
Output Format:
You must output a single, valid JSON object.
The JSON object must have exactly one key: "answer", and its value must be the extracted answer string.
STRICT CONSTRAINTS:
1. Pure JSON Output: Output ONLY the JSON string.
- NEVER use markdown code blocks.
이 버전이 0.9643점을 기록하며 최종 채택됐습니다. #2와 #3을 나란히 놓고 보면, 단순히 규칙을 더 세게 조인다고 점수가 오르는 게 아니라 "구조를 명확히 고정"하는 쪽이 실제로 먹혔다는 걸 확인할 수 있습니다.
최적화 전/후 실제 예측 비교 (검증셋)
정답과 일치하면 O, 틀리면 X.

결론
검증 점수는 0.0000점에서 0.9643점(96.43점)까지 올랐고, 이 과정에서 총 5개의 프롬프트 버전이 만들어졌으며 그중 #4가 최종 채택됐습니다.
총 metric 호출은 398회, 전체 검증셋 평가는 5회 진행됐습니다. 예측 비교에서 확인했듯 최적화 전 답변의 내용 자체는 대부분 정답과 일치했고, 실패 원인은 JSON 형식을 지키지 않은 것이었습니다. 버전 #1에서 JSON 형식 규칙이 처음 추가됐지만 점수는 0점 그대로였고, #2에서 한정사 보존 규칙이 추가되며 0.5점으로 올랐습니다.
#3은 #2보다 규칙을 더 엄격하게 조였지만 점수는 0.25점으로 떨어졌고, 이 버전은 채택되지 않았습니다. #4는 #2에서 다시 갈라져 나와 출력 구조를 {"answer": "<값>"}로 고정하고 역할을 명시하는 방식으로 바뀌었고, 이 변경으로 점수가 0.9643점까지 올랐습니다. 즉 규칙 개수를 늘리는 것보다 출력 구조를 명확히 고정하는 쪽이 이번 태스크에서는 더 높은 점수로 이어졌습니다.
입력 값은 DSPy가 자동 생성한 시드 프롬프트 한 줄, Signature 정의, 검증 데이터셋 16개, 채점 기준뿐이었고, 프롬프트의 JSON 형식 강제·한정사 보존·구조 고정 규칙은 모두 리플렉션 모델이 오답 피드백을 근거로 자동 생성한 것입니다.
DSPy는 프롬프트 문자열을 사람이 직접 관리하지 않고 입출력 선언(Signature)과 채점 기준(Metric)만 정의하면, 실제 프롬프트 문장은 옵티마이저가 만들고 개선하는 구조입니다.
GEPA는 그 옵티마이저 중 하나로, 무작위 탐색이 아니라 틀린 사례에 대한 자연어 피드백을 리플렉션 모델이 읽고 프롬프트를 수정하는 방식으로 동작합니다. 이번 실행에서 확인했듯 사람이 관리해야 하는 건 시드 프롬프트, Signature, 검증 데이터셋, 채점 기준 정도이고 세부 규칙 문구는 GEPA가 세대별로 직접 만들어냅니다.
실무에 적용한다면 출력 형식이 명확히 정의돼 있고(JSON 스키마, 특정 필드 구조 등) 그 형식을 지키는지 아닌지로 채점이 가능한 태스크에 우선 적용하는 게 맞습니다.
이번 태스크처럼 모델이 정답 자체는 맞히는데 형식 때문에 채점에서 떨어지는 경우, GEPA는 그 형식 문제를 반복 실행으로 빠르게 좁혀줍니다. 다만 metric 호출이 398회 필요했던 것처럼 rollout 비용이 들기 때문에, 검증 데이터셋 규모와 타겟/리플렉션 모델 비용을 먼저 계산해두는 게 좋습니다.
특히나 프론티어 모델 API로 호출할 경우 비용 폭탄을 맞을 위험성이 있습니다. 그래서 저도 이번 실험에서는 로컬 서빙된 LLM으로 타겟 모델, 리플렉션 모델을 사용하였습니다.
프롬프트를 손으로 계속 고쳐 쓰는 반복 작업이 많은 파이프라인일수록, 그 반복을 GEPA에 넘기는 게 시간 대비 효율이 높습니다.
'AI 개발' 카테고리의 다른 글
| Qwen 3.7 Max vs GLM-5.2 오픈웨이트 코딩모델 실전비교: 셀프호스팅과 API (0) | 2026.07.02 |
|---|---|
| SGLang vs vLLM, 어떤 추론 엔진을 써야 하나 (0) | 2026.07.01 |
| CLAUDE.md / AGENTS.md, 에이전트 운영 파일 표준화 (1) | 2026.07.01 |
| AI 코딩 에이전트 터미널벤치 2.1 최신 순위표, 코덱스 vs 클로드 코드 (0) | 2026.07.01 |
| Meta Llama가 랭킹에서 사라졌다 — 중국 오픈웨이트 모델 OpenRouter 61% 장악 (0) | 2026.06.30 |