Study/LangChain

[인프런] 입문자를 위한 LangChain 기초

bluebamus 2025. 12. 25.

강의 정보 : 

https://www.inflearn.com/course/%EC%9E%85%EB%AC%B8%EC%9E%90%EB%A5%BC%EC%9C%84%ED%95%9C-%EB%9E%AD%EC%B2%B4%EC%9D%B8-%EA%B8%B0%EC%B4%88/dashboard

 


github 저장소 : https://github.com/tsdata/langchain-study

 

wikidocs : https://wikidocs.net/book/14473

 

1. 학습 준비

ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate, from_template, StrOutputParser를 실무 수준에서 활용 가능

---

## 전체 구성

```
I. 기초 개념
   ├─ 1. 채팅 모델의 메시지 시스템
   ├─ 2. 메시지의 역할 (role)
   └─ 3. 템플릿 (Template)의 개념

II. 프롬프트 템플릿 (Prompt Template)
   ├─ 1. PromptTemplate (일반 텍스트)
   ├─ 2. ChatPromptTemplate (채팅 모델용)
   ├─ 3. MessagePromptTemplate 계층구조
   └─ 4. 메시지 템플릿들

III. 메시지 템플릿 상세
   ├─ 1. SystemMessagePromptTemplate
   ├─ 2. HumanMessagePromptTemplate
   ├─ 3. AIMessagePromptTemplate
   └─ 4. 동적 메시지 삽입 (MessagesPlaceholder)

IV. from_template 메서드
   ├─ 1. 작동 원리
   ├─ 2. 사용 패턴
   ├─ 3. 변수 자동 감지
   └─ 4. 다른 생성 방식과의 비교

V. 출력 파싱 (Output Parsing)
   ├─ 1. StrOutputParser
   ├─ 2. 다른 파서들
   ├─ 3. LCEL 체인에서의 사용
   └─ 4. 에러 처리

VI. 실전 프로젝트
   ├─ 1. 기초 예제
   ├─ 2. 중급 예제
   ├─ 3. 고급 예제
   └─ 4. 모범 사례

VII. 문제 해결
   ├─ 1. 일반적인 실수
   ├─ 2. 디버깅 기법
   ├─ 3. 성능 최적화
   └─ 4. 체크리스트
```

---

# I. 기초 개념

## 1. 채팅 모델의 메시지 시스템

### 1.1 일반 LLM vs 채팅 모델

LangChain이 복잡한 프롬프트 시스템을 제공하는 이유를 이해하려면, 두 가지 LLM 타입의 차이를 먼저 알아야 합니다.

**일반 LLM (Text Completion)**

```
입력: "당신은 번역가입니다. 이 문장을 한국어로 번역하세요: I love programming."
출력: "나는 프로그래밍을 사랑합니다."
```

- 단순 텍스트 → 텍스트 변환
- 역할 정의가 모호함
- 입력 형식이 자유로움

**채팅 모델 (Chat Completion)**

메시지 리스트:
[
  SystemMessage("당신은 전문 번역가입니다."),
  HumanMessage("이 문장을 한국어로 번역하세요: I love programming.")
]
     ↓
  ChatOpenAI
     ↓
AIMessage("나는 프로그래밍을 사랑합니다.")


- 메시지 객체들의 리스트 입력
- 각 메시지는 **역할(role)**을 가짐
- 대화형 상호작용에 최적화
- 더 정확한 응답 생성

### 1.2 왜 구조화된 메시지가 중요한가?

| 특성 | 일반 텍스트 | 구조화된 메시지 |
|------|-----------|--------------|
| 역할 명확성 | 낮음 | 높음 |
| 응답 일관성 | 낮음 | 높음 |
| 대화 기록 관리 | 어려움 | 쉬움 |
| 멀티턴 대화 | 복잡함 | 자연스러움 |
| 메모리 통합 | 수동 | 자동 |

**핵심**: 채팅 모델에 메시지를 보낼 때, LLM이 "누가 말하는지", "어떤 맥락인지"를 명확히 이해하도록 구조화해야 합니다.

---

## 2. 메시지의 역할 (Role)

### 2.1 4가지 기본 역할

**SystemMessage** - 모델의 지시사항

from langchain.schema import SystemMessage

msg = SystemMessage(content="당신은 Python 전문가입니다. 기술적이고 정확하게 답변하세요.")


- 모델의 **성격, 규칙, 지시사항** 정의
- 대화 맨 처음 **한 번만** 포함
- 시스템 메시지는 모든 응답에 영향을 줌

**HumanMessage** - 사용자의 질문/요청

from langchain.schema import HumanMessage

msg = HumanMessage(content="Python에서 async/await은 어떻게 작동하나요?")


- 사용자(인간)이 말하는 부분
- 매 턴마다 새로운 입력으로 변함
- 실제 사용자 요청 포함

**AIMessage** - 모델의 이전 응답

from langchain.schema import AIMessage

msg = AIMessage(content="async/await는 비동기 프로그래밍을 위한 문법입니다...")



- 모델이 생성한 이전 응답
- 대화 기록으로 사용
- 멀티턴 대화에서 문맥 제공

