Skip to main content

6장. 프롬프트 설계와 응답 제어

5장에서 우리는 첫 번째 LLM API 기능을 직접 붙여 봤다. 입력을 받고, 서버에서 모델을 호출하고, 응답을 화면에 보여 주는 흐름까지 연결했다. 그런데 그 다음 단계에서 거의 모든 개발팀이 같은 벽을 만난다. 어제는 괜찮았는데 오늘은 왜 말투가 다르지? 같은 기능인데 응답 길이가 들쭉날쭉하네? 분류 기능인데 가끔 설명까지 붙여서 오네?

바로 이 지점부터 프롬프트 설계가 시작된다.

많은 사람이 프롬프트를 "모델에게 보내는 문장 한 줄" 정도로 생각한다. 하지만 서비스 개발에서 프롬프트는 훨씬 더 큰 의미를 가진다. 프롬프트는 모델의 기분을 맞추는 문장이 아니라, 서비스가 어떤 방식으로 동작할지를 결정하는 로직이다. 버튼을 누를 때마다 어떤 역할로 답할지, 어떤 형식으로 출력할지, 무엇을 하지 말아야 할지, 모를 때는 어떻게 행동할지까지 여기에 들어간다.

이 장에서는 시스템 프롬프트, 예시, 제약 조건, 생성 파라미터를 이용해 응답의 품질과 일관성을 어떻게 제어하는지 살펴본다. 핵심은 단순하다. 좋은 프롬프트는 예쁜 문장이 아니라, 예측 가능한 동작을 만드는 설계다.

2부. LLM API의 공통 원리 이해하기 / 6장. 프롬프트 설계와 응답 제어

프롬프트는 문장이 아니라 동작 명세다

웹 서비스 개발자는 원래부터 동작을 명세하는 데 익숙하다. 회원가입 API에는 필수 필드가 있고, 결제 API에는 허용된 상태 전이가 있고, 검색 API에는 정렬 규칙이 있다. LLM 기능도 본질적으로 다르지 않다. 다만 규칙이 코드만으로 끝나지 않고, 모델에게 전달하는 지시문과 예시의 형태로 함께 표현될 뿐이다.

예를 들어 "문장을 업무용으로 다듬어 주세요"라고만 쓰면 얼핏 충분해 보인다. 하지만 서비스 관점에서는 빠진 것이 많다. 존댓말을 유지해야 하는지, 의미를 바꾸면 안 되는지, 결과만 출력해야 하는지, 길이는 어느 정도여야 하는지, 공격적인 표현이 들어오면 어떻게 순화해야 하는지 같은 규칙이 비어 있다.

이 빈칸이 많을수록 응답은 흔들린다. 모델이 멍청해서가 아니다. 우리가 서비스 동작을 충분히 정의하지 않았기 때문이다.

즉, 프롬프트 설계란 모델을 설득하는 기술이라기보다, 서비스 요구사항을 모델이 이해할 수 있는 형태로 번역하는 작업에 가깝다.

시스템 프롬프트는 서비스의 헌법이다

프롬프트에는 여러 층이 있지만, 그중 가장 먼저 잡아야 할 것은 시스템 프롬프트다. 시스템 프롬프트는 모델의 역할과 기본 규칙을 정한다. 한마디로 말해, 서비스 전체의 분위기와 행동 기준을 정하는 상위 정책이다.

재작성 기능을 예로 들어 보자. 아래처럼 짧게 시작할 수 있다.

너는 한국어 업무 글쓰기 도우미다. 사용자의 문장을 더 명확하고 자연스러운 업무용 문장으로 다시 써라.

이 문장만으로도 어느 정도는 작동한다. 하지만 실무에서는 대개 부족하다. 출력 형식이 흔들릴 수 있고, 너무 장황해질 수 있고, 원래 뜻을 살짝 바꾸는 경우도 생긴다.

조금 더 실무적으로 다듬으면 이렇게 된다.

너는 한국어 업무 글쓰기 도우미다.
사용자의 문장을 명확하고 자연스러운 업무용 문장으로 다시 쓴다.
의미를 바꾸지 않는다.
과장하거나 새로운 사실을 추가하지 않는다.
존댓말을 유지한다.
결과 문장만 출력한다.
가능하면 2문장 이내로 작성한다.

같은 기능이지만 통제력이 훨씬 좋아진다. 이유는 간단하다. 역할, 목표, 금지 사항, 출력 방식이 분리되어 있기 때문이다.

좋은 시스템 프롬프트에는 보통 다음 네 요소가 들어간다.

  • 모델의 역할

  • 해야 할 일

  • 하지 말아야 할 일

  • 출력 형식이나 길이 같은 기본 규칙

여기서 중요한 것은 길이가 아니라 선명함이다. 시스템 프롬프트를 길게 쓴다고 반드시 좋아지지는 않는다. 오히려 규칙이 서로 충돌하면 모델도 헷갈린다. 헌법이 수천 조문인데 서로 모순되면 나라가 시끄러워지는 것과 비슷하다.

사용자 입력과 지시문을 섞지 마라

초보자가 자주 하는 실수는 지시와 데이터를 한 덩어리로 보내는 것이다. 예를 들면 이런 식이다.

이 문장을 업무용으로 바꿔 줘: 지금 계속 안 되는데 빨리 좀 봐 주세요. 의미는 바꾸지 말고 결과만 출력해.

이렇게 보내도 될 때가 많다. 하지만 서비스가 커질수록 불안해진다. 어디까지가 규칙이고 어디부터가 원문인지 흐려지기 때문이다. 특히 사용자 입력이 길어지거나, 문서 안에 지시처럼 보이는 문장이 들어가면 더 헷갈린다.

그래서 실무에서는 역할을 분리하는 편이 훨씬 낫다.

messages: [
  {
    role: "system",
    content: `너는 한국어 업무 글쓰기 도우미다.
사용자의 문장을 명확하고 자연스러운 업무용 문장으로 다시 쓴다.
의미를 바꾸지 않는다.
과장하거나 새로운 사실을 추가하지 않는다.
결과 문장만 출력한다.`
  },
  {
    role: "user",
    content: "지금 계속 안 되는데 빨리 좀 봐 주세요."
  }
]

이렇게 나누면 모델은 훨씬 안정적으로 해석한다. 개발자도 나중에 유지보수하기 쉽다. 규칙은 시스템 프롬프트에서 바꾸고, 실제 입력은 사용자 메시지에서만 바꾸면 되기 때문이다.

원문이 길거나 여러 필드를 함께 전달해야 할 때는 구분자를 두는 것도 좋다.

아래 원문을 업무용 문장으로 다시 써라.
[원문 시작]
...
[원문 끝]

이 단순한 구분 하나가 프롬프트의 해석 안정성을 꽤 높여 준다.

예시는 설명보다 빠르게 모델을 길들인다

시스템 프롬프트가 헌법이라면, 예시는 판례에 가깝다. 규칙만 읽는 것보다 실제 사례를 보는 편이 훨씬 빠르게 감이 오기 때문이다.

예를 들어 재작성 기능에서 우리가 원하는 톤이 분명하다면, 한두 개의 예시만으로도 응답 품질이 눈에 띄게 안정되는 경우가 많다.

messages: [
  {
    role: "system",
    content: `너는 한국어 업무 글쓰기 도우미다.
사용자의 문장을 명확하고 자연스러운 업무용 문장으로 다시 쓴다.
의미를 바꾸지 않는다.
존댓말을 유지한다.
결과 문장만 출력한다.`
  },
  {
    role: "user",
    content: "이거 오늘 안으로 꼭 해주세요"
  },
  {
    role: "assistant",
    content: "해당 작업을 오늘 안으로 처리해 주시면 감사하겠습니다."
  },
  {
    role: "user",
    content: "지금 계속 에러 나는데 빨리 확인 부탁드려요"
  },
  {
    role: "assistant",
    content: "현재 오류가 지속적으로 발생하고 있습니다. 빠른 확인 부탁드립니다."
  },
  {
    role: "user",
    content: inputText
  }
]

