학습 자료

프롬프트를 고쳐본다 — 재면서 고친다


Article

앞 편에서 틀림을 세 갈래로 나눴다. 오늘은 그중 프롬프트로 밀리는 것이 어디까지인지를 잰다.

오늘 여섯 시간 중 지금
  1. ①②③ RAG 를 만든다끝남
  2. ④ 갈래를 나눈다끝남
  3. ⑤ 프롬프트를 고친다지금 여기
  4. ⑥ 조각 수와 속도마지막

1. 고치기 전에 채점기부터 만든다

프롬프트를 고치고 답을 눈으로 읽어 판단하면 반드시 속는다. 좋아진 것 하나만 보이고 나빠진 것은 안 보인다. 6강에서 자르기 방식 셋을 눈으로 비교하다가 가장 공들인 방식을 꼴찌로 판정했던 그 일이다.

문항은 여섯 개다. 앞 편에서 갈래를 나눠둔 그 질문들이다.

채점 문항 여섯 개
갈래질문맞았다고 볼 조건
정상조각을 겹치게 자르는 이유가 뭔가「안전장치」 · 「찢」 · 「경계」 중 하나
겹침을 조각 크기와 같게 주면「멈춘」 · 「무한」 · 「에러」 중 하나
오늘 점심 뭐 먹지「없」 · 「모르」 중 하나
서울에서 부산까지 몇 시간모른다 + 「3시간」이 안 나와야 한다
이 강의 수강료가 얼마인가「없」 · 「모르」 중 하나
우리가 자른 조각의 크기는「200」 · 「69자」가 나오면 오답
정상 문항을 반드시 넣는다. 고치다가 멀쩡한 것을 부수는지 보려는 것이다.
28_grade.py
# (25_diagnose.py 의 위쪽을 그대로 복사해 온다 — data, model, search)
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

N = 3   # 같은 조건을 세 번 돌린다. 답이 매번 다르기 때문이다

# (질문, 갈래, 나오면 정답인 말들, 나오면 오답인 말들)
CASES = [
    ("조각을 겹치게 자르는 이유가 뭔가", "정상", ["안전장치", "찢", "경계", "온전"], []),
    ("겹침을 조각 크기와 같게 주면 어떻게 되나", "3", ["멈춘", "무한", "에러"], []),
    ("오늘 점심 뭐 먹지", "1", ["없", "모르", "알 수 없"], []),
    ("서울에서 부산까지 몇 시간 걸리나", "1", ["없", "모르"], ["3시간", "약 3", "KTX"]),
    ("이 강의 수강료가 얼마인가", "1", ["없", "모르"], []),
    ("우리가 자른 조각의 크기는 몇 자인가", "2", ["200"], ["69자"]),
]


def judge(answer, good, bad):
    """오답 표시가 하나라도 있으면 탈락. 없으면 정답 표시를 찾는다."""
    if any(b in answer for b in bad):
        return False
    return any(g in answer for g in good)


def grade(system, template):
    """프롬프트 한 벌을 여섯 문항으로 채점한다."""
    total = 0
    for question, kind, good, bad in CASES:
        hits = search(question, k=3)
        context = "\n\n".join(text for score, slug, text in hits)
        prompt = template.format(ctx=context, q=question)

        messages = []
        if system:
            messages.append({"role": "system", "content": system})
        messages.append({"role": "user", "content": prompt})

        ok = 0
        for _ in range(N):
            res = client.chat.completions.create(model="qwen2.5:3b", messages=messages)
            if judge(res.choices[0].message.content, good, bad):
                ok += 1

        print(f"  [{kind}] {ok}/{N}  {question}")
        total += ok
    print(f"  합계 {total}/{len(CASES) * N}")
    return total

글자가 들었는지로 채점하는 것은 조잡하다. 「멈춘다」를 「멈추지 않는다」로 써도 정답 처리된다. 그래도 없는 것보다 압도적으로 낫다 — 오늘 이 조잡한 채점기가 잡아낼 사고가 하나 있는데, 눈으로 봤으면 절대 못 봤을 것이다.

2. A — 지금 쓰던 프롬프트 (기준선)

A = "아래 자료를 읽고 질문에 답해라.\n\n[자료]\n{ctx}\n\n[질문]\n{q}\n"
grade(None, A)
터미널 — A 원본
  • [정상] 3/3 조각을 겹치게 자르는 이유가 뭔가
  • [3] 0/3 겹침을 조각 크기와 같게 주면 어떻게 되나
  • [1] 2/3 오늘 점심 뭐 먹지
  • [1] 1/3 서울에서 부산까지 몇 시간 걸리나
  • [1] 0/3 이 강의 수강료가 얼마인가
  • [2] 0/3 우리가 자른 조각의 크기는 몇 자인가
  • 합계 6/18