**ToolMessage** - 도구 실행 결과

from langchain.schema import ToolMessage

msg = ToolMessage(
    content="검색 결과: Python 3.11은 2024년 5월에 출시됨",
    tool_call_id="search_001"
)


- 도구/함수 실행 결과
- 에이전트 시스템에서 사용
- 모델이 사용한 도구의 응답

### 2.2 메시지 흐름 예시

**고객 서비스 챗봇**

시간 흐름 →

[시스템 설정]
SystemMessage: "당신은 친절한 고객 서비스 담당자입니다. 한국어로 응답하세요."

[첫 번째 턴]
HumanMessage: "배송된 제품이 손상되었어요."
        ↓
[AI 응답 생성]
AIMessage: "죄송합니다. 새 제품으로 교환해드리겠습니다."

[두 번째 턴]
HumanMessage: "언제 도착할까요?"
ChatHistory: [
  HumanMessage("배송된 제품이 손상되었어요."),
  AIMessage("죄송합니다. 새 제품으로 교환해드리겠습니다."),
  HumanMessage("언제 도착할까요?")
]
        ↓
[AI가 문맥을 이해하고 응답]
AIMessage: "3-5일 이내에 배송됩니다."

---

## 3. 템플릿 (Template) 개념

### 3.1 왜 템플릿이 필요한가?

**문제 상황:**

```python
# ❌ 반복되는 코드
msg1 = HumanMessage(content="Python에서 비동기 프로그래밍은 어떻게 하나요?")
msg2 = HumanMessage(content="JavaScript에서 비동기 프로그래밍은 어떻게 하나요?")
msg3 = HumanMessage(content="Go에서 비동기 프로그래밍은 어떻게 하나요?")
# ... 계속 반복

# 패턴: "{language}에서 비동기 프로그래밍은 어떻게 하나요?"
```



**해결책: 템플릿 사용**

```python
# 한 번의 정의로 재사용
template = "사용자 질문: {language}에서 비동기 프로그래밍은 어떻게 하나요?"

# 변수를 바꿔가며 메시지 생성
msg1 = create_message(template, language="Python")
msg2 = create_message(template, language="JavaScript")
msg3 = create_message(template, language="Go")
```



### 3.2 LangChain 템플릿의 층계

```
┌─────────────────────────────────────────────┐
│  고수준 인터페이스                            │
│  ChatPromptTemplate.from_messages()         │
└────────────────────┬────────────────────────┘
                     ↓
┌─────────────────────────────────────────────┐
│  메시지 템플릿들 (MessagePromptTemplate)    │
│  ├─ SystemMessagePromptTemplate             │
│  ├─ HumanMessagePromptTemplate              │
│  ├─ AIMessagePromptTemplate                 │
│  └─ ToolMessagePromptTemplate               │
└────────────────────┬────────────────────────┘
                     ↓
┌─────────────────────────────────────────────┐
│  기본 템플릿                                 │
│  PromptTemplate (변수 감지, 포맷팅)        │
└────────────────────┬────────────────────────┘
                     ↓
┌─────────────────────────────────────────────┐
│  문자열 패턴 매칭                            │
│  Python str.format() 문법                   │
│  "문자 {variable} 문자" → {variable} 감지  │
└─────────────────────────────────────────────┘
```

---

# II. 프롬프트 템플릿 (Prompt Template)

## 1. PromptTemplate (일반 텍스트)

### 1.1 정의

- **일반 LLM 또는 텍스트 생성 모델**을 위한 템플릿
- 변수가 포함된 문자열
- 포맷팅 후 일반 문자열 반환

### 1.2 생성 방식

**방식 1: from_template() 사용 (권장)**

```python
from langchain_core.prompts import PromptTemplate

# 템플릿 문자열에서 변수 자동 감지
prompt = PromptTemplate.from_template(
    "'{topic}'에 대해 {sentences}문장으로 설명해주세요."
)

print(prompt.input_variables)  # ['topic', 'sentences']

# 포맷팅
formatted = prompt.format(topic="블랙홀", sentences="3")
print(formatted)
# 출력: '블랙홀'에 대해 3문장으로 설명해주세요.
```



**방식 2: 직접 생성**

```python
from langchain_core.prompts import PromptTemplate

# 명시적으로 변수 지정
prompt = PromptTemplate(
    input_variables=["topic", "sentences"],
    template="'{topic}'에 대해 {sentences}문장으로 설명해주세요."
)
```



### 1.3 주요 메서드

```python
prompt = PromptTemplate.from_template(
    "'{topic}'을 {language}로 설명해주세요."
)

# 1. 변수 확인
print(prompt.input_variables)  # ['topic', 'language']

# 2. 포맷팅 (문자열 반환)
text = prompt.format(topic="API", language="초보자용으로")
print(type(text))  # <class 'str'>

# 3. 부분 포맷팅 (일부 변수만)
partial = prompt.partial(language="초보자용으로")
text2 = partial.format(topic="데이터베이스")
```

---

## 2. ChatPromptTemplate (채팅 모델용)

### 2.1 정의