이런 방식을 보통 few-shot, 즉 소수 예시 기반 프롬프팅이라고 부른다. 말이 어려워 보여도 뜻은 단순하다. 원하는 답의 샘플을 먼저 보여 주는 것이다.

예시는 특히 이런 작업에서 강하다.

  • 분류 라벨을 일정하게 맞추고 싶을 때

  • 문장 재작성 톤을 통일하고 싶을 때

  • 요약 길이나 형식을 안정시키고 싶을 때

  • 애매한 규칙을 말로 설명하기 어려울 때

다만 예시는 많이 넣는다고 무조건 좋은 것이 아니다. 너무 많으면 토큰만 늘고, 오히려 핵심이 흐려질 수 있다. 처음에는 대표성이 높은 짧은 예시 2~4개 정도로 시작하는 편이 실용적이다.

제약 조건은 친절이 아니라 안전장치다

LLM은 아주 친절하다. 문제는 가끔 너무 친절하다는 것이다. 한 줄만 원했는데 설명을 세 단락으로 붙여 주고, 분류만 원했는데 이유까지 길게 써 주고, 모르는 정보도 그럴듯하게 메워 버린다.

그래서 서비스에서는 제약 조건이 꼭 필요하다. 제약 조건은 모델을 괴롭히는 규칙이 아니라, 제품을 예측 가능하게 만드는 안전장치다.

가장 자주 쓰는 제약은 이런 종류다.

  • 출력은 한 문단만 작성할 것

  • 결과만 출력하고 설명은 생략할 것

  • 카테고리는 billing, bug, feature 중 하나만 사용할 것

  • 정보가 부족하면 추측하지 말고 "판단 불가"라고 답할 것

  • 100자 이내로 작성할 것

예를 들어 문의 분류 기능이라면 아래처럼 쓰는 편이 낫다.

당신은 고객 문의 분류기다.
아래 문의를 읽고 다음 라벨 중 하나만 출력하라.
허용 라벨: billing, bug, feature, account
라벨 외의 설명은 출력하지 않는다.
애매하면 가장 가까운 하나를 고르되, 근거를 추가하지 않는다.

이 프롬프트의 장점은 예쁘다는 데 있지 않다. 후처리가 쉬워진다는 데 있다. 서버 입장에서는 네 가지 값 중 하나만 오면 되기 때문이다. 반대로 "문의 유형을 알려 주세요"처럼 두루뭉술하게 쓰면, 모델은 친절하게 "이 문의는 버그 관련 문제로 보입니다"라고 답할 수 있다. 사람은 이해하지만, 코드는 귀찮아진다.

서비스에서 프롬프트는 결국 사람과 모델만을 위한 것이 아니다. 뒤에 기다리고 있는 코드와 운영까지 함께 고려한 설계여야 한다.

모르면 모른다고 말하게 만들어라

실무에서 꽤 중요한 규칙 하나가 있다. 모델이 모를 때 억지로 채우지 않게 하는 것이다. 이 원칙이 빠지면 서비스는 그럴듯하지만 위험한 답을 만들 수 있다.

예를 들어 사내 문서를 요약하는 기능에서 문서에 없는 일정이나 책임자를 모델이 추측해 넣으면, 문장이 매끄러워도 제품 품질은 나빠진다. 그래서 다음과 같은 문장을 자주 넣는다.

입력에 없는 사실을 추가하지 않는다.
불확실하면 추측하지 않는다.
정보가 부족하면 부족하다고 명시한다.

이 세 줄은 소박해 보여도 효과가 크다. 많은 실패는 모델이 거짓말을 해서가 아니라, 우리가 추측하지 말라는 규칙을 주지 않아서 생긴다.

물론 이 규칙 하나로 모든 문제가 사라지지는 않는다. 그래도 최소한 서비스의 기본 태도는 정해진다. 공격적으로 아는 척하는 비서보다, 모르면 확인이 필요하다고 말하는 비서가 실무에서는 훨씬 낫다.

temperature는 창의성 버튼이 아니라 변동성 조절기다

프롬프트만큼 중요한 것이 생성 파라미터다. 그중 가장 널리 알려진 것이 temperature다. 흔히 창의성을 조절한다고 설명하지만, 서비스 관점에서는 응답의 변동성을 얼마나 허용할 것인가로 이해하는 편이 더 정확하다.

