Skip to main content
Summary

프롬프트는 모델과 앱 사이의 계약이다

이전 장에서는 ChatOpenAI로 모델을 호출하고, SystemMessage, HumanMessage, AIMessage가 어떤 역할을 하는지 살펴보았다. 이제 한 단계 더 나아가야 한다. 실제 앱에서는 매번 문자열을 직접 이어 붙여 모델에 보내면 금방 한계가 온다.

예를 들어 다음과 같은 코드는 처음에는 단순해 보인다.

question = "환불 정책을 알려 주세요"

prompt = "너는 고객 상담원이다. 다음 질문에 친절하게 답해라: " + question
response = model.invoke(prompt)

문제는 요구사항이 늘어날 때 시작된다. 답변 톤을 바꿔야 하고, 참고 문서를 넣어야 하고, 출력 형식을 JSON으로 고정해야 하고, 금지해야 할 표현도 추가해야 한다. 문자열 하나에 모든 지시를 섞으면 수정하기 어렵고, 실수하기 쉽고, 테스트하기도 어렵다.

랭체인의 프롬프트 템플릿은 이 문제를 해결한다. 프롬프트를 재사용 가능한 형식으로 만들고, 바뀌는 값만 변수로 주입하게 해 준다. 좋은 랭체인 앱은 좋은 프롬프트 구조에서 시작된다.

프롬프트와 메시지 설계의 기본기AI 생성 이미지

PromptTemplate으로 문자열 프롬프트 만들기

PromptTemplate은 하나의 문자열 프롬프트를 만들 때 사용한다. 모델에 전달할 문장을 미리 정의하고, 실행 시점에 필요한 값만 채운다.

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template("""
너는 초보 개발자를 돕는 Python 튜터다.

주제: {topic}
학습자 수준: {level}

요구사항:
- 핵심 개념을 먼저 설명한다.
- 짧은 예제 코드를 포함한다.
- 마지막에 연습 문제 1개를 제시한다.
""")

formatted = prompt.invoke({
    "topic": "Python 리스트 컴프리헨션",
    "level": "입문자"
})

print(formatted.text)

여기서 {topic}, {level}은 변수다. 매번 전체 프롬프트를 다시 만들 필요 없이 입력값만 바꿀 수 있다. LangChain의 프롬프트 템플릿은 이런 변수 자리를 인식해 실행 시점에 값을 채운다.1

PromptTemplate은 다음 상황에 적합하다.

  • 단일 문장 또는 긴 문자열을 모델에 전달할 때

  • 채팅 역할 구분이 중요하지 않을 때

  • 요약, 변환, 분류처럼 입력과 출력이 단순할 때

예를 들어 문장을 특정 형식으로 바꾸는 작업은 문자열 템플릿만으로 충분하다.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

prompt = PromptTemplate.from_template("""
다음 문장을 비즈니스 이메일 문체로 바꿔라.

원문:
{sentence}

조건:
- 과장하지 않는다.
- 3문장 이내로 작성한다.
""")

message = prompt.invoke({
    "sentence": "자료 늦게 보내서 미안해요. 오늘 안에 다시 정리해서 드릴게요."
})

response = model.invoke(message)
print(response.content)

실습 단계에서는 이 정도로도 충분하다. 하지만 챗봇이나 업무용 어시스턴트처럼 역할이 중요한 앱에서는 메시지 구조를 분리하는 편이 낫다.

ChatPromptTemplate으로 역할을 분리하기

채팅 모델은 단순 문자열보다 메시지 목록을 더 자연스럽게 다룬다. 각 메시지는 역할을 가진다.

  • system: 모델의 역할, 규칙, 답변 기준

  • human: 사용자의 질문이나 요청

  • ai: 이전 모델 응답 또는 예시 응답

ChatPromptTemplate은 이런 메시지 구조를 템플릿으로 만든다.

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

prompt = ChatPromptTemplate.from_messages([
    ("system", """
너는 Python을 가르치는 친절한 튜터다.
답변은 초보자도 이해할 수 있게 작성한다.
모르는 내용은 추측하지 말고 모른다고 말한다.
"""),
    ("human", "{question}")
])

messages = prompt.invoke({
    "question": "딕셔너리와 리스트는 언제 다르게 쓰나요?"
})

response = model.invoke(messages)
print(response.content)

이 구조의 장점은 명확하다. 모델의 기본 역할은 system에 둔다. 사용자가 바꾸는 질문은 human에 둔다. 이렇게 나누면 사용자의 입력이 지시문과 섞이지 않는다. 프롬프트가 길어져도 어느 부분을 수정해야 하는지 쉽게 찾을 수 있다.

앞으로 LCEL을 배우면 이 코드는 더 간결해진다.

chain = prompt | model
response = chain.invoke({
    "question": "딕셔너리와 리스트는 언제 다르게 쓰나요?"
})
print(response.content)

여기서는 | 연산자의 자세한 의미를 몰라도 된다. 지금은 "프롬프트를 만들고, 그 결과를 모델에 넘긴다"는 흐름만 기억하면 된다.

프롬프트에 담아야 할 네 가지 요소

프롬프트를 잘 쓴다는 것은 긴 지시문을 쓰는 것이 아니다. 필요한 정보를 적절한 위치에 나누어 담는 것이다. 실전에서는 다음 네 가지를 분리해 생각하면 좋다.

첫째, 역할 지시다. 모델이 어떤 관점으로 답해야 하는지 정한다.

너는 SaaS 제품의 고객 지원 담당자다.

둘째, 출력 형식이다. 답변을 문단으로 할지, 목록으로 할지, JSON으로 할지 정한다.

답변은 다음 형식으로 작성한다.
1. 요약
2. 고객에게 보낼 답변
3. 내부 확인이 필요한 항목

셋째, 참고 맥락이다. 모델이 답변에 사용할 자료를 넣는다. RAG를 만들 때 검색된 문서가 이 위치에 들어간다.

아래 정책 문서만 참고해서 답한다.
{context}

넷째, 금지 조건이다. 해서는 안 되는 행동을 명시한다.

정책 문서에 없는 내용은 지어내지 않는다.
가격, 환불 가능 여부를 임의로 약속하지 않는다.

이 네 가지를 하나의 ChatPromptTemplate에 담아 보자.

from langchain_core.prompts import ChatPromptTemplate

support_prompt = ChatPromptTemplate.from_messages([
    ("system", """
너는 온라인 교육 서비스의 고객 지원 담당자다.
항상 정중하고 간결하게 답한다.

규칙:
- 참고 정책에 있는 내용만 근거로 답한다.
- 확실하지 않은 내용은 "확인이 필요합니다"라고 말한다.
- 환불 가능 여부를 임의로 약속하지 않는다.
"""),
    ("human", """
고객 질문:
{question}

참고 정책:
{context}

답변 형식:
- 결론:
- 근거:
- 고객에게 보낼 답변:
""")
])

여기서 중요한 점은 context와 question을 구분하는 것이다. 사용자의 질문은 요청이고, 참고 정책은 근거다. 이 둘을 한 문자열에 아무 구분 없이 넣으면 모델이 무엇을 지시로 따라야 하고 무엇을 자료로 읽어야 하는지 혼동할 수 있다.

few-shot 예시로 응답 기준 고정하기

모델은 지시문만으로도 꽤 잘 답한다. 하지만 응답 스타일이나 판단 기준을 안정적으로 맞추려면 예시를 넣는 것이 효과적이다. 이를 few-shot prompting이라고 한다. 몇 개의 입력과 이상적인 출력을 보여 주고, 새 입력도 같은 방식으로 처리하게 만드는 방법이다.

예를 들어 고객 문의를 환불, 기술지원, 계정, 기타 중 하나로 분류하는 프롬프트를 만든다고 하자.

from langchain_core.prompts import ChatPromptTemplate

classification_prompt = ChatPromptTemplate.from_messages([
    ("system", """
너는 고객 문의를 분류하는 assistant다.
분류값은 반드시 다음 중 하나만 사용한다.
- 환불
- 기술지원
- 계정
- 기타
"""),
    ("human", "결제했는데 강의를 못 들었습니다. 취소하고 싶어요."),
    ("ai", "환불"),
    ("human", "비밀번호 재설정 메일이 오지 않습니다."),
    ("ai", "계정"),
    ("human", "동영상이 계속 끊기고 재생이 안 됩니다."),
    ("ai", "기술지원"),
    ("human", "{question}")
])

messages = classification_prompt.invoke({
    "question": "수강권을 잘못 구매했는데 환불 가능한가요?"
})

예시는 길 필요가 없다. 중요한 것은 대표적인 판단 기준을 보여 주는 것이다. 예시가 너무 많으면 비용이 늘고, 실제 질문이 들어갈 공간이 줄어든다. 보통은 자주 헷갈리는 사례부터 3~5개 정도 넣어 시작하는 편이 좋다.

예시를 넣을 때는 다음 기준을 지키자.

  • 정답이 명확한 예시를 먼저 넣는다.

  • 서로 비슷해서 헷갈리는 예시를 일부 포함한다.

  • 실제 서비스에서 자주 들어오는 입력과 비슷하게 작성한다.

  • 예시의 출력 형식과 실제 출력 형식을 완전히 같게 맞춘다.

few-shot은 모델을 완벽하게 통제하는 장치가 아니다. 하지만 같은 입력 유형에 대해 더 일관된 답을 내도록 돕는 실용적인 방법이다.

부분 변수와 고정 설정 분리하기

프롬프트에는 자주 바뀌는 값과 거의 바뀌지 않는 값이 함께 들어간다. 예를 들어 사용자의 질문은 매번 바뀌지만, 답변 언어나 서비스 이름은 배포 환경에서 고정될 수 있다. 이럴 때는 일부 값을 미리 묶어 둘 수 있다.

from langchain_core.prompts import ChatPromptTemplate

base_prompt = ChatPromptTemplate.from_messages([
    ("system", """
너는 {service_name}의 고객 지원 assistant다.
답변 언어는 {language}로 한다.
"""),
    ("human", "{question}")
])

