포스트

프롬프트 엔지니어링 기초: LLM에게 일을 정확히 시키는 법

프롬프트가 무엇이고 왜 결과가 매번 달라지는지에서 출발해, 역할 부여, 예시 제공(few-shot), 단계적 사고 유도, 출력 형식 고정 같은 핵심 기법을 정리하고 파이썬 코드로 직접 확인한 프롬프트 엔지니어링 기초 노트입니다.

프롬프트 엔지니어링 기초: LLM에게 일을 정확히 시키는 법

대형 언어 모델(LLM)에게 원하는 결과를 안정적으로 얻어내는 기술이 프롬프트 엔지니어링입니다. 이 글은 프롬프트가 무엇인지에서 출발해, 실무에서 반복해서 쓰이는 기법을 하나씩 정리하고 파이썬으로 직접 확인합니다. LLM API를 한 번이라도 호출해 본 적이 있다면 따라올 수 있습니다.

프롬프트란 무엇인가

LLM은 결국 “지금까지의 텍스트 다음에 올 단어”를 확률적으로 이어 붙이는 모델입니다. 우리가 모델에게 건네는 입력 텍스트 전체를 프롬프트라고 부릅니다. 모델은 이 프롬프트를 읽고 그 뒤를 이어 씁니다. 같은 질문이라도 프롬프트를 어떻게 쓰느냐에 따라 답의 품질이 크게 달라지는 이유가 여기에 있습니다.

예를 들어 “요약해줘”라고만 던지는 것과, “다음 기사를 세 문장으로, 초등학생도 이해할 수준으로 요약해줘”라고 쓰는 것은 모델 입장에서 전혀 다른 작업입니다. 프롬프트 엔지니어링은 이 입력 텍스트를 설계해 원하는 출력을 안정적으로 끌어내는 일입니다.

왜 결과가 매번 달라지는가

프롬프트를 다듬기 전에 알아둘 것이 하나 있습니다. LLM은 기본적으로 확률에 따라 다음 단어를 고르기 때문에, 같은 프롬프트를 넣어도 실행할 때마다 답이 조금씩 달라집니다. 이 무작위성의 폭을 조절하는 값이 온도(temperature)입니다. 값이 낮으면 가장 그럴듯한 단어에 집중해 답이 일관되게 나오고, 높으면 다양한 표현이 나오는 대신 흔들립니다.

분류나 추출처럼 정답이 정해진 작업은 온도를 낮게, 아이디어를 넓게 뽑는 작업은 높게 두는 식으로 씁니다. 다만 온도를 0으로 두어도 완전히 똑같은 답이 보장되지는 않는다는 점은 기억해 둘 만합니다.

프롬프트를 아무리 잘 써도 출력이 흔들린다면, 먼저 온도부터 확인하세요. 온도가 높으면 문장 설계의 효과가 무작위성에 묻혀 잘 드러나지 않습니다.

기법 1: 역할과 맥락을 준다

가장 먼저 할 일은 모델에게 “너는 누구이고 무엇을 하는 사람인지”를 알려주는 것입니다. 역할을 지정하면 모델은 그 역할에 맞는 어휘와 관점으로 답을 씁니다.

대부분의 챗 모델 API는 메시지를 역할별로 나눠 받습니다. system은 모델의 정체성과 규칙을 정하는 자리이고, user는 실제 요청을 담는 자리입니다. 아래는 Anthropic의 Claude를 파이썬 SDK로 호출하는 예시입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
from anthropic import Anthropic

client = Anthropic()  # ANTHROPIC_API_KEY 환경변수를 읽습니다

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system="너는 10년차 데이터 분석가야. 통계 용어를 초보자가 이해할 수 있게 풀어서 설명해.",
    messages=[
        {"role": "user", "content": "p-value가 무엇인지 설명해줘."}
    ],
)
print(message.content[0].text)

같은 질문이라도 system에 “10년차 데이터 분석가”를 넣었을 때와 넣지 않았을 때 답의 눈높이가 달라집니다. 역할 지정은 가장 값싸면서 효과가 큰 기법입니다.

기법 2: 예시를 보여준다 (few-shot)

말로 설명하기 어려운 규칙은 예시로 보여주는 편이 빠릅니다. 원하는 입력과 출력의 짝을 몇 개 프롬프트에 넣어주면, 모델은 그 패턴을 따라 새 입력을 처리합니다. 예시를 하나도 주지 않는 방식을 zero-shot, 몇 개 주는 방식을 few-shot이라고 부릅니다.

아래는 문장의 감정을 분류시키는 프롬프트입니다. 원하는 라벨 형식(긍정, 부정, 중립)을 예시로 못 박아 둔 것이 핵심입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
prompt = """다음 문장의 감정을 긍정, 부정, 중립 중 하나로만 분류해.

문장: 오늘 점심 정말 맛있었어.
감정: 긍정

문장: 배송이 일주일이나 걸렸다.
감정: 부정

문장: 회의는 오후 3시에 시작한다.
감정: 중립

문장: 새로 산 이어폰 음질이 기대 이하야.
감정:"""

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=10,
    messages=[{"role": "user", "content": prompt}],
)
print(message.content[0].text)  # 부정

예시 세 개만으로 “긍정/부정/중립 중 하나만, 다른 말 없이” 라는 규칙이 전달됩니다. 라벨을 자유롭게 서술하는 대신 정해진 값으로 뽑고 싶을 때 특히 잘 듣습니다.