- **채팅 모델(ChatGPT, Claude 등)**을 위한 메시지 기반 템플릿
- 여러 메시지 역할을 조합
- 메시지 객체 리스트 반환

### 2.2 작동 원리

```python
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {role}입니다."),
    ("human", "{question}")
])

# 내부 구조
# prompt.messages = [
#   SystemMessagePromptTemplate(...),
#   HumanMessagePromptTemplate(...)
# ]

# 포맷팅
messages = prompt.format_messages(
    role="미슐랭 저지",
    question="서울의 최고 식당은?"
)

# 결과: [SystemMessage, HumanMessage] 객체 리스트
```



### 2.3 생성 방식 비교

| 방식 | 사용 시기 | 코드 |
|------|---------|------|
| **from_template()** | 간단한 프롬프트 | `ChatPromptTemplate.from_template("안녕 {name}")` |
| **from_messages()** | 여러 메시지 역할 필요 | `ChatPromptTemplate.from_messages([...])` |
| **from_role_strings()** | 커스텀 역할 | `ChatPromptTemplate.from_role_strings(...)` |

---

## 3. MessagePromptTemplate 계층구조

### 3.1 클래스 관계

```
┌─────────────────────────────────┐
│   Runnable                       │
│   (LCEL 인터페이스)              │
└─────────────┬───────────────────┘
              ↑
┌─────────────────────────────────┐
│   BasePromptTemplate             │
│   (기본 템플릿 기능)             │
└─────────────┬───────────────────┘
              ↑
┌─────────────────────────────────┐
│   StringPromptTemplate           │
│   (문자열 기반 템플릿)           │
└─────────────┬───────────────────┘
              ↑
┌─────────────────────────────────┐
│   PromptTemplate                 │
│   (일반 텍스트 템플릿)           │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│   BaseMessagePromptTemplate      │
│   (메시지 템플릿 기본)           │
└─────────────┬───────────────────┘
              ↑
    ┌─────────┴─────────┬─────────────┐
    ↓                   ↓             ↓
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ System...   │  │ Human...    │  │ AI...       │
│ Template    │  │ Template    │  │ Template    │
└─────────────┘  └─────────────┘  └─────────────┘
```

---

## 4. 메시지 템플릿들

### 4.1 종류별 정리

| 템플릿 | 용도 | 반환 | 사용 빈도 |
|--------|------|------|---------|
| **SystemMessagePromptTemplate** | 시스템 지시사항 | SystemMessage | 높음 |
| **HumanMessagePromptTemplate** | 사용자 입력 | HumanMessage | 높음 |
| **AIMessagePromptTemplate** | 이전 AI 응답 | AIMessage | 중간 |
| **ChatMessagePromptTemplate** | 커스텀 역할 | ChatMessage | 낮음 |
| **ToolMessagePromptTemplate** | 도구 결과 | ToolMessage | 낮음 |

---

# III. 메시지 템플릿 상세

## 1. SystemMessagePromptTemplate

### 1.1 정의 및 목적

- 모델의 **성격, 역할, 행동 규칙** 정의
- 대화 맨 처음에 **한 번만** 설정
- 모든 응답에 영향을 미치는 지시사항

### 1.2 생성 방식

**from_template() 사용**

```python
from langchain_core.prompts import SystemMessagePromptTemplate

template = SystemMessagePromptTemplate.from_template(
    "당신은 {language} 전문가입니다. {tone} 톤으로 답변하세요."
)

message = template.format(
    language="Python",
    tone="친절하고 상세한"
)

print(message)
# SystemMessage(content='당신은 Python 전문가입니다. 친절하고 상세한 톤으로 답변하세요.')
```


### 1.3 실전 예제

**예제 1: 고객 서비스**

```python
from langchain_core.prompts import SystemMessagePromptTemplate

system = SystemMessagePromptTemplate.from_template("""
당신은 {company}의 고객 서비스 담당자입니다.

성격:
- 친절하고 전문적
- {language}로 응답
- 최대 {max_sentences}문장으로 답변

규칙:
1. 항상 감사로 시작
2. 구체적 솔루션 제시
3. 추가 도움 여부 확인
""")

message = system.format(
    company="TechStore",
    language="한국어",
    max_sentences="3"
)
```



**예제 2: 코드 리뷰어**

```python
system = SystemMessagePromptTemplate.from_template("""
당신은 {years}년 경력의 {language} 엔지니어입니다.

평가 기준:
- 코드 품질: {quality_focus}
- 성능: {performance_focus}  
- 보안: {security_focus}

응답 형식: 장점 → 개선점 → 제안
""")

message = system.format(
    years="10",
    language="Python",
    quality_focus="가독성",
    performance_focus="최적화",
    security_focus="SQL 인젝션 방지"
)
```



### 1.4 변수 관리

```python
system = SystemMessagePromptTemplate.from_template(
    "당신은 {role}입니다. {context}를 고려하여 답변하세요."
)

# 변수 확인
print(system.input_variables)  # ['role', 'context']

# 부분 포맷팅
partial = system.partial(role="미슐랭 저지")
message = partial.format(context="한식")
```

---

