- Authors

- Name
- Youngju Kim
- @fjvbn20031
LLM 애플리케이션 개발 실전 가이드
LLM API를 처음 사용하면 "Hello World"는 금방 만들 수 있지만, 실제 프로덕션 서비스를 개발할 때는 수많은 함정이 있습니다. 스트리밍 구현, 비용 폭탄 방지, 속도 제한 처리, 할루시네이션 최소화... 이 가이드에서는 OpenAI, Anthropic Claude, Google Gemini API를 활용한 실전 LLM 애플리케이션 개발의 모든 것을 다룹니다.
1. LLM API 생태계 개요
주요 API 제공사 비교
| 제공사 | 주요 모델 | 강점 | 약점 |
|---|---|---|---|
| OpenAI | GPT-4o, o1, o3 | 생태계, 함수 호출 | 비용 |
| Anthropic | Claude 3.5, Claude 3.7 | 긴 컨텍스트, 안전성 | 상대적 높은 비용 |
| Gemini 1.5 Pro/Flash | 멀티모달, 1M 컨텍스트 | 한국어 | |
| Mistral AI | Mistral Large/NeMo | 비용 효율 | 생태계 |
| Cohere | Command R+ | 기업용, RAG | 제한된 기능 |
| Together AI | 오픈소스 모델 호스팅 | 오픈소스 모델 접근 | 신뢰성 |
API 비용 비교 (2025년 기준, 1M 토큰당)
# API 비용 비교 (입력/출력 토큰 USD)
api_costs = {
"gpt-4o": {"input": 2.50, "output": 10.00},
"gpt-4o-mini": {"input": 0.15, "output": 0.60},
"claude-3-5-sonnet": {"input": 3.00, "output": 15.00},
"claude-3-haiku": {"input": 0.25, "output": 1.25},
"gemini-1.5-pro": {"input": 1.25, "output": 5.00},
"gemini-1.5-flash": {"input": 0.075, "output": 0.30},
"mistral-large": {"input": 2.00, "output": 6.00},
"mistral-small": {"input": 0.20, "output": 0.60},
}
def estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
"""API 비용 예상 계산"""
if model not in api_costs:
return 0.0
costs = api_costs[model]
input_cost = (input_tokens / 1_000_000) * costs["input"]
output_cost = (output_tokens / 1_000_000) * costs["output"]
return input_cost + output_cost
# 예시: 하루 10,000번 요청, 평균 200 입력 / 500 출력 토큰
daily_cost_gpt4o = estimate_cost("gpt-4o", 200 * 10000, 500 * 10000)
daily_cost_mini = estimate_cost("gpt-4o-mini", 200 * 10000, 500 * 10000)
print(f"GPT-4o 일일 비용: ${daily_cost_gpt4o:.2f}")
print(f"GPT-4o-mini 일일 비용: ${daily_cost_mini:.2f}")
2. OpenAI API 완전 가이드
기본 설정
pip install openai
from openai import OpenAI
import os
# 클라이언트 초기화
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
# 기본값이지만 커스텀 가능
max_retries=3,
timeout=60.0,
)
기본 Chat Completions
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "당신은 친절하고 정확한 한국어 AI 어시스턴트입니다."
},
{
"role": "user",
"content": "파이썬의 장단점을 알려주세요."
}
],
temperature=0.7, # 0: 결정론적, 1: 창의적
max_tokens=1024, # 최대 출력 토큰
top_p=0.9, # nucleus sampling
frequency_penalty=0.0, # 반복 단어 패널티
presence_penalty=0.0, # 새 토픽 도입 패널티
)
print(response.choices[0].message.content)
print(f"사용 토큰 - 입력: {response.usage.prompt_tokens}, 출력: {response.usage.completion_tokens}")
스트리밍 응답
def stream_chat(messages: list, model: str = "gpt-4o") -> str:
"""스트리밍 응답 처리"""
full_response = ""
with client.chat.completions.create(
model=model,
messages=messages,
stream=True,
) as stream:
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)
full_response += delta.content
print() # 줄바꿈
return full_response
# 사용 예시
messages = [{"role": "user", "content": "머신러닝의 종류를 설명해주세요."}]
response = stream_chat(messages)
Function Calling (도구 활용)
import json
from openai import OpenAI
client = OpenAI()
# 도구 정의
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "특정 도시의 현재 날씨를 가져옵니다.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "도시 이름 (예: 서울, 부산)"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "온도 단위"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "웹에서 최신 정보를 검색합니다.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색 쿼리"
},
"num_results": {
"type": "integer",
"description": "반환할 결과 수",
"default": 5
}
},
"required": ["query"]
}
}
}
]
def get_weather(city: str, unit: str = "celsius") -> dict:
"""날씨 API 호출 (실제 구현 필요)"""
# 실제로는 날씨 API 호출
return {
"city": city,
"temperature": 22,
"unit": unit,
"condition": "맑음",
"humidity": 45
}
def search_web(query: str, num_results: int = 5) -> list:
"""웹 검색 (실제 구현 필요)"""
return [{"title": f"검색 결과 {i}", "url": f"https://example.com/{i}"} for i in range(num_results)]
def run_conversation(user_message: str) -> str:
messages = [
{"role": "system", "content": "당신은 날씨와 정보 검색을 도와주는 AI 어시스턴트입니다."},
{"role": "user", "content": user_message}
]
while True:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
messages.append(message)
# 도구 호출이 없으면 응답 반환
if not message.tool_calls:
return message.content
# 도구 호출 처리
for tool_call in message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
if function_name == "get_weather":
result = get_weather(**function_args)
elif function_name == "search_web":
result = search_web(**function_args)
else:
result = {"error": "알 수 없는 함수"}
# 도구 결과 추가
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
# 사용 예시
print(run_conversation("서울과 부산의 현재 날씨를 비교해주세요."))
Structured Outputs (구조화된 출력)
from pydantic import BaseModel
from typing import List, Optional
class ProductReview(BaseModel):
product_name: str
overall_rating: int # 1-5
pros: List[str]
cons: List[str]
summary: str
recommendation: bool
def analyze_review(review_text: str) -> ProductReview:
response = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "제품 리뷰를 분석하여 구조화된 정보로 추출하세요."
},
{
"role": "user",
"content": review_text
}
],
response_format=ProductReview,
)
return response.choices[0].message.parsed
# 사용 예시
review = """
삼성 갤럭시 S24를 2달 동안 사용해봤습니다.
카메라 품질이 정말 훌륭하고 배터리도 하루 종일 쓰기에 충분합니다.
AI 기능들도 실용적으로 잘 활용하고 있어요.
다만 가격이 좀 비싸고 발열이 있을 때가 있습니다.
전반적으로 만족스러운 플래그십 스마트폰이에요.
"""
result = analyze_review(review)
print(f"제품: {result.product_name}")
print(f"평점: {result.overall_rating}/5")
print(f"장점: {', '.join(result.pros)}")
print(f"단점: {', '.join(result.cons)}")
print(f"추천: {'예' if result.recommendation else '아니오'}")
Vision API
import base64
from pathlib import Path
def encode_image(image_path: str) -> str:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
def analyze_image(image_path: str, question: str = "이 이미지를 자세히 설명해주세요.") -> str:
# 로컬 이미지 분석
base64_image = encode_image(image_path)
ext = Path(image_path).suffix.lower().replace('.', '')
media_type = f"image/{ext if ext in ['jpeg', 'png', 'gif', 'webp'] else 'jpeg'}"
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:{media_type};base64,{base64_image}",
"detail": "high" # "low" 또는 "high"
}
},
{
"type": "text",
"text": question
}
]
}
],
max_tokens=1024,
)
return response.choices[0].message.content
# URL 이미지 분석
def analyze_image_url(url: str, question: str) -> str:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": url}},
{"type": "text", "text": question}
]
}
],
)
return response.choices[0].message.content
임베딩 API
def get_embeddings(texts: list, model: str = "text-embedding-3-small") -> list:
response = client.embeddings.create(
input=texts,
model=model,
)
return [item.embedding for item in response.data]
# 의미론적 검색
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
class SemanticSearch:
def __init__(self):
self.documents = []
self.embeddings = None
def index(self, documents: list):
self.documents = documents
self.embeddings = np.array(get_embeddings(documents))
def search(self, query: str, top_k: int = 3) -> list:
query_emb = np.array(get_embeddings([query]))
sims = cosine_similarity(query_emb, self.embeddings)[0]
top_idx = np.argsort(sims)[::-1][:top_k]
return [(self.documents[i], float(sims[i])) for i in top_idx]
3. Anthropic Claude API
기본 설정
pip install anthropic
import anthropic
client = anthropic.Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY"),
)
Messages API
def claude_chat(
user_message: str,
system: str = "You are a helpful assistant.",
model: str = "claude-3-5-sonnet-20241022",
max_tokens: int = 1024,
) -> str:
message = client.messages.create(
model=model,
max_tokens=max_tokens,
system=system,
messages=[
{"role": "user", "content": user_message}
]
)
return message.content[0].text
# 멀티턴 대화
def claude_conversation(messages: list, system: str = None) -> str:
kwargs = {
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 2048,
"messages": messages
}
if system:
kwargs["system"] = system
message = client.messages.create(**kwargs)
return message.content[0].text
스트리밍
def claude_stream(user_message: str, system: str = None) -> str:
full_text = ""
kwargs = {
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 2048,
"messages": [{"role": "user", "content": user_message}]
}
if system:
kwargs["system"] = system
with client.messages.stream(**kwargs) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
full_text += text
print()
return full_text
System Prompt 설계 모범 사례
# 좋은 시스템 프롬프트 구성 요소
system_prompt_template = """
당신은 [역할]입니다.
## 주요 역할
- [역할 1]
- [역할 2]
## 행동 지침
1. [지침 1]
2. [지침 2]
## 제약 사항
- [제약 1]
- [제약 2]
## 응답 형식
- [형식 지침]
"""
# 한국어 고객 지원 봇 예시
cs_system_prompt = """
당신은 한국의 전자상거래 플랫폼 "쇼핑몰"의 고객 지원 AI 상담원입니다.
## 주요 역할
- 주문 상태 조회 및 안내
- 환불 및 교환 정책 안내
- 배송 관련 문의 처리
- 제품 정보 제공
## 행동 지침
1. 항상 정중하고 친절한 말투를 사용하세요.
2. 질문에 대한 답을 모를 경우 솔직히 인정하고 전문 상담원 연결을 안내하세요.
3. 개인정보(주소, 카드번호 등)를 요청하지 마세요.
4. 경쟁사 비교 질문에는 중립적으로 답변하세요.
## 제약 사항
- 회사 내부 정보나 직원 개인 정보를 공개하지 마세요.
- 법적 조언이나 의료 정보를 제공하지 마세요.
- 확인되지 않은 프로모션이나 할인 정보를 약속하지 마세요.
## 응답 형식
- 간결하고 명확하게 답변하세요.
- 필요시 번호 목록을 사용해 단계별로 안내하세요.
- 마지막에 추가 도움이 필요한지 물어보세요.
"""
Extended Thinking (Claude 3.7+)
def claude_think(question: str, budget_tokens: int = 8000) -> dict:
"""Extended Thinking으로 복잡한 추론 문제 해결"""
response = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=16000,
thinking={
"type": "enabled",
"budget_tokens": budget_tokens # 내부 추론에 사용할 최대 토큰
},
messages=[{"role": "user", "content": question}]
)
result = {"thinking": "", "answer": ""}
for block in response.content:
if block.type == "thinking":
result["thinking"] = block.thinking
elif block.type == "text":
result["answer"] = block.text
return result
# 복잡한 수학 문제
problem = """
한 회사의 연간 매출이 매년 15% 씩 증가합니다.
현재 매출이 100억 원이라면, 5년 후 매출이 200억 원을 넘는지 계산하고
정확한 5년 후 매출액도 계산해주세요.
"""
result = claude_think(problem)
print("추론 과정 (일부):", result["thinking"][:300])
print("\n최종 답변:", result["answer"])
Tool Use (Claude)
import anthropic
import json
client = anthropic.Anthropic()
# 도구 정의
tools = [
{
"name": "calculate",
"description": "수학 계산을 수행합니다.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "계산할 수식 (예: 2 + 2, 15 * 30)"
}
},
"required": ["expression"]
}
}
]
def process_tool_call(tool_name: str, tool_input: dict) -> str:
if tool_name == "calculate":
try:
# 실제 프로덕션에서는 eval 대신 안전한 계산기 사용
result = eval(tool_input["expression"])
return str(result)
except Exception as e:
return f"오류: {str(e)}"
return "알 수 없는 도구"
def claude_with_tools(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
# 도구 호출 없으면 최종 응답 반환
if response.stop_reason == "end_turn":
return response.content[0].text
# 도구 호출 처리
tool_results = []
for content_block in response.content:
if content_block.type == "tool_use":
result = process_tool_call(content_block.name, content_block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": content_block.id,
"content": result
})
# 메시지 기록 업데이트
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
Prompt Caching (비용 절감)
# 긴 시스템 프롬프트나 문서를 캐시해 비용 절감
# 첫 호출 후 동일한 프롬프트는 90% 할인
long_document = "..." * 1000 # 긴 문서
def analyze_document_with_cache(question: str) -> str:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=[
{
"type": "text",
"text": "다음 문서를 분석하고 질문에 답변하세요:",
"cache_control": {"type": "ephemeral"} # 캐시 지시자
},
{
"type": "text",
"text": long_document,
"cache_control": {"type": "ephemeral"}
}
],
messages=[{"role": "user", "content": question}]
)
# 캐시 사용 여부 확인
usage = response.usage
print(f"입력 토큰: {usage.input_tokens}")
print(f"캐시 생성 토큰: {getattr(usage, 'cache_creation_input_tokens', 0)}")
print(f"캐시 읽기 토큰: {getattr(usage, 'cache_read_input_tokens', 0)}")
return response.content[0].text
4. Google Gemini API
기본 설정
pip install google-generativeai
import google.generativeai as genai
import os
genai.configure(api_key=os.environ.get("GOOGLE_API_KEY"))
# 모델 초기화
model = genai.GenerativeModel(
model_name="gemini-1.5-flash",
generation_config=genai.GenerationConfig(
temperature=0.7,
top_p=0.9,
top_k=40,
max_output_tokens=2048,
),
system_instruction="당신은 도움이 되는 한국어 AI 어시스턴트입니다."
)
기본 사용
# 텍스트 생성
response = model.generate_content("Python의 비동기 프로그래밍을 설명해주세요.")
print(response.text)
# 채팅 세션
chat = model.start_chat(history=[])
response1 = chat.send_message("안녕하세요! 머신러닝에 대해 알고 싶어요.")
print(response1.text)
response2 = chat.send_message("그럼 딥러닝과의 차이점은 무엇인가요?")
print(response2.text)
# 대화 기록 확인
for turn in chat.history:
print(f"{turn.role}: {turn.parts[0].text[:100]}...")
멀티모달 (텍스트 + 이미지)
import PIL.Image
import requests
from io import BytesIO
model_vision = genai.GenerativeModel("gemini-1.5-pro")
# 로컬 이미지
image = PIL.Image.open("chart.png")
response = model_vision.generate_content([
image,
"이 차트를 분석하고 주요 트렌드를 한국어로 설명해주세요."
])
print(response.text)
# URL 이미지
response_url = requests.get("https://example.com/image.jpg")
image_from_url = PIL.Image.open(BytesIO(response_url.content))
response = model_vision.generate_content([
image_from_url,
"이 이미지에서 텍스트를 추출해주세요."
])
# PDF 처리 (Gemini 1.5의 강점)
with open("report.pdf", "rb") as f:
pdf_data = f.read()
response = model_vision.generate_content([
{"mime_type": "application/pdf", "data": pdf_data},
"이 PDF 문서의 핵심 내용을 요약해주세요."
])
print(response.text)
1M 컨텍스트 윈도우 활용
# Gemini 1.5 Pro: 최대 1,000,000 토큰 컨텍스트
# 코드베이스 전체를 한 번에 분석 가능
def analyze_codebase(code_files: dict) -> str:
"""여러 파일로 구성된 코드베이스 분석"""
model_pro = genai.GenerativeModel("gemini-1.5-pro")
content_parts = ["다음 코드베이스를 분석해주세요:\n\n"]
for filename, code in code_files.items():
content_parts.append(f"파일: {filename}\n```\n{code}\n```\n\n")
content_parts.append("아키텍처, 잠재적 버그, 개선 사항을 알려주세요.")
response = model_pro.generate_content(content_parts)
return response.text
# 긴 문서 요약
def summarize_long_document(document: str) -> str:
model_flash = genai.GenerativeModel("gemini-1.5-flash")
# Flash 모델도 1M 토큰 지원, 더 빠르고 저렴
response = model_flash.generate_content(
f"다음 문서를 5개 핵심 포인트로 요약하세요:\n\n{document}"
)
return response.text
구조화된 출력 (JSON Mode)
import json
def extract_structured_data(text: str, schema: dict) -> dict:
model = genai.GenerativeModel(
"gemini-1.5-flash",
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=schema,
)
)
response = model.generate_content(text)
return json.loads(response.text)
# 예시: 이력서에서 정보 추출
resume_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"skills": {"type": "array", "items": {"type": "string"}},
"experience_years": {"type": "number"},
}
}
resume_text = """
홍길동
이메일: hong@example.com
경력: Python 5년, JavaScript 3년, Docker 2년
총 경력: 6년
"""
result = extract_structured_data(
f"이 이력서에서 정보를 추출하세요:\n{resume_text}",
resume_schema
)
print(result)
5. 스트리밍 챗봇 구현
FastAPI 백엔드
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import asyncio
import json
import time
from openai import AsyncOpenAI
app = FastAPI()
client = AsyncOpenAI()
class ChatRequest(BaseModel):
message: str
conversation_id: str = None
model: str = "gpt-4o-mini"
# 대화 기록 저장 (실제로는 Redis나 DB 사용)
conversations = {}
@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
conv_id = request.conversation_id or str(time.time())
# 대화 기록 가져오기
history = conversations.get(conv_id, [])
history.append({"role": "user", "content": request.message})
async def generate():
full_response = ""
try:
stream = await client.chat.completions.create(
model=request.model,
messages=[
{"role": "system", "content": "당신은 도움이 되는 AI 어시스턴트입니다."},
*history
],
stream=True,
max_tokens=2048,
)
async for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response += content
data = json.dumps({
"content": content,
"conversation_id": conv_id,
"done": False
}, ensure_ascii=False)
yield f"data: {data}\n\n"
# 대화 기록 업데이트
history.append({"role": "assistant", "content": full_response})
conversations[conv_id] = history[-20:] # 최근 20개만 유지
# 완료 신호
done_data = json.dumps({
"content": "",
"conversation_id": conv_id,
"done": True
})
yield f"data: {done_data}\n\n"
except Exception as e:
error_data = json.dumps({"error": str(e), "done": True})
yield f"data: {error_data}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
}
)
@app.get("/chat/history/{conversation_id}")
async def get_history(conversation_id: str):
history = conversations.get(conversation_id, [])
return {"conversation_id": conversation_id, "messages": history}
WebSocket 챗봇
from fastapi import WebSocket, WebSocketDisconnect
import asyncio
@app.websocket("/ws/chat")
async def websocket_chat(websocket: WebSocket):
await websocket.accept()
conversation_history = []
try:
while True:
# 메시지 수신
data = await websocket.receive_json()
user_message = data.get("message", "")
if not user_message:
continue
conversation_history.append({
"role": "user",
"content": user_message
})
# 스트리밍 응답 전송
full_response = ""
stream = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "당신은 도움이 되는 AI 어시스턴트입니다."},
*conversation_history[-10:]
],
stream=True,
max_tokens=1024,
)
async for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response += content
await websocket.send_json({
"type": "stream",
"content": content
})
# 완료 신호
await websocket.send_json({"type": "done", "content": full_response})
conversation_history.append({
"role": "assistant",
"content": full_response
})
except WebSocketDisconnect:
print("클라이언트 연결 끊김")
except Exception as e:
await websocket.send_json({"type": "error", "message": str(e)})
6. 대화 메모리 관리
요약 메모리
from openai import OpenAI
import tiktoken
client = OpenAI()
class SummaryMemory:
def __init__(self, model: str = "gpt-4o-mini", max_tokens: int = 3000):
self.model = model
self.max_tokens = max_tokens
self.summary = ""
self.recent_messages = []
self.encoder = tiktoken.encoding_for_model(model)
def count_tokens(self, text: str) -> int:
return len(self.encoder.encode(text))
def add_message(self, role: str, content: str):
self.recent_messages.append({"role": role, "content": content})
total_tokens = sum(self.count_tokens(m['content']) for m in self.recent_messages)
if total_tokens > self.max_tokens:
self._summarize_old_messages()
def _summarize_old_messages(self):
# 절반의 메시지를 요약
messages_to_summarize = self.recent_messages[:len(self.recent_messages)//2]
self.recent_messages = self.recent_messages[len(self.recent_messages)//2:]
conversation_text = "\n".join(
f"{m['role']}: {m['content']}"
for m in messages_to_summarize
)
summary_prompt = f"""
기존 요약: {self.summary}
새 대화:
{conversation_text}
위 대화를 간결하게 요약하세요. 중요한 정보, 결정 사항, 사용자 선호도를 포함하세요.
"""
response = client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": summary_prompt}],
max_tokens=500,
)
self.summary = response.choices[0].message.content
def get_messages_with_context(self) -> list:
messages = []
if self.summary:
messages.append({
"role": "system",
"content": f"이전 대화 요약:\n{self.summary}"
})
messages.extend(self.recent_messages)
return messages
# 사용 예시
memory = SummaryMemory()
def chat_with_memory(user_input: str) -> str:
memory.add_message("user", user_input)
messages_with_context = memory.get_messages_with_context()
messages_with_context.insert(0, {
"role": "system",
"content": "당신은 도움이 되는 AI 어시스턴트입니다."
})
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages_with_context,
max_tokens=1024,
)
assistant_reply = response.choices[0].message.content
memory.add_message("assistant", assistant_reply)
return assistant_reply
7. 비용 최적화 전략
입력 토큰 최적화
import tiktoken
def optimize_prompt(prompt: str, max_tokens: int = 1000) -> str:
"""프롬프트 최적화"""
encoder = tiktoken.encoding_for_model("gpt-4o")
tokens = encoder.encode(prompt)
if len(tokens) <= max_tokens:
return prompt
# 토큰 수 초과 시 잘라내기
truncated_tokens = tokens[:max_tokens]
return encoder.decode(truncated_tokens)
# 불필요한 공백 제거
def clean_prompt(prompt: str) -> str:
import re
# 연속 공백 제거
prompt = re.sub(r' +', ' ', prompt)
# 연속 줄바꿈 최소화
prompt = re.sub(r'\n{3,}', '\n\n', prompt)
return prompt.strip()
# 문서 요약 후 사용
def compress_document(document: str, ratio: float = 0.3) -> str:
"""문서를 요약해 토큰 수 절감"""
response = client.chat.completions.create(
model="gpt-4o-mini", # 요약에는 저렴한 모델 사용
messages=[
{
"role": "user",
"content": f"다음 문서를 원문의 {ratio*100:.0f}% 분량으로 핵심만 요약하세요:\n\n{document}"
}
],
max_tokens=int(len(document.split()) * ratio * 1.5),
)
return response.choices[0].message.content
모델 라우팅 전략
from enum import Enum
class TaskComplexity(Enum):
SIMPLE = "simple"
MEDIUM = "medium"
COMPLEX = "complex"
def classify_task_complexity(query: str) -> TaskComplexity:
"""쿼리 복잡도에 따라 모델 선택"""
# 간단한 규칙 기반 분류
simple_keywords = ["안녕", "날씨", "간단한", "번역", "정의"]
complex_keywords = ["분석", "추론", "코드 작성", "논문", "전략"]
query_lower = query.lower()
if any(kw in query_lower for kw in simple_keywords):
return TaskComplexity.SIMPLE
elif any(kw in query_lower for kw in complex_keywords):
return TaskComplexity.COMPLEX
else:
return TaskComplexity.MEDIUM
def get_optimal_model(complexity: TaskComplexity) -> str:
"""복잡도에 따른 최적 모델 선택"""
model_mapping = {
TaskComplexity.SIMPLE: "gpt-4o-mini", # 빠르고 저렴
TaskComplexity.MEDIUM: "gpt-4o-mini", # 균형적
TaskComplexity.COMPLEX: "gpt-4o", # 고성능
}
return model_mapping[complexity]
def smart_chat(query: str) -> str:
"""복잡도에 따른 스마트 라우팅"""
complexity = classify_task_complexity(query)
model = get_optimal_model(complexity)
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": query}],
max_tokens=1024,
)
print(f"사용 모델: {model} (복잡도: {complexity.value})")
return response.choices[0].message.content
배치 API 활용
import json
def batch_process_documents(documents: list, task: str) -> list:
"""배치 API로 여러 문서 동시 처리 (50% 비용 절감)"""
# 배치 요청 준비
batch_requests = []
for i, doc in enumerate(documents):
batch_requests.append({
"custom_id": f"request-{i}",
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": "gpt-4o-mini",
"messages": [
{"role": "user", "content": f"{task}\n\n{doc}"}
],
"max_tokens": 512
}
})
# JSONL 파일 생성
with open("batch_requests.jsonl", "w", encoding="utf-8") as f:
for request in batch_requests:
f.write(json.dumps(request, ensure_ascii=False) + "\n")
# 배치 파일 업로드
with open("batch_requests.jsonl", "rb") as f:
batch_file = client.files.create(
file=f,
purpose="batch"
)
# 배치 작업 생성
batch = client.batches.create(
input_file_id=batch_file.id,
endpoint="/v1/chat/completions",
completion_window="24h"
)
print(f"배치 ID: {batch.id}, 상태: {batch.status}")
return batch.id
8. 프로덕션 베스트 프랙티스
재시도 로직
import time
import random
from openai import OpenAI, RateLimitError, APITimeoutError, APIConnectionError
class RobustLLMClient:
def __init__(self, max_retries: int = 5, base_delay: float = 1.0):
self.client = OpenAI()
self.max_retries = max_retries
self.base_delay = base_delay
def create_with_retry(self, **kwargs) -> any:
"""지수 백오프(Exponential Backoff)를 사용한 재시도"""
last_exception = None
for attempt in range(self.max_retries):
try:
return self.client.chat.completions.create(**kwargs)
except RateLimitError as e:
# 속도 제한: 더 오래 대기
delay = self.base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f"속도 제한 도달. {delay:.1f}초 후 재시도 ({attempt+1}/{self.max_retries})")
time.sleep(delay)
last_exception = e
except APITimeoutError as e:
# 타임아웃: 빠르게 재시도
delay = self.base_delay * (1.5 ** attempt)
print(f"타임아웃. {delay:.1f}초 후 재시도 ({attempt+1}/{self.max_retries})")
time.sleep(delay)
last_exception = e
except APIConnectionError as e:
# 연결 오류: 네트워크 복구 대기
delay = self.base_delay * (2 ** attempt) + random.uniform(1, 3)
print(f"연결 오류. {delay:.1f}초 후 재시도 ({attempt+1}/{self.max_retries})")
time.sleep(delay)
last_exception = e
except Exception as e:
# 재시도 불가능한 오류 (400, 401 등)
raise e
raise last_exception
# 사용
robust_client = RobustLLMClient()
response = robust_client.create_with_retry(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "안녕하세요"}],
max_tokens=100,
)
로깅과 모니터링
import logging
import time
import uuid
from functools import wraps
from dataclasses import dataclass
import json
# 구조화된 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s'
)
logger = logging.getLogger(__name__)
@dataclass
class LLMCallLog:
request_id: str
model: str
input_tokens: int
output_tokens: int
latency_ms: float
success: bool
error: str = None
estimated_cost_usd: float = 0.0
def log_llm_call(func):
"""LLM API 호출 자동 로깅 데코레이터"""
@wraps(func)
def wrapper(*args, **kwargs):
request_id = str(uuid.uuid4())[:8]
start_time = time.time()
log = LLMCallLog(
request_id=request_id,
model=kwargs.get('model', 'unknown'),
input_tokens=0,
output_tokens=0,
latency_ms=0,
success=False
)
try:
result = func(*args, **kwargs)
log.success = True
log.latency_ms = (time.time() - start_time) * 1000
if hasattr(result, 'usage'):
log.input_tokens = result.usage.prompt_tokens
log.output_tokens = result.usage.completion_tokens
log.estimated_cost_usd = estimate_cost(
log.model,
log.input_tokens,
log.output_tokens
)
logger.info(json.dumps({
"request_id": log.request_id,
"model": log.model,
"input_tokens": log.input_tokens,
"output_tokens": log.output_tokens,
"latency_ms": round(log.latency_ms, 2),
"cost_usd": round(log.estimated_cost_usd, 6),
"success": log.success
}))
return result
except Exception as e:
log.success = False
log.error = str(e)
log.latency_ms = (time.time() - start_time) * 1000
logger.error(json.dumps({
"request_id": log.request_id,
"model": log.model,
"error": log.error,
"latency_ms": round(log.latency_ms, 2),
"success": False
}))
raise
return wrapper
@log_llm_call
def logged_chat(model: str, messages: list, **kwargs) -> any:
return client.chat.completions.create(
model=model,
messages=messages,
**kwargs
)
9. 실전 프로젝트: 코드 리뷰 봇
GitHub 웹훅 설정
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
import hmac
import hashlib
import os
import httpx
from openai import AsyncOpenAI
app = FastAPI()
client = AsyncOpenAI()
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET")
def verify_webhook_signature(payload: bytes, signature: str) -> bool:
"""GitHub 웹훅 서명 검증"""
expected = hmac.new(
GITHUB_WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
async def get_pr_diff(owner: str, repo: str, pr_number: int) -> str:
"""PR의 코드 변경 사항 가져오기"""
async with httpx.AsyncClient() as http_client:
response = await http_client.get(
f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}",
headers={
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3.diff"
}
)
return response.text
async def review_code_with_llm(diff: str, pr_title: str) -> str:
"""LLM으로 코드 리뷰 생성"""
system_prompt = """당신은 경험 많은 시니어 개발자입니다.
PR의 코드 변경 사항을 검토하고 건설적인 피드백을 한국어로 제공하세요.
검토 항목:
1. 버그 가능성
2. 성능 이슈
3. 보안 취약점
4. 코드 가독성
5. 모범 사례 준수 여부
마크다운 형식으로 작성하세요."""
user_prompt = f"""PR 제목: {pr_title}
코드 변경 사항(Diff excerpt):
{diff[:8000]}
위 코드 변경 사항을 리뷰해주세요."""
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
max_tokens=2048,
temperature=0.3,
)
return response.choices[0].message.content
async def post_review_comment(owner: str, repo: str, pr_number: int, review: str):
"""PR에 리뷰 코멘트 작성"""
async with httpx.AsyncClient() as http_client:
await http_client.post(
f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_number}/comments",
json={"body": f"## AI 코드 리뷰\n\n{review}"},
headers={
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
)
async def process_pr_event(payload: dict):
"""PR 이벤트 처리"""
action = payload.get("action")
if action not in ["opened", "synchronize", "reopened"]:
return
pr = payload["pull_request"]
owner = payload["repository"]["owner"]["login"]
repo = payload["repository"]["name"]
pr_number = pr["number"]
pr_title = pr["title"]
print(f"PR #{pr_number} 리뷰 시작: {pr_title}")
try:
diff = await get_pr_diff(owner, repo, pr_number)
review = await review_code_with_llm(diff, pr_title)
await post_review_comment(owner, repo, pr_number, review)
print(f"PR #{pr_number} 리뷰 완료")
except Exception as e:
print(f"PR #{pr_number} 리뷰 실패: {e}")
@app.post("/webhook/github")
async def github_webhook(
request: Request,
background_tasks: BackgroundTasks
):
payload_bytes = await request.body()
signature = request.headers.get("X-Hub-Signature-256", "")
if not verify_webhook_signature(payload_bytes, signature):
raise HTTPException(status_code=401, detail="Invalid signature")
event_type = request.headers.get("X-GitHub-Event")
payload = await request.json()
if event_type == "pull_request":
background_tasks.add_task(process_pr_event, payload)
return {"status": "accepted"}
고급 코드 리뷰 기능
from typing import List, Dict
class CodeReviewBot:
def __init__(self):
self.client = AsyncOpenAI()
async def review_file(self, filename: str, content: str, diff: str) -> Dict:
"""파일별 상세 리뷰"""
response = await self.client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": """코드 리뷰 전문가입니다. JSON 형식으로 피드백을 제공하세요.
각 이슈에 심각도(critical/major/minor)와 제안 사항을 포함하세요."""
},
{
"role": "user",
"content": f"""
파일명: {filename}
변경 내용(Diff excerpt):
{diff}
현재 파일 전체(Excerpt):
{content[:3000]}
이 파일의 코드를 리뷰하고 JSON 형식으로 반환하세요.
형식: {{"issues": [{{"line": 0, "severity": "critical", "message": "", "suggestion": ""}}], "summary": ""}}
"""
}
],
response_format={"type": "json_object"},
temperature=0,
)
import json
return json.loads(response.choices[0].message.content)
def format_review_comment(self, file_reviews: List[Dict]) -> str:
"""리뷰 결과를 마크다운으로 포매팅"""
comment = "## 자동 코드 리뷰 결과\n\n"
critical_count = 0
major_count = 0
minor_count = 0
for file_review in file_reviews:
filename = file_review.get('filename', 'unknown')
review = file_review.get('review', {})
issues = review.get('issues', [])
for issue in issues:
severity = issue.get('severity', 'minor')
if severity == 'critical':
critical_count += 1
elif severity == 'major':
major_count += 1
else:
minor_count += 1
# 요약 섹션
comment += f"### 요약\n"
comment += f"- 치명적(Critical): {critical_count}개\n"
comment += f"- 중요(Major): {major_count}개\n"
comment += f"- 경미(Minor): {minor_count}개\n\n"
# 파일별 상세 내용
for file_review in file_reviews:
filename = file_review.get('filename', 'unknown')
review = file_review.get('review', {})
comment += f"### {filename}\n"
comment += f"{review.get('summary', '')}\n\n"
issues = review.get('issues', [])
if issues:
comment += "**이슈:**\n"
severity_emoji = {'critical': '🔴', 'major': '🟡', 'minor': '🟢'}
for issue in issues:
emoji = severity_emoji.get(issue.get('severity', 'minor'), '⚪')
comment += f"- {emoji} 라인 {issue.get('line', '?')}: {issue.get('message', '')}\n"
if issue.get('suggestion'):
comment += f" - 제안: {issue['suggestion']}\n"
comment += "\n"
return comment
마무리
LLM API를 활용한 애플리케이션 개발에서 핵심 원칙들:
선택 기준:
- 범용 챗봇: GPT-4o-mini (비용 효율) 또는 Claude 3.5 Sonnet (품질)
- 코딩/추론: GPT-4o 또는 Claude 3.7 (Extended Thinking)
- 긴 문서 처리: Gemini 1.5 Pro (1M 컨텍스트)
- 비용 최소화: Gemini 1.5 Flash 또는 gpt-4o-mini
프로덕션 필수 요소:
- 재시도 로직 (지수 백오프)
- 속도 제한 처리
- 구조화된 로깅
- 비용 모니터링
- 스트리밍으로 UX 개선
비용 절감:
- 작은 태스크에는 소형 모델 사용
- 배치 API 활용 (50% 절감)
- Anthropic Prompt Cache 활용 (90% 절감)
- 프롬프트 최적화
LLM API 개발에서 가장 중요한 것은 "올바른 도구를 올바른 용도로 사용하는 것"입니다. 모든 태스크에 GPT-4o를 쓸 필요가 없고, 적절한 모델 라우팅과 캐싱 전략으로 비용을 크게 줄일 수 있습니다.