korean_support_prompt = base_prompt.partial(
    service_name="CodeClass",
    language="한국어"
)

messages = korean_support_prompt.invoke({
    "question": "수강 기간은 어디서 확인하나요?"
})

이 방식은 운영 환경에서 유용하다. 같은 앱을 여러 브랜드나 언어로 배포할 때, 공통 프롬프트는 유지하고 일부 설정만 바꿀 수 있다.

다만 모든 것을 변수로 만들 필요는 없다. 변수는 바뀌는 값에만 쓰는 편이 좋다. 너무 많은 변수를 만들면 프롬프트를 호출하는 코드가 복잡해지고, 누락된 값 때문에 오류가 나기 쉽다.

긴 프롬프트를 유지보수하는 방법

실습 코드에서는 프롬프트를 Python 파일 안에 바로 작성해도 된다. 하지만 프로젝트가 커지면 프롬프트도 코드처럼 관리해야 한다. 특히 업무용 AI 앱에서는 프롬프트 변경이 곧 제품 동작 변경이다.

추천하는 기본 구조는 다음과 같다.

my-langchain-app/
  app/
    chains/
      support_chain.py
    prompts/
      support_answer_v1.txt
      classify_ticket_v1.txt
      summarize_document_v1.txt
    tests/
      test_prompts.py

프롬프트를 별도 파일로 분리하면 다음 장점이 있다.

  • 개발자가 아닌 팀원도 내용을 검토하기 쉽다.

  • 프롬프트 변경 이력을 Git으로 추적할 수 있다.

  • 체인 코드와 문장 수정 작업을 분리할 수 있다.

  • 버전별 성능 비교가 쉬워진다.

예를 들어 support_answer_v1.txt 파일을 만들 수 있다.

너는 온라인 교육 서비스의 고객 지원 담당자다.

규칙:
- 참고 정책에 있는 내용만 사용한다.
- 확실하지 않은 내용은 확인이 필요하다고 말한다.
- 답변은 정중하게 작성한다.

고객 질문:
{question}

참고 정책:
{context}

답변 형식:
- 결론:
- 근거:
- 고객에게 보낼 답변:

Python 코드에서는 이 파일을 읽어 템플릿으로 만든다.

from pathlib import Path
from langchain_core.prompts import PromptTemplate

prompt_text = Path("app/prompts/support_answer_v1.txt").read_text(encoding="utf-8")
prompt = PromptTemplate.from_template(prompt_text)

파일로 분리할 때는 이름에 버전을 붙이는 습관이 좋다. support_answer_final.txt 같은 이름은 피하자. 실제 프로젝트에서 "final"은 거의 항상 다시 바뀐다. 대신 support_answer_v1.txt, support_answer_v2.txt처럼 명확하게 관리한다.

프롬프트를 수정할 때는 다음 항목을 함께 기록하면 좋다.

버전: support_answer_v2
변경일: 2026-07-20
변경 이유: 환불 가능 여부를 단정하는 답변 감소
변경 내용: 금지 조건에 "환불을 임의로 약속하지 않는다" 추가
확인 결과: 테스트 질문 20개 중 단정 답변 5개에서 1개로 감소

프롬프트도 실험 대상이다. 감으로 고치고 끝내면 어떤 변경이 좋아졌는지 알 수 없다. 나중에 LangSmith와 평가를 다룰 때 이 기록 습관이 큰 도움이 된다.

좋은 프롬프트를 위한 실전 점검표

마지막으로, 프롬프트를 작성한 뒤 아래 질문을 확인하자.

  • 모델의 역할이 분명한가?

  • 사용자의 입력과 시스템 지시가 분리되어 있는가?

  • 참고해야 할 맥락이 명확히 표시되어 있는가?

  • 출력 형식이 예시나 목록으로 구체화되어 있는가?

  • 하지 말아야 할 행동이 적혀 있는가?

  • 자주 헷갈리는 사례에 대한 few-shot 예시가 있는가?

  • 프롬프트 변경 이력을 남길 수 있는 구조인가?

프롬프트는 모델을 완전히 제어하지 못한다. 같은 프롬프트라도 모델 종류, 온도 설정, 입력 길이, 참고 문서 품질에 따라 결과가 달라진다. 그래서 프롬프트 설계의 목표는 "항상 완벽한 답변"이 아니라 "일관되게 개선 가능한 입력 구조"를 만드는 것이다.

이번 장에서는 PromptTemplate과 ChatPromptTemplate으로 재사용 가능한 입력을 만드는 방법을 배웠다. 다음 장에서는 이 프롬프트를 모델, 출력 파서, 검색기와 연결하는 방식인 LCEL을 다룬다. 프롬프트가 앱의 입력 설계라면, LCEL은 그 입력이 흘러가는 실행 경로를 설계하는 도구다.

참고

1LangChain Prompt Templates: Dynamic Prompts with Variables and Chat History

책 목록

랭체인 활용법: LLM 애플리케이션 개발부터 RAG·에이전트·운영까지

전체 목차 보기

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

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

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

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

참여하기