기법 3: 단계적으로 생각하게 한다

복잡한 추론 문제는 답을 곧장 내놓게 하기보다, 풀이 과정을 먼저 쓰게 하면 정확도가 올라갑니다. “단계별로 생각해 보자”처럼 사고 과정을 유도하는 방식을 chain-of-thought라고 부릅니다.

1
2
3
4
5
6
7
8
9
prompt = """카페에 사과가 23개 있었다. 오전에 8개를 팔고, 오후에 15개를 더 들여왔다.
지금 사과는 몇 개인가? 풀이 과정을 단계별로 쓴 다음 마지막 줄에 '답: N' 형식으로 답해."""

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=512,
    messages=[{"role": "user", "content": prompt}],
)
print(message.content[0].text)

이렇게 하면 모델이 23 - 8 = 15, 15 + 15 = 30 같은 중간 계산을 적어가며 답에 도달합니다. 중간 과정을 건너뛰고 숫자만 내놓을 때보다 실수가 줄어듭니다. 최신 모델 중에는 이런 사고 과정을 내부적으로 알아서 수행하는 것도 있지만, 프롬프트로 명시적으로 유도하는 방법은 여전히 유효합니다.

기법 4: 출력 형식을 고정한다

프로그램이 모델의 답을 이어받아 처리해야 한다면, 사람이 읽기 좋은 문장보다 정해진 형식이 필요합니다. 출력을 JSON 같은 구조로 못 박으면 뒷단에서 파싱하기 쉬워집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
prompt = """다음 문장에서 사람 이름과 소속을 뽑아 JSON으로만 답해.
다른 설명은 붙이지 마.

문장: 카카오의 김지연 팀장이 네이버의 박준호 연구원과 미팅했다.

형식:
{"people": [{"name": "이름", "org": "소속"}]}"""

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=256,
    messages=[{"role": "user", "content": prompt}],
)
print(message.content[0].text)
# {"people": [{"name": "김지연", "org": "카카오"}, {"name": "박준호", "org": "네이버"}]}

“다른 설명은 붙이지 마”라는 한 줄이 중요합니다. 이 지시가 없으면 모델이 JSON 앞뒤로 “네, 아래와 같습니다” 같은 문장을 덧붙여 파싱을 방해하는 경우가 있습니다. 형식을 예시로 함께 보여주면 더 안정적입니다.

기법 5: 지시는 구체적으로, 제약은 명시적으로

지금까지의 기법을 관통하는 원칙은 하나입니다. 모델은 사람의 의도를 알아서 채워주지 않으니, 원하는 것을 구체적으로 적고 원하지 않는 것을 명시적으로 막아야 합니다.

  • “짧게 써줘”보다 “세 문장 이내로 써줘”가 낫습니다.
  • “쉽게 설명해줘”보다 “중학생이 이해할 수준으로, 전문 용어는 괄호 안에 뜻을 달아서”가 낫습니다.
  • “코드 짜줘”보다 “파이썬으로, 표준 라이브러리만 써서, 주석을 달아서”가 낫습니다.

모호한 지시는 모델이 알아서 해석하게 만들고, 그 해석은 실행할 때마다 달라집니다. 제약을 문장으로 못 박을수록 결과가 예측 가능해집니다.

프레임워크를 쓰면 달라지는 것

프롬프트가 길어지고 변수가 늘어나면, 문자열을 직접 이어 붙이는 방식은 관리하기 번거로워집니다. LangChain 같은 프레임워크는 프롬프트를 템플릿으로 다루게 해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate

llm = ChatAnthropic(model="claude-sonnet-5")

prompt = ChatPromptTemplate.from_messages([
    ("system", "너는 {role}야. {tone} 말투로 대답해."),
    ("user", "{question}"),
])

chain = prompt | llm

answer = chain.invoke({
    "role": "여행 가이드",
    "tone": "친근한",
    "question": "제주도 2박 3일 코스를 추천해줘.",
})
print(answer.content)

{role}, {tone} 같은 자리를 나중에 값으로 채우는 구조라, 프롬프트의 뼈대와 그때그때 바뀌는 값을 분리할 수 있습니다. 프롬프트 엔지니어링에서 배운 원칙(역할, 형식, 제약)은 그대로 쓰고, 그것을 다루는 방식만 정돈되는 것입니다. LangChain 자체는 다음 글에서 자세히 다룹니다.

정리

기법한 줄 요약
역할과 맥락system에 정체성과 규칙을 정해 답의 눈높이를 맞춘다
예시 제공 (few-shot)입력과 출력의 짝을 보여줘 원하는 패턴을 학습시킨다
단계적 사고풀이 과정을 먼저 쓰게 해 추론 정확도를 높인다
출력 형식 고정JSON 등 정해진 구조로 뽑아 뒷단 처리를 쉽게 한다
구체적 지시원하는 것은 수치로, 원하지 않는 것은 명시적으로 막는다
온도 조절정답형 작업은 낮게, 발산형 작업은 높게 둔다

프롬프트 엔지니어링은 특별한 문법을 외우는 일이 아니라, 모델이 무엇을 보고 답을 만드는지 이해하고 그 입력을 설계하는 일입니다. 위 여섯 가지는 대부분의 상황에 적용되는 기본기이고, 실제 작업에서는 이들을 조합해 씁니다. 다음 글에서는 모델이 모르는 우리 문서를 답의 근거로 넣어주는 RAG를 다룹니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.