학습 자료

점수를 올린다 — 그 전에 자가 얼마나 흔들리는지부터 잰다


Article

앞 편이 불편한 질문 하나를 남겼다. 모델이 벡터가 이미 낸 값을 그대로 낸다.

12편이 남긴 자리 · 30명
벡터 상위 5개 (모델 없이)23.3%
모델이 고른 5개23.3%
천장 (후보 안에 정답이 있던 비율)30%

가운데 막대를 오른쪽 막대 쪽으로 밀어야 한다. 오늘 하는 일이 그것이다.

1. 손잡이가 넷 있다

바꿔볼 수 있는 것
손잡이지금 값바꿔볼 값
후보 개수10개5 · 20 · 50
시키는 말「좋은 순서대로 5개」「이미 산 것과 비슷한 것 말고 다음에 살 것」
이유 길이한 문장40자 이내 · 아예 없이
고객 문장이름 + 별점 + 후기이름만 · 후기만 · 별점 높은 것만
넷 다 코드 한두 줄이면 바뀐다. 어려운 것은 바꾸는 게 아니라 좋아졌는지 판정하는 쪽이다.

2. 실험에는 규칙이 있다

세 가지만 지킨다
  1. 01

    ① 한 번에 하나만 바꾼다

    후보를 20개로 늘리면서 프롬프트도 같이 고치면, 좋아져도 무엇 때문인지 모른다. 나빠져도 마찬가지다.

  2. 02

    ② hit@5 로 판정한다

    「답이 그럴듯해졌다」는 판정이 아니다. 11편에서 만든 자로만 잰다.

  3. 03

    ③ 여러 번 돌린다

    이게 오늘의 진짜 이야기다. 왜 필요한지는 바로 아래에서 본다.

조합을 표로 만들어둔다

손잡이를 돌릴 때마다 코드를 고치면 무엇을 무엇과 비교했는지 잊어버린다. 조합에 이름을 붙여 표로 둔다.

26_sweep.py
# 시키는 말은 따로 모아둔다. 프롬프트를 고칠 때 여기만 본다
TASKS = {
    "base": "좋은 순서대로 5개를 고른다.",
    "next_buy": ("이미 산 것과 비슷한 것을 또 고르지 마라. "
                 "이력을 보고 다음에 살 만한 것을 좋은 순서대로 5개 고른다."),
}

# 조합에 이름을 붙인다. dict(...) 는 {"n": 10, ...} 과 같은 뜻이다
CONFIGS = {
    "base":     dict(n=10, task="base",     reason="sentence"),
    "cand5":    dict(n=5,  task="base",     reason="sentence"),
    "cand20":   dict(n=20, task="base",     reason="sentence"),
    "next_buy": dict(n=10, task="next_buy", reason="sentence"),
    "short":    dict(n=10, task="base",     reason="short"),
}
낯선 이름 풀이
이름무슨 뜻인가왜 여기 나오나
dict(n=10, ...)딕셔너리를 만드는 다른 방법따옴표를 안 써도 되어 표처럼 읽힌다
조합에 이름base · cand20 · next_buy결과를 적을 때 이름으로 적는다. 나중에 대조된다
TASKS 를 따로시키는 말만 모은 표프롬프트를 고칠 때 여기 한 곳만 본다

이유 길이는 받을 모양을 바꾸는 것이라 10편의 클래스를 하나 더 만든다.

26_sweep.py — 이어서
class ItemWithReason(BaseModel):
    product_id: str = Field(description="후보 목록에 있는 상품 번호")
    reason: str = Field(description="추천 이유 한 문장")


class ItemShort(BaseModel):
    product_id: str = Field(description="후보 목록에 있는 상품 번호")
    reason: str = Field(description="추천 이유. 40자 이내")


class ItemOnly(BaseModel):                 # 이유를 아예 안 받는다
    product_id: str = Field(description="후보 목록에 있는 상품 번호")

3. 🔴 조합을 비교하기 전에 — 자가 얼마나 흔들리나

아무것도 안 바꾸고 같은 조합을 네 번 돌렸다.