temperature가 낮으면 모델은 상대적으로 더 보수적이고 안정적인 답을 내놓는다. 같은 요청에 대해 비슷한 패턴으로 답할 가능성이 높다. 반대로 값이 높아지면 더 다양한 표현이 나오기 쉽다.

실무에서는 보통 작업 성격에 맞춰 감을 잡는다.

작업 유형권장 감각
분류, 추출, 재작성낮게 설정
요약, 제목 생성낮거나 중간
아이디어 발산, 카피 초안중간 이상
문학적 글쓰기, 다양한 표현 실험상대적으로 높게

여기서 중요한 점은 temperature가 만능 해결책이 아니라는 것이다. 프롬프트가 모호한데 온도만 낮춘다고 답이 좋아지지는 않는다. 규칙이 불명확한 회의에서 참석자에게 "좀 차분하게 말해 주세요"라고 부탁하는 것과 비슷하다. 회의 주제가 엉망이면 여전히 엉망이다.

처음 튜닝할 때는 대체로 이렇게 접근하면 좋다.

  1. 먼저 프롬프트를 명확하게 만든다.

  2. 그다음 temperature를 낮은 값부터 시작한다.

  3. 다양성이 부족할 때만 조금씩 올린다.

많은 서비스는 처음부터 높은 창의성이 필요하지 않다. 대부분은 안정성이 먼저다.

max tokens, stop 같은 옵션도 품질에 직접 연결된다

응답 제어는 temperature만의 일이 아니다. 출력 길이와 종료 조건도 아주 중요하다.

max_tokens 또는 비슷한 옵션은 모델이 얼마나 길게 말할 수 있는지 제한한다. 이 값은 단순히 비용을 아끼는 도구가 아니다. 결과를 서비스 화면과 후처리에 맞는 크기로 유지하는 장치이기도 하다.

예를 들어 제목 생성 기능인데 제한이 없으면 모델이 제목 세 개, 부제 두 개, 설명 한 단락까지 덤으로 줄 수 있다. 사용자 입장에서는 친절해 보여도, 제품 입장에서는 요구사항 위반이다.

stop 계열 옵션은 특정 문자열에서 생성을 끊는 기능이다. 모든 기능에 필요한 것은 아니지만, 목록 생성이나 특정 포맷 제어에서 유용할 때가 있다. 다만 초반에는 temperature, 길이 제한, 명확한 출력 지시만으로도 충분한 경우가 많다.

중요한 것은 파라미터를 많이 아는 것이 아니라, 어떤 기능에서 어떤 흔들림을 줄이고 싶은지 분명히 아는 것이다.

좋은 프롬프트는 보통 이런 순서를 가진다

서비스에서 자주 쓰는 프롬프트는 일정한 뼈대를 갖고 있는 경우가 많다. 정답 템플릿이 있는 것은 아니지만, 실무적으로는 아래 순서가 잘 먹힌다.

역할
해야 할 일
중요한 제약 조건
입력 데이터의 범위 또는 의미
예시
출력 형식
실패 시 행동 원칙

재작성 기능으로 옮기면 이렇게 만들 수 있다.

너는 한국어 업무 글쓰기 도우미다.
사용자의 문장을 명확하고 자연스러운 업무용 문장으로 다시 쓴다.
의미를 바꾸지 않는다.
새로운 사실을 추가하지 않는다.
존댓말을 유지한다.
가능하면 2문장 이내로 작성한다.
결과 문장만 출력한다.
불완전한 문장이라도 의도를 보존하며 다듬는다.

분류 기능이라면 이렇게 바뀔 수 있다.

너는 고객 문의 분류기다.
문의 내용을 읽고 정확히 하나의 라벨만 선택한다.
허용 라벨은 billing, bug, feature, account다.
라벨 외의 설명은 출력하지 않는다.
입력이 불명확해도 가장 가까운 하나를 고른다.

결국 좋은 프롬프트는 화려한 문장이 아니라, 기능 요구사항이 잘 정리된 설계 문서의 축약본에 가깝다.

