프롬프트 엔지니어링을 "좋은 문장 작성법"으로 이해하면 금방 한계가 온다.
LLM 애플리케이션에서 prompt는 단순 문자열이 아니다. 모델 호출 앞단의 입력 인터페이스다. 어떤 변수를 받을지, 어떤 context를 넣을지, 어떤 constraints를 강제할지, 어떤 output schema를 기대할지 정의하는 레이어에 가깝다.
교육 현장에서 자주 보는 패턴이 있다.
답변이 마음에 들지 않으면 prompt text를 계속 세게 쓴다.
정확하게 답변해.
절대 틀리지 마.
전문가처럼 자세히 설명해.
이런 표현이 전혀 의미 없다는 뜻은 아니다. 하지만 애플리케이션 품질을 안정적으로 만들기에는 부족하다. prompt가 제어해야 하는 것은 말투만이 아니라 입력 조건 전체다.
예를 들어 고객 문의 분류를 만든다고 하자.
prompt = "아래 고객 문의를 보고 적절히 분류해줘."
이 코드는 간단하지만 운영 관점에서는 너무 많은 것이 비어 있다.
이런 요구사항이 명시되지 않으면 모델은 매번 그럴듯한 방식으로 빈칸을 채운다. 개발자는 "모델이 이상하다"고 느끼지만, 실제 문제는 입력 스펙이 비어 있는 경우가 많다.
실무에서는 prompt를 다음 요소로 나누어 보는 편이 낫다.
prompt_spec = {
"role": "고객 문의 분류 도우미",
"task": "고객 문의를 사전에 정의된 카테고리로 분류한다.",
"context": "온라인 교육 플랫폼의 고객센터 문의",
"input_data": "{customer_message}",
"constraints": [
"문의 내용에 없는 사실을 추론하지 않는다.",
"환불 가능 여부를 단정하지 않는다.",
"확신이 낮으면 needs_review=true로 표시한다."
],
"output_schema": {
"category": "refund | account | course | payment | technical | other",
"priority": "low | medium | high",
"needs_review": "boolean",
"reason": "string"
},
"failure_policy": "정보가 부족하면 other로 분류하고 needs_review=true를 반환한다."
}
핵심은 prompt를 자연어 문장 하나로 보지 않는 것이다. 모델에 전달될 입력 contract로 본다.
이렇게 나누면 다음 장점이 생긴다.
LangChain을 쓰면 ChatPromptTemplate로 system message와 user message를 분리해서 관리할 수 있다. 중요한 것은 도구 자체보다 이 분리가 주는 설계 효과다.
from langchain_core.prompts import ChatPromptTemplate
classification_prompt = ChatPromptTemplate.from_messages([
(
"system",
"""
당신은 온라인 교육 플랫폼의 고객 문의 분류 도우미입니다.
규칙:
- 문의 내용에 없는 사실을 추론하지 마세요.
- 환불 가능 여부를 단정하지 마세요.
- 확신이 낮으면 needs_review를 true로 설정하세요.
- 반드시 지정된 JSON 형식으로만 답하세요.
카테고리:
- refund: 환불, 취소, 결제 취소
- account: 로그인, 계정, 비밀번호
- course: 강의 내용, 수강 기간, 진도
- payment: 결제 실패, 영수증, 카드
- technical: 영상 재생, 접속 오류
- other: 위에 해당하지 않거나 정보 부족
"""
),
(
"user",
"""
고객 문의:
{customer_message}
출력 형식:
{{
"category": "...",
"priority": "...",
"needs_review": true 또는 false,
"reason": "분류 근거를 한 문장으로 작성"
}}
"""
)
])
여기서 prompt는 단순 문자열이 아니라 다음을 분리한다.
이 분리가 없다면 prompt는 금방 거대한 문자열 덩어리가 된다. 그러면 수정도 어렵고, 리뷰도 어렵고, 테스트도 어렵다.
프롬프트 설계에서 자주 빠지는 것이 output schema다.
사람이 읽을 답변만 필요하다면 문단형 응답도 괜찮다. 하지만 서비스나 자동화 흐름에 붙이려면 구조가 필요하다.
from pydantic import BaseModel, Field
from typing import Literal
class InquiryClassification(BaseModel):
category: Literal[
"refund",
"account",
"course",
"payment",
"technical",
"other"
] = Field(description="고객 문의 카테고리")
priority: Literal["low", "medium", "high"] = Field(description="처리 우선순위")
needs_review: bool = Field(description="사람 검토 필요 여부")
reason: str = Field(description="분류 근거")
구조화 출력의 장점은 명확하다.
교육에서 structured output을 다루면 수강생들이 prompt를 다르게 보기 시작한다. "답변을 잘 쓰게 하는 문장"이 아니라 "다음 시스템이 읽을 수 있게 만드는 입력 설계"로 이해하기 때문이다.
Few-shot prompting도 단순히 예시를 많이 붙이는 기술로 보면 아쉽다.
Few-shot은 모델에게 다음 정보를 알려주는 방식이다.
예를 들어 환불 문의 분류에서 애매한 사례를 넣을 수 있다.
예시 1
문의: 강의를 거의 못 들었는데 결제 취소 가능한가요?
출력:
{
"category": "refund",
"priority": "medium",
"needs_review": true,
"reason": "환불 가능 여부는 수강 이력과 약관 확인이 필요하므로 사람 검토가 필요합니다."
}
예시 2
문의: 영상이 계속 멈춰요.
출력:
{
"category": "technical",
"priority": "medium",
"needs_review": false,
"reason": "영상 재생 문제에 해당합니다."
}
좋은 few-shot은 쉬운 정답 예시보다 경계 사례를 포함한다. 실제 서비스에서 문제가 되는 것은 대부분 경계 사례이기 때문이다.
프롬프트를 코드처럼 본다면 변경도 코드처럼 다뤄야 한다.
최소한 다음 정도는 기록하는 편이 좋다.
test_cases = [
{
"id": "refund_ambiguous_01",
"input": "강의 신청했는데 생각보다 어려워요. 취소되나요?",
"expected_category": "refund",
"expected_needs_review": True
},
{
"id": "technical_video_01",
"input": "모바일에서 영상이 안 열립니다.",
"expected_category": "technical",
"expected_needs_review": False
},
{
"id": "account_login_01",
"input": "비밀번호 재설정 메일이 안 와요.",
"expected_category": "account",
"expected_needs_review": False
}
]
프롬프트를 수정할 때마다 "좋아진 것 같다"로 끝내면 운영 품질을 설명하기 어렵다.
적어도 다음 질문에 답해야 한다.
프롬프트 엔지니어링은 결국 실험 관리와 연결된다.
중요한 구분이 하나 더 있다.
모든 품질 문제가 prompt 문제는 아니다.
RAG 애플리케이션에서 답변이 틀렸다면 먼저 확인해야 할 것은 prompt가 아니라 retrieval result일 수 있다. 고객 정책 챗봇이 잘못 답했다면 모델 입력에 최신 정책 문서가 들어갔는지부터 봐야 한다.
프롬프트는 모델의 행동을 유도한다. 하지만 없는 context를 만들어내는 장치는 아니다.
그래서 운영 시스템에서는 prompt와 함께 다음을 봐야 한다.
Prompt engineering은 감이 아니다.
프롬프트는 문자열이 아니라 입력 인터페이스다.
좋은 prompt는 다음을 명확히 한다.
문장을 더 멋지게 쓰는 것보다 중요한 것은 요구사항을 모델 입력으로 명시하는 것이다.
LLM 애플리케이션을 만들 때 prompt를 코드 밖의 임시 문장으로 두지 말자. 설계하고, 버전 관리하고, 테스트하고, 로그로 검증해야 한다. 그래야 프롬프트는 감이 아니라 시스템의 일부가 된다.