터미널 — base 조합 4회 (설정을 한 글자도 안 바꿨다)
  • 조합 후보 천장 hit@1 hit@3 hit@5
  • base 10 30.0% 10.0% 13.3% 23.3%
  • base 10 30.0% 10.0% 13.3% 26.7%
  • base 10 30.0% 10.0% 16.7% 26.7%
  • base 10 30.0% 10.0% 16.7% 20.0%
  • hit@5 이 20.0% 에서 26.7% 사이에서 흔들린다

4. 손잡이를 하나씩 돌린다

조합마다 세 번 이상 돌렸다. **범위가 겹치면 「차이 없음」**으로 읽는다.

조합 하나를 세 번씩 돌려 범위를 본다.

26_sweep.py — 이어서
import statistics

for name, cfg in CONFIGS.items():
    scores = []

    for _ in range(3):                      # 세 번씩. 한 번은 아무 증거가 아니다
        scores.append(run(name, cfg))       # 11편의 채점기를 설정만 바꿔 부른다

    # 평균만 적으면 안 된다. 최저와 최고를 같이 적는다
    print(f"{name:12s} {min(scores):5.1f} ~ {max(scores):5.1f}%  "
          f"평균 {statistics.mean(scores):5.1f}%")
터미널
  • base 20.0 ~ 26.7% 평균 24.4%
  • cand5 23.3 ~ 23.3% 평균 23.3%
  • cand20 16.7 ~ 23.3% 평균 18.7%
  • next_buy 23.3 ~ 26.7% 평균 26.0%

후보 개수 — 늘리면 천장은 오르는데 점수가 안 따라온다

후보를 몇 개 줄 것인가 · 30명 · 조합마다 3회 이상
후보천장hit@5 (최저~최고)평균회수율
5개23.3%23.3 ~ 23.3%23.3%100%
10개30.0%20.0 ~ 26.7%24.4%81%
20개33.3%16.7 ~ 23.3%18.7%56%
50개50.0%20.0 ~ 23.3%21.7%43%
「회수율」은 천장 대비 실제로 건진 비율이다. 후보 50개는 천장을 50%까지 올려놓고 21.7%밖에 못 건진다.
후보를 늘릴수록 회수율이 무너진다
후보 5개100%
후보 10개81%
후보 20개56%
후보 50개43%

시키는 말 — 여기가 유일하게 움직였다

프롬프트 마지막 줄만 바꿨다

지금까지

base

  • 「좋은 순서대로 5개를 고른다.」
  • hit@5 20.0 ~ 26.7% (평균 24.4)
  • 회수율 81%

바꾼 뒤

next_buy

  • 「이미 산 것과 비슷한 것을 또 고르지 마라.
  • 이력을 보고 다음에 살 만한 것을 골라라.」
  • hit@5 23.3 ~ 26.7% (평균 26.0)
  • 회수율 87%
평균이 1.6%p 올랐다. 그런데 범위가 겹친다 — 「올랐다」고 단정하면 안 되는 크기다.

이유 길이 — 점수는 그대로, 시간과 돈이 준다

이유를 얼마나 쓰게 할 것인가 · 30명
시킨 것hit@5 평균출력 토큰걸린 시간
한 문장 (지금)24.4%4,24475초
40자 이내24.4%3,52669초
아예 안 씀22.8%1,05033초
이유를 아예 빼면 시간이 절반 이하, 출력 토큰이 4분의 1이다. 그런데 점수는 거의 안 변한다.

🔴 좋은 것 둘을 합치면 좋아질까

next_buy(26.0%)가 제일 좋았고 후보를 늘리면 천장이 오르니, 둘을 합쳐봤다.

터미널 — 좋은 프롬프트 + 후보 20개
  • 조합 후보 천장 hit@5 평균 회수율
  • next_buy 10 30.0% 26.0% 87%
  • cand20 20 33.3% 18.7% 56%
  • next_cand20 20 33.3% 20.0% 60%

5. 고객 문장을 바꾸면 — 여기는 모델이 필요 없다

