Python & AI Tutorials Logo
LangChain & LangGraph

4. 템플릿을 사용한 재사용 가능한 프롬프트 설계

3장에서는 프롬프트가 Python 코드에 직접 포함된 스트리밍 채팅 CLI를 구축했습니다. 이는 빠른 프로토타입에는 적합하지만, AI 애플리케이션이 성장함에 따라 하드코딩된 프롬프트는 유지보수의 악몽이 됩니다. 여러 파일에서 동일한 프롬프트 로직을 업데이트하거나, 코드를 재배포하지 않고 다양한 프롬프트 변형을 A/B 테스트하려고 한다고 상상해보세요.

이 장에서는 LangChain의 템플릿 시스템을 사용하여 재사용 가능하고 유지보수 가능한 프롬프트를 설계하는 방법을 배웁니다. 프롬프트 로직을 애플리케이션 코드에서 분리하고, 더 나은 LLM 제어를 위해 역할 기반 메시징을 활용하며, 팀 협업을 위해 프롬프트를 YAML 파일로 외부화하고, 실행 전에 템플릿을 검증하여 오류를 조기에 발견하는 방법을 배우게 됩니다.

이 장에서 다루는 내용(그리고 다루지 않는 내용):

이 장에서는 템플릿과 프롬프트를 수동으로 작업합니다. 템플릿을 메시지로 명시적으로 렌더링한 다음 llm.invoke()를 사용하여 해당 메시지를 LLM에 전송합니다. 이러한 실습 접근 방식은 템플릿이 무엇을 하고 어떻게 작동하는지 정확히 이해하는 데 도움이 됩니다.

6장에서는 | 연산자를 사용하여 템플릿과 LLM을 파이프라인으로 구성할 수 있는 LCEL(LangChain Expression Language)을 배우게 됩니다. 지금은 오케스트레이션 레이어 없이 템플릿 기본 사항에 집중하겠습니다.

이 장을 마치면 간단한 챗봇에서 복잡한 멀티 에이전트 워크플로우까지 확장 가능한 강력한 프롬프트 관리 시스템을 갖추게 됩니다.

4.1) 관심사의 분리: 코드와 프롬프트 분리하기

프롬프트를 코드에서 분리하는 이유는?

프롬프트를 애플리케이션 로직에 직접 하드코딩하면 여러 문제를 야기하는 강한 결합이 생성됩니다:

유지보수 부담: 프롬프트를 변경하려면 Python 코드를 수정하고, 테스트를 실행하고, 재배포해야 합니다. 일반적으로 프롬프트 수정은 코드 수정보다 훨씬 더 빈번하게 일어나는데, 단순한 텍스트 편집을 위해 매번 코드 수정-테스트-재배포 사이클을 거치는 것은 매우 비효율적입니다.

버전 관리 문제: 코드와 프롬프트가 혼재되어 있으면 버전 관리가 어려워집니다. 병합 충돌이 발생할 가능성이 높아지고, 충돌이 발생할 때마다 수동으로 해결하고 리팩토링해야 합니다.

협업 마찰: 비기술 팀원(제품 관리자, 도메인 전문가)은 프롬프트가 .py 파일에 있을 때 프롬프트를 직접 수정하기 어렵고 개발자의 도움을 받아야 합니다. 이러한 의존성은 프롬프트 개선 사이클을 상당히 느리게 만듭니다.

테스트 복잡성: 다른 프롬프트 변형을 테스트하려면 코드를 복사하고, 문자열을 수정하고, 여러 브랜치를 관리해야 합니다—실험을 느리고 오류가 발생하기 쉽게 만듭니다.

프롬프트를 전통적인 애플리케이션의 SQL 쿼리처럼 생각하세요. Python 코드 전체에 SQL 문자열을 하드코딩하지 않을 것입니다—ORM을 사용하거나 최소한 쿼리를 중앙 집중화할 것입니다. 프롬프트도 동일한 아키텍처 규율을 받을 자격이 있습니다.

LangChain의 템플릿 시스템

LangChain은 프롬프트의 고정된 구조와 변경되는 데이터를 분리하기 위해 PromptTemplateChatPromptTemplate 클래스를 제공합니다. {placeholders}를 사용해 프롬프트를 한 번 작성하면, 매번 다른 값을 넣어 사용할 수 있습니다—더 이상 f-string이나 문자열 연결로 프롬프트를 재구성할 필요가 없습니다.

템플릿 구문과 사용법

플레이스홀더 구문

템플릿은 {variable_name}을 플레이스홀더로 사용합니다. 런타임에 일치하는 키가 있는 딕셔너리를 제공합니다:

python
from langchain_core.prompts import PromptTemplate
 
# 플레이스홀더가 있는 템플릿 정의
template = PromptTemplate.from_template(
    "Translate {content} from {source_lang} to {target_lang}"
)
 
# 딕셔너리로 플레이스홀더 채우기
result = template.invoke({
    "content": "Hello world",
    "source_lang": "English", 
    "target_lang": "Korean"
})
 
print(result.text)

출력:

Translate Hello world from English to Korean

주요 규칙:

  • 플레이스홀더 이름은 딕셔너리 키와 정확히 일치해야 합니다
  • 모든 플레이스홀더가 제공되어야 합니다 (누락된 키는 KeyError를 발생시킵니다)
  • 추가 딕셔너리 키는 무시됩니다
  • invoke()를 사용하여 값으로 템플릿을 렌더링합니다

PromptTemplate vs ChatPromptTemplate

PromptTemplate: 일반 문자열 반환 (StringPromptValue로 래핑됨)

  • 용도: 단순 텍스트 완성 또는 레거시 모델용
  • 출력: "Summarize: {content}"와 같은 단일 문자열

ChatPromptTemplate: 역할이 있는 구조화된 메시지 반환 (ChatPromptValue로 래핑됨)

  • 용도: 현대적인 채팅 모델(GPT-4, Claude, Gemini)용
  • 출력: 역할로 구분된 메시지(system/user/assistant)
  • 선호 선택: 사용자 입력과 별도로 시스템 지침을 유지하는 데 더 좋습니다

언제 어떤 것을 사용할까?

  • 채팅 모델에는 기본적으로 ChatPromptTemplate을 사용하세요—더 명확하고 유지보수하기 쉽습니다
  • 역할 구분이 필요 없거나 단순 완성만 필요한 경우에만 PromptTemplate을 사용하세요
python
# PromptTemplate - 단일 문자열 출력
from langchain_core.prompts import PromptTemplate
 
template1 = PromptTemplate.from_template("Summarize: {content}")
result1 = template1.invoke({"content": "LangChain is a framework..."})  
print(result1)

출력:

text='Summarize: LangChain is a framework...'
python
# ChatPromptTemplate - 역할 기반 메시지
from langchain_core.prompts import ChatPromptTemplate
 
template2 = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant"),
    ("user", "{question}")
])
result2 = template2.invoke({"question": "What is LangChain?"})
print(result2)

출력:

messages=[SystemMessage(content='You are a helpful assistant'), HumanMessage(content='What is LangChain?')]

템플릿은 한 번만 정의하면 됩니다. 템플릿 정의를 수정하지 않고도 다른 값들로 재사용할 수 있습니다. PromptTemplate.invoke()ChatPromptTemplate.invoke() 모두 LLM에 바로 전송할 수 있는 프롬프트 값을 반환합니다.

문자열 포매팅에서 템플릿으로

하드코딩된 프롬프트를 템플릿을 사용하도록 리팩토링해봅시다. 다음은 3장의 "이전" 버전입니다:

python
# 하드코딩된 접근 방식 (3장 스타일)
from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
user_input = "Explain quantum computing"
# 프롬프트 로직이 코드와 혼합됨
prompt = f"You are a helpful assistant. Answer this question: {user_input}"
 
response = llm.invoke(prompt)
print(response.content)

이제 템플릿을 사용하여—이 장 전체에서 연습할 단계별 접근 방식을 사용합니다:

python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
 
# 템플릿을 별도로 정의
template = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    ("user", "{user_input}")
])
 
# 애플리케이션 로직 - 단계별 실행
llm = ChatOpenAI(model="gpt-4o-mini")
 
user_input = "Explain quantum computing"
 
# 1단계: 템플릿을 메시지로 렌더링
messages = template.invoke({"user_input": user_input})
 
# 2단계: 메시지를 LLM에 전송
response = llm.invoke(messages)
print(response.content)

무엇이 바뀌었나요?

  1. 템플릿 정의: 프롬프트 구조가 실행 로직과 별도로 template에 한 번 정의됩니다.
  2. 플레이스홀더 구문: {user_input}은 런타임에 채워지는 플레이스홀더입니다.
  3. 단계별 실행: 템플릿을 명시적으로 렌더링(template.invoke())한 다음 결과를 LLM에 전송(llm.invoke())합니다. 이 2단계 프로세스는 템플릿이 실제로 무엇을 하는지 이해하는 데 도움이 됩니다.
  4. 재사용성: 동일한 template을 수정 없이 모든 사용자 질문에 사용할 수 있습니다.
  5. 메시지 구조: template.invoke()는 LLM이 기대하는 적절하게 포맷된 ChatPromptValue를 반환합니다.

단계별 접근 방식을 사용하는 이유는?

이 장 전체에서 이 패턴을 반복적으로 보게 됩니다:

python
messages = template.invoke(inputs)  # 1단계: 템플릿 렌더링
response = llm.invoke(messages)     # 2단계: LLM에 전송

여기서는 학습 목적으로 의도적으로 2단계 접근 방식을 사용하고 있습니다—템플릿이 정확히 무엇을 하는지 보여줍니다(입력 데이터를 구조화된 메시지로 변환). 6장에서는 실제 프로덕션 패턴을 배우게 됩니다: LCEL 파이프라인(template | llm)으로 이 단계들을 결합하는 방법입니다. 하지만 각 단계를 먼저 따로 이해하는 것이 탄탄한 기초를 만듭니다.

템플릿 검증

템플릿은 오류를 조기에 발견합니다. 존재하지 않는 플레이스홀더를 참조하면 LangChain은 API 호출을 하기 전에 오류를 발생시킵니다:

python
template = PromptTemplate.from_template("Summarize: {text}")
 
# 이것은 실패합니다 - 'text' 키가 누락됨
try:
    template.invoke({"content": "Some text"})  # 잘못된 키 이름
except KeyError as e:
    print(f"Template error: {e}")

출력:

Template error: "Input to PromptTemplate is missing variables {'text'}.  Expected: ['text'] Received: ['content']

이 검증은 LLM 실행 중이 아닌 템플릿 렌더링 시점에 발생합니다—시간과 API 비용을 모두 절약할 수 있습니다.

4.2) 역할 인식 프롬프트 템플릿 (System, User, Assistant)

메시지 역할 이해하기

현대의 LLM(GPT-4, GPT-5, Claude, Gemini)은 메시지 역할을 통해 대화 구조를 이해합니다. 각 메시지는 모델에게 해당 내용을 어떻게 해석할지 알려주는 특정 역할을 가집니다.

세 가지 핵심 역할:

System: AI가 어떻게 행동해야 하는지 정의합니다

  • 목적: AI의 성격, 전문성, 운영 규칙을 설정
  • 예시: "당신은 간결한 코드 예제를 작성하는 Python 전문가입니다"
  • 적용 시점: 시작할 때 한 번 설정되며, 모든 응답에 영향을 미침
  • 비유: AI의 사용 설명서

User: 사람의 입력을 나타냅니다

  • 목적: AI에게 질문하거나 요청을 함
  • 예시: "Python에서 파일을 어떻게 읽나요?"
  • 적용 시점: 사람이 메시지를 보낼 때마다
  • 비유: 당신이 묻는 질문

Assistant: AI의 이전 응답을 나타냅니다

  • 목적: 대화 히스토리를 제공
  • 예시: "open() 함수를 사용하여 파일을 읽을 수 있습니다"
  • 적용 시점: 다회차 대화가 필요할 때
  • 비유: AI가 이전에 답변한 내용에 대한 기억

System 메시지: 제어 메커니즘

System 메시지는 사용자 상호작용이 시작되기 전에 AI가 누구이고 어떻게 작동해야 하는지를 알려줍니다.

제어할 수 있는 것:

  1. 전문성: "당신은 시니어 Python 개발자입니다"
  2. 출력 형식: "항상 JSON 형식으로 응답하세요"
  3. 행동 규칙: "확실하지 않으면 '모르겠습니다'라고 말하세요"
  4. 응답 스타일: "간결하고 기술적으로 작성하세요"

왜 중요한가:

System 메시지 없음 → 일반적이고 장황한 응답

System 메시지 있음 → 일관되고 맞춤화된 행동

System 메시지의 실제 영향

동일한 질문에 대해 System 메시지가 있을 때와 없을 때를 비교해봅시다. 길이뿐만 아니라 톤, 복잡도, 교육 방식이 얼마나 극적으로 변하는지 주목하세요.

System 메시지 없음:

python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
 
template = ChatPromptTemplate.from_messages([
    ("user", "Python이 뭔가요?")
])
 
llm = ChatOpenAI(model="gpt-4o-mini")
messages = template.invoke({})
response = llm.invoke(messages)
print(response.content)

출력:

Python은 가독성과 단순성으로 알려진 고수준 인터프리터 프로그래밍 언어입니다.
Guido van Rossum이 만들었으며 1991년에 처음 출시되었습니다.
Python은 코드 가독성을 강조하여 프로그래머가 C++나 Java 같은 언어보다 적은 코드 라인으로 개념을 표현할 수 있게 합니다.
 
Python의 주요 특징은 다음과 같습니다:
...

System 메시지 있음:

python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
 
# System 메시지로 페르소나와 출력 스타일 제어
template = ChatPromptTemplate.from_messages([
    ("system", """당신은 15년 경력의 시니어 Python 강사입니다.
학생들은 프로그래밍을 한 번도 해본 적 없는 완전 초보자입니다.
 
교육 스타일:
- 일상적인 비유를 사용하세요
- 전문 용어를 피하세요
- 실생활의 실용적인 예를 보여주세요
- 격려하고 인내심을 가지세요"""),
    ("user", "Python이 뭔가요?")
])
 
llm = ChatOpenAI(model="gpt-4o-mini")
messages = template.invoke({})
response = llm.invoke(messages)
print(response.content)

출력:

좋은 질문이에요! Python을 당신의 공구함에 있는 아주 유용한 도구라고 생각해보세요.
망치나 드라이버가 집에서 물건을 만들거나 고치는 데 도움을 주는 것처럼, Python은 소프트웨어를 만들거나 컴퓨터 작업을 자동화하는 데 도움을 줍니다.
 
케이크를 굽고 싶다고 상상해보세요.
따라야 할 레시피가 필요하죠? 이 비유에서 Python은 그 레시피와 같습니다.
수학 계산을 하든, 파일을 정리하든, 게임을 실행하든 컴퓨터에게 목표를 달성하기 위해 어떤 단계를 밟아야 하는지 알려줍니다.
...

차이점:

System 메시지 없음:

  • AI가 기본 동작을 사용: 정중하고 유익하지만 일반적임
  • 응답이 백과사전식이고 형식적—광범위한 청중을 위해 최적화됨
  • 일관된 페르소나 없음: 각 응답의 톤과 스타일이 다를 수 있음
  • 제약 없음: AI가 스스로 얼마나 자세하거나 기술적일지 결정

System 메시지 있음:

  • AI가 당신의 구체적인 지시를 따름: 당신이 정의한 페르소나, 스타일, 규칙
  • 응답이 일관되고 예측 가능—모든 답변이 당신의 요구사항과 일치
  • 명확한 페르소나 유지: 당신이 할당한 역할(교사, 전문가, 어시스턴트)로 행동
  • 명시적 제약 적용: 당신이 설정한 출력 형식, 언어 수준, 행동 경계

핵심 통찰: System 메시지 없이는 AI의 기본 모드를 얻습니다. System 메시지와 함께라면 당신의 AI를 얻습니다—애플리케이션의 요구에 맞춰진. System 메시지는 AI를 범용 도구에서 당신이 원하는 대로 정확히 행동하는 전문 어시스턴트로 변환시킵니다.

User와 Assistant 역할: 대화 구축하기

단일 질문 (User만 사용):

python
template = ChatPromptTemplate.from_messages([
    ("system", "You are a Python expert."),
    ("user", "{question}")
])
 
llm = ChatOpenAI(model="gpt-4o-mini")
messages = template.invoke({"question": "How do I read a CSV?"})
response = llm.invoke(messages)

독립적인 질문에는 잘 작동합니다.

맥락이 있는 다회차 대화 (User + Assistant):

히스토리 없음:

python
template = ChatPromptTemplate.from_messages([
    ("system", "You are a Python expert."),
    ("user", "How does it work?")  # "it" = ???
])

AI는 "it"이 무엇을 가리키는지 모릅니다.

히스토리 있음:

python
template = ChatPromptTemplate.from_messages([
    ("system", "You are a Python expert."),
    ("user", "What's the pandas library?"),
    ("assistant", "Pandas is a data analysis library."),
    ("user", "How does it work?")  # 이제 "it" = pandas
])

대화 히스토리(이전 사용자 질문 + Assistant 응답)가 맥락을 제공합니다. AI는 이제 "it"이 pandas를 의미한다는 것을 이해합니다.

예제: 대화 히스토리를 사용한 대화 구축하기

이제 이전 대화를 기억하는 예제를 만들어 보겠습니다. 이 함수는 대화 히스토리를 유지하고 새로운 질문마다 AI에게 전달합니다:

python
llm = ChatOpenAI(model="gpt-4o-mini")
 
def chat_with_history(user_input: str, history: list):
    messages = [("system", "You are a Python expert.")]
    
    # 히스토리 추가
    for msg in history:
        messages.append((msg["role"], msg["content"]))
    
    # 현재 입력 추가
    messages.append(("user", user_input))
    
    # 내용에 {중괄호}가 포함될 때 오류를 방지하기 위해 mustache 형식 사용
    template = ChatPromptTemplate.from_messages(messages, template_format="mustache")
    
    formatted = template.format()
    response = llm.invoke(formatted)
    return response.content
 
# 사용법
history = []
 
# 턴 1
resp1 = chat_with_history("What's a Python dictionary?", history)
print(resp1)
 
history.append({"role": "user", "content": "What's a Python dictionary?"})
history.append({"role": "assistant", "content": resp1})
 
# 턴 2 - 맥락 사용
resp2 = chat_with_history("Show an example.", history)
print(resp2)

메시지 순서 규칙

LLM은 특정 대화 구조를 기대합니다: System → User → Assistant → User → Assistant → ...

왜 이 순서인가?

이 패턴은 자연스러운 인간-AI 대화를 반영합니다:

  1. System이 먼저 옴 (선택사항): 전체 대화에 적용되는 행동 규칙을 설정하므로, 모든 상호작용이 시작되기 전에 정의되어야 합니다. 누군가가 작업을 시작하기 전에 브리핑하는 것과 같습니다.

  2. User와 Assistant가 번갈아 나타남: 실제 대화에서 사람이 말하고 (User), AI가 응답하고 (Assistant), 사람이 후속 질문을 하고 (User), AI가 다시 응답합니다 (Assistant). 이 턴테이킹 패턴은 AI가 훈련된 방식이므로, 이 구조를 기대합니다.

  3. User로 끝나야 함: AI는 마지막 User 메시지에 대한 응답을 생성합니다. 대화가 Assistant로 끝나면 AI가 응답할 것이 없습니다.

유효한 예제:

python
# System + 단일 User
[("system", "..."), ("user", "...")]
 
# System + 대화
[("system", "..."), ("user", "..."), ("assistant", "..."), ("user", "...")]

문제가 있는 패턴:

python
# User 전에 Assistant - AI가 맥락을 혼동
[("system", "..."), ("assistant", "..."), ("user", "...")]
# AI가 질문 없이 응답을 봅니다. 이것이 어떤 질문에 대한 답변인지 환각을 일으켜
# 관련 없거나 혼란스러운 응답으로 이어질 수 있습니다.
python
# 연속된 두 개의 User 메시지 - AI 응답 누락
[("system", "..."), ("user", "..."), ("user", "...")]
# AI는 어떤 User 메시지에 응답해야 할지 모르거나, 어색하게 병합할 수 있습니다.
# 대화 흐름을 잃습니다.
python
# Assistant로 끝남 - 응답할 것이 없음
[("system", "..."), ("user", "..."), ("assistant", "...")]
# 대화가 완료되었습니다. 대기 중인 User 질문이 없으므로 AI는 생성할 것이 없습니다.
# 오류를 발생시키거나 빈 응답을 생성할 가능성이 높습니다.

핵심 포인트: 이러한 패턴이 항상 하드 에러를 발생시키는 것은 아니지만, AI가 훈련된 대화 논리를 깨트리기 때문에 AI를 혼란스럽게 만듭니다. AI가 응답을 생성할 수 있지만, 신뢰할 수 없거나 무의미할 것입니다. 예측 가능한 동작을 위해 항상 예상되는 패턴을 따르세요.

단순 히스토리를 넘어서: 프로덕션 패턴 (미리보기)

중요한 참고사항: 방금 배운 대화 히스토리 패턴은 훌륭한 기초이지만, 프로덕션 시스템은 더 정교한 접근 방식을 사용합니다.

원시 히스토리의 문제점:

모든 대화 히스토리를 AI에 그대로 전달하는 것에는 한계가 있습니다:

  1. 토큰 낭비: 모든 메시지(오래된 것 포함)가 토큰 한도에 포함되고 비용이 발생
  2. 초점 상실: AI가 관련 없는 이전 대화에 주의가 산만해질 수 있음
  3. 명시적 작업 없음: AI가 히스토리에서 무엇을 해야 할지 추론하며, 명확한 지시를 받지 못함

더 나은 접근 방식:

프로덕션 시스템은 맥락과 지시를 분리합니다:

단순 히스토리 접근 방식 (방금 배운 것):

python
messages = [
    ("system", "You are a Python expert."),
    ("user", "What's a dictionary?"),
    ("assistant", "A dictionary is a key-value data structure."),
    ("user", "Show an example.")
]

프로덕션 접근 방식 (이후 챕터에서 다룰 내용):

python
messages = [
    ("system", "You are a Python expert."),
    ("user", """Context: The user previously asked about Python dictionaries and learned they are key-value structures.
 
Task: Provide a code example demonstrating dictionary usage.""")
]

차이점:

  • 원시 히스토리: AI가 전체 대화를 보고 무엇을 해야 할지 파악
  • 프로덕션 패턴: AI가 요약된 맥락 + 명시적 지시를 받음

분리의 이점:

  • 더 적은 토큰 (낮은 비용, 빠른 응답)
  • 더 신뢰할 수 있는 동작 (명확한 지시)
  • 더 나은 제어 (어떤 맥락이 중요한지 당신이 결정)

어디서 배울 수 있나:

  • Chapter 8: 대화 상태와 메모리 관리
  • Chapter 11: 맥락적 검색 (RAG와 대화 메모리 결합)
  • Chapter 16: 대화 맥락 기반 동적 라우팅

지금은 원시 히스토리를 이해하는 것이 필수적입니다—이것이 이러한 고급 패턴의 기초입니다. 하지만 기억하세요: 방금 배운 것은 교육 도구이지, 최종 솔루션이 아닙니다.

4.3) 프롬프트 외부화하기: 템플릿 파일 관리 (.yaml)

프롬프트를 외부화하는 이유는?

AI 애플리케이션이 성장함에 따라 Python 코드에서 프롬프트를 관리하는 것은 다루기 어려워집니다. 프롬프트를 YAML 파일로 외부화하면 다음과 같은 이점이 있습니다:

비기술자 협업: 제품 관리자, 도메인 전문가, 프롬프트 엔지니어가 Python 코드를 건드리거나 프로그래밍 개념을 이해하지 않고도 YAML 파일을 편집할 수 있습니다.

버전 관리 명확성: 코드와 프롬프트가 섞여 있으면 무엇이 변경되었는지 추적하기 어렵습니다. YAML 파일로 분리하면 프롬프트 변경사항을 별도로 확인할 수 있습니다—커밋 히스토리에서 Python 코드를 헤집고 다닐 필요가 없습니다.

환경별 프롬프트: 코드 변경 없이 개발, 스테이징, 프로덕션에 대해 서로 다른 프롬프트를 사용할 수 있습니다.

A/B 테스팅: 코드 변경 없이 단순히 다른 파일을 로드하여(예: prompt_v1.yaml vs prompt_v2.yaml) 다른 프롬프트 변형을 테스트할 수 있습니다.

YAML 프롬프트 파일을 전통적인 애플리케이션의 구성 파일처럼 생각하세요—코드 변경이나 재배포 없이 동작을 정의합니다.

YAML이란?

YAML은 구성 파일에 일반적으로 사용되는 사람이 읽을 수 있는 데이터 형식입니다. YAML을 처음 접한다면, JSON의 더 깔끔한 대안으로 생각하세요—중괄호 대신 들여쓰기를 사용하며 읽고 편집하기 더 쉽습니다.

YAML 프롬프트 구조

LangChain은 프롬프트를 위한 표준 YAML 파일 구조를 정의했습니다. 예제를 살펴봅시다:

예제 1: 변수가 없는 프롬프트

프롬프트가 런타임 값을 필요로 하지 않을 때, input_variables를 빈 리스트로 설정합니다:

yaml
# prompts/system_prompt.yaml
_type: prompt
input_variables: []
template: |
  You are a helpful assistant.
  Please answer in a friendly and encouraging tone.

| 기호를 사용하면 여러 줄로 작성할 수 있으며, 줄 바꿈이 그대로 유지됩니다.

예제 2: 변수가 있는 프롬프트

프롬프트가 런타임 값을 필요로 할 때, input_variables에 나열합니다:

yaml
# prompts/user_prompt.yaml
_type: prompt
input_variables:
  - user_input
template: |
  User question: {user_input}
  Please provide a clear answer.

런타임에 {user_input} 부분이 실제 값으로 대체됩니다.

주요 구성 요소:

  • _type: prompt: 이것이 프롬프트 템플릿임을 식별합니다
  • input_variables: 템플릿에 사용된 모든 플레이스홀더를 나열합니다 (없으면 빈 리스트 [])
  • template: {placeholders}가 있는 실제 프롬프트 텍스트

YAML 프롬프트 로드하고 사용하기

기본 로딩:

이제 앞서 생성한 YAML 파일을 로드하고 LLM과 함께 사용해봅시다:

python
from langchain_core.prompts import load_prompt, ChatPromptTemplate
from langchain_openai import ChatOpenAI
 
# YAML 파일에서 프롬프트 로드
system_prompt_template = load_prompt("prompts/system_prompt.yaml")
user_prompt_template = load_prompt("prompts/user_prompt.yaml")
 
# 로드된 프롬프트를 채팅 템플릿으로 결합
chat_template = ChatPromptTemplate.from_messages([
    ("system", system_prompt_template.template),
    ("user", user_prompt_template.template)
])
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
# 1단계: 런타임 값으로 템플릿 렌더링
messages = chat_template.invoke({"user_input": "What is LangChain?"})
 
# 2단계: LLM에 전송
response = llm.invoke(messages)
print(response.content)

로드된 템플릿 검증하기:

템플릿을 사용하기 전에 올바르게 로드되었는지 확인합니다:

python
from langchain_core.prompts import load_prompt
 
# 템플릿 로드
user_prompt_template = load_prompt("prompts/user_prompt.yaml")
 
# 어떤 변수를 기대하는지 확인
print("Input variables:", user_prompt_template.input_variables)
 
# 템플릿 텍스트 보기
print("Template:", user_prompt_template.template)

출력:

Input variables: ['user_input']
Template: User question: {user_input}
Please provide a clear answer.

흔한 YAML 실수

실수 1: 일관되지 않은 들여쓰기

YAML은 일관된 들여쓰기(일반적으로 2칸)를 요구합니다. 각 레벨은 동일한 간격을 사용해야 합니다:

잘못된 예:

yaml
_type: prompt
input_variables:
- user_input      # 잘못됨: 리스트 항목도 들여쓰기해야 함
  - question      # 잘못됨: 들여쓰기 레벨이 섞임

올바른 예:

yaml
_type: prompt
input_variables:
  - user_input    # 올바름: 두 항목 모두 같은 들여쓰기 레벨
  - question

실수 2: 플레이스홀더 불일치

template의 플레이스홀더는 input_variables와 일치해야 합니다:

잘못된 예:

yaml
input_variables:
  - user_input
template: "Question: {question}"  # 'question'이 input_variables에 없음!

올바른 예:

yaml
input_variables:
  - user_input
template: "Question: {user_input}"

LangChain은 플레이스홀더가 선언된 변수와 일치하지 않으면 오류를 발생시킵니다.

4.4) 실행 전 템플릿 미리보기 및 검증

템플릿을 미리보는 이유는?

프롬프트 엔지니어링은 반복적입니다. 문구를 조정하고, 구조를 조정하고, 예제를 추가합니다—각 반복은 API 토큰과 시간이 소요됩니다. 실행 전에 템플릿을 미리보면 다음을 수행할 수 있습니다:

시간과 비용 절약: 비용이 많이 드는 API 호출을 하기 전에 오류를 발견합니다.

정확성 검증: 변수가 올바르게 채워지고 포매팅이 예상대로인지 확인합니다.

효율적인 디버그: 모든 변수가 채워지고 포매팅이 적용된 상태로 LLM에 전송되는 정확한 프롬프트를 확인합니다.

템플릿 미리보기를 print 디버깅처럼 생각하세요—정확성을 확인하기 위해 실행 전에 중간 상태를 검사합니다.

기본 템플릿 미리보기

템플릿 구조 검사:

LLM과 함께 템플릿을 사용하기 전에, 템플릿의 구조를 검사하고 샘플 데이터로 렌더링되는 모습을 미리 봅니다:

python
from langchain_core.prompts import ChatPromptTemplate
 
template = ChatPromptTemplate.from_messages([
    ("system", "You are a {role}."),
    ("user", "{user_input}")
])
 
# 템플릿 구조 미리보기
print("Input variables:", template.input_variables)
print("Message count:", len(template.messages))
 
# 샘플 데이터로 미리보기
prompt_value = template.invoke({
    "role": "Python programming expert",
    "user_input": "What is Python?"
})
 
print("\nPreview:")
for msg in prompt_value.to_messages():
    print(f"{msg.type}: {msg.content}")

출력:

Input variables: ['role', 'user_input']
Message count: 2
 
Preview:
system: You are a Python programming expert.
human: What is Python?

이것은 LLM에 전송될 정확한 내용을 보여주어 실행 전에 프롬프트를 확인할 수 있게 합니다.

템플릿 검증: 누락된 변수 발견

가장 일반적인 템플릿 오류는 필수 변수 누락입니다. 다음은 누락된 변수를 포착하는 재사용 가능한 검증 함수입니다:

python
from langchain_core.prompts import ChatPromptTemplate
 
def preview_template(template: ChatPromptTemplate, inputs: dict):
    """주어진 입력으로 템플릿 미리보기, 오류 포착."""
    try:
        prompt_value = template.invoke(inputs)
        
        print("TEMPLATE PREVIEW")
        print("=" * 60)
        
        for i, msg in enumerate(prompt_value.to_messages(), 1):
            print(f"Message {i} ({msg.type.upper()}):")
            print(msg.content)
            print("-" * 60)
                
    except KeyError as e:
        print(f"ERROR: {e}")
        print(f"Required variables: {template.input_variables}")
 
# 사용법
template = ChatPromptTemplate.from_messages([
    ("system", "You are a {role}."),
    ("user", "{user_input}")
])
 
# 유효한 입력
preview_template(template, {
    "role": "Python programming expert",
    "user_input": "What is Python?"
})
 
# 누락된 변수
preview_template(template, {
    "user_input": "What is Python?"  # 'role' 누락
})

출력:

TEMPLATE PREVIEW
============================================================
Message 1 (SYSTEM):
You are a Python programming expert.
------------------------------------------------------------
Message 2 (HUMAN):
What is Python?
------------------------------------------------------------
 
ERROR: "Input to ChatPromptTemplate is missing variables {'role'}.
Expected: ['role', 'user_input'] Received: ['user_input']
...
Required variables: ['role', 'user_input']

검증 워크플로우:

다음은 일반적인 템플릿 검증 프로세스입니다:

오류

유효

아니오

템플릿 정의

샘플 데이터 로드

입력 검증

템플릿/데이터 수정

메시지 미리보기

준비됨?

LLM으로 실행

이 반복적인 프로세스는 비용이 많이 드는 LLM 호출 전에 오류를 포착하는 데 도움이 됩니다.

실행 전 체크리스트

템플릿을 프로덕션에 보내기 전에:

  • 모든 input_variables가 YAML/템플릿에 선언되어 있음
  • 샘플 데이터가 오류 없이 렌더링됨
  • 여러 줄 프롬프트가 올바르게 표시됨
  • 플레이스홀더가 변수 이름과 정확히 일치함
  • 엣지 케이스(빈 문자열, 긴 텍스트)로 테스트함

장 요약:

LangChain의 템플릿 시스템을 사용하여 유지보수 가능하고 재사용 가능한 프롬프트를 설계하는 방법을 배웠습니다:

  1. 관심사의 분리: 더 쉬운 유지보수와 반복을 위해 프롬프트를 코드에서 분리
  2. 역할 인식 템플릿: 적절한 지침 계층 구조를 가진 구조화된 LLM 상호 작용을 위해 시스템, 사용자, 어시스턴트 메시지 사용
  3. 외부화된 프롬프트: 비기술적 협업과 버전 관리를 위해 YAML 파일에서 프롬프트 관리
  4. 미리보기 및 검증: 실행 전에 오류를 조기에 발견하고 템플릿 확인

다음 단계:

5장에서는 템플릿이 미리보기 에이전트 예제에서 자율적 의사 결정을 어떻게 가능하게 하는지 살펴봅니다. 그런 다음 6장에서는 LCEL(LangChain Expression Language)을 배워 | 연산자를 사용하여 이러한 템플릿을 강력한 파이프라인으로 구성하는 방법을 배웁니다.