## 2. HumanMessagePromptTemplate

### 2.1 정의 및 목적

- 사용자(인간)의 **질문, 요청** 정의
- 매 턴마다 **다른 입력**을 받음
- 동적 사용자 입력 처리

### 2.2 생성 방식

**from_template() 사용**

```python
from langchain_core.prompts import HumanMessagePromptTemplate

template = HumanMessagePromptTemplate.from_template(
    "다음 텍스트를 {target_language}로 번역해주세요:\n{text}"
)

message = template.format(
    target_language="일본어",
    text="안녕하세요, 만나서 반갑습니다!"
)

print(message)
# HumanMessage(content='다음 텍스트를 일본어로 번역해주세요:\n안녕하세요, 만나서 반갑습니다!')
```


### 2.3 실전 예제

**예제 1: 코드 분석**

```python
human = HumanMessagePromptTemplate.from_template("""
프로그래밍 언어: {language}
코드:
```
{code}
```

질문: {question}
요청: {requirements}
""")

message = human.format(
    language="Python",
    code="def add(a, b):\n    return a + b",
    question="이 함수의 시간 복잡도는?",
    requirements="Big O 표기법으로 설명"
)
```



**예제 2: Q&A**

```python
human = HumanMessagePromptTemplate.from_template("""
주제: {topic}
난이도: {level}
질문: {question}
""")

message = human.format(
    topic="머신러닝",
    level="초급",
    question="신경망이란?"
)
```



### 2.4 SystemMessagePromptTemplate과의 관계

```python
# 둘 다 필요!
system = SystemMessagePromptTemplate.from_template(
    "당신은 전문가입니다."
)

human = HumanMessagePromptTemplate.from_template(
    "'{topic}'에 대해 설명해줘"
)

# 일부만 사용하면 불완전한 대화가 됨
```


---

## 3. AIMessagePromptTemplate

### 3.1 정의

- 모델의 **이전 응답** 포함
- 멀티턴 대화에서 **문맥 제공**
- 대화 기록 재현

### 3.2 사용 예시

```python
from langchain_core.prompts import AIMessagePromptTemplate

ai = AIMessagePromptTemplate.from_template(
    "나는 {previous_response}라고 답했습니다."
)

message = ai.format(
    previous_response="async/await은 비동기 프로그래밍을 위한 문법입니다"
)
```



### 3.3 대화 흐름에서의 역할

```
[대화 구성]
SystemMessage: "당신은 전문가입니다."
HumanMessage: "첫 질문"
AIMessage: "첫 답변"
        ↓ (시간 경과)
HumanMessage: "후속 질문"
        ↓
AIMessagePromptTemplate으로 이전 답변 재현
        ↓
모델이 문맥을 이해한 후 답변
```


---

## 4. 동적 메시지 삽입 (MessagesPlaceholder)

### 4.1 개념

- **변수 리스트를 메시지로 삽입**
- 대화 기록을 동적으로 추가
- 멀티턴 대화에 필수

### 4.2 문제 상황

```python
# 문제: 대화 기록이 계속 늘어남
messages = [
    SystemMessage("당신은 어시스턴트입니다."),
    HumanMessage("질문 1"),
    AIMessage("답변 1"),
    HumanMessage("질문 2"),
    AIMessage("답변 2"),
    # ... 계속 늘어남
]
```


**해결책:**

```python
# MessagesPlaceholder 사용
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 어시스턴트입니다."),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{current_question}")
])

# 사용
chat_history = [
    HumanMessage("질문 1"),
    AIMessage("답변 1"),
    # ... 동적으로 추가됨
]

messages = prompt.format_messages(
    chat_history=chat_history,
    current_question="새로운 질문"
)
```



### 4.3 실전 예제

```python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.schema import HumanMessage, AIMessage

prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 한국 역사 전문가입니다."),
    MessagesPlaceholder(variable_name="conversation"),
    ("human", "{user_question}")
])

# 대화 기록 준비
conversation = [
    HumanMessage(content="삼국시대는 언제입니까?"),
    AIMessage(content="삼국시대는 기원후 57년부터 668년까지입니다."),
    HumanMessage(content="신라가 이룬 성과는?"),
    AIMessage(content="신라는 당과 연합하여 삼국통일을 이루었습니다.")
]

# 포맷팅
messages = prompt.format_messages(
    conversation=conversation,
    user_question="그 당시 문화 발전은?"
)

# 결과: System → 대화 기록 → 현재 질문 순서
```

 

---

# IV. from_template 메서드

## 1. 작동 원리

### 1.1 개념

- Python `str.format()` 문법 활용
- `{}` 안의 변수명 자동 감지
- 가장 간단한 템플릿 생성 방식

### 1.2 변수 감지 메커니즘

```python
import re

template = "'{topic}'에 대해 {language}로 설명해줘"

# 내부 동작 (단순화)
variables = re.findall(r'\{(\w+)\}', template)
print(variables)  # ['topic', 'language']

# 결과
# input_variables = ['topic', 'language']
# template = "'{topic}'에 대해 {language}로 설명해줘"
```


### 1.3 복수 변수 처리