재작성 기능을 한 단계 더 실무적으로 바꿔 보자

5장에서 만든 문장 재작성 기능을 조금 더 발전시켜 보자. 기존 프롬프트는 시작점으로는 좋았지만, 장기적으로는 규칙이 더 분명한 편이 좋다.

아래는 한 단계 개선한 예시다.

const systemPrompt = `
너는 한국어 업무 글쓰기 도우미다.
사용자의 문장을 명확하고 자연스러운 업무용 문장으로 다시 쓴다.
의미를 바꾸지 않는다.
과장하거나 새로운 사실을 추가하지 않는다.
감정적인 표현은 정중한 표현으로 순화할 수 있다.
존댓말을 유지한다.
가능하면 2문장 이내로 작성한다.
결과 문장만 출력한다.
`;

const messages = [
  { role: "system", content: systemPrompt },
  { role: "user", content: "이거 왜 아직도 안 되죠 빨리 확인해 주세요" },
  { role: "assistant", content: "해당 문제가 아직 해결되지 않고 있습니다. 빠른 확인 부탁드립니다." },
  { role: "user", content: userInput }
];

const response = await client.chat.completions.create({
  model,
  temperature: 0.2,
  max_tokens: 120,
  messages
});

이 코드에서 봐야 할 포인트는 세 가지다.

첫째, 시스템 프롬프트를 별도 변수로 뺐다. 이렇게 해야 나중에 수정 이력이 남고, A/B 테스트도 가능하다.

둘째, 예시를 추가했다. 이 예시는 모델에게 "아, 이 서비스는 이런 톤을 원하네"라고 빠르게 학습시키는 역할을 한다.

셋째, 출력 길이와 변동성을 동시에 제한했다. 재작성 기능은 소설가보다 교정가에 가깝기 때문에, 낮은 온도와 짧은 출력 제한이 잘 맞는다.

작은 차이처럼 보여도, 실제 서비스에서는 이런 정리가 응답 품질을 훨씬 안정적으로 만든다.

프롬프트는 코드처럼 버전 관리해야 한다

현업에서 흔한 안 좋은 패턴이 하나 있다. 프롬프트가 여기저기 흩어져 있는 것이다. 어떤 파일에는 한 줄짜리 문자열이 있고, 다른 파일에는 비슷하지만 조금 다른 문장이 또 있고, 운영 중에 급하게 고친 문구가 커밋 메시지에도 남지 않는다.

이 상태가 되면 금방 이런 일이 벌어진다. 같은 기능인데 서버마다 응답 톤이 다르고, 언제부터 품질이 나빠졌는지 추적이 안 되고, 모델을 바꿀 때 어떤 프롬프트가 영향을 주는지도 모른다.

그래서 프롬프트는 보통 코드처럼 관리하는 편이 좋다. 예를 들면 다음 원칙이 도움이 된다.

  • 프롬프트를 상수나 별도 파일로 분리한다.

  • 기능별로 이름과 버전을 둔다.

  • 변경 이유를 코드 리뷰와 함께 남긴다.

  • 대표 테스트 입력 세트를 함께 관리한다.

  • 모델 변경과 프롬프트 변경을 동시에 하지 않는다.

이 다섯 가지를 지키면 디버깅이 쉬워진다. 특히 마지막 원칙이 중요하다. 모델도 바꾸고 프롬프트도 바꾸면, 품질 변화의 원인이 무엇인지 분간하기 어렵다.

프롬프트를 문장으로만 보면 감으로 수정하게 된다. 하지만 프롬프트를 서비스 로직으로 보면, 자연스럽게 버전 관리와 실험 설계를 하게 된다.

흔한 실패 패턴을 미리 알아 두자

프롬프트 설계는 생각보다 단순한 실수에서 자주 무너진다. 아래 패턴은 정말 자주 보인다.

  • 한 번의 요청에 너무 많은 일을 시킨다.

  • 출력 형식을 지정하지 않고 알아서 잘하길 기대한다.

  • 예시는 없는데 규칙만 길게 적는다.

  • 모를 때의 행동 규칙이 없다.

  • 프롬프트가 모호한데 temperature만 계속 만진다.

  • 서비스 정책과 사용자 입력이 뒤섞여 있다.

