LangChain으로 프롬프트 엔지니어링 기초 실습하기

JunSuPark·2026년 6월 16일

1. 실습 환경 준비

먼저 OpenAI API를 사용하기 위해 .env 파일에 API 키를 저장했다.

OPENAI_API_KEY=your_api_key_here

그리고 노트북에서는 python-dotenv를 사용해서 환경변수를 불러왔다.

from dotenv import load_dotenv

load_dotenv()

이때 한 가지 주의할 점이 있었다.

.env 파일명이 정확해야 한다.
파일명에 공백이 붙어 있거나 다른 이름으로 저장되어 있으면 load_dotenv()가 파일을 찾지 못한다.

실제로 API 키가 제대로 로드되지 않으면 다음과 같은 에러가 발생한다.

OpenAIError: Missing credentials.
Please pass an api_key or set the OPENAI_API_KEY environment variable.

즉, LangChain이나 OpenAI 코드 문제가 아니라 환경변수가 제대로 설정되지 않은 문제일 수 있다.


2. PromptTemplate 기본 사용

가장 먼저 배운 것은 PromptTemplate이다.

PromptTemplate은 프롬프트 문자열 안에 변수를 넣어 재사용 가능한 템플릿을 만드는 기능이다.

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template(
    "다음 주제에 대해 핵심 내용을 정리해주세요: {topic}"
)

여기서 {topic}은 나중에 입력값으로 채워질 변수다.

chain = prompt | llm

output = chain.invoke({
    "topic": "인공지능"
})

이렇게 하면 {topic} 자리에 "인공지능"이 들어간 프롬프트가 모델에게 전달된다.


4. LCEL 체인 구성

LangChain에서는 여러 구성 요소를 | 연산자로 연결해서 체인을 만들 수 있다.

chain = prompt | llm

출력 결과를 문자열로 받고 싶을 때는 StrOutputParser()를 함께 사용했다.

from langchain_core.output_parsers import StrOutputParser

chain = prompt | llm | StrOutputParser()

이 구조는 다음과 같은 흐름으로 이해할 수 있다.

PromptTemplate -> LLM -> OutputParser

즉, 프롬프트를 만들고, 모델에 전달하고, 모델의 응답을 원하는 형태로 파싱하는 흐름이다.


5. 실습 2: 두 문장의 의미적 유사도 비교

두 번째 실습에서는 두 개의 문장을 입력받아 의미적 유사도와 맥락 일치 여부를 평가하는 체인을 만들었다.

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

sentence1 = input("첫 번째 문장: ")
sentence2 = input("두 번째 문장: ")

similarity_prompt = PromptTemplate(
    template="""
다음 두 문장의 의미적 유사도를 0에서 1 사이의 값으로 평가하세요.
1은 완전히 동일한 의미, 0은 완전히 다른 의미입니다.
맥락이 일치하는 여부도 고려하여 평가해주세요.

문장 1: {sentence1}
문장 2: {sentence2}

출력 형식:
- 유사도:
- 맥락 일치 여부:
- 이유:
""",
    input_variables=["sentence1", "sentence2"]
)

chain = similarity_prompt | llm | StrOutputParser()

output = chain.invoke({
    "sentence1": sentence1,
    "sentence2": sentence2
})

print(output)

여기서 중요한 점은 input_variables에 변수명을 적는 것만으로는 부족하다는 것이다.

input_variables=["sentence1", "sentence2"]

이렇게 선언했더라도, 실제 템플릿 안에 아래처럼 변수를 넣어야 한다.

문장 1: {sentence1}
문장 2: {sentence2}

처음에는 템플릿 안에 변수를 넣지 않아서 모델이 “두 문장을 제공해주세요”라고 응답했다.
즉, invoke()에서 값을 넘겼더라도 프롬프트 본문에 변수가 없으면 모델에게 실제 값이 전달되지 않는다.


6. ChatPromptTemplate 사용하기

이후에는 ChatPromptTemplate을 사용했다.

PromptTemplate이 하나의 문자열 프롬프트를 만드는 방식이라면, ChatPromptTemplate은 메시지 역할을 나누어 대화형 프롬프트를 구성할 수 있다.

예를 들어 시스템 메시지와 사용자 메시지를 나눌 수 있다.

from langchain_core.prompts import ChatPromptTemplate

chat_prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 친절한 고객 서비스 담당자입니다."),
    ("human", "{customer_message}")
])

이렇게 하면 모델에게 단순히 질문만 보내는 것이 아니라, 모델이 어떤 역할을 수행해야 하는지도 함께 전달할 수 있다.


7. 실습 3: 상품 리뷰 분석 체인 만들기

세 번째 실습에서는 상품 리뷰를 분석하는 AI 체인을 만들었다.

목표는 리뷰를 입력받아 감성, 장점, 단점, 핵심 요약을 분석하는 것이었다.

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

reviews = [
    "노트북이 생각보다 가볍고 화면도 선명해서 만족스럽습니다. 다만 팬 소음이 조금 있는 편이에요.",
    "커피머신 디자인은 예쁜데 사용법이 조금 복잡합니다. 추출 맛은 괜찮지만 청소가 번거로워요.",
    "운동화 착용감이 정말 좋고 오래 걸어도 발이 편합니다. 사이즈는 평소보다 반 치수 크게 사는 걸 추천해요.",
    "무선 마우스 연결이 빠르고 클릭감도 마음에 듭니다. 하지만 배터리 커버가 약간 헐거운 느낌이 있어요.",
    "가방 수납공간이 넉넉하고 마감도 깔끔합니다. 출퇴근용으로 좋지만 비 오는 날에는 방수가 조금 아쉽습니다."
]

프롬프트는 다음과 같이 구성했다.

chat_prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 상품 리뷰를 분석하는 AI assistant입니다."),
    ("human", """
다음 상품 리뷰를 분석해주세요.

리뷰:
{review}

출력 형식:
- 감성:
- 장점:
- 단점:
- 핵심 요약:
""")
])

review_chain = chat_prompt | llm | StrOutputParser()

여러 개의 리뷰는 반복문으로 하나씩 분석했다.

for idx, review in enumerate(reviews, 1):
    print(f"\n=== 리뷰 {idx} 분석 결과 ===")
    result = review_chain.invoke({"review": review})
    print(result)

이 실습을 통해 같은 프롬프트 구조를 여러 입력에 반복 적용할 수 있다는 점을 배웠다.


8. 실습 4: 예시를 포함한 프롬프트 작성

네 번째 실습에서는 앞에서 만든 상품 리뷰 분석 시스템에 입출력 예시를 추가했다.

프롬프트에 예시를 넣으면 모델이 어떤 형식으로 답변해야 하는지 더 잘 이해한다.

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

few_shot_prompt = PromptTemplate(
    template="""
당신은 상품 리뷰를 분석하는 AI입니다.
다음 예시를 참고하여 리뷰를 분석하세요.

예시 입력:
리뷰: 배송은 빨랐지만 제품 마감이 아쉬웠습니다.

예시 출력:
- 감성: 복합적
- 장점: 배송이 빠름
- 단점: 제품 마감이 아쉬움
- 핵심 요약: 배송에는 만족했지만 제품 품질에는 아쉬움이 있음

이제 아래 리뷰를 분석하세요.

리뷰:
{review}

출력 형식:
- 감성:
- 장점:
- 단점:
- 핵심 요약:
""",
    input_variables=["review"]
)

chain = few_shot_prompt | llm | StrOutputParser()

이런 방식은 Few-shot Prompting의 기본 형태라고 볼 수 있다.

모델에게 단순히 “이렇게 해줘”라고 말하는 것보다, 예시를 함께 제공하면 더 일관된 결과를 얻기 쉽다.


9. 프롬프트 엔지니어링에서 중요한 세 가지

이번 실습을 통해 프롬프트를 설계할 때 특히 중요하다고 느낀 점은 세 가지였다.

1. 명확성

모델에게 무엇을 해야 하는지 명확히 알려줘야 한다.

나쁜 예시는 다음과 같다.

인공지능에 대해 알려줘.

조금 더 명확하게 작성하면 다음과 같다.

인공지능의 개념, 활용 사례, 장단점을 각각 3문장 이내로 설명해주세요.

2. 맥락성

모델이 어떤 역할을 해야 하는지 알려주는 것도 중요하다.

("system", "당신은 초보자를 위한 친절한 AI 강사입니다.")

이런 시스템 메시지는 모델의 답변 톤과 방향을 잡아준다.

3. 구조화

원하는 출력 형식을 지정하면 결과를 훨씬 다루기 쉬워진다.

