티스토리 뷰
LangChain PromptTemplate 제대로 이해하기 — 프롬프트를 문자열이 아니라 “코드”로 다루는 순간
octo54 2026. 3. 19. 11:08LangChain PromptTemplate 제대로 이해하기 — 프롬프트를 문자열이 아니라 “코드”로 다루는 순간
지난 글에서는 LangChain을 설치하고,
invoke()로 모델을 한 번 호출해보고,
ChatPromptTemplate이 대충 어떤 느낌인지 맛만 봤습니다.
근데 여기서 한 번쯤 이런 생각이 들어요.
“아니… 그냥 f-string 써도 되는 거 아닌가?”
“굳이 템플릿까지 써야 하나?”
“이게 왜 그렇게 중요하지?”
저도 처음엔 진짜 그렇게 생각했어요.
오히려 PromptTemplate이 좀 과해 보였어요.
질문 문자열 만들면 끝 아닌가 싶었죠.
근데 조금만 프로젝트가 커지면 바로 티가 납니다.
프롬프트가 여기저기 흩어지고,
입력값이 꼬이고,
시스템 지시와 사용자 요청이 섞이고,
출력 품질이 들쭉날쭉해져요.
그때 느끼게 됩니다.
아… 프롬프트도 결국 코드처럼 관리해야 하는 대상이구나.
LangChain의 ChatPromptTemplate은 바로 그걸 위한 도구입니다. 공식 레퍼런스는 이를 chat model용 유연한 템플릿 프롬프트를 만드는 도구로 설명하고 있고, 메시지 단위로 system/human/ai 역할을 포함한 프롬프트를 구성할 수 있게 해줍니다. 또한 LangChain 메시지 문서는 system message는 모델의 동작 방식을 지시하고, human message는 사용자 입력을 나타낸다고 설명해요. (LangChain Reference)
오늘 글에서 얻어갈 것
이번 글은 “PromptTemplate 문법 설명”에서 끝내지 않겠습니다.
그건 솔직히 재미도 없고, 오래 기억에도 안 남아요.
오늘은 이걸 잡겠습니다.
- 왜 f-string만으로는 한계가 생기는지
- ChatPromptTemplate이 왜 실무에서 중요한지
- 입력값을 어떻게 분리해야 하는지
- system / human 역할을 어떻게 나누는지
- 재사용 가능한 프롬프트 구조를 어떻게 만드는지
오늘 이 감각만 잡혀도,
뒤에 나올 체인, 구조화 출력, RAG, Agent가 훨씬 덜 어렵게 느껴질 겁니다.
1. 그냥 문자열로 만들면 뭐가 문제일까
처음엔 보통 이렇게 시작하죠.
topic = "LangChain"
level = "주니어 개발자"
prompt = f"{topic}을 {level} 눈높이로 설명해줘."
response = model.invoke(prompt)
print(response.content)
이 코드, 전혀 문제 없습니다.
오히려 아주 작은 실험에는 빠르고 좋아요.
근데 여기서 요구사항이 조금만 늘어나보겠습니다.
- 친절한 멘토처럼 답해줘
- 예시는 꼭 하나 포함해줘
- 너무 장황하지 않게 5문장 이하로 답해줘
- 실무에서 어디에 쓰는지도 설명해줘
- 사용자 수준에 따라 표현을 바꿔줘
이걸 계속 f-string에 붙이다 보면 금방 이런 상태가 됩니다.
topic = "LangChain"
level = "주니어 개발자"
prompt = f"""
너는 친절한 AI 멘토다.
{topic}을 {level} 눈높이로 설명해줘.
예시는 1개 포함하고, 5문장 이하로 답하고,
실무 활용도도 말해줘.
"""
처음엔 괜찮아 보여요.
근데 프로젝트가 커질수록 문제는 명확해집니다.
- 프롬프트 재사용이 어렵다
- 입력값이 많아질수록 실수하기 쉽다
- 어떤 문장이 system 역할인지, user 요청인지 구분이 안 간다
- 테스트가 어렵다
- 나중에 여러 화면/여러 API에서 같은 프롬프트를 재사용하기 힘들다
이게 바로 PromptTemplate이 필요한 이유예요.
2. PromptTemplate은 “문자열 도구”가 아니라 “입력 구조 도구”다
이걸 꼭 이렇게 이해했으면 좋겠습니다.
많은 사람이 PromptTemplate을
“프롬프트 문자열 편하게 만드는 유틸” 정도로 생각해요.
근데 실제로는 그보다 더 중요합니다.
PromptTemplate은
프롬프트를 하드코딩된 텍스트에서, 입력값을 받는 구조체처럼 바꾸는 도구에 더 가깝습니다.
LangChain의 prompts 레퍼런스는 prompt를 템플릿으로 정의하고, 변수 값을 주입해 재사용 가능하게 만드는 구조를 제공한다고 설명합니다. ChatPromptTemplate은 특히 chat model용으로 여러 메시지를 기반으로 프롬프트를 구성하게 해줍니다. (LangChain Reference)
쉽게 말하면 이겁니다.
예전엔:
- 문자열 하나를 직접 만든다
이제는:
- 프롬프트 틀을 정의하고
- 입력 변수를 분리하고
- 메시지 역할도 나누고
- 실행 시점에 값을 넣는다
이렇게 사고방식이 바뀝니다.
이게 은근히 크더라고요.
프롬프트가 “그때그때 적는 문장”이 아니라
“설계된 입력 포맷”처럼 보이기 시작하니까요.
3. 가장 기본적인 ChatPromptTemplate 예제
바로 코드부터 보겠습니다.
import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
def main() -> None:
if not os.environ.get("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")
model = init_chat_model("gpt-5-nano", model_provider="openai")
prompt = ChatPromptTemplate.from_messages(
[
("system", "너는 {target}를 가르치는 친절한 멘토다."),
("human", "{topic}을 쉽게 설명하고 예시 1개를 들어줘."),
]
)
chain = prompt | model
response = chain.invoke(
{
"target": "주니어 개발자",
"topic": "LangChain",
}
)
print(response.content)
if __name__ == "__main__":
main()
ChatPromptTemplate.from_messages()는 메시지들의 리스트로 chat prompt를 만들도록 설계되어 있고, 공식 예시도 ("system", "..."), ("human", "...") 같은 형태를 보여줍니다. 메시지 문서는 system message가 모델 동작 지시, human message가 사용자 입력을 나타낸다고 설명합니다. (LangChain Reference)
이 코드에서 중요한 건 3가지입니다.
첫째, 프롬프트가 메시지 단위로 나뉜다
이제 “전체 문자열”이 아니라
시스템 지시와 사용자 요청이 역할별로 분리됩니다.
둘째, 변수 자리가 명확하다
{target}, {topic}처럼
어떤 값이 나중에 들어갈지 명확히 보입니다.
셋째, 실행 시점에 값을 넣는다
체인 실행할 때 딕셔너리로 값을 넘겨주면 됩니다.
이렇게만 바뀌어도 프롬프트가 훨씬 덜 지저분해져요.
4. system 메시지와 human 메시지는 왜 나누는가
처음엔 이것도 조금 번거롭게 느껴질 수 있습니다.
“그냥 한 문장 안에 다 쓰면 안 되나?”
물론 됩니다.
근데 나누는 게 훨씬 좋습니다.
LangChain 메시지 문서에 따르면 system message는 모델이 어떻게 행동해야 하는지와 컨텍스트를 제공하고, human message는 실제 사용자 입력을 나타냅니다. 즉 역할이 다릅니다. (LangChain Docs)
실무적으로 보면 이렇게 구분하면 좋아요.
system 메시지에 넣을 것
- 모델의 역할
- 말투/톤
- 응답 규칙
- 안전장치
- 반드시 지켜야 할 형식
예:
- 너는 백엔드 개발 멘토다
- 설명은 쉽고 실무적으로 해라
- 허위 사실은 만들지 마라
- 답변은 5문장 이하로 하라
human 메시지에 넣을 것
- 사용자 질문
- 사용자가 선택한 옵션
- 검색된 문서 내용
- 화면에서 넘겨준 실제 입력값
예:
- LangChain이 무엇인지 설명해줘
- 이 문서를 요약해줘
- 아래 요구사항에 맞는 API 설계안을 작성해줘
이렇게 나누면 프롬프트의 정책과 실제 입력이 분리돼서 관리가 쉬워집니다.
5. 입력 변수를 어떻게 나누면 좋을까
이건 진짜 실무에서 차이가 납니다.
처음엔 그냥 되는 대로 변수 넣기 쉽거든요.
예를 들어 이런 식은 별로 좋지 않습니다.
prompt = ChatPromptTemplate.from_messages(
[
("system", "{all_in_one_prompt}")
]
)
왜 별로냐면, 결국 모든 게 다시 문자열 하나에 몰리기 때문이에요.
그럼 PromptTemplate의 장점이 많이 사라집니다.
대신 이런 식으로 나누는 게 좋습니다.
- 역할 관련 변수
- 사용자 입력 변수
- 출력 제약 변수
- 컨텍스트 변수
예제를 보죠.
import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
def main() -> None:
if not os.environ.get("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")
model = init_chat_model("gpt-5-nano", model_provider="openai")
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"너는 {audience}를 돕는 {role}다. "
"답변은 {max_sentences}문장 이하로 하고, "
"톤은 {tone}하게 유지해라."
),
(
"human",
"{topic}을 설명해줘. "
"실무 예시는 {example_count}개 포함해줘."
),
]
)
chain = prompt | model
response = chain.invoke(
{
"audience": "주니어 백엔드 개발자",
"role": "친절한 LangChain 멘토",
"max_sentences": 5,
"tone": "쉽고 담백",
"topic": "PromptTemplate",
"example_count": 1,
}
)
print(response.content)
if __name__ == "__main__":
main()
이 코드가 좋은 이유는
“어떤 요소를 바꾸고 싶은지”가 명확하기 때문입니다.
- 독자 대상 바꾸기
- 말투 바꾸기
- 문장 수 바꾸기
- 주제 바꾸기
- 예시 수 바꾸기
즉, 프롬프트가 튜닝 가능한 구성요소가 됩니다.
6. 재사용 가능한 프롬프트를 만든다는 건 무슨 뜻일까
이 부분이 은근 중요해요.
처음에는 “한 번 실행되면 됐지” 싶지만,
서비스 코드에서는 같은 스타일의 프롬프트를 여러 곳에서 쓰게 됩니다.
예를 들어 블로그 도우미 서비스를 만든다고 해볼게요.
- 글 주제 설명
- 소제목 생성
- 예제 코드 설명
- 요약문 작성
이 모든 작업이 “주니어 개발자 대상, 친절한 멘토 톤”이라는 공통 정책을 가질 수 있어요.
그럼 base prompt를 하나 만들어두고, human 메시지만 바꾸는 구조가 훨씬 낫습니다.
예:
from langchain_core.prompts import ChatPromptTemplate
BASE_PROMPT = ChatPromptTemplate.from_messages(
[
(
"system",
"너는 주니어 개발자를 돕는 친절한 기술 멘토다. "
"설명은 쉽고, 뜬구름 잡지 말고, 실무적으로 답해라."
),
("human", "{user_input}"),
]
)
이제 어디서든 user_input만 바꿔서 재사용할 수 있죠.
이런 구조가 좋은 이유는 명확합니다.
- 시스템 정책이 일관된다
- 여러 기능에서 말투가 통일된다
- 유지보수가 쉽다
- 나중에 A/B 테스트도 쉬워진다
진짜 서비스 코드에 가까워지는 거예요.
7. PromptTemplate을 쓸 때 자주 하는 실수
이건 좀 현실적으로 적어둘게요.
처음엔 거의 다 한 번씩 합니다.
실수 1. 변수 이름이 중구난방이다
topic, user_input, question, input_text를 여기저기 섞어 쓰면 나중에 헷갈립니다.
하나 정하면 통일하세요.
예를 들어 사용자 질문은 계속 question으로 간다든지.
실수 2. system 메시지에 사용자 데이터까지 다 넣는다
그러면 역할/정책과 실제 입력이 섞입니다.
나중에 디버깅할 때 굉장히 피곤해져요.
실수 3. 템플릿은 썼지만 결국 큰 문자열 하나다
이건 좀 아깝습니다.
변수는 분리하되, 의미 있는 단위로 쪼개는 게 좋습니다.
실수 4. 응답 형식을 말로만 요청한다
예를 들어 “JSON으로 줘”만 적고 끝내면 품질이 들쭉날쭉할 수 있어요.
이건 다음 글에서 구조화 출력 쪽으로 다룰 건데, PromptTemplate과 출력 제약은 같이 가는 편입니다.
8. 실무 감각으로 보는 좋은 프롬프트 구조
개인적으로는 아래 구조를 추천합니다.
system
- 역할
- 톤
- 응답 규칙
- 금지사항
human
- 실제 요청
- 문서 내용
- 질문
- 옵션 값
예를 들면 이런 식입니다.
import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
def main() -> None:
if not os.environ.get("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")
model = init_chat_model("gpt-5-nano", model_provider="openai")
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"너는 주니어 개발자를 위한 기술 블로그 멘토다. "
"설명은 쉽게 하고, 과장하지 말고, "
"실행 가능한 방향으로 답변해라."
),
(
"human",
"주제: {topic}\n"
"독자 수준: {reader_level}\n"
"요청: {request}\n"
"추가 조건: {constraints}"
),
]
)
chain = prompt | model
response = chain.invoke(
{
"topic": "LangChain PromptTemplate",
"reader_level": "Python 기초는 아는 주니어 개발자",
"request": "이 개념을 이해하기 쉽게 설명하고, 실무 예시를 1개 들어줘.",
"constraints": "답변은 6문장 이내, 너무 추상적으로 쓰지 말 것.",
}
)
print(response.content)
if __name__ == "__main__":
main()
이 구조가 좋은 이유는
백엔드 API로 넘길 때도 자연스럽기 때문입니다.
예를 들어 프론트에서 보내는 JSON이 이렇게 생겼다고 해보죠.
{
"topic": "LangChain PromptTemplate",
"reader_level": "Python 기초는 아는 주니어 개발자",
"request": "이 개념을 쉽게 설명하고 실무 예시를 1개 들어줘.",
"constraints": "답변은 6문장 이내"
}
그러면 거의 그대로 PromptTemplate 변수로 넣으면 됩니다.
이게 바로 프롬프트를 “문자열”이 아니라 “입력 구조”로 봐야 하는 이유예요.
9. 이번 글의 전체 실행 코드
복붙해서 실행할 수 있게 하나로 정리해둘게요.
import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
def build_prompt() -> ChatPromptTemplate:
return ChatPromptTemplate.from_messages(
[
(
"system",
"너는 {audience}를 돕는 {role}이다. "
"답변은 {tone}하게 작성하고, "
"{max_sentences}문장 이하로 설명해라."
),
(
"human",
"주제: {topic}\n"
"요청: {request}\n"
"예시 개수: {example_count}"
),
]
)
def main() -> None:
if not os.environ.get("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")
model = init_chat_model("gpt-5-nano", model_provider="openai")
prompt = build_prompt()
chain = prompt | model
response = chain.invoke(
{
"audience": "주니어 개발자",
"role": "친절한 LangChain 멘토",
"tone": "쉽고 담백",
"max_sentences": 5,
"topic": "ChatPromptTemplate",
"request": "왜 필요한지 설명하고, f-string과 차이를 알려줘.",
"example_count": 1,
}
)
print(response.content)
if __name__ == "__main__":
main()
10. 오늘 글에서 꼭 가져가야 할 한 문장
오늘 내용을 한 줄로 줄이면 이겁니다.
PromptTemplate은 프롬프트를 예쁘게 쓰는 도구가 아니라, 프롬프트를 유지보수 가능한 입력 구조로 바꾸는 도구다.
이 관점이 잡히면
다음 단계가 훨씬 쉬워집니다.
왜냐면 그 다음부터는 자연스럽게 이런 질문으로 넘어가거든요.
- 그럼 출력도 구조화할 수 있나?
- JSON으로 안정적으로 받으려면 어떻게 하지?
- 프롬프트에 문서 내용을 넣으려면 어떻게 설계하지?
- 툴 결과도 이런 식으로 넣는 건가?
맞습니다.
이제 그 단계로 갈 차례예요.
마무리
오늘 글은 생각보다 중요한 글입니다.
RAG나 Agent보다 덜 화려해서 그렇지, 여기서 감각이 안 잡히면 뒤에 가서 계속 흔들립니다.
저도 처음엔 PromptTemplate을 너무 가볍게 봤어요.
근데 나중에 프로젝트가 커지니까
결국 잘 만든 프롬프트 구조가 유지보수를 살리더라고요.
좀 웃기지만,
생성형 AI 서비스도 결국은 개발입니다.
그리고 개발에서는 언제나
“작동하느냐”보다 “관리 가능하냐”가 더 오래 갑니다.
PromptTemplate은 딱 그 지점에서 빛납니다.
다음 글 예고
다음 글에서는
“LangChain 출력 제어하기 — Structured Output과 JSON 응답을 안정적으로 다루는 방법”
으로 이어가겠습니다.
사실 여기서부터 진짜 재밌어집니다.
이제 답변을 그냥 텍스트로 받는 게 아니라,
백엔드와 프론트엔드가 다루기 좋은 형태 있는 데이터로 받는 쪽으로 들어갈 거예요.
출처
- LangChain ChatPromptTemplate 레퍼런스: chat model용 프롬프트 템플릿 생성, from_messages() 기반 메시지 템플릿 구성 설명. (LangChain Reference)
- LangChain prompts 레퍼런스: prompt 템플릿 계층과 관련 클래스 설명. (LangChain Reference)
- LangChain messages 공식 문서: system/human/AI/tool message 역할과 사용 목적 설명. (LangChain Docs)
- LangSmith prompt template format 가이드: f-string 스타일 변수 치환 문법 설명. (LangChain Docs)
LangChain, PromptTemplate, ChatPromptTemplate, LangChain Python, Structured Prompt, 생성형AI, LLM개발, AI Agent, 주니어개발자, Python튜토리얼
'study > langchain' 카테고리의 다른 글
| LangChain 대화형 챗봇 만들기 — 메시지 히스토리와 문맥 유지를 처음부터 이해하는 방법 (0) | 2026.03.26 |
|---|---|
| LangChain 체인 연결하기 — Prompt | Model | Parser를 부품처럼 조립하는 법 (0) | 2026.03.23 |
| LangChain 출력 제어하기 — Structured Output으로 JSON 응답을 안정적으로 받는 방법 (0) | 2026.03.20 |
| Python으로 LangChain 첫 실행하기 — 설치부터 첫 번째 invoke, 그리고 PromptTemplate까지 (0) | 2026.03.18 |
| LangChain, 왜 다들 이야기할까? 주니어 개발자가 처음 LangChain을 공부해야 하는 진짜 이유 (0) | 2026.03.17 |
- Total
- Today
- Yesterday
- PostgreSQL
- REACT
- JAX
- DevOps
- 개발블로그
- nextJS
- SEO최적화
- 웹개발
- Next.js
- JWT
- fastapi
- 쿠버네티스
- Redis
- 압박면접
- flax
- LangChain
- kotlin
- Docker
- ai철학
- llm
- Python
- 백엔드개발
- Prisma
- node.js
- rag
- CI/CD
- Express
- NestJS
- seo 최적화 10개
- 딥러닝
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |

