LangChain 프롬프트 구성
모델을 직접 호출할 때는 문자열이나 메시지를 그대로 넘길 수 있습니다. 하지만 실제 애플리케이션의 프롬프트는 전부 고정되어 있지 않습니다. 사용자 질문, 검색해 온 문서, 대화 이력처럼 요청할 때마다 달라지는 값이 들어가고, system과 user 같은 메시지 역할도 함께 관리해야 합니다.
이때 사용하는 것이 ChatPromptTemplate입니다. 프롬프트의 고정된 구조와 실행할 때마다 달라지는 값을 분리해 두고, 입력값을 받아 모델에 전달할 chat prompt를 만드는 컴포넌트입니다. 예를 들어 system 메시지의 규칙은 고정해 두고 {question}이나 {context} 같은 자리만 호출할 때 채울 수 있습니다.
파이썬의 f-string으로도 최종 문자열 자체는 만들 수 있습니다. 차이는 문자열을 만들 수 있느냐가 아니라 프롬프트를 role과 message, variable이라는 구조로 유지하면서 LangChain의 실행 인터페이스 안에서 다룰 수 있느냐에 있습니다.
ChatPromptTemplate 자체도 Runnable이기 때문에 입력값을 invoke()하면 ChatPromptValue가 만들어지고, 이를 그대로 ChatModel에 연결할 수 있습니다. 그래서 prompt | model처럼 프롬프트 생성과 모델 호출을 하나의 실행 흐름으로 묶고, 이후 output parser까지 같은 방식으로 이어 붙일 수 있습니다.
ChatPromptTemplate이 변수로부터 실제 메시지를 만드는 과정을 먼저 보고, 단순한 f-string과 무엇이 다른지 비교합니다. 이어서 prompt | model 구성이 어떻게 Runnable 체인이 되는지까지 연결해서 봅니다.
1장. 프롬프트가 모델 입력이 되는 과정
1편에서 모델은 이런 입력을 받았습니다.
model.invoke("LangChain이 무엇인가요?")
또는 역할을 직접 나눴습니다.
model.invoke([
SystemMessage(content="한 문장으로만 답하세요."),
HumanMessage(content="LangChain이 무엇인가요?"),
])
고정된 한 번의 호출이라면 이것으로 충분합니다. 문제는 같은 구조를 유지하면서 입력만 계속 바뀌는 경우입니다.
고정되는 것
system 지시
답변 규칙
작업 형식
매번 바뀌는 것
사용자 질문
검색 결과
대화 이력
설정값
이 둘을 분리하는 층이 프롬프트 템플릿입니다.
입력값
↓
ChatPromptTemplate
↓
ChatPromptValue / 메시지 목록
↓
ChatModel
↓
AIMessage
이 편에서는 입력을 모델이 받을 메시지로 만드는 부분까지만 다룹니다. 모델의 출력을 Pydantic이나 JSON 구조로 받는 것은 3편, 프롬프트와 모델과 파서를 |로 조합하는 실행 구조는 4편에서 이어집니다.
2장. ChatPromptTemplate
가장 기본적인 형태입니다.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You translate {src} to {dst}."),
("human", "{text}"),
])
튜플 하나가 메시지 하나입니다.
(역할, 템플릿)
("system", ...) 시스템 지시
("human", ...) 사용자 입력
("ai", ...) 모델 응답 또는 예시 응답
{src}, {dst}, {text}가 호출할 때 채워지는 변수입니다.
messages = prompt.format_messages(
src="English",
dst="Korean",
text="I love programming.",
)
결과는 문자열 하나가 아니라 역할이 유지된 메시지 목록입니다.
SystemMessage(content='You translate English to Korean.')
HumanMessage(content='I love programming.')
역할 이름
LangChain 문서에서는 주로 human, ai, system을 사용합니다. 각각 HumanMessage, AIMessage, SystemMessage와 대응해 읽기 쉽기 때문입니다.
다만 user와 assistant도 지원됩니다.
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
("user", "{question}"),
])
user는 HumanMessage, assistant는 AIMessage로 변환됩니다. 따라서 OpenAI식 user를 쓰면 오류가 난다는 식으로 구분할 필요는 없습니다. 프로젝트 안에서 한 표기를 정해 일관되게 쓰는 편이 중요합니다.
3장. format과 invoke는 무엇이 다른가
템플릿에 값을 채우는 방법은 몇 가지가 있습니다.
prompt.format_messages(text="Hello", src="English", dst="Korean")
format_messages()는 최종 메시지 목록을 직접 확인할 때 유용합니다.
반면 invoke()는 LangChain의 공통 실행 인터페이스를 따릅니다.
value = prompt.invoke({
"src": "English",
"dst": "Korean",
"text": "Hello",
})
반환값은 ChatPromptValue입니다. 이 객체는 내부에 메시지 목록을 가지고 있고 그대로 ChatModel 입력으로 사용할 수 있습니다.
format_messages() → list[BaseMessage]
invoke() → ChatPromptValue
invoke()가 중요한 이유는 프롬프트도 모델처럼 Runnable이기 때문입니다. 그래서 나중에 모델과 같은 실행 규약으로 연결할 수 있습니다.
chain = prompt | model
여기서는 이 코드가 가능하다는 것만 기억합니다. |가 실제로 무엇을 만들고 invoke, batch, stream이 어떻게 이어지는지는 4편에서 다룹니다.
4장. f-string과 무엇이 다른가
f-string을 쓰면 안 되는 것은 아닙니다.
text = "Hello"
prompt_text = f"Translate this to Korean: {text}"
model.invoke(prompt_text)
작은 스크립트나 한 번만 쓰는 프롬프트라면 충분히 좋은 방법입니다.
차이는 프롬프트가 애플리케이션의 한 컴포넌트가 될 때 나타납니다.
| f-string | ChatPromptTemplate |
|---|---|
| 문자열을 즉시 만든다 | 변수 자리가 남아 있는 템플릿 객체다 |
| 역할 구분을 직접 관리한다 | system/human/ai를 메시지 단위로 관리한다 |
| 일부 값을 고정하려면 코드를 직접 구성한다 | partial()을 제공한다 |
| 결과는 일반 문자열이다 | 자체가 Runnable이다 |
| 대화 이력 삽입을 직접 처리한다 | MessagesPlaceholder를 제공한다 |
중요한 것은 f-string이 파이프에 절대 들어갈 수 없다는 뜻이 아닙니다. 문자열을 밖에서 만들어 넘기거나 별도의 Runnable로 감쌀 수도 있습니다.
ChatPromptTemplate의 장점은 별도 어댑터 없이 프롬프트 자체가 LangChain 실행 모델의 한 컴포넌트가 된다는 것입니다.
5장. 일부 변수 미리 채우기
매번 바뀌지 않는 값은 partial()로 미리 채울 수 있습니다.
prompt = ChatPromptTemplate.from_messages([
("system", "You translate {src} to {dst}."),
("human", "{text}"),
])
korean_prompt = prompt.partial(
src="English",
dst="Korean",
)
이제 호출할 때는 text만 필요합니다.
korean_prompt.invoke({"text": "I love programming."})
partial()은 원본을 수정하는 것이 아니라 일부 변수가 채워진 새 ChatPromptTemplate을 반환합니다.
원본
src + dst + text 필요
↓ partial(src, dst)
새 템플릿
text만 필요
고정 설정과 요청마다 달라지는 값을 분리할 때 유용합니다.
6장. 대화 이력 넣기
대화 이력은 문자열 하나가 아니라 개수가 달라지는 메시지 목록입니다.
HumanMessage
AIMessage
HumanMessage
AIMessage
...
이런 목록을 프롬프트 중간에 끼우는 자리가 MessagesPlaceholder입니다.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
MessagesPlaceholder("history"),
("human", "{question}"),
])
호출할 때 메시지 목록을 넘깁니다.
from langchain_core.messages import HumanMessage, AIMessage
value = prompt.invoke({
"history": [
HumanMessage(content="내 이름은 홍길동이야."),
AIMessage(content="알겠습니다."),
],
"question": "내 이름이 뭐였지?",
})
최종 메시지는 다음 순서가 됩니다.
SystemMessage
HumanMessage ← history
AIMessage ← history
HumanMessage ← 현재 question
역할을 유지한 채 여러 메시지를 한 위치에 삽입하는 것이 핵심입니다.
optional=True와 최근 메시지 제한
첫 대화처럼 이력이 없을 수도 있습니다.
MessagesPlaceholder("history", optional=True)
이 경우 history 키를 전달하지 않아도 빈 목록처럼 처리됩니다.
최근 몇 개만 넣고 싶다면 n_messages도 사용할 수 있습니다.
MessagesPlaceholder("history", n_messages=4)
MessagesPlaceholder가 하지 않는 일
MessagesPlaceholder는 대화 이력을 저장하거나 관리하지 않습니다. 전달받은 메시지를 프롬프트의 해당 위치에 삽입할 뿐입니다.
MessagesPlaceholder
어디에 넣을지 결정
이력 저장 / 복원
별도의 상태 관리 문제
이력 자르기 / 요약
별도의 컨텍스트 관리 문제
뒤의 LangGraph 편에서 나오는 checkpointer는 그래프 상태를 저장하고 복원할 수 있게 하지만, 그것이 자동으로 대화 이력을 요약하거나 적절한 길이로 잘라 주는 것은 아닙니다.
7장. few-shot 예시 넣기
ChatPromptTemplate에는 실제 대화처럼 예시를 넣을 수도 있습니다.
prompt = ChatPromptTemplate.from_messages([
("system", "Classify sentiment as positive or negative."),
("human", "이 영화 최고였어"),
("ai", "positive"),
("human", "돈 아까웠다"),
("ai", "negative"),
("human", "{text}"),
])
모델 입장에서는 앞의 human/ai 쌍이 과제를 보여 주는 예시가 됩니다.
예시가 많아지면 직접 메시지를 반복해서 쓰기보다 FewShotChatMessagePromptTemplate로 예시 데이터와 예시 형식을 분리할 수 있습니다.
여기서 중요한 것은 LangChain이 새로운 few-shot 기법을 만드는 것이 아니라, 원래 프롬프트에 들어갈 예시를 역할이 있는 메시지 구조로 관리해 준다는 것입니다.
8장. 템플릿에서 자주 틀리는 부분
중괄호
ChatPromptTemplate의 기본 템플릿 형식은 f-string 방식입니다. 따라서 {name}은 변수로 해석됩니다.
prompt = ChatPromptTemplate.from_messages([
("human", 'JSON 예시: {"name": "Alice"}'),
])
문자 그대로 중괄호를 넣으려면 두 번 씁니다.
prompt = ChatPromptTemplate.from_messages([
("human", 'JSON 예시: {{"name": "Alice"}}'),
])
다만 모델 출력 형식을 JSON으로 강제하기 위해 긴 JSON 예시와 파싱 규칙을 프롬프트에 계속 넣는 방식은 관리하기 어렵습니다. 모델 출력을 구조로 받는 문제는 3편의 with_structured_output()에서 따로 다룹니다.
변수 이름
템플릿이 커질수록 변수 이름이 곧 인터페이스가 됩니다.
question
context
history
language
같은 의미에 text, input, query, question을 뒤섞으면 나중에 체인을 조합할 때 헷갈립니다. 프로젝트 안에서 이름을 통일하는 편이 좋습니다.
9장. 프롬프트 템플릿을 사용할 때 주의할 점
단순하면 단순하게. 한 번 호출하고 끝나는 코드라면 f-string이나 메시지 목록을 직접 만들어도 됩니다. 모든 프롬프트를 억지로 템플릿화할 필요는 없습니다.
반복되면 템플릿으로. 같은 system 지시와 구조를 여러 입력에 반복해서 사용한다면 ChatPromptTemplate이 관리하기 쉽습니다.
실제 메시지를 확인. 프롬프트 문제가 의심되면 모델 호출 전에 무엇이 만들어졌는지 먼저 봅니다.
messages = prompt.format_messages(**inputs)
for message in messages:
print(type(message).__name__, message.content)
이력의 자리와 이력 관리를 구분. MessagesPlaceholder는 삽입 위치입니다. 저장, 검색, trimming, summarization은 별도의 문제입니다.
출력 형식을 프롬프트 문제와 섞지 않기. 입력을 만드는 것은 이번 편이고, 출력 구조를 보장하는 것은 다음 편입니다.
10장. 정리
1편에서 모델과 메시지를 배웠다면, 이번 편에서는 그 메시지를 매 호출마다 동적으로 만드는 방법을 배운 셈입니다.
이 편에서 연결해야 할 개념은 ChatPromptTemplate, partial(), MessagesPlaceholder입니다. 각각 프롬프트의 구조를 정의하고, 일부 값을 미리 고정하고, 가변 길이 메시지 목록을 원하는 위치에 삽입합니다.
f-string과 ChatPromptTemplate의 차이는 문자열을 만들 수 있느냐가 아니라, 프롬프트를 LangChain의 실행 가능한 컴포넌트로 다룰 수 있느냐입니다.
최소 코드는 다음과 같습니다.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "You answer in {language}."),
MessagesPlaceholder("history", optional=True),
("human", "{question}"),
]).partial(language="Korean")
value = prompt.invoke({"question": "LangChain이 무엇인가요?"})
여기까지가 입력 쪽입니다.
다음 편에서는 모델이 자유로운 문자열을 반환했을 때 생기는 문제를 다룹니다. JSON을 직접 파싱하는 대신 with_structured_output()으로 출력을 구조로 받는 방법으로 이어집니다.
용어 정리
| 용어 | 한 줄 뜻 |
|---|---|
ChatPromptTemplate |
역할별 메시지와 변수 자리를 정의하는 채팅 프롬프트 템플릿 |
from_messages |
메시지 표현 목록으로 ChatPromptTemplate을 만드는 메서드 |
format_messages |
변수를 채워 최종 메시지 목록을 만드는 메서드 |
invoke |
Runnable 규약으로 템플릿을 실행해 ChatPromptValue를 반환하는 메서드 |
ChatPromptValue |
ChatModel에 전달할 메시지 목록을 담은 PromptValue |
partial |
일부 입력 변수를 미리 채운 새 템플릿을 만드는 메서드 |
MessagesPlaceholder |
여러 메시지를 프롬프트의 특정 위치에 삽입하는 자리 |
optional |
해당 MessagesPlaceholder 입력이 없어도 되게 하는 옵션 |
n_messages |
플레이스홀더에 포함할 최근 메시지 수를 제한하는 옵션 |
| few-shot | 몇 개의 입력·출력 예시를 프롬프트에 넣어 과제와 형식을 보여 주는 방식 |
FewShotChatMessagePromptTemplate |
여러 few-shot 예시를 채팅 메시지 형식으로 만드는 템플릿 |
| Runnable | invoke, batch, stream 등 공통 실행 인터페이스를 따르는 LangChain 컴포넌트 |
참고자료
'AI Agent > Frameworks' 카테고리의 다른 글
| LangGraph (2) - State, Node, Edge (0) | 2024.08.05 |
|---|---|
| LangChain (4) - 조합하기 (0) | 2024.05.30 |
| LangGraph (1) - 왜 그래프인가 (0) | 2024.05.10 |
| LangChain (3) - 출력 구조 (0) | 2024.03.31 |
| LangChain (1) - 패키지 구조와 첫 호출 (0) | 2024.01.18 |
댓글