출력 형식:
- 핵심 요약:
- 장점:
- 단점:
- 추천 여부:

출력 형식을 정해두면 모델의 응답을 사람이 읽기에도 좋고, 코드에서 후처리하기에도 좋다.


10. OutputParser 사용하기

실습에서는 StrOutputParser()를 사용해 모델 응답을 문자열로 받았다.

from langchain_core.output_parsers import StrOutputParser

chain = prompt | llm | StrOutputParser()

StrOutputParser()를 사용하면 모델 응답 객체에서 .content를 따로 꺼내지 않아도 바로 문자열 결과를 받을 수 있다.

예를 들어 아래처럼 출력할 수 있다.

output = chain.invoke({"topic": "인공지능"})

print(output)

처음에는 pprint()도 사용했는데, pprint는 pretty print의 줄임말로 복잡한 리스트나 딕셔너리를 보기 좋게 출력해주는 함수다.

문자열 결과만 출력할 때는 print()만 사용해도 충분하다.


11. 실습 프로젝트: 맞춤형 학습 도우미 챗봇 만들기

마지막으로 앞에서 배운 내용을 조합해서 맞춤형 학습 도우미 챗봇을 만들어보았다.

목표는 특정 주제와 난이도를 입력하면 학습용 퀴즈를 만들거나 개념을 설명해주는 체인을 구성하는 것이었다.

이 부분에서는 단순 문자열 출력이 아니라, Pydantic 모델을 사용해서 구조화된 출력을 받아보았다.


12. 퀴즈 문제 생성 체인

먼저 퀴즈 문제의 출력 형식을 Pydantic 모델로 정의했다.

from typing import List
from pydantic import BaseModel, Field

class QuizQuestion(BaseModel):
    """퀴즈 문제 스키마"""
    question: str = Field(..., description="퀴즈 문제")
    options: List[str] = Field(..., description="보기 4개")
    correct_answer: int = Field(..., description="정답 번호 1-4")
    explanation: str = Field(..., description="정답 설명")

그 다음 퀴즈 생성을 위한 프롬프트를 작성했다.

from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

quiz_prompt = PromptTemplate(
    template="""
다음 주제에 대한 퀴즈 문제를 만들어주세요.

주제: {topic}
난이도(상/중/하): {difficulty}

다음 조건을 만족하는 퀴즈를 생성해주세요:
1. 문제는 명확하고 이해하기 쉽게 작성
2. 4개의 보기 제공
3. 정답과 오답은 비슷한 수준으로 구성
4. 상세한 정답 설명 포함
""",
    input_variables=["topic", "difficulty"]
)

모델은 다음과 같이 설정했다.

llm = ChatOpenAI(
    model="gpt-4.1-mini",
    temperature=0.3
)

그리고 with_structured_output()을 사용해서 모델의 출력을 QuizQuestion 형태로 받았다.

structured_llm = llm.with_structured_output(QuizQuestion)

quiz_chain = quiz_prompt | structured_llm

실행은 다음과 같이 했다.

output = quiz_chain.invoke({
    "topic": "인공지능",
    "difficulty": "상"
})

print(f"퀴즈 문제: {output.question}")
print(f"보기: {output.options}")
print(f"정답: {output.correct_answer}")
print(f"정답 설명: {output.explanation}")

이 방식의 장점은 모델 응답을 단순 텍스트가 아니라 정해진 필드가 있는 객체처럼 다룰 수 있다는 점이다.


13. 개념 설명 체인 만들기

다음으로는 특정 주제에 대해 개념 설명을 제공하는 체인을 만들었다.

먼저 출력 스키마를 정의했다.

from typing import List
from pydantic import BaseModel, Field

class ConceptExplanation(BaseModel):
    """개념 설명 스키마"""
    topic: str = Field(..., description="학습 주제")
    explanation: str = Field(..., description="주제에 대한 설명")
    examples: List[str] = Field(..., description="이해를 돕는 예시")
    related_concepts: List[str] = Field(..., description="함께 학습하면 좋은 관련 개념")

그리고 개념 설명을 위한 프롬프트를 작성했다.

concept_prompt = PromptTemplate(
    template="""
당신은 학습자를 돕는 친절한 AI 튜터입니다.

다음 주제에 대해 난이도에 맞게 설명해주세요.

주제: {topic}
난이도(상/중/하): {difficulty}

설명 조건:
1. 학습자의 수준에 맞게 쉽게 설명
2. 핵심 개념을 먼저 설명
3. 이해를 돕는 예시 포함
4. 함께 공부하면 좋은 관련 개념 제안
""",
    input_variables=["topic", "difficulty"]
)

구조화된 출력 체인을 구성했다.

structured_llm = llm.with_structured_output(ConceptExplanation)

concept_chain = concept_prompt | structured_llm

실행 예시는 다음과 같다.

output = concept_chain.invoke({
    "topic": "인공지능",
    "difficulty": "하"
})

print(f"주제: {output.topic}")
print(f"설명: {output.explanation}")
print(f"예시: {output.examples}")
print(f"관련 개념: {output.related_concepts}")

이렇게 하면 학습 주제에 대해 설명, 예시, 관련 개념을 구조화된 형태로 받을 수 있다.


14. 이번 실습에서 배운 점 정리

이번 실습을 하면서 배운 내용을 정리하면 다음과 같다.

PromptTemplate

PromptTemplate은 재사용 가능한 프롬프트를 만들 때 사용한다.

PromptTemplate.from_template("주제: {topic}")

변수를 사용할 때는 반드시 템플릿 문자열 안에 {변수명} 형태로 넣어야 한다.

ChatPromptTemplate

ChatPromptTemplate은 시스템 메시지와 사용자 메시지를 구분해서 대화형 프롬프트를 만들 때 사용한다.

ChatPromptTemplate.from_messages([
    ("system", "당신은 친절한 AI 튜터입니다."),
    ("human", "{question}")
])

StrOutputParser

StrOutputParser()는 모델의 응답을 문자열로 변환할 때 사용한다.

chain = prompt | llm | StrOutputParser()

LCEL

LCEL은 LangChain Expression Language의 약자로, | 연산자를 사용해 프롬프트, 모델, 파서를 연결할 수 있다.

chain = prompt | llm | parser

Structured Output

with_structured_output()을 사용하면 모델의 응답을 Pydantic 스키마에 맞게 받을 수 있다.

structured_llm = llm.with_structured_output(QuizQuestion)

이 방식은 응답 결과를 코드에서 안정적으로 다루고 싶을 때 유용하다.


15. 실습하면서 만난 오류들

이번 실습에서는 몇 가지 오류도 만났다.

Missing credentials 에러

OpenAIError: Missing credentials.

OpenAI API 키가 환경변수로 로드되지 않았을 때 발생했다.
.env 파일명과 OPENAI_API_KEY 설정을 확인해야 한다.

expected str, got PromptTemplate 에러

TypeError: expected str, got PromptTemplate

PromptTemplate.from_template()에 문자열이 아니라 이미 만들어진 PromptTemplate 객체를 넣어서 발생했다.

입력값을 넘겼는데 모델이 못 알아듣는 문제

invoke()에 값을 넘겼지만 템플릿 안에 {sentence1}, {sentence2} 같은 변수를 작성하지 않으면 모델에게 실제 값이 전달되지 않는다.

즉, 아래 두 부분의 이름이 정확히 맞아야 한다.

문장 1: {sentence1}
문장 2: {sentence2}
chain.invoke({
    "sentence1": sentence1,
    "sentence2": sentence2
})

마무리

이번 실습을 통해 프롬프트 엔지니어링은 단순히 좋은 질문을 작성하는 것이 아니라는 것을 알게 되었다.

중요한 것은 다음과 같았다.

  • 모델에게 명확한 역할을 부여하기
  • 입력값을 템플릿 변수로 구조화하기
  • 원하는 출력 형식을 구체적으로 제시하기
  • 체인 형태로 프롬프트, 모델, 파서를 연결하기
  • 필요하면 구조화된 출력 스키마를 사용하기

특히 마지막 학습 도우미 챗봇 실습을 통해 LLM 응답을 단순 텍스트가 아니라 QuizQuestion, ConceptExplanation 같은 구조화된 객체로 받을 수 있다는 점이 인상적이었다.

앞으로는 단순히 LLM에게 질문을 던지는 수준을 넘어서, 입력과 출력이 명확한 작은 AI 기능들을 직접 설계해볼 수 있을 것 같다.

profile
배움을 추구하는 개발자

0개의 댓글