```python
from langchain_core.prompts import PromptTemplate

# 변수 여러 개
template = PromptTemplate.from_template(
    "{greeting}, {name}! 오늘은 {day}인데 {mood}신가요?"
)

print(template.input_variables)  # ['greeting', 'name', 'day', 'mood']

# 중복 변수
template2 = PromptTemplate.from_template(
    "{name}님, {name}은 좋은 개발자네요."
)

print(template2.input_variables)  # ['name'] - 한 번만 포함
```


---

## 2. 사용 패턴

### 2.1 PromptTemplate.from_template()

```python
from langchain_core.prompts import PromptTemplate

# 패턴: 문자열 템플릿 직접 전달
prompt = PromptTemplate.from_template(
    "다음 {content_type}을 {language}로 정리해줘"
)

text = prompt.format(content_type="블로그 글", language="한국어")
```

### 2.2 SystemMessagePromptTemplate.from_template()

```python
from langchain_core.prompts import SystemMessagePromptTemplate

system = SystemMessagePromptTemplate.from_template(
    "당신은 {role}입니다. {tone} 톤으로 답변하세요."
)

message = system.format(
    role="마케팅 전문가",
    tone="친근하고 활발한"
)
```



### 2.3 HumanMessagePromptTemplate.from_template()

```python
from langchain_core.prompts import HumanMessagePromptTemplate

human = HumanMessagePromptTemplate.from_template(
    "'{title}'의 주요 내용을 {format}으로 요약해줘"
)

message = human.format(
    title="Python 동시성 프로그래밍",
    format="3줄 요약"
)
```



### 2.4 ChatPromptTemplate.from_template()

```python
from langchain_core.prompts import ChatPromptTemplate

# 단순 인터페이스 (내부적으로 human message로 변환)
prompt = ChatPromptTemplate.from_template(
    "다음 {subject}에 대해 설명해줘"
)

# 내부 구조:
# ChatPromptTemplate.from_messages([
#     ("human", "다음 {subject}에 대해 설명해줘")
# ])

messages = prompt.format_messages(subject="FastAPI")
```


---

## 3. from_template() vs 다른 생성 방식

### 3.1 비교표

| 메서드 | 입력 | 용도 | 코드 길이 |
|--------|------|------|---------|
| **from_template()** | 문자열 | 빠른 프로토타입 | 최소 |
| **from_messages()** | 튜플/객체 리스트 | 복잡한 구조 | 중간 |
| **__init__()** | 명시적 파라미터 | 세밀한 제어 | 최대 |

### 3.2 코드 비교

```python
from langchain_core.prompts import PromptTemplate

#  from_template() - 최간단
prompt1 = PromptTemplate.from_template(
    "'{topic}'에 대해 설명해줘"
)

#  __init__() 직접 사용
prompt2 = PromptTemplate(
    input_variables=["topic"],
    template="'{topic}'에 대해 설명해줘"
)

#  from_template() + partial
base = PromptTemplate.from_template(
    "'{topic}'에 대해 {language}로 설명해줘"
)
partial = base.partial(language="초보자용")
```


---

## 4. 주의사항

### 4.1 변수명 규칙

```python
# 올바른 변수명
"{topic}"          # 문자
"{max_count}"      # 언더스코어
"{class_type}"     # 언더스코어

# 잘못된 변수명
"{topic-name}"     # 하이픈 불가
"{123}"            # 숫자로 시작 불가
"{topic name}"     # 공백 불가
```



### 4.2 이스케이프 처리

```python
# 템플릿에서 {} 사용하려면
template = "배열 {array}를 JSON 형식으로: {{\"key\": \"{array}\"}}"
# 이중 중괄호 {{ }}로 이스케이프
```


---

# V. 출력 파싱 (Output Parsing)

## 1. StrOutputParser

### 1.1 정의

- LLM의 응답을 **순수 문자열로 변환**
- `AIMessage` 객체에서 `content` 추출
- 가장 간단한 파서

### 1.2 필요성

```python
from langchain_openai import ChatOpenAI
from langchain.schema import AIMessage

model = ChatOpenAI(model="gpt-4o-mini")
response = model.invoke("안녕하세요")

print(response)
# AIMessage(
#   content='안녕하세요, 무엇을 도와드릴까요?',
#   response_metadata={...},
#   ...
# )

# 문제: 순수 텍스트만 필요한데 객체가 반환됨
print(type(response))  # <class 'langchain_core.messages.ai.AIMessage'>
```



**해결책:**

```python
from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()
text = parser.invoke(response)

print(text)  # "안녕하세요, 무엇을 도와드릴까요?"
print(type(text))  # <class 'str'>
```



### 1.3 기본 사용법

```python
from langchain_core.output_parsers import StrOutputParser

# 파서 생성
parser = StrOutputParser()

# AIMessage 파싱
from langchain.schema import AIMessage
response = AIMessage(content="블랙홀은...")

text = parser.invoke(response)
# 또는
text = parser.parse(response)
```


---

## 2. LCEL 체인에서의 StrOutputParser

### 2.1 개념: LCEL (LangChain Expression Language)

```
입력 → 프롬프트 → 모델 → 파서 → 출력
   [prompt]     [model]  [parser]
```



파이프 연산자 `|`로 연결:

```python
chain = prompt | model | parser
```



### 2.2 완전한 예제

```python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

# 프롬프트 정의
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {expertise} 전문가입니다."),
    ("human", "{question}")
])

# 모델 선택
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

# 파서 선택
parser = StrOutputParser()

# 체인 구성
chain = prompt | model | parser

# 실행
result = chain.invoke({
    "expertise": "천체물리학",
    "question": "블랙홀이란?"
})

print(result)  # 순수 문자열
print(type(result))  # <class 'str'>
```



### 2.3 파이프 흐름 상세

```python
# 내부 동작
result = chain.invoke({"expertise": "천체물리학", "question": "블랙홀이란?"})

# 1단계: prompt.invoke()
#  입력: {"expertise": "천체물리학", "question": "블랙홀이란?"}
#  출력: ChatPromptValue(messages=[...])

# 2단계: model.invoke()
#  입력: [SystemMessage(...), HumanMessage(...)]
#  출력: AIMessage(content="블랙홀은...")

# 3단계: parser.invoke()
#  입력: AIMessage(content="블랙홀은...")
#  출력: "블랙홀은..."
```

---

## 3. 다른 파서들

### 3.1 파서 종류별 비교

| 파서 | 반환 타입 | 사용 사례 | 복잡도 |
|------|---------|---------|--------|
| **StrOutputParser** | `str` | 텍스트 응답 | 낮음 |
| **JsonOutputParser** | `dict` | JSON 데이터 | 중간 |
| **PydanticOutputParser** | Python 객체 | 구조화 데이터 (타입 검증) | 높음 |
| **CommaSeparatedListOutputParser** | `list` | 목록/태그 | 낮음 |
| **XMLOutputParser** | `dict` | XML 데이터 | 중간 |

### 3.2 JsonOutputParser 예제

```python
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field

# 스키마 정의
class Restaurant(BaseModel):
    name: str = Field(description="식당 이름")
    rating: int = Field(description="별점 1-5")

# 파서 생성
parser = JsonOutputParser(pydantic_object=Restaurant)

# 체인 구성
prompt = ChatPromptTemplate.from_template(
    "서울 미슐랭 식당을 JSON 형식으로 반환해줘:\n{format_instructions}"
)

chain = prompt | model | parser

result = chain.invoke({
    "format_instructions": parser.get_format_instructions()
})

# 결과: {"name": "가온", "rating": 3}
print(type(result))  # <class 'dict'>
```



### 3.3 PydanticOutputParser 예제

```python
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

# 스키마 정의 (타입 검증 포함)
class Task(BaseModel):
    title: str = Field(description="작업 제목")
    priority: int = Field(description="1-5 사이의 우선순위")
    due_date: str = Field(description="YYYY-MM-DD 형식")

# 파서 생성
parser = PydanticOutputParser(pydantic_object=Task)

# 형식 지시사항 자동 생성
format_instructions = parser.get_format_instructions()

# 체인
prompt = ChatPromptTemplate.from_template(
    "다음 작업을 JSON으로 파싱해줘:\n{task_text}\n{format_instructions}"
)

chain = prompt | model | parser

result = chain.invoke({
    "task_text": "12월 25일까지 문서 완성 (높은 우선순위)",
    "format_instructions": format_instructions
})

# 결과: Task 객체
print(result.title)  # "문서 완성"
print(result.due_date)  # "2025-12-25"
print(type(result.priority))  # <class 'int'>
```

---

## 4. 에러 처리

### 4.1 OutputParserException

```python
from langchain_core.output_parsers import (
    OutputParserException,
    JsonOutputParser
)

parser = JsonOutputParser()

try:
    result = parser.parse("{'invalid': json}")
except OutputParserException as e:
    print(f"파싱 에러: {e}")
    # 에러 처리 로직
```



### 4.2 재시도 로직

```python
from langchain_core.output_parsers import PydanticOutputParser

parser = PydanticOutputParser(pydantic_object=Task)

# with_retry() 사용
chain = (
    prompt 
    | model.with_retry(
        stop_after_attempt=3,
        wait_random_min=1,
        wait_random_max=3
    ) 
    | parser
)
```


---

# VI. 실전 프로젝트

## 1. 기초 예제: 간단한 번역기

```python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

# 프롬프트
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 전문 번역가입니다."),
    ("human", "{source_language}을 {target_language}로 번역:\n{text}")
])

# 모델
model = ChatOpenAI(model="gpt-4o-mini")

# 파서
parser = StrOutputParser()

# 체인
chain = prompt | model | parser

# 실행
result = chain.invoke({
    "source_language": "English",
    "target_language": "한국어",
    "text": "Hello, how are you?"
})

print(result)
# 출력: "안녕하세요, 어떻게 지내세요?"
```


---

## 2. 중급 예제: 고객 서비스 챗봇

```python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, AIMessage

# 프롬프트 (대화 기록 포함)
prompt = ChatPromptTemplate.from_messages([
    ("system", """당신은 {company} 고객 서비스 담당자입니다.

성격:
- 친절하고 전문적
- {language}로 응답
- 최대 {max_sentences}문장으로 답변

규칙:
1. 감사로 시작
2. 구체적 솔루션 제시
3. 추가 도움 여부 확인"""),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{current_inquiry}")
])

# 모델
model = ChatOpenAI(model="gpt-4o-mini")

# 체인
chain = prompt | model | StrOutputParser()

# 첫 턴
response1 = chain.invoke({
    "company": "TechStore",
    "language": "한국어",
    "max_sentences": "3",
    "chat_history": [],
    "current_inquiry": "배송된 제품이 손상되었어요."
})

print("고객: 배송된 제품이 손상되었어요.")
print(f"챗봇: {response1}\n")

# 두 번째 턴 (대화 기록 포함)
chat_history = [
    HumanMessage(content="배송된 제품이 손상되었어요."),
    AIMessage(content=response1)
]

response2 = chain.invoke({
    "company": "TechStore",
    "language": "한국어",
    "max_sentences": "3",
    "chat_history": chat_history,
    "current_inquiry": "환불은 가능한가요?"
})

print("고객: 환불은 가능한가요?")
print(f"챗봇: {response2}")
```

---

## 3. 고급 예제: 구조화된 데이터 추출

```python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field

# 데이터 모델 정의
class UserInfo(BaseModel):
    name: str = Field(description="사용자 이름")
    email: str = Field(description="이메일 주소")
    age: int = Field(description="나이")
    interests: list[str] = Field(description="관심사 목록")

# 파서 생성
parser = PydanticOutputParser(pydantic_object=UserInfo)

# 프롬프트 (형식 지시사항 포함)
format_instructions = parser.get_format_instructions()

prompt = ChatPromptTemplate.from_template(
    """다음 텍스트에서 사용자 정보를 추출하세요:

텍스트: {text}

{format_instructions}"""
)

# 모델
model = ChatOpenAI(model="gpt-4o-mini")

# 체인
chain = prompt | model | parser

# 실행
result = chain.invoke({
    "text": "내 이름은 김철수이고 이메일은 kim@example.com입니다. 29살이고 Python, 머신러닝, 영화에 관심 있어요.",
    "format_instructions": format_instructions
})

print(result.name)       # "김철수"
print(result.email)      # "kim@example.com"
print(result.interests)  # ["Python", "머신러닝", "영화"]
print(type(result))      # <class '__main__.UserInfo'>
```

---

## 4. 모범 사례

### 4.1 변수 관리

```python
# 좋은 예: 변수 메타데이터와 함께
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {expertise_level} {role}입니다."),
    ("human", "요청: {user_request}\n추가 정보: {context}")
])

# 변수 확인
print(prompt.input_variables)
# ['expertise_level', 'role', 'user_request', 'context']

# 나쁜 예: 변수명이 모호함
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {a} {b}입니다."),
    ("human", "{c}")
])
```



### 4.2 에러 처리

```python
from langchain_core.output_parsers import OutputParserException

try:
    result = chain.invoke(...)
except OutputParserException as e:
    print(f"출력 파싱 에러: {e}")
    # 폴백 로직
    result = fallback_result
except Exception as e:
    print(f"예상 외 에러: {e}")
    # 로깅 및 재시도
```



### 4.3 성능 최적화

```python
# 캐싱 활용
from langchain.caches import InMemoryCache
from langchain.globals import set_llm_cache

set_llm_cache(InMemoryCache())

# 동일한 입력에 대해 캐시 사용
result1 = chain.invoke({"topic": "Python"})
result2 = chain.invoke({"topic": "Python"})  # 캐시에서 반환
```

---

# VII. 문제 해결

## 1. 일반적인 실수

### 1.1 변수 누락

```python
# 실수
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {role}입니다."),
    ("human", "{question}")
])

# 변수를 하나만 전달
messages = prompt.format_messages(role="전문가")
# KeyError: 'question'

# 해결책: 모든 변수 전달
messages = prompt.format_messages(
    role="전문가",
    question="안녕?"
)
```



### 1.2 파서 선택 오류

```python
# 실수: 구조화된 데이터에 StrOutputParser
response = chain.invoke(...)
# "{"name": "Kim", "age": 30}"  ← 문자열
print(type(response))  # <class 'str'>

# 이후 접근 불가
# response.name  # AttributeError

# 해결책: 적절한 파서 사용
from langchain_core.output_parsers import JsonOutputParser
```



### 1.3 메시지 구조 오류

```python
# 실수: from_template()으로 여러 메시지 정의
prompt = ChatPromptTemplate.from_template(
    "system: 당신은 전문가입니다.\nhuman: {question}"
)
# ← 이는 단일 human 메시지로 취급됨

# 해결책: from_messages() 사용
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 전문가입니다."),
    ("human", "{question}")
])
```


---

## 2. 디버깅 기법

### 2.1 변수 확인

