3. 첫 번째 스트리밍 CLI 채팅 만들기
1장에서는 첫 번째 LLM 호출을 만들고 완전한 응답이 한 번에 나타나는 것을 보았습니다. 2장에서는 에이전틱 AI(agentic AI)의 개념적 토대와 LangChain이 존재하는 이유를 배웠습니다. 이제 실용적인 무언가를 만들 차례입니다. 반응성이 좋고 전문적으로 느껴지는 스트리밍 채팅 애플리케이션을 구축해 봅시다.
스트리밍이 중요한 이유: LLM에 복잡한 질문을 했을 때 완전한 응답을 10~30초 기다리는 경험은 고장 난 것처럼 느껴집니다. 스트리밍은 토큰이 생성되는 대로 나타나게 하여 자연스러운 대화 흐름을 만들어 줍니다. 이 장에서는 스트리밍 출력, 올바른 구성 관리, 디버깅 기능, 견고한 오류 처리를 갖춘 CLI 채팅 애플리케이션을 구축합니다.
만들게 될 것: 이 장이 끝나면 다음을 수행하는 작동하는 chat.py 스크립트를 갖게 됩니다:
- 터미널로 LLM 응답을 토큰 단위로 스트리밍합니다
- 환경 변수에서 API 키를 안전하게 로드합니다
- 적절한 파라미터로 서로 다른 모델 유형(채팅 모델 vs 추론 모델)을 처리합니다
- 실제로 LLM에 무엇이 전송되는지 검사할 수 있는 디버깅 도구를 제공합니다
- 흔한 오류(API 키 누락, 네트워크 실패, 잘못된 입력)를 매끄럽게 처리합니다
3.1) 작업 폴더 만들기 및 패키지 설치
코드를 작성하기 전에, 깔끔한 프로젝트 구조와 올바른 의존성이 필요합니다. 이 섹션에서는 유지보수 가능한 Python 프로젝트의 기반을 마련합니다.
프로젝트 구조
채팅 애플리케이션을 위한 새 디렉터리를 만드세요:
mkdir langchain-chat
cd langchain-chatPython 환경 설정
의존성을 격리하기 위해 가상 환경을 생성하세요:
# 가상 환경 생성
python -m venv venv
# 활성화 (macOS/Linux)
source venv/bin/activate
# 활성화 (Windows)
venv\Scripts\activate왜 가상 환경이 필요한가요? LangChain은 많은 의존성(OpenAI SDK, Pydantic, async 라이브러리 등)을 가집니다. 가상 환경은 다음을 보장합니다:
- 시스템 Python을 깨끗하게 유지합니다
- 서로 다른 프로젝트가 서로 다른 LangChain 버전을 사용할 수 있습니다
- 의존성이 재현 가능합니다(
requirements.txt를 통해)
활성화되면 터미널 프롬프트에 (venv)가 표시될 것입니다.
LangChain 설치
핵심 LangChain 패키지를 설치합니다:
pip install langchain-core==1.2.7 langchain-openai==1.1.7 python-dotenv패키지 구성:
langchain-core: 핵심 추상화(메시지, 프롬프트, 체인(chain), 러너블(runnable))langchain-openai: OpenAI 전용 구현(ChatOpenAI, 임베딩)python-dotenv:.env파일에서 환경 변수를 로드
버전 참고: 이 책은 2026년 1월 기준 LangChain 1.2.x를 사용합니다. 나중에 이 글을 읽는다면 최신 버전을 위해 LangChain 문서를 확인하세요.
설치 확인
모든 것이 정상 작동하는지 확인하기 위해 간단한 테스트를 만드세요:
# test_install.py
try:
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
print("✓ langchain-core: OK")
print("✓ langchain-openai: OK")
print("\n설치 완료!")
except ImportError as e:
print(f"✗ Import 실패: {e}")
print("가상 환경이 활성화되어 있는지 확인하세요.")실행하세요:
python test_install.py예상 출력:
✓ langchain-core: OK
✓ langchain-openai: OK
설치 완료!"설치 완료!"가 보이면 다음 단계로 진행할 준비가 된 것입니다. import 오류가 발생하면 다음을 다시 확인하세요:
- 가상 환경이 활성화되어 있는지(프롬프트에
(venv)가 있는지 확인) - 패키지가 성공적으로 설치되었는지(
pip list실행 시도)
requirements.txt 생성
방금 pip install 명령으로 패키지를 설치했습니다. 이것도 작동하지만 더 나은 방법이 있습니다: requirements.txt 파일입니다. 이는 Python 프로젝트의 표준 관행이며 다음과 같은 이유로 사용됩니다:
왜 requirements.txt를 사용하나요?
- 재현 가능성: 다른 사람(또는 6개월 후의 나)이 정확히 같은 버전의 패키지를 설치할 수 있습니다
- 명확한 의존성 관리: 프로젝트가 어떤 패키지를 필요로 하는지 한눈에 볼 수 있습니다
- 협업 편의성: 팀원들이 동일한 버전을 사용하여 "내 컴퓨터에서는 되는데요?" 문제를 방지합니다
- 자동화: 서버나 CI/CD 파이프라인에서
pip install -r requirements.txt한 줄로 환경을 구성할 수 있습니다
프로젝트 루트에 requirements.txt 파일을 생성하세요:
# requirements.txt
langchain-core==1.2.7
langchain-openai==1.1.7
python-dotenv문법 참고:
==1.2.7은 정확한 버전으로 고정합니다(재현 가능성을 위해 권장)- 버전 지정자가 없으면(
python-dotenv처럼) 최신 안정 버전을 설치합니다 #로 시작하는 줄은 주석입니다
이제 누구나 하나의 명령으로 모든 의존성을 설치할 수 있습니다:
pip install -r requirements.txt이는 각 패키지를 개별적으로 입력하는 것보다 훨씬 낫습니다. 팀원이 프로젝트를 클론하면 다음만 하면 됩니다:
- 가상 환경 생성
pip install -r requirements.txt실행
패키지 이름이나 버전을 기억할 필요가 없습니다. 모든 것이 파일에 있습니다.
프로젝트 구조
이 섹션을 완료한 후, 폴더는 다음과 같아야 합니다:
langchain-chat/
├── venv/ # 가상 환경(git에 커밋하지 마세요)
├── requirements.txt # 의존성 목록
└── test_install.py # 설치 확인 스크립트다음: 섹션 3.2에서는 .env 파일을 사용하여 API 키를 안전하게 로드하는 방법을 보여줍니다.
3.2) .env로 환경 변수 사용하기
API 키는 비밀입니다. 코드에 하드코딩하는 것은 보안 위험입니다(특히 git에 커밋하는 경우). 이 섹션에서는 표준 접근법을 보여줍니다: .env 파일에서 로드되는 환경 변수입니다.
왜 환경 변수를 사용하나요?
하드코딩된 키의 문제:
# ❌ 절대 이렇게 하지 마세요
llm = ChatOpenAI(api_key="sk-proj-abc123...")이 코드를 GitHub에 커밋하면 API 키가 공개됩니다. 누구나 이를 사용해 여러분의 계정에 비용을 발생시키거나, 키가 폐기되게 만들 수 있습니다.
해결책: 비밀 값은 환경 변수에 저장하고, 런타임에 로드합니다.
.env 파일 생성
프로젝트 루트에 .env 파일을 생성하세요:
# .env
OPENAI_API_KEY=sk-proj-your-actual-key-hereAPI 키 받기:
- platform.openai.com/api-keys로 이동합니다
- 새 시크릿 키를 생성합니다
- 즉시 복사합니다(다시 볼 수 없습니다)
.env파일에 붙여넣어sk-proj-your-actual-key-here를 교체합니다
중요한 보안 단계: 다른 작업을 하기 전에, API 키가 git에 커밋되지 않도록 보호하세요.
프로젝트 루트에 .gitignore 파일을 생성하고 다음 줄을 추가하세요:
# .gitignore
venv/
__pycache__/
*.pyc
.env.env 줄은 git이 API 키 파일을 무시하도록 합니다. 이렇게 하면 실수로 비밀 값이 버전 관리에 커밋되는 것을 방지합니다.
현재 프로젝트 구조:
langchain-chat/
├── venv/
├── .env # API 키 (git에서 무시됨)
├── .gitignore # 포함 내용: .env, venv/ 등
├── requirements.txt
└── test_install.py환경 변수 로드
python-dotenv 패키지는 .env 파일을 os.environ으로 로드합니다:
# chat.py
import os
from dotenv import load_dotenv
# .env 파일 로드
load_dotenv()
# 환경 변수 접근
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("환경에서 OPENAI_API_KEY를 찾을 수 없습니다")
print(f"API key loaded: {api_key[:8]}...") # 앞 8자만 표시load_dotenv() 동작 방식:
- 스크립트를 실행하는 위치에서 시작하여
.env파일을 찾습니다 - 각 줄을
KEY=value형식으로 읽습니다 - 각 변수를
os.environ에 추가합니다 - 이미 설정된 변수(예: 호스팅 플랫폼에서 설정한 경우)는 덮어쓰지 않고 기존 값을 유지합니다
LangChain에서 API 키 사용하기
LangChain의 OpenAI 구현체(ChatOpenAI 등)는 os.environ에서 OPENAI_API_KEY를 자동으로 찾습니다:
from langchain_openai import ChatOpenAI
load_dotenv()
# 자동으로 os.environ["OPENAI_API_KEY"]를 사용합니다
llm = ChatOpenAI(model="gpt-4o-mini")LangChain의 규칙: api_key 파라미터 없이 ChatOpenAI()를 생성하면, 자동으로 환경에서 OPENAI_API_KEY를 찾습니다. 이는 LangChain 통합 전반에 걸친 표준 패턴입니다.
명시적 API 키(테스트 또는 여러 키 사용 시):
llm = ChatOpenAI(
model="gpt-4o-mini",
api_key=os.environ.get("OPENAI_API_KEY")
)이는 여러 API 키(개발 vs 프로덕션)가 있거나 어떤 키가 사용되는지 명시적으로 하고 싶을 때 유용합니다.
프로덕션에서의 환경 변수
프로덕션 환경(클라우드 플랫폼, Docker 컨테이너)에서는 .env 파일을 사용하지 않습니다. 대신 플랫폼 설정을 통해 환경 변수를 구성합니다:
- Docker: 컨테이너 실행 시
-e플래그 사용 - 클라우드 플랫폼: 구성 대시보드에서 환경 변수 설정
- CI/CD: 비밀 관리 도구 사용
중요한 점: 코드는 변경되지 않습니다. os.environ.get("OPENAI_API_KEY")는 변수가 .env 파일에서 오든 클라우드 플랫폼에서 오든 동일하게 작동합니다. 배포에 대한 자세한 내용은 이후 챕터에서 다룹니다.
설정 확인
모든 것이 제대로 작동하는지 확인하려면, 이 섹션 앞부분에서 보여준 환경 변수 로드 코드를 테스트해볼 수 있습니다. .env 파일이 올바르게 구성되었다면 os.environ.get("OPENAI_API_KEY")가 API 키를 반환할 것입니다.
만약 os.environ.get("OPENAI_API_KEY")가 None을 반환한다면, 다음을 확인하세요:
- 환경 변수에 접근하기 전에
load_dotenv()를 호출했는지 - 프로젝트 루트에
.env가 존재하는지 .env에OPENAI_API_KEY=sk-proj-...가 올바르게 작성되어 있는지- 프로젝트 루트 디렉터리에서 실행하고 있는지
다음: 섹션 3.3에서는 스트리밍 출력을 사용하는 실제 채팅 루프를 구현합니다.
3.3) 스트리밍 출력으로 채팅 루프 구현하기
이제 핵심 채팅 루프를 구축합니다. 이 섹션에서는 스트리밍(streaming)을 소개합니다. 느린 챗봇과 반응형 챗봇을 가르는 핵심 차이입니다.
스트리밍 이해하기
스트리밍 없이(1장 접근):
response = llm.invoke("AI에 대해 500단어 에세이를 작성해줘")
print(response.content) # 20초 기다린 뒤 전체 에세이가 한 번에 표시됩니다스트리밍 사용:
for chunk in llm.stream("AI에 대해 500단어 에세이를 작성해줘"):
print(chunk.content, end="", flush=True) # 토큰이 생성되는 대로 표시됩니다스트리밍이 중요한 이유:
- 즉각적인 피드백: 20초 동안 빈 화면을 보는 대신, 단어가 바로 나타나기 시작합니다
- 자연스러운 대화 느낌: 사람과 대화하는 것처럼 - 응답이 한 번에가 아니라 점진적으로 나옵니다
- 시간과 비용 절약: LLM이 잘못된 답변을 시작하면, 쓸모없는 응답이 완성될 때까지 기다리지 않고 일찍 중단할 수 있습니다
- 더 나은 디버깅: 애플리케이션을 만들 때, 문제(포맷 오류 등)를 오래 기다린 후가 아니라 발생하는 즉시 발견할 수 있습니다
스트리밍이 실제로 무엇인가: 스트리밍은 동일한 응답 텍스트를 점진적으로 전달하는 것입니다. 숨겨진 추론이나 내부 모델 프로세스를 노출하지 않습니다. 단지 API에서 부분 출력이 가능해지는 즉시 보여줄 뿐입니다. 파일 다운로드처럼 생각하면 됩니다. 청크가 도착하는 대로 진행 상황을 보지만, 한 번에 받든 나눠 받든 파일 내용은 같습니다.
청크 경계에 대한 참고사항: 청크는 단어나 문장 경계에 맞춰진다는 보장이 없습니다. API는 효율성을 위해 토큰을 작은 배치로 전송하므로, 청크가 "Hel", "lo! How", " can I", " help you", "?"처럼 올 수 있습니다. 이는 정상이며 예상된 동작입니다. 개별 청크에서 의미를 해석하려고 하지 마세요.
기본 채팅 루프
다음은 최소한의 스트리밍 채팅 루프입니다:
# chat.py
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
def main():
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
print("Chat started. Type 'quit' or 'exit' to stop.\n")
while True:
user_input = input("You: ")
if user_input.lower() in ["quit", "exit"]:
print("Goodbye!")
break
print("Assistant: ", end="", flush=True)
for chunk in llm.stream([HumanMessage(content=user_input)]):
print(chunk.content, end="", flush=True)
print("\n")
if __name__ == "__main__":
main()동작 방식:
while True:: 지속적인 대화를 위한 무한 루프input("You: "): 터미널에서 사용자 입력을 받습니다llm.stream([HumanMessage(...)]): LLM 응답을 스트리밍합니다- 특수 파라미터를 사용한 스트리밍 출력:
end="": 각 청크 뒤에 줄바꿈을 추가하지 않습니다(같은 줄에 출력 유지)flush=True: 버퍼링 없이 터미널로 즉시 출력을 강제합니다
왜 [HumanMessage(content=user_input)]인가요?
LangChain의 채팅 모델은 raw string이 아니라 메시지 목록을 기대합니다. 각 메시지는 역할을 가집니다:
- HumanMessage: 사용자 입력
- AIMessage: LLM 응답
- SystemMessage: LLM에 대한 지시사항(4장에서 다룹니다)
단일 사용자 메시지라도 리스트로 전달합니다: [HumanMessage(content="Hello")].
핵심 제약사항 - 단일 턴 대화: 이 채팅 루프는 의도적으로 상태가 없습니다(stateless). 각 요청은 현재 메시지만 전송하며, 이전 대화 히스토리는 전송하지 않습니다. 이는 다음을 의미합니다:
- LLM은 이전에 무엇을 물었는지 기억하지 못합니다
- "프랑스의 수도는?"을 물은 후 "그 인구는?"과 같은 후속 질문은 작동하지 않습니다
- 이는 LLM의 근본적인 특성입니다 - 명시적으로 컨텍스트를 제공하지 않는 한 메모리가 없습니다
제약사항 예시:
You: What's the capital of France?
Assistant: Paris.
You: What's its population?
Assistant: I don't have enough context. What city are you asking about?while True 루프는 UX 연속성(계속 채팅 가능)을 제공하지만, 각 턴은 독립적입니다. 8장에서 다룰 내용: 메시지 히스토리를 저장하고 각 요청마다 다시 보내는 방식으로 대화 메모리를 구현합니다.
채팅 루프 실행하기
python chat.py예시 상호작용:
Chat started. Type 'quit' or 'exit' to stop.
You: What is LangChain?
Assistant: LangChain is a framework for developing applications powered by language models. It provides tools for prompt management, chains, agents, and memory.
You: Give me a simple example
Assistant: Here's a basic example: ...
You: quit
Goodbye!스트리밍 API 이해하기
"청크(chunk)"란 무엇인가요?
각 청크는 다음을 가진 AIMessageChunk 객체입니다:
content: 생성된 텍스트 토큰response_metadata: 모델 정보, 토큰 수 등
for chunk in llm.stream([HumanMessage(content="Hello")]):
print(f"Chunk: {chunk}")
print(f"Content: {chunk.content}")
print(f"Type: {type(chunk)}")출력:
Chunk: content='Hello' response_metadata={'model_provider': 'openai', ...}
Content: Hello
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>
Chunk: content='!' response_metadata={...}
Content: !
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>
Chunk: content=' How' response_metadata={...}
Content: How
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>전체 응답 누적하기
때로는 완전한 응답이 필요합니다(로깅, 테스트, 추가 처리를 위해):
def chat_with_accumulation():
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
user_input = input("You: ")
full_response = ""
print("Assistant: ", end="", flush=True)
for chunk in llm.stream([HumanMessage(content=user_input)]):
print(chunk.content, end="", flush=True)
full_response += chunk.content
print("\n")
# 이제 완전한 응답을 가지고 있습니다
print(f"[DEBUG] Full response length: {len(full_response)} chars")
return full_response이 패턴은 다음이 필요할 때 흔히 사용됩니다:
- 대화를 데이터베이스에 저장
- 응답을 파싱하여 구조화된 데이터로 변환
- 토큰 사용량 또는 비용 계산
이 섹션 이후의 프로젝트 구조:
langchain-chat/
├── venv/
├── .env
├── .gitignore
├── requirements.txt
├── test_install.py
└── chat.py # 스트리밍 채팅 루프 (새로 추가!)다음: 섹션 3.4에서는 스마트 파라미터 구성으로 다양한 모델 타입을 처리하는 방법을 보여줍니다.
3.4) 스마트 설정: 추론 모델 vs 채팅 모델의 파라미터 처리하기
OpenAI는 서로 다른 기능과 제어 메커니즘을 가진 두 가지 유형의 모델을 제공합니다:
채팅 모델 (gpt-4o, gpt-4o-mini):
- 빠르고 대화에 적합
- 랜덤성과 창의성을 제어하는
temperature지원 - 일반 작업, 창작 글쓰기, 일상적인 코딩에 최적
추론 모델 (o1, o3, GPT-5):
- 더 느리지만 더 논리적이고 일관적
temperature미지원 (대신 내부 추론 사용)- 복잡한 수학, 다단계 계획, 형식적 분석에 최적
핵심 차이점: 채팅 모델은 확률적 샘플링을 사용하며(사용자가 랜덤성 제어), 추론 모델은 결정론적 내부 논리를 사용합니다(모델이 자체 추론 프로세스 제어).
Temperature 이해하기 (채팅 모델만 해당)
Temperature란 무엇인가요?
Temperature는 모델의 응답 방식을 제어하는 0.0에서 2.0 사이의 값입니다. 값이 낮으면 같은 질문에 항상 비슷하게 답하고, 값이 높으면 매번 다르게 창의적으로 답합니다. "창의성 다이얼"처럼 생각하면 됩니다.
작동 방식: 각 단어를 생성할 때, 모델은 서로 다른 확률을 가진 많은 가능한 다음 단어를 봅니다. Temperature는 모델이 선택하는 방식에 영향을 줍니다:
- 낮은 temperature (0.0): 거의 항상 가장 높은 확률의 단어 선택 → 일관되고 집중된 응답
- 높은 temperature (2.0): 낮은 확률의 단어를 선택할 가능성이 더 높음 → 다양하고 창의적인 응답
중요: Temperature는 채팅 모델에서만 작동합니다 (gpt-4o, gpt-4o-mini). 확률적 샘플링 대신 내부 논리를 사용하는 추론 모델(GPT-5, o1, o3)에는 적용되지 않습니다.
Temperature 값 가이드:
-
0.0: 매우 결정론적, 집중적, 일관적
- 사용처: 사실 Q&A, 일상적인 코드 생성, 구조화된 출력
- 같은 입력 → 매번 거의 동일한 출력
- 예시: "2+2는?" → 항상 "4"
-
0.7–1.0: 표준 샘플링 동작 (기본값은 1.0)
- 사용처: 일반 대화, 설명, 균형 잡힌 응답
- 표현과 예시에 적당한 변동
- 예시: "광합성을 설명해주세요" → 매번 다른 표현, 같은 핵심 정보
-
1.2–2.0: 더 창의적이고 다양하며, 덜 예측 가능
- 사용처: 창작 글쓰기, 브레인스토밍, 아이디어 도출
- 톤, 구조, 표현에 높은 변동
- 예시: "달에 대한 시를 써주세요" → 매번 매우 다른 스타일
참고: 1.0 이상의 값은 창의성을 높이지만 사실적 정확성과 일관성을 낮출 수 있습니다. 최대값은 2.0입니다.
예시: Temperature 영향 (채팅 모델만 해당)
# Temperature 0.0 - 결정론적, 매번 같은 답변
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
response = llm.invoke([HumanMessage(content="What is 2+2?")])
print(response.content) # 출력: 4
# Temperature 1.0 - 기본 동작, 약간의 변동 가능
llm = ChatOpenAI(model="gpt-4o-mini", temperature=1.0)
response = llm.invoke([HumanMessage(content="What is 2+2?")])
print(response.content) # 출력: 4 (간단한 설명 포함 가능)정답이 정해진 사실 질문의 경우, temperature는 정확성에 거의 영향을 미치지 않습니다.
개방형이거나 창의적인 작업의 경우, temperature는 다양성, 톤, 스타일에 큰 영향을 미칩니다.
채팅 모델 파라미터를 추론 모델에 사용하면?
모델에 따라 다릅니다 - 일부는 거부하고, 일부는 조용히 무시합니다:
# ❌ o3 모델에서는 실패합니다
llm = ChatOpenAI(model="o3-mini", temperature=0.7)오류:
BadRequestError: Temperature is not supported with this model모델마다 다른 정책:
- o1 / o3 모델: 지원하지 않는 파라미터를 명시적으로 거부합니다. temperature가 포함되면 API가 즉시 400 BadRequest 오류를 반환합니다.
- GPT-5 모델: 더 관대함 - 파라미터가 수락되지만 조용히 무시됩니다. 요청은 성공하지만 temperature는 효과가 없습니다.
왜 중요한가: 사용 중인 모델을 항상 확인하고 그에 맞게 파라미터를 설정하세요. 잘못된 파라미터를 사용하면 오류가 발생하거나 조용히 실패하여 디버깅 시간을 낭비할 수 있습니다.
추론 모델 동작을 제어하는 방법
이제 채팅 모델은 temperature를 사용하고 추론 모델은 사용하지 않는다는 것을 알았습니다. 그렇다면 추론 모델은 어떻게 제어할까요?
추론 모델은 파라미터가 아닌 프롬프트 디자인으로 조정됩니다:
- 추론 모델은
temperature또는 유사한 제어를 노출하지 않습니다 - 대신 프롬프트를 작성하는 방식으로 동작을 안내합니다:
- 명시적 지시사항: "단계별로 생각하세요", "작업을 보여주세요"
- 규칙으로서의 제약: "가정하면 안 됩니다...", "항상 검증하세요..."
- 구조화된 요구사항: "JSON 형식으로 출력", "답변 전에 추론 포함"
- 결정 논리: "조건 A이면 X를 수행, 그렇지 않으면 Y를 수행"
예시: 채팅 파라미터 vs 추론 프롬프트
# ❌ 채팅 방식 - 추론 모델에서 작동하지 않음
llm = ChatOpenAI(model="o3-mini", temperature=0.5)
# 오류: BadRequestError: Temperature is not supported
# ✅ 추론 방식 - 프롬프트 구조로 안내
prompt = """
이 문제를 단계별로 풀어주세요:
1. 알고 있는 것을 진술하세요
2. 계산을 보여주세요
3. 답을 검증하세요
문제: If x + 5 = 12, what is x?
"""
llm = ChatOpenAI(model="o3-mini")
response = llm.invoke([HumanMessage(content=prompt)])
print(response.content)출력:
1. 알고 있는 것: x + 5 = 12
2. 계산: x = 12 - 5 = 7
3. 검증: 7 + 5 = 12 ✓
답: x = 7핵심 인사이트: 채팅 모델은 파라미터로 제어되고, 추론 모델은 프롬프트로 제어됩니다.
모델 선택 결정 테이블
이제 두 유형의 모델을 제어하는 방법을 이해했으니, 각각을 언제 사용할지 알아봅시다:
| 작업 유형 | 권장 모델 | 이유 |
|---|---|---|
| 일반 대화 | gpt-4o-mini | 빠르고 저렴하며 대화에 적합 |
| 간단한 Q&A | gpt-4o-mini | 사실 조회에 충분 |
| 창작 글쓰기 | gpt-4o-mini (temp 0.8–1.0) | Temperature가 창의성 가능하게 함 |
| 코드 생성 | GPT-5 | 더 나은 논리적 계획 |
| 복잡한 추론 | GPT-5 | 다단계 논리에 최적화 |
| 수학 문제 | o3 / o1 | 전용 추론 모델 |
| 다단계 계획 | GPT-5 | 장기 계획에 강함 |
| 형식적 분석 (법률/정책) | o3 | 엄격히 결정론적 |
비용과 지연 시간 트레이드오프
사용 사례에 맞는 올바른 모델을 선택하는 데 도움이 되는 실용적인 트레이드오프를 이해하세요:
| 모델 유형 | 속도 (일반적인 지연 시간) | 비용 (상대적) | 최적 용도 |
|---|---|---|---|
| gpt-4o-mini | 매우 빠름 (<2초) | 매우 낮음 | 일반 대화, 간단한 작업 |
| gpt-4o | 빠름 (1–4초) | 중간 | 높은 품질의 채팅, 멀티모달 작업 |
| GPT-5 | 보통 (3–8초) | 높음 | 복잡한 추론, 계획 |
| o1 / o3 | 가장 느림 (5–15초+) | 가장 높음 | 결정론적 추론, 형식 논리 |
참고:
- 속도는 일반적인 응답 지연 시간을 반영 (프롬프트 길이와 복잡도에 따라 다름)
- 비용은 상대적 비교 - OpenAI 웹사이트에서 현재 가격 확인
- 추론 모델은 일관성과 정확성을 위해 속도와 비용을 희생
- 채팅 모델은 응답성과 효율성 우선
추론 모델을 사용해야 할 때 (GPT-5, o1, o3):
- 올바른 중간 단계가 필요한 다단계 수학 및 STEM(과학·기술·공학·수학) 문제
- 종속성과 제약 조건이 있는 복잡한 논리 분석
- 여러 상호작용하는 원인이 있는 코드 디버깅
- 많은 규칙, 엣지 케이스 또는 트레이드오프가 있는 계획 작업
- 일관성, 검증 및 장기 사고가 필요한 에이전트 워크플로
채팅 모델을 사용해야 할 때 (gpt-4o, gpt-4o-mini):
- 일반 대화 및 대화형 채팅
- 제한된 추론 깊이의 간단한 Q&A
- 콘텐츠 생성 (블로그, 요약, 창작 글쓰기)
- 일상적인 코드 생성 및 보일러플레이트 작업
- 깊은 추론보다 속도와 비용이 더 중요한 애플리케이션
다음: 섹션 3.5에서는 LLM에 실제로 전송되는 내용을 검사하는 디버깅 기법을 보여줍니다.
3.5) 디버깅: 응답과 토큰 사용량 검사하기
LLM이 예상과 다르게 동작할 때는, 무엇이 전송되고 무엇을 받았는지 정확히 봐야 합니다. 이 섹션에서는 LLM 호출을 검사하고 문제를 디버깅하는 방법을 보여줍니다.
디버깅이 중요한 이유
흔한 디버깅 시나리오:
- "왜 LLM이 이런 답을 했지?" → 정확한 프롬프트 확인
- "이 요청 비용이 얼마지?" → 토큰 사용량 확인
- "왜 이렇게 느리지?" → 지연 시간 측정
- "내 메시지 포맷이 맞나?" → 메시지 구조 검사
문제: llm.invoke()를 호출하면 응답 객체를 받습니다. 하지만 실제로 그 안에 무엇이 들어있을까요? 디버깅에 어떤 정보를 사용할 수 있을까요?
응답 객체 이해하기
디버깅하기 전에, llm.invoke()가 무엇을 반환하는지 이해해야 합니다.
기본 구조:
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])
# 응답에 무엇이 들어있나요?
print(type(response)) # AIMessage
print(response.content) # 실제 텍스트
print(response.response_metadata) # 토큰 사용량, 모델 정보 등출력:
<class 'langchain_core.messages.ai.AIMessage'>
Hello! How can I assist you today?
{
'token_usage': {
'completion_tokens': 9,
'prompt_tokens': 8,
'total_tokens': 17
},
'model_name': 'gpt-4o-mini-2024-07-18',
'finish_reason': 'stop',
...
}응답의 주요 부분:
response.content: LLM이 생성한 텍스트response.response_metadata: 다음을 포함하는 딕셔너리:token_usage: 사용된 토큰 수 (비용 계산용)model_name: 응답한 정확한 모델 버전finish_reason: 생성이 중단된 이유 (자세한 내용은 디버그 모드 섹션 참조)
토큰 사용량 접근하기:
token_usage = response.response_metadata['token_usage']
print(f"Prompt tokens: {token_usage['prompt_tokens']}")
print(f"Response tokens: {token_usage['completion_tokens']}")
print(f"Total: {token_usage['total_tokens']}")출력:
Prompt tokens: 8
Response tokens: 9
Total: 17왜 이것이 중요한가: 이 값들은 디버깅, 비용 추적, 프롬프트 최적화에 필요합니다.
토큰 사용량으로 비용 계산하기
토큰 사용량이 비용을 결정합니다. 각 모델은 가격이 다릅니다:
GPT-4o-mini (2026년 1월 기준):
- 입력: 토큰 100만 개당 $0.15
- 출력: 토큰 100만 개당 $0.60
GPT-4o:
- 입력: 토큰 100만 개당 $2.50
- 출력: 토큰 100만 개당 $10.00
비용 계산 함수:
def calculate_cost(token_usage, model_name):
"""토큰 사용량을 기반으로 비용을 계산합니다."""
prompt_tokens = token_usage.get('prompt_tokens', 0)
completion_tokens = token_usage.get('completion_tokens', 0)
# 토큰 100만 개당 가격 (2026년 1월 기준)
pricing = {
'gpt-4o-mini': {'input': 0.15, 'output': 0.60},
'gpt-4o': {'input': 2.50, 'output': 10.00},
'gpt-5': {'input': 1.25, 'output': 10.00},
}
if model_name not in pricing:
return None
input_cost = (prompt_tokens / 1_000_000) * pricing[model_name]['input']
output_cost = (completion_tokens / 1_000_000) * pricing[model_name]['output']
return input_cost + output_cost
# 예시
response = llm.invoke([HumanMessage(content="Explain quantum computing")])
token_usage = response.response_metadata['token_usage']
cost = calculate_cost(token_usage, "gpt-4o-mini")
print(f"Cost: ${cost:.6f}")출력:
Cost: $0.000123왜 이것이 중요한가: 프로덕션 앱은 하루에 50,000건 이상의 요청을 처리할 수 있습니다. 요청당 $0.002면 월 $3,000입니다. 잘못된 모델이나 비대한 프롬프트를 사용하면 비용이 월 $30,000로 급증합니다. 재시도 루프 버그는 하룻밤에 수천 달러를 태울 수 있습니다. 처음부터 토큰 사용량을 추적하세요.
디버그 모드 활성화 (원시 API 세부사항이 필요할 때)
응답 객체와 커스텀 래퍼가 대부분의 디버깅 요구사항을 처리합니다. 하지만 때로는 LangChain이 OpenAI로 전송하는 정확한 내용을 봐야 할 때가 있습니다 - 원시 JSON 요청과 응답을 말이죠.
이것이 필요할 때:
- LangChain의 메시지 포맷팅 디버깅
- API 매개변수가 올바르게 설정되었는지 확인
- 예상치 못한 API 오류 조사
- 정확한 API 페이로드 이해
LangChain은 langchain_core.globals를 통한 내장 디버그 로깅을 제공합니다:
from langchain_core.globals import set_debug
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
set_debug(True)
# 이제 모든 LLM 호출이 디버그 정보를 출력합니다
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])출력:
[llm/start] [llm:ChatOpenAI] Entering LLM run with input:
{
"prompts": [
"Human: Hello"
]
}
[llm/end] [llm:ChatOpenAI] [1.45s] Exiting LLM run with output:
{
"generations": [
[
{
"text": "Hello! How can I assist you today?",
"generation_info": {
"finish_reason": "stop",
"logprobs": null
},
"type": "ChatGeneration",
...
}
]
],
"llm_output": {
"token_usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
...
},
"model_provider": "openai",
"model_name": "gpt-4o-mini-2024-07-18",
...
},
}참고: 출력 형식은 LLM 제공자마다 다릅니다. 이 예시는 OpenAI의 구조를 보여줍니다.
디버그 출력이 보여주는 것:
디버그 모드는 완전한 LangChain → OpenAI 통신 흐름을 보여줍니다:
1. 메시지 형식 변환:
# 작성한 코드
[HumanMessage(content="Hello")]
# 디버그 출력에 표시되는 것
{
"prompts": ["Human: Hello"]
}디버그 모드는 LangChain이 LLM에 전송하기 전에 메시지를 내부적으로 어떻게 표현하는지 보여줍니다.
2. 생성 완료 상태:
"finish_reason": "stop"생성이 종료된 이유:
"stop": 모델이 자연스럽게 응답을 완료함"length": max_tokens 제한에 도달하여 응답이 잘림"tool_calls": 모델이 최종 텍스트 응답 대신 도구 호출 명령을 생성하여 생성을 종료함 (12장)"content_filter": 안전 또는 콘텐츠 조정 규칙으로 인해 응답이 차단되거나 억제됨
"length"가 보이면, 전체 응답을 받기 위해 max_tokens를 늘리세요.
3. 토큰 사용량 세부사항:
"token_usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
"completion_tokens_details": {
"reasoning_tokens": 0 # 추론 모델용 (o1/o3 등)
},
"prompt_tokens_details": {
"cached_tokens": 0 # 프롬프트 캐싱 (비용 절감)
}
}기본 카운트 외에도 다음을 볼 수 있습니다:
- reasoning_tokens: 내부 추론 단계 (추론 모델 전용)
- cached_tokens: 캐시에서 제공된 프롬프트 토큰 수 (비용 절감)
4. 모델 버전과 핑거프린트:
"model_name": "gpt-4o-mini-2024-07-18",
"system_fingerprint": "fp_8bbc38b4db"- model_name: 정확한 스냅샷 버전 (시간이 지나면서 응답이 변하는 이유를 설명)
- system_fingerprint: OpenAI의 백엔드 설정 ID (시스템 업데이트 시 변경됨)
5. 요청 타이밍:
[llm/end] [llm:ChatOpenAI] [1.56s][1.45s]는 총 요청 시간을 보여줍니다—느린 쿼리를 식별하는 데 유용합니다.
다음: 섹션 3.6에서는 일반적인 오류를 유연하게 처리하는 방법을 살펴봅니다.
3.6) 실패 처리 (흔한 오류 시뮬레이션 및 수정)
프로덕션 LLM 애플리케이션은 예측 가능한 실패 모드를 가집니다: 자격 증명 누락, 네트워크 타임아웃, 레이트 리밋, 잘못된 입력. 이 섹션에서는 이러한 오류를 우아하게 처리하고 첫날부터 견고한 애플리케이션을 구축하는 방법을 보여줍니다.
6가지 흔한 오류
1. API 키 누락
언제 발생하나: ChatOpenAI 인스턴스를 생성하려 하지만, 환경에 OPENAI_API_KEY가 설정되지 않았을 때.
예시:
# .env 파일이 없거나, OPENAI_API_KEY가 정의되지 않음
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])보게 될 오류:
OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable해결 방법:
- 프로젝트 루트에
.env파일이 존재하는지 확인하세요 - 키 이름이 정확히
OPENAI_API_KEY인지 확인하세요 (흔한 오타:OPENAPI_KEY) - LLM을 생성하기 전에
load_dotenv()를 호출했는지 확인하세요
2. 잘못된 API 키
언제 발생하나: .env 파일에 유효하지 않거나, 만료되었거나, 잘못 복사된 API 키가 들어있을 때.
예시:
# .env에 다음과 같이 설정됨: OPENAI_API_KEY=sk-invalid-key-12345
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])보게 될 오류:
AuthenticationError: Incorrect API key provided해결 방법:
- https://platform.openai.com/api-keys 로 이동하세요
- 키가 여전히 활성 상태인지 확인하세요 (폐기되거나 만료되지 않았는지)
- 필요하면 새 키를 생성하세요
- 전체 키를 주의깊게 복사하세요 (흔한 실수: 앞뒤 문자 누락)
- 공백 없이
.env에 붙여넣으세요:
OPENAI_API_KEY=sk-proj-exactkeyhere3. 네트워크 실패
언제 발생하나: 인터넷 연결이 끊어지거나, 요청 중 OpenAI 서버에 일시적으로 접근할 수 없을 때.
예시:
# WiFi가 요청 중에 끊어지거나, OpenAI API가 다운됨
response = llm.invoke([HumanMessage(content="Hello")])보게 될 오류:
APIConnectionError: Connection error해결 방법:
- 인터넷 연결을 확인하세요
- OpenAI 상태를 https://status.openai.com 에서 확인하세요
4. 레이트 리밋
언제 발생하나: 짧은 시간에 너무 많은 요청을 보내 API 할당량을 초과할 때.
예시:
# 1000개의 요청을 즉시 전송
for i in range(1000):
llm.invoke([HumanMessage(content=f"Request {i}")])보게 될 오류:
RateLimitError: Rate limit reached for requests해결 방법:
- https://platform.openai.com/account/limits 에서 레이트 리밋을 확인하세요
- 더 높은 제한이 필요하면 플랜을 업그레이드하세요
- 대량 작업에는 배치 처리를 사용하세요 (6장에서 다룸)
5. 잘못된 모델 이름
언제 발생하나: 존재하지 않거나 플랜에서 사용할 수 없는 모델 이름을 지정할 때.
예시:
llm = ChatOpenAI(model="gpt-99-ultra") # 존재하지 않음
response = llm.invoke([HumanMessage(content="Hello")])보게 될 오류:
NotFoundError: The model `gpt-99-ultra` does not exist or you do not have access to it해결 방법:
- https://platform.openai.com/docs/models 에서 플랜에서 사용 가능한 모델을 확인하세요
6. 토큰 제한 초과
언제 발생하나: 프롬프트가 너무 길어서 모델의 최대 컨텍스트 윈도우를 초과할 때.
예시:
# 100만 문자 프롬프트 생성
huge_prompt = "x" * 1_000_000
response = llm.invoke([HumanMessage(content=huge_prompt)])보게 될 오류:
BadRequestError: This model's maximum context length is 128000 tokens. However, your messages resulted in 250000 tokens.해결 방법:
- 전송하기 전에 입력 길이를 확인하세요
- 모델의 제한을 알아두세요:
- gpt-4o-mini: 128K 토큰
- gpt-4o: 128K 토큰
- gpt-5: 400K 토큰
- 긴 문서의 경우, 청킹(chunking) 또는 요약을 사용하세요 (9장에서 다룸)
다음 단계: 4장에서는 프롬프트 엔지니어링을 애플리케이션 코드와 분리하여, 재사용 가능한 프롬프트 템플릿을 설계하는 방법을 살펴봅니다.