프롬프팅


프롬프트란 모델이 예측을 하기 전에 앞에 놓아두는 텍스트입니다. 이 장이 다듬으려는 핵심 개념도 바로 이것입니다. 프롬프트는 어떤 지능체에게 내리는 명령이 아니라, 원하는 답이 가장 그럴듯한 다음 내용이 되도록 배치해두는 **맥락**입니다.
프롬프트란 실제로 무엇인가
같은 모델에 두 개의 프롬프트를 넣어보겠습니다. "기후 변화에 대해 써줘"라고 하면 원하지도 않은 두서없는 글이 나옵니다. "12살 아이를 위해 기후 변화의 주요 원인을 세 문장으로, 통계 없이 써줘"라고 하면 바로 쓸 수 있는 결과가 나옵니다. 두 호출 사이에 모델이 바뀐 것은 아닙니다. 바뀐 것은 맥락이고, 결과는 그 맥락을 따라간 것입니다.
이전 장에서는 모델이 앞에 놓인 모든 것을 참고해 가장 그럴듯한 다음 토큰을 예측하며 텍스트를 생성한다는 것을 다뤘습니다. 프롬프트란 바로 그 "앞에 놓인 모든 것"입니다. 이것이 꼭 기억해야 할 정의입니다. 프롬프트는 사람에게 하는 부탁이 아니라, 여러분이 바라는 다음 내용을 모델이 가장 그럴듯하다고 판단하도록 준비해두는 **맥락**입니다.
이렇게 관점을 바꾸면 글 쓰는 방식도 달라집니다. 누군가를 설득하거나 지시하는 것이 아니라, 원하는 답이 그럴듯한 다음 결과가 되도록 텍스트를 배치하는 것입니다. 이 장에서 다루는 모든 기법은 그 일을 하는 서로 다른 방법이며, 그 밑바탕에 있는 예측 기계를 이해하고 나면 각 기법이 훨씬 더 이해가 잘 됩니다.
세 가지 역할
코드를 통해 모델과 대화할 때는 하나의 텍스트 덩어리를 보내는 것이 아닙니다. 메시지 목록을 보내며, 각 메시지는 어디서 온 것인지를 나타내는 역할을 가지고 있습니다. 역할은 세 가지입니다.
- system: 고정적인 지시문입니다. 모델이 누구로 행동해야 하는지, 따라야 할 규칙, 원하는 형식 등입니다. 한 번 설정하면 대화 전체에 적용됩니다.
- user: 앱을 사용하는 사람이 보낸 메시지입니다. 실제 요청입니다.
- assistant: 모델이 이전에 생성한 메시지입니다. 대화가 이전 기록을 유지하는 방식이 이것입니다.
messages = [
{"role": "system", "content": "You are a concise assistant for a cooking app. Answer in one sentence."},
{"role": "user", "content": "What can I use instead of butter in cookies?"},
]내부적으로는 이 역할들이 모델로 들어가는 별도의 채널이 아닙니다. 각 메시지 주변에 특수 마커 토큰을 붙여 하나의 텍스트 스트림으로 합쳐지고, 모델은 그 전체 스트림을 바탕으로 예측을 합니다. 역할이 의미를 갖는 이유는 모델이 이런 형식으로 배치된 대화 데이터로 학습되었기 때문입니다. 그래서 system 내용은 권위 있는 고정 규칙으로, user 내용은 답해야 할 요청으로 다루도록 학습되어 있습니다.
초보자가 가장 덜 활용하는 것이 system 메시지입니다. 매 턴마다 원하는 동작, 즉 어조, 대상, 형식, 모델이 절대 하지 말아야 할 것을 여기에 설정합니다. 지속적으로 유지되어야 할 규칙은 여기에 넣고, 구체적인 요청은 user 메시지에 넣으세요.
모델은 요청 사이에 기억이 없기 때문에, assistant 역할은 이전에 나온 말을 다시 재생하는 방법이기도 합니다. 대화를 이어가려면 이전 대화 내용을 assistant와 user 메시지로 다시 보내는 것입니다. 이 내용은 코드로 모델 호출하기를 시작할 때 더 자세히 다룹니다.
system, user, assistant로 태그가 붙은 메시지 목록입니다. 이 태그들은 모델이 예측하는 하나의 텍스트 스트림으로 합쳐지지만, 모델은 system을 권위 있는 규칙으로, user를 요청으로 다루도록 학습되어 있습니다. 지속적인 지시문은 system에, 구체적인 요청은 user에 넣고, 대화 기록을 이어가려면 과거의 assistant와 user 메시지를 다시 재생하세요. 구체적인 것이 모호한 것을 이긴다
두 개의 프롬프트, 하나의 모델을 놓고 살펴보겠습니다. "기후 변화에 대해 써줘"는 길이도 어조도 예측할 수 없는 일반적인 글을 내놓습니다. "12살 아이를 위해, 평이한 말로, 통계 없이, 기후 변화의 주요 원인을 세 문장으로 요약해줘"는 여러분이 그려놓은 것과 거의 비슷한 결과를 내놓습니다. 같은 기계인데 결과는 완전히 다르고, 유일한 차이는 얼마나 구체적으로 못을 박아두었는가입니다.
그 이유는 예측 루프로 거슬러 올라갑니다. 매 단계에서 모델은 그럴듯한 여러 다음 내용 중 하나를 고릅니다. 모호한 프롬프트는 그 선택 범위를 넓게 열어두고, 모델은 일반적으로 가장 흔한 것으로 그 틈을 채웁니다. 그것은 여러분이 실제로 염두에 두었던 것과는 거의 맞지 않습니다. 구체적인 프롬프트는 모델이 토큰을 하나 쓰기도 전에, 원하는 답 쪽으로 그 선택 범위를 좁힙니다.
# vague: the model decides length, tone, audience, format
"Write about climate change."
# specific: you decide
(
"Write a 3-sentence summary of the main causes of climate change, "
"for a 12-year-old, in plain language, with no statistics."
)모호한 버전은 수천 가지의 그럴듯한 방향으로 이어질 수 있고, 여러분은 그중 하나를 무작위로 받는 셈입니다. 구체적인 버전은 그중 거의 전부를 미리 배제합니다. 프롬프트를 쓸 때는 실제로 원하는 것을 말하세요.
- 형식: 문장, 목록, JSON, 표.
- 길이: 한 단락, 세 개의 항목, 50단어 이하.
- 대상: 초보자, 전문가, 12살 아이.
- 불확실할 때 할 일: "본문에 명시되어 있지 않다면, 추측하지 말고 '명시되지 않음'이라고 답해라."
마지막 항목은 겉보기보다 훨씬 중요합니다. 그냥 놓아두면 모델은 자신 있어 보이는 추측으로 빈틈을 채웁니다. 유창해 보이는 추측이 그럴듯한 다음 내용이기 때문입니다. "모르겠다"고 말할 수 있는 선택지를 명시적으로 주면 그것이 오히려 그럴듯한 경로가 되며, 이는 지난 장에서 다룬 환각 문제를 프롬프트 수준에서 줄이는 몇 안 되는 방법 중 하나입니다. 구체성은 모델이 토큰을 쓰기도 전에 선택지를 좁혀줍니다.
예시로 보여주기
때로는 원하는 다음 내용을 준비하는 가장 확실한 방법이 직접 보여주는 것입니다. 프롬프트에 몇 가지 완성된 예시를 넣는 것을 **퓨샷(few-shot) 프롬프팅**이라고 부릅니다(예시가 없는 것은 **제로샷(zero-shot)**입니다). 이것이 통하는 이유는 모델이 본질적으로 패턴을 이어가는 존재이기 때문입니다. 입력과 그에 대한 올바른 출력의 예시를 두세 개 주면, 그 패턴을 새 입력에 그대로 적용하는 것이 다음에 나올 가장 그럴듯한 내용이 됩니다.
messages = [
{"role": "system", "content": "Label each review as POSITIVE, NEGATIVE, or NEUTRAL. Reply with only the label."},
{"role": "user", "content": "The food was cold and slow to arrive."},
{"role": "assistant", "content": "NEGATIVE"},
{"role": "user", "content": "Decent meal, nothing special."},
{"role": "assistant", "content": "NEUTRAL"},
{"role": "user", "content": "Best burger I have had in years!"},
]
# the model now answers: POSITIVE완성된 두 개의 예시는 정확한 패턴을 확립합니다. 정해진 집합 안에서 대문자로 된 단어 하나입니다. 이제 세 번째 리뷰에 대해서도 그 패턴을 이어가는 것이 분석 단락을 쓰는 것보다 훨씬 그럴듯해지고, 그래서 그런 결과를 얻는 것입니다. 이 때문에 명확한 예시 몇 개가 설명 단락보다 나은 경우가 많습니다. 특히 분류나 형식 지정 작업에서 그렇습니다. 결과의 형태를 설명하고 바라는 대신 직접 보여주는 것입니다. 곤란한 사례를 포함하는 예시를 고르세요. 모델은 여러분이 보여준 것의 패턴을 따라가며, 예시 속 실수까지도 따라갑니다.
추론하게 만들기
여기 가장 유용한 프롬프팅 아이디어 중 하나가 있습니다. 동시에 가장 직관적이지 않은 아이디어이기도 한데, 이는 모델의 작동 방식에서 곧바로 따라 나오는 결과입니다. 모델은 한 번에 한 토큰씩 생성하며, 가진 작업 공간은 이미 써놓은 텍스트뿐입니다. 답하기 전에 어려운 문제를 조용히 풀어볼 숨겨진 메모지 같은 것은 없습니다. 모델의 "생각"이라는 것은 소리를 내며 이루어지고, 생성해내는 토큰 안에서 이루어집니다.
그러니 다단계 질문을 하면서 최종 답만 요구하면, 모델은 작업할 공간도 없이 단 하나의 토큰으로 결론까지 뛰어넘어야 하고, 추론이나 수학, 논리가 관련된 문제라면 종종 잘못 뛰어넘습니다. 해결책은 모델이 드러내면서 작업하게 두는 것입니다. 답하기 전에 단계를 풀어놓으라고 요청하세요.
messages = [
{"role": "system", "content": "Work through the problem step by step, then give the final answer on its own line."},
{"role": "user", "content": "A shop sells pens at 3 for $2. How much do 12 pens cost?"},
]
# without "step by step", models often blurt a wrong number
# with it, the model writes out the working, and each step makes the next more reliable이것을 사고의 연쇄(chain-of-thought) 프롬프팅이라고 부르며, 이것이 통하는 이유는 마법이 아니라 기계적인 것입니다. 모델이 써놓은 각 단계는 다음 토큰을 위한 맥락의 일부가 됩니다. 그러니 "펜 12개는 3개짜리 4묶음이다"라고 쓰면, "4묶음에 2달러니까 8달러다"가 자연스럽고 그럴듯한 다음 내용이 됩니다. 불가능한 한 번의 도약으로 답을 요구하는 대신, 모델이 지면 위에서 계산할 여지를 주는 것입니다. "단계별로 생각해봐" 또는 "풀이 과정을 보여줘"가 이것의 일상적인 버전입니다.
두 가지 주의할 점이 있습니다. 써놓은 추론도 여전히 생성된 텍스트이므로, 그럴듯해 보이면서도 잘못된 답에 도달할 수 있습니다. 이를 보장된 증명으로 여기지 마세요. 그리고 일부 최신 모델은 묻지 않아도 이런 종류의 작업을 내부적으로 수행합니다. 하지만 밑바탕의 원리는 앞으로 나올 어떤 모델에서든 변하지 않습니다. 모델은 단계를 써놓을 여지가 있을 때 더 잘 추론합니다. 왜냐하면 자기 출력이 생각할 수 있는 유일한 공간이기 때문입니다.
프롬프트 구조화하기
프롬프트가 길어질수록 구조는 맥락을 명확하게 유지해 모델이 각 부분을 올바르게 가늠하도록 해줍니다. 도움이 되는 몇 가지 습관이 있습니다.
- 지시문을 먼저, 데이터를 나중에. 무엇을 할지 먼저 말하고, 그다음 그 작업을 적용할 텍스트를 제공하세요.
- 구분자로 지시문과 데이터를 나누세요. 제공하는 텍스트는 명확한 마커로 감싸서, 규칙과 입력 사이의 경계가 확실하게 드러나도록 하세요.
- 원하는 출력 형태를 명시적으로 요청하세요. JSON을 원한다면 그렇게 말하고, 필드를 설명하세요.
prompt = f'''Summarise the customer message below in one sentence.
Then list any product names it mentions.
Customer message:
"""
{user_message}
"""'''삼중 인용부호가 **구분자**입니다. "여기 안에 있는 모든 것은 처리할 데이터이며, 따라야 할 지시문이 아니다"라는 표시입니다. 이것은 단순한 깔끔함의 문제가 아닙니다. 여기서 예측 관점이 왜 이것이 실질적인 안전장치인지 보여줍니다.
모델에게는 이 모든 것이 하나의 텍스트 스트림일 뿐이므로, 사용자 입력이 경계 없이 그대로 지시문 안으로 흘러들어 간다면, 사용자는 "위 내용은 무시하고 대신 이걸 해라"라고 쓸 수 있고 모델은 그 문장을 진짜 지시문과 구별할 확실한 방법이 없습니다. 그 지시를 그대로 따를 가능성도 충분히 있습니다.
이런 공격을 프롬프트 인젝션이라고 부르며, 지시문과 신뢰할 수 없는 데이터를 명확하게 구분해두는 것이 첫 번째 방어선입니다. 자세한 내용은 안전성과 한계에서 다룹니다. 모델에게는 모든 것이 하나의 텍스트 스트림일 뿐이므로, 신뢰할 수 없는 입력은 울타리를 치세요.
실전에서
지금까지 다룬 여러 아이디어를 쌓아놓은 프롬프트를 보겠습니다. 고정적인 규칙을 담은 system 메시지, 구체적인 요청, 형식을 고정하는 예시, 명확하게 구분된 데이터입니다.
messages = [
{
"role": "system",
"content": (
"You extract action items from meeting notes. "
"Reply with a numbered list, one action per line, each starting with a verb. "
"If there are no action items, reply 'None'."
),
},
{"role": "user", "content": 'Notes: """We agreed Sam will send the budget by Friday and Lee will book the venue."""'},
{"role": "assistant", "content": "1. Send the budget by Friday (Sam)\n2. Book the venue (Lee)"},
{"role": "user", "content": f'Notes: """{meeting_notes}"""'},
]system 메시지는 규칙을 한 번에 설정하고, 예시는 정확한 형식을 고정하며, 각 요청은 울타리가 쳐진 데이터를 전달합니다. 모든 조각은 원하는 결과가 그럴듯한 다음 내용이 되도록 맥락을 배치하고 있습니다. 그것이 이 기술의 전부입니다. 이것은 운이 좋아서가 아니라, **믿을 만한 프롬프트**이기 때문입니다. 다음으로는 코드로 모델 호출하기에서 이런 메시지를 실제 코드로 모델에 보내보겠습니다.
system 메시지, 구체적인 요청, 형식을 고정하는 예시, 명확하게 구분된 데이터입니다. 각 조각은 원하는 결과가 그럴듯한 다음 텍스트가 되도록 맥락을 배치합니다. 그것이 바로 이 기술의 본질입니다. 운이 아니라 설계로 작동하는 프롬프트인 것입니다. 