```python
# 프롬프트의 변수 확인
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {role}입니다."),
    ("human", "{question}")
])

print("변수:", prompt.input_variables)
# ['role', 'question']

print("메시지 수:", len(prompt.messages))
# 2

print("메시지 타입:")
for msg in prompt.messages:
    print(f"  - {type(msg).__name__}")
```



### 2.2 포맷팅 결과 확인

```python
# 포맷팅 전 확인
messages = prompt.format_messages(
    role="Python 전문가",
    question="async/await이란?"
)

for i, msg in enumerate(messages):
    print(f"\n메시지 {i}:")
    print(f"  역할: {msg.type}")
    print(f"  내용: {msg.content}")
```



### 2.3 체인 단계별 검증

```python
# 각 단계 검증
prompt = ChatPromptTemplate.from_template("...")
model = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()

# 1단계: 프롬프트
prompt_result = prompt.invoke({"topic": "Python"})
print("프롬프트 결과:", type(prompt_result))

# 2단계: 모델
model_result = model.invoke(prompt_result)
print("모델 결과:", type(model_result))

# 3단계: 파서
parser_result = parser.invoke(model_result)
print("파서 결과:", type(parser_result))
```

---

## 3. 성능 최적화

### 3.1 토큰 사용량 모니터링

```python
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")

response = chain.invoke({...})

# 토큰 사용량 확인
if hasattr(response, 'response_metadata'):
    usage = response.response_metadata.get('token_usage', {})
    print(f"입력 토큰: {usage.get('prompt_tokens')}")
    print(f"출력 토큰: {usage.get('completion_tokens')}")
```



### 3.2 프롬프트 단순화

```python
# 복잡한 프롬프트
system = """당신은 매우 유능한 프로그래밍 전문가입니다.
당신은 Python, JavaScript, Java, C++ 등 모든 언어에 능통합니다.
당신은 항상 명확하고 정확한 답변을 제공합니다.
... (200줄 이상)
"""

# 간결한 프롬프트
system = "당신은 {language} 전문가입니다. 명확하고 정확하게 답변하세요."
```



### 3.3 배치 처리

```python
# 여러 요청을 배치로 처리
inputs = [
    {"topic": "Python"},
    {"topic": "JavaScript"},
    {"topic": "Go"}
]

# batch_invoke 사용
results = chain.batch(inputs)

# 또는 async 버전
import asyncio

async def batch_async():
    results = await asyncio.gather(*[
        chain.ainvoke(input_dict) for input_dict in inputs
    ])
    return results
```



---

## 4. 체크리스트

### 4.1 프롬프트 설계

```
☐ 필요한 메시지 역할 파악 (system, human, ai)
☐ 동적 변수 식별 및 명명
☐ 변수명이 명확한지 확인
☐ 템플릿 문법 검증 ({variable} 형식)
☐ 메시지 순서 확인
☐ 변수 개수와 전달 파라미터 일치 확인
```



### 4.2 체인 구성

```
☐ ChatPromptTemplate 또는 PromptTemplate 선택
☐ 모델 선택 (ChatOpenAI, Anthropic 등)
☐ 출력 파서 선택 (StrOutputParser, JsonOutputParser 등)
☐ LCEL로 체인 구성 (prompt | model | parser)
☐ invoke() 파라미터 검증
☐ 출력 타입 확인
```



### 4.3 테스트 및 배포

```
☐ 샘플 입력으로 테스트
☐ 모든 변수 조합 테스트
☐ 에러 케이스 테스트
☐ 출력 형식 검증
☐ 토큰 사용량 모니터링
☐ 성능 측정
☐ 프롬프트 버전 관리
```



---

# VIII. 요약 및 빠른 참조

## 명령어 빠른 참조

```python
# 프롬프트 생성
from langchain_core.prompts import (
    PromptTemplate,
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    MessagesPlaceholder
)

# 출력 파싱
from langchain_core.output_parsers import (
    StrOutputParser,
    JsonOutputParser,
    PydanticOutputParser,
    CommaSeparatedListOutputParser
)

# 모델
from langchain_openai import ChatOpenAI

# 메시지
from langchain.schema import (
    SystemMessage,
    HumanMessage,
    AIMessage,
    ToolMessage
)

# 체인 구성
chain = prompt | model | parser

# 실행
result = chain.invoke({...})
```



## 의사결정 플로우

```
프롬프트 설계 시작
    ↓
메시지 역할 몇 개?
├─ 1개 → ChatPromptTemplate.from_template()
└─ 2개+ → ChatPromptTemplate.from_messages()
    ↓
동적 대화 기록 필요?
├─ 예 → MessagesPlaceholder 추가
└─ 아니오 → 정적 메시지만 사용
    ↓
출력 형식?
├─ 텍스트 → StrOutputParser
├─ JSON → JsonOutputParser
└─ 구조화 데이터 → PydanticOutputParser
    ↓
체인 구성: prompt | model | parser
    ↓
invoke({변수들})로 실행
```


**학습을 마친 후:**

- LangChain 공식 문서: https://python.langchain.com/
- 샘플 프로젝트 구현 시작
- 실제 프로덕션 시스템에 통합
- 프롬프트 버전 관리 및 A/B 테스팅 실시

 

 

 

 

 

 

 

 

댓글