예를 들어 "요약하고, 중요도를 매기고, 액션 아이템도 뽑고, 표로 정리해 줘" 같은 요청은 데모에서는 그럴듯해 보여도 운영에서는 흔들리기 쉽다. 한 번에 너무 많은 목표가 들어가면 어느 하나도 안정적으로 통제하기 어렵다.

처음에는 기능을 나누는 편이 낫다. 먼저 요약만 잘되게 만들고, 그 다음 중요도 분류를 붙이고, 필요하면 다음 장에서 다룰 구조화 출력으로 넘어가면 된다.

프롬프트 디버깅은 문장 미학이 아니라 실험이다

프롬프트가 원하는 대로 동작하지 않을 때, 감으로 문장을 이리저리 바꾸기 시작하면 끝이 없다. 실무에서는 프롬프트 디버깅도 실험처럼 접근하는 편이 좋다.

예를 들면 이런 순서가 효과적이다.

먼저 실패 사례를 몇 개 모은다. 그런 다음 무엇이 문제인지 유형을 나눈다. 너무 길게 답하는지, 금지된 형식을 섞는지, 어조가 흔들리는지, 사실을 추측하는지 식으로 말이다. 그 뒤에 프롬프트를 한 번에 하나씩만 바꾼다. 마지막으로 대표 입력 세트에 다시 돌려 본다.

이 과정은 지루해 보여도 아주 중요하다. LLM 기능은 정답 여부만 보는 전통적인 API와 달리, 품질이 조금씩 흔들릴 수 있다. 그래서 "느낌상 좀 나아진 것 같다"보다 "이 입력 세트에서 실패율이 줄었다"가 더 믿을 만하다.

프롬프트는 감각의 영역처럼 보이지만, 운영 단계로 가면 점점 실험과 비교의 영역이 된다.

사람이 읽기 쉬운 프롬프트가 결국 유지보수도 쉽다

실무에서 좋은 프롬프트는 모델뿐 아니라 사람에게도 읽히기 쉬워야 한다. 규칙을 번호로 나누고, 금지 사항을 분리하고, 출력 형식을 명시해 두면 팀원이 읽고 이해하기 좋다. 나중에 왜 이런 규칙을 넣었는지 설명하기도 쉽다.

반대로 온갖 지시가 한 문단에 뒤섞여 있으면, 모델보다 사람이 먼저 포기한다. 유지보수하기 어려운 프롬프트는 결국 품질도 흔들린다. 수정할 때마다 무엇을 건드렸는지 알기 어렵기 때문이다.

다시 말해, 프롬프트는 AI를 위한 시가 아니라 팀이 함께 관리하는 제품 사양이다.

정리

이 장의 핵심은 분명하다. 프롬프트는 한 줄 지시문이 아니다. 서비스가 어떤 역할로 답하고, 어떤 제약을 지키며, 어떤 형식으로 결과를 내야 하는지를 결정하는 핵심 로직이다.

시스템 프롬프트는 서비스의 기본 정책을 정한다. 예시는 원하는 답의 패턴을 빠르게 학습시킨다. 제약 조건은 출력의 흔들림을 줄이고 후처리를 쉽게 만든다. temperature와 출력 길이 제한 같은 파라미터는 응답의 변동성과 크기를 제어한다. 그리고 이 모든 것은 감으로 쓰는 문장이 아니라, 버전 관리하고 실험하며 다듬어야 할 제품 설계다.

다음 장에서는 여기서 한 걸음 더 나아가, 모델의 출력을 사람이 읽는 문장 수준이 아니라 코드가 바로 처리할 수 있는 구조화된 형태로 다루는 방법을 살펴본다. 프롬프트가 서비스 로직이라면, 구조화 출력과 툴 호출은 그 로직을 더 자동화 가능한 시스템으로 바꾸는 단계다.

책 목록

실전 LLM 서비스 개발: ChatGPT·Claude·Gemini API로 만드는 AI 웹 서비스의 설계부터 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기