넷째 손잡이는 고객 벡터를 만드는 방식이다. 이건 후보를 뽑는 단계라 모델을 안 부른다. 그래서 300명을 다 돌려도 몇 초다.

터미널 — 고객 문장을 다르게 만들어 300명 · 모델 없이
  • 고객 문장을 만드는 방식 평균 글자 천장@10 벡터 hit@5
  • 이름 + 별점 + 후기 (지금) 260 20.3% 13.7%
  • 이름만 49 20.3% 13.0%
  • 이름 + 후기 232 19.7% 13.7%
  • 별점 4 이상만 167 20.0% 12.3%
  • 후기만 182 18.3% 9.3%

6. 오늘 고른 것

12편 → 13편 · 같은 30명
벡터 상위 5개 (모델 없이)23.3%
12편 — 모델이 고른 5개23.3%
13편 — 프롬프트를 고친 뒤26%
천장30%

드디어 모델이 벡터를 넘었다. 다만 평균으로만 넘었다 — 나쁜 회차에는 23.3% 로 벡터와 같다.

최종 설정
손잡이고른 값왜
후보 개수10개5개는 고를 게 없고, 20개부터 회수율이 무너진다
시키는 말「다음에 살 것」평균 +1.6%p · 바닥이 20.0 → 23.3%
이유 길이40자 이내점수 그대로 · 출력 17% 절약
고객 문장이름 + 별점 + 후기 유지후기를 빼도 같지만, 차이가 두 명이라 지금은 안 건드린다
네 손잡이 중 점수를 실제로 움직인 것은 하나였다. 나머지 셋은 「안 나빠지는 선에서 싸게」를 고른 것이다.

이번 편에 나온 것

정리
한 것 · 안 것내용
🔴 자의 흔들림아무것도 안 바꿔도 20.0~26.7%. 이 폭보다 작은 차이는 못 믿는다
실험 규칙하나씩 바꾼다 · hit@5 로 판정한다 · 여러 번 돌린다
회수율천장 대비 실제로 건진 비율. 후보가 길수록 무너진다
후보 5개회수율 100% — 모델이 고를 게 없다는 뜻이다
🔴 후보 50개천장 50% 를 만들어놓고 21.7% 만 건진다 (회수율 43%)
후보 10개모델이 고를 여지가 있으면서 감당되는 길이
프롬프트「다음에 살 것」 — 평균 +1.6%p · 바닥이 올랐다
이유 길이점수는 그대로, 시간 절반 · 출력 4분의 1. 그래도 화면에는 필요하다
🔴 좋은 것 둘을 합치면나쁜 쪽을 따라간다. 합친 것도 따로 재야 한다
🔴 후기거의 일을 안 하고 있었다. 신호는 상품 이름에 있다
결론하나만 움직였다. 나머지는 안 나빠지는 선에서 싸게 고른 것이다

미션

미션
  1. 01

    [필수] 내 자가 얼마나 흔들리는지 잰다

    mission_tune_1.py. 아무것도 안 바꾸고 세 번 돌려 hit@5 의 최저~최고를 적는다. 이 폭이 내 판정 기준이 된다.

  2. 02

    [응용] 손잡이 하나를 골라 돌린다

    mission_tune_2.py. 후보 개수나 프롬프트 중 하나만 바꿔 세 번씩 돌린다. 범위가 겹치나 안 겹치나로 판정한다.

  3. 03

    [도전] 내가 지은 프롬프트로 이겨본다

    mission_tune_3.py. 「다음에 살 것」 말고 다른 말을 지어 넣어본다. 26.0% 를 넘는 것을 찾으면 그게 오늘의 성과다.

[도전] 프롬프트를 지을 때 걸리는 것들충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요

「더 잘 골라라」류는 거의 안 듣는다. 「신중하게」 「전문가처럼」 같은 말은 모델이 할 일을 바꾸지 않는다. 이미 잘 고르려고 하고 있다.

듣는 것은 우리가 아는 약점을 말로 메울 때다. 「다음에 살 것」이 들은 이유가 그것이다 — 5편에서 벡터가 「비슷한 것」만 찾는다는 것을 이미 재서 알고 있었고, 그 빈자리를 지시로 채웠다.