기준선이 6/18 이다. 여기서 시작한다.

3. B — 「자료에 없는 건 쓰지 마라」

제일 먼저 떠오르는 처방이고 실제로 잘 듣는다.

B = """아래 자료만 근거로 질문에 답해라.
자료에 없는 내용은 쓰지 마라. 자료로 답할 수 없으면 「자료에 없다」고만 답해라.

[자료]
{ctx}

[질문]
{q}
"""
grade(None, B)
터미널 — B 자료밖 금지
  • [정상] 0/3 조각을 겹치게 자르는 이유가 뭔가
  • [3] 0/3 겹침을 조각 크기와 같게 주면 어떻게 되나
  • [1] 3/3 오늘 점심 뭐 먹지
  • [1] 3/3 서울에서 부산까지 몇 시간 걸리나
  • [1] 3/3 이 강의 수강료가 얼마인가
  • [2] 0/3 우리가 자른 조각의 크기는 몇 자인가
  • 합계 9/18

4. 🔴 갈래 ① 을 고쳤더니 정상 질문이 죽었다

같은 질문, 같은 근거, 프롬프트만 바꿨다. A 에서는 「겹침은 데이터의 일부가 여러 조각으로 분리되지 않도록 하는 안전장치입니다」로 3/3 이었다. B 에서는 「자료에는 조각을 겹치게 자르는 이유가 명시되어 있지 않습니다」로 0/3 이다. 근거는 1위로 제대로 들어왔는데 있는데 없다고 한다.

이것이 과잉 거부다. 「없으면 없다고 해라」를 세게 시키면 모델은 안전한 쪽으로 도망친다 — 「자료에 없다」는 언제나 무난한 답이기 때문이다.

그리고 이게 사고인 줄 모르고 배포된다. 헛소리가 사라졌으니 고쳐진 것처럼 보이고, 사용자가 「자꾸 모른대요」라고 말해줘야 안다. 정상 문항을 채점표에 넣어두지 않았으면 우리도 못 봤다.

5. C — 세게 말고 정확하게

금지를 늘리는 대신 역할과 형식을 준다.

C_system = """너는 사내 문서 안내원이다. 주어진 자료만 근거로 답한다.
자료에 없는 내용은 절대 쓰지 않는다. 자료로 답할 수 없으면 「자료에 없다」고만 답한다.
반드시 한국어로만 답한다. 세 문장 이내로 답한다."""

C_user = "[자료]\n{ctx}\n\n[질문]\n{q}\n"
grade(C_system, C_user)
터미널 — C 역할 + 한국어 + 세 문장
  • [정상] 3/3 조각을 겹치게 자르는 이유가 뭔가
  • [3] 1/3 겹침을 조각 크기와 같게 주면 어떻게 되나
  • [1] 3/3 오늘 점심 뭐 먹지
  • [1] 3/3 서울에서 부산까지 몇 시간 걸리나
  • [1] 3/3 이 강의 수강료가 얼마인가
  • [2] 0/3 우리가 자른 조각의 크기는 몇 자인가
  • 합계 13/18
B 와 C 는 뭐가 달랐나
B — 0/3 로 부순 쪽C — 3/3 을 지킨 쪽
말투금지 두 줄역할 한 줄 + 금지 두 줄
역할없음「사내 문서 안내원이다」
형식없음한국어로만 · 세 문장 이내
효과모르겠다는 쪽으로 도망감답할 자리에서는 답한다
역할을 주면 「나는 답해주는 사람이다」가 기본값이 된다. 금지만 주면 기본값이 침묵이 된다.

「반드시 한국어로만」 한 줄이 앞 편에서 본 중국어 답을 막았다. A 원본에서 「오늘 점심 뭐 먹지」가 2/3 였던 이유가 그거다. 「세 문장 이내」도 같이 값을 한다 — 길게 쓰게 두면 뒤로 갈수록 자료에서 멀어진다.

6. D — 근거를 먼저 옮겨 적게 하면?

더 좋아질 것 같은데 실제로는 나빠졌다.

D_user = """[자료]
{ctx}

[질문]
{q}

다음 두 줄로만 답해라.
근거: (자료에서 그대로 옮긴 한 문장)
답: (세 문장 이내)
"""
grade(C_system, D_user)
네 프롬프트 · 18점 만점
A 원본6/18
B 자료밖 금지9/18
C 역할+형식13/18
D 인용 먼저11/18
D 는 C 에서 형식만 더 조인 것인데 두 점 떨어졌다.

D 에서 정상 문항이 3/3 에서 2/3 로 떨어졌다. 「근거 한 문장 + 답」 틀에 맞추느라 답이 짧아졌고, 짧아지면서 채점 낱말이 빠졌다. 틀을 씌우는 것과 내용을 잘 쓰는 것은 서로 예산을 다툰다. 작은 모델일수록 심하다.