그래서 프롬프트를 짓기 전에 틀린 것들을 먼저 들여다보는 것이 순서다. 12편에서 아홉 명을 한 줄씩 본 것처럼. 무엇을 틀리는지 모르면 무슨 말을 넣을지도 모른다.

그리고 길게 쓴다고 좋아지지 않는다. 지시가 길어지면 입력 토큰이 늘고, 모델이 지켜야 할 것이 늘어 오히려 흔들린다.

TASKS = {
    "base": "좋은 순서대로 5개를 고른다.",
    "next_buy": (
        "이미 산 것과 비슷한 것을 또 고르지 마라. "
        "이력을 보고 다음에 살 만한 것을 좋은 순서대로 5개 고른다."
    ),
    # 여기에 내 것을 하나 더 만든다
    "mine": "...",
}

# 조합마다 세 번씩. 범위가 겹치면 차이 없음으로 읽는다
for name in ("base", "next_buy", "mine"):
    for _ in range(3):
        print(run(name, CONFIGS[name]))

26.0% 를 못 넘어도 실패가 아니다. 「이 말은 안 듣는다」를 하나 알아낸 것이고, 그건 다음에 안 해도 되는 일이 하나 준 것이다.

실무에서 프롬프트를 다듬는 일이 대개 이렇다. 열 개를 지어 하나가 걸린다. 그 하나를 찾으려면 아홉 개를 재봐야 한다.

정리하면

오늘 만든 것은 없다. 이미 있는 것의 손잡이를 넷 돌려봤고, 그중 하나만 실제로 움직였다.

제일 먼저 한 일이 조합 비교가 아니라 자가 얼마나 흔들리는지 재는 것이었다. 아무것도 안 바꾸고 네 번 돌렸더니 20.0%에서 26.7%까지 흔들렸다 — 30명 기준으로 두 명이다. 이걸 모르고 조합을 한 번씩 돌려 제일 높은 것을 고르면, 좋아진 것을 고른 게 아니라 운 좋은 회차를 고른 것이 된다.

후보 개수에서 제일 뜻밖의 것이 나왔다. 50개를 주면 천장이 50%까지 오르는데 점수는 21.7%다. 회수율이 43%로 무너진다. 10개일 때가 81%였다. 많이 주는 것과 잘 주는 것은 다르고, 그 사이 어딘가에 답이 있다.

실제로 점수를 움직인 것은 프롬프트 한 줄이었다. 「이미 산 것과 비슷한 것 말고 다음에 살 것」 — 평균이 24.4%에서 26.0%로, 무엇보다 바닥이 20.0%에서 23.3%로 올랐다. 그게 값을 한다. 서비스는 평균으로 도는 게 아니라 매 요청마다 돌기 때문이다.

그리고 이 한 줄이 들은 이유가 있다. 5편에서 「벡터는 비슷한 것을 찾지 다음에 살 것을 찾지 않는다」를 재서 알고 있었다. 그 약점을 말로 메운 것이라 우연이 아니다. 무엇을 틀리는지 모르면 무슨 말을 넣을지도 모른다.

이유 길이에서는 점수가 아니라 시간과 돈을 얻었다. 아예 빼면 시간이 절반 이하인데 점수는 거의 같다 — 그래도 화면에 이유가 없으면 사용자가 안 믿으니 40자로 제한하는 것이 답이었다. 공짜로 얻는 것은 챙긴다.

마지막으로 후기가 거의 일을 안 하고 있었다. 상품 이름만 넣어도 점수가 같다. 8·9편에서 후기의 개인정보를 가리느라 한 편 반을 썼는데, 애초에 안 꺼내면 그 문제가 통째로 사라진다. 재보기 전에는 못 하던 판단이다.

다음 편에서 서버를 세운다. 지금까지 만든 것은 전부 내 컴퓨터에서 파일을 실행하는 것이라 남이 못 쓴다. 브라우저에서 열리는 것으로 만들고, 그 앞에 자물쇠를 하나 단다.

Share
  • 파이썬
  • AI
  • 측정
  • 프롬프트
  • 추천