7. system 에 넣는 게 마법인가

C 는 지시를 system 에 넣었고 B 는 user 에 넣었다. 차이가 그것 때문인지 확인한다.

29_where.py
# 문구는 그대로 두고 자리만 맞바꿔 네 벌을 만든다
grade(None,     STRICT + "\n\n[자료]\n{ctx}\n\n[질문]\n{q}\n")   # B  강한문구 · user
grade(STRICT,   "[자료]\n{ctx}\n\n[질문]\n{q}\n")                # B' 강한문구 · system
grade(SOFT,     "[자료]\n{ctx}\n\n[질문]\n{q}\n")                # C  부드러운 · system
grade(None,     SOFT   + "\n\n[자료]\n{ctx}\n\n[질문]\n{q}\n")   # C' 부드러운 · user
문항 셋만 골라 다시 — 자리를 맞바꿔본다
정상 문항갈래 ③갈래 ①
B 강한문구 · user0/30/33/3
B' 강한문구 · system0/30/33/3
C 부드러운 · system2/30/33/3
C' 부드러운 · user2/30/33/3
위아래 두 줄씩 값이 같다. 자리를 바꿔도 안 바뀌고, 문구를 바꾸면 바뀐다.

「시스템 프롬프트에 넣으면 더 잘 듣는다」는 인터넷에서 제일 흔히 보는 조언인데, 우리가 쓰는 3b 모델에서는 차이가 안 났다. 결과를 가른 것은 문구였다.

그렇다고 system 을 안 쓸 이유는 없다. 자리가 나뉘어 있으면 매번 안 바뀌는 규칙과 매번 바뀌는 질문이 갈려서 코드가 정리된다. 효과가 아니라 정리 때문이다. 큰 모델에서는 이야기가 달라지므로 다음 주 월요일에 같은 실험을 상용 API 로 다시 돌린다.

8. 갈래 ② 는 네 번 다 0/3 이었다

「우리가 자른 조각의 크기는 몇 자인가」 — 정답 200자
프롬프트점수
A 원본0/3「6자 이상인 것으로 추론할 수 있습니다」
B 자료밖 금지0/3「69자」 — 한 마디로 단호하게
C 역할+형식0/3「6자부터 69자까지 조각이 존재한다」
D 인용 먼저0/3「47자」
정답이 든 조각은 23위라 네 번 다 근처에도 안 갔다.

여기서 하나 더 볼 것이 있다. A 는 「6자 이상으로 추론할 수 있습니다」라고 말끝을 흐려서 읽는 사람이 의심할 여지가 있었다. B 는 「69자」 두 글자로 끝냈다 — 틀린 답인데 더 믿음직하게 생겼다. 형식을 조이면 자신 없음이 먼저 깎인다.

내일 JSON 으로 답을 받게 만들면 이 문제가 더 심해진다. 필드가 비어 있는 것보다 아무 값이나 채우는 쪽이 형식에 맞기 때문이다. 그래서 내일 「모르겠다」를 담을 칸을 스키마에 만들어 둔다.

9. 3번으로는 아직 못 잰다

정직하게 하나 적고 간다. 갈래 ③ 문항이 C 에서 1/3 이었는데, 7번에서 같은 조건으로 다시 재니 0/3 이 나왔다.

답이 매번 다르니 3번 돌린 결과는 3번마다 흔들린다. 6/18 에서 13/18 같은 큰 차이는 믿어도 되지만 0/3 과 1/3 은 못 믿는다. 어디까지 믿을 수 있는지를 정하는 것이 수요일 오전이고, 지금 기억할 것은 하나다 — 한 번 돌려보고 좋아졌다고 말하지 않는다.

이번 편에 나온 것

정리
쓴 것 · 안 것내용
채점기 먼저고치기 전에 만든다. 정상 문항을 반드시 넣는다
judge(답, 정답말, 오답말)글자가 들었는지로 채점. 조잡해도 없는 것보다 낫다
기준선 A6/18. 여기서 시작한다
과잉 거부금지를 세게 주면 있는 답도 없다고 한다
역할 + 형식13/18. 금지만 주지 말고 답할 사람이라고 정해준다
반드시 한국어로만 답한다작은 모델의 언어 섞임을 막는다
형식의 대가틀을 조이면 내용이 준다. D 가 C 보다 낮았다
system vs user이 모델에서는 차이가 없었다. 문구가 갈랐다
갈래 ② 는 그대로0/3 · 0/3 · 0/3 · 0/3. 프롬프트로는 검색을 못 고친다
N=3 의 한계0/3 과 1/3 을 구분해 말하지 않는다

다음 편에서 조각을 몇 개 넣을지를 재서 정한다.

Share
  • 파이썬
  • AI
  • LLM
  • RAG
  • 프롬프트