๐ค AI-Powered App Stack
ํ๋ก๋์ ๋ ๋ฒจ AI ์ ํ๋ฆฌ์ผ์ด์ ๊ตฌ์ถ์ ์ํ ๊ฒ์ฆ๋ ๊ธฐ์ ์คํ - LangChain, FastAPI, Vector DB๋ก RAG ์์คํ ๊ตฌํ
๐ค AI-Powered App Stack
ํ๋ก๋์ ๋ ๋ฒจ AI ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ๊ฒ์ฆ๋ ๊ธฐ์ ์คํ
"Build AI that scales, not just demos" - ์ค์ ์๋น์ค๋ฅผ ์ํ AI ์ธํ๋ผ ๊ตฌ์ถ
๐ฏ ์ด ์คํ์ด ์ ํฉํ ๊ฒฝ์ฐ
โ ์ถ์ฒํ๋ ๊ฒฝ์ฐ
- RAG (Retrieval-Augmented Generation) ์์คํ ๊ตฌ์ถ
- ๋๊ท๋ชจ ๋ฌธ์ ์ฒ๋ฆฌ์ ์๋ฏธ ๊ฒ์ ํ์
- ๋ฉํฐ LLM ์ง์ (OpenAI, Anthropic, Gemini ๋ฑ)
- ์ค์๊ฐ AI ์๋ต์ด ์ค์ํ ์๋น์ค
- ๋น์ฉ ์ต์ ํ์ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ํ์
โ ๋ค๋ฅธ ์คํ์ ๊ณ ๋ คํด์ผ ํ ๊ฒฝ์ฐ
- ๋จ์ ์ฑ๋ด๋ง ํ์ํ ๊ฒฝ์ฐ โ OpenAI Assistants API
- ์ด๋ฏธ์ง/๋น๋์ค AI๊ฐ ์ค์ฌ์ธ ๊ฒฝ์ฐ โ GPU ํนํ ์คํ
- ์ฃ์ง ๋๋ฐ์ด์ค AI โ TensorFlow Lite / ONNX
๐งฉ ํต์ฌ ๊ตฌ์ฑ์์
AI/ML ๋ ์ด์ด
# ํต์ฌ AI ์คํ ai_stack = { "orchestration": "LangChain", "llm_providers": ["OpenAI", "Anthropic", "Google", "Local LLMs"], "embeddings": "OpenAI Ada-002 / Cohere", "vector_db": "Pinecone / Weaviate", "memory": "Redis", "monitoring": "Langfuse / Helicone" } LangChain์ ๊ฐ์ :
- ๋ค์ํ LLM ํตํฉ ์ง์
- ์ฒด์ธ๊ณผ ์์ด์ ํธ ํจํด
- ๋ฌธ์ ๋ก๋์ ํ ์คํธ ๋ถํ
- ํ๋กฌํํธ ํ ํ๋ฆฟ ๊ด๋ฆฌ
Backend API
# FastAPI ๊ธฐ๋ฐ ๋ฐฑ์๋ from fastapi import FastAPI, BackgroundTasks from langchain.chains import ConversationalRetrievalChain from langchain.memory import ConversationSummaryBufferMemory app = FastAPI() @app.post("/chat") async def chat_endpoint( message: str, session_id: str, background_tasks: BackgroundTasks ): # ๋ฒกํฐ ๊ฒ์ relevant_docs = await vector_store.similarity_search(message) # LLM ์ฒด์ธ ์คํ response = await chain.arun( question=message, chat_history=memory.get(session_id) ) # ๋น๋๊ธฐ ๋ก๊น
background_tasks.add_task(log_interaction, message, response) return {"response": response, "sources": relevant_docs} ๋ฐ์ดํฐ ํ์ดํ๋ผ์ธ
# ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ํคํ
์ฒ pipeline: ingestion: - Document Loaders (PDF, Web, APIs) - Text Splitters (Recursive, Token-based) - Embedding Generation storage: - PostgreSQL: ๋ฉํ๋ฐ์ดํฐ, ์ฌ์ฉ์ ๋ฐ์ดํฐ - Pinecone: ๋ฒกํฐ ์๋ฒ ๋ฉ - S3: ์๋ณธ ๋ฌธ์ - Redis: ์ธ์
๋ฉ๋ชจ๋ฆฌ, ์บ์ฑ processing: - Celery: ๋น๋๊ธฐ ์์
ํ - Apache Airflow: ๋ฐฐ์น ์ฒ๋ฆฌ ์ธํ๋ผ & ๋ฐฐํฌ
# Kubernetes ๋ฐฐํฌ ๊ตฌ์ฑ services: api: replicas: 3 resources: requests: memory: "2Gi" cpu: "1000m" limits: memory: "4Gi" cpu: "2000m" worker: replicas: 5 autoscaling: minReplicas: 2 maxReplicas: 10 targetCPUUtilization: 70 vector-db: type: "managed" # Pinecone Cloud cache: type: "redis-cluster" nodes: 3 ๐ฐ ์์ธ ๋น์ฉ ๋ถ์
| ์๋น์ค | ์ต์ ๊ตฌ์ฑ | ํ์ค ๊ตฌ์ฑ | ์ํฐํ๋ผ์ด์ฆ | |--------|-----------|-----------|--------------| | LLM API | $100/์(GPT-3.5) | $500/์(GPT-4 ํผํฉ) | $2,000+/์(์ ์ฉ ์ธ์คํด์ค) | | Vector DB | $70/์(Pinecone Starter) | $400/์(Standard) | $2,000+/์(Enterprise) | | ์ธํ๋ผ (K8s) | $200/์(3 nodes) | $800/์(5 nodes) | $3,000+/์(10+ nodes) | | ๋ชจ๋ํฐ๋ง | $50/์ | $200/์ | $500+/์ | | PostgreSQL | $25/์ | $100/์ | $500+/์ | | Redis | $25/์ | $100/์ | $300+/์ | | ์ด๊ณ | ~$470/์ | ~$2,100/์ | ~$8,300+/์ |
๐ก ๋น์ฉ ์ต์ ํ ์ ๋ต
# 1. ์บ์ฑ์ผ๋ก API ํธ์ถ ๊ฐ์ @cache(ttl=3600) async def get_embedding(text: str): return await openai.embeddings.create( input=text, model="text-embedding-ada-002" ) # 2. ๋ชจ๋ธ ๋ผ์ฐํ
์ผ๋ก ๋น์ฉ ์ ๊ฐ def route_to_model(query_complexity: float): if query_complexity < 0.3: return "gpt-3.5-turbo" # ๊ฐ๋จํ ์ง๋ฌธ elif query_complexity < 0.7: return "gpt-4" # ์ค๊ฐ ๋ณต์ก๋ else: return "gpt-4-turbo" # ๋ณต์กํ ์ถ๋ก ๐ ๊ตฌํ ๋ก๋๋งต
Phase 1: MVP (2์ฃผ)
# ํ๋ก์ ํธ ์ด๊ธฐํ mkdir ai-app && cd ai-app python -m venv venv source venv/bin/activate # ํต์ฌ ์์กด์ฑ ์ค์น pip install fastapi langchain openai pinecone-client redis # ๊ธฐ๋ณธ API ์๋ฒ uvicorn main:app --reload ๊ตฌํ ๋ชฉํ:
- ๊ธฐ๋ณธ ์ฑํ ์ธํฐํ์ด์ค
- ๋ฌธ์ ์ ๋ก๋ ๋ฐ ์๋ฒ ๋ฉ
- ๊ฐ๋จํ RAG ํ์ดํ๋ผ์ธ
- ์ธ์ ๊ด๋ฆฌ
Phase 2: ํ๋ก๋์ ์ค๋น (2์ฃผ)
# ํ๋ก๋์
์ค์ class ProductionConfig: # LLM ์ค์ LLM_PROVIDER = "openai" LLM_MODEL = "gpt-4-turbo-preview" LLM_TEMPERATURE = 0.7 LLM_MAX_TOKENS = 2000 # ๋ฒกํฐ DB PINECONE_API_KEY = os.getenv("PINECONE_API_KEY") PINECONE_INDEX = "production-index" PINECONE_DIMENSION = 1536 # ์บ์ฑ REDIS_URL = os.getenv("REDIS_URL") CACHE_TTL = 3600 # ๋ชจ๋ํฐ๋ง LANGFUSE_PUBLIC_KEY = os.getenv("LANGFUSE_PUBLIC_KEY") SENTRY_DSN = os.getenv("SENTRY_DSN") Phase 3: ์ค์ผ์ผ๋ง (2-4์ฃผ)
- ๋ก๋ ๋ฐธ๋ฐ์ฑ: ๋ค์ค API ์๋ฒ
- ์์ ํ: Celery + RabbitMQ
- ๋ชจ๋ํฐ๋ง: Prometheus + Grafana
- A/B ํ ์คํ : ํ๋กฌํํธ ์ต์ ํ
Phase 4: ๊ณ ๊ธ ๊ธฐ๋ฅ (4์ฃผ+)
- ํ์ธํ๋: ๋๋ฉ์ธ ํนํ ๋ชจ๋ธ
- ์์ด์ ํธ: ์์จ ์์ ์ํ
- ๋ฉํฐ๋ชจ๋ฌ: ์ด๋ฏธ์ง/์์ฑ ์ฒ๋ฆฌ
- ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ: WebSocket ์ง์
๐๏ธ ์ํคํ ์ฒ ํจํด
RAG ํ์ดํ๋ผ์ธ
class RAGPipeline: def __init__(self): self.embeddings = OpenAIEmbeddings() self.vector_store = Pinecone( index_name="knowledge-base", embedding_function=self.embeddings ) self.llm = ChatOpenAI(temperature=0.7) async def process_query(self, query: str, k: int = 5): # 1. ์ฟผ๋ฆฌ ์๋ฒ ๋ฉ query_embedding = await self.embeddings.aembed_query(query) # 2. ์ ์ฌ ๋ฌธ์ ๊ฒ์ relevant_docs = await self.vector_store.asimilarity_search( query_embedding, k=k ) # 3. ์ปจํ
์คํธ ๊ตฌ์ฑ context = "\n".join([doc.page_content for doc in relevant_docs]) # 4. LLM ์๋ต ์์ฑ prompt = f""" Context: {context} Question: {query} Please provide a comprehensive answer based on the context. """ response = await self.llm.agenerate([prompt]) return { "answer": response.generations[0][0].text, "sources": [doc.metadata for doc in relevant_docs] } ํ๋กฌํํธ ์์ง๋์ด๋ง
# ํ๋กฌํํธ ํ
ํ๋ฆฟ ๊ด๋ฆฌ class PromptManager: templates = { "qa": """You are a helpful AI assistant. Context: {context} Question: {question} Instructions: {instructions} Answer:""", "summarization": """Summarize the following text: {text} Summary (max {max_words} words):""", "extraction": """Extract the following information: {fields} From text: {text} Output as JSON:""" } @classmethod def get_prompt(cls, template_name: str, **kwargs): template = cls.templates.get(template_name) return template.format(**kwargs) ์๋ฌ ์ฒ๋ฆฌ ๋ฐ ํด๋ฐฑ
class LLMService: def __init__(self): self.primary_llm = ChatOpenAI(model="gpt-4") self.fallback_llm = ChatAnthropic(model="claude-2") self.emergency_llm = ChatOpenAI(model="gpt-3.5-turbo") async def get_response(self, prompt: str, max_retries: int = 3): # ๊ธฐ๋ณธ LLM ์๋ try: return await self.primary_llm.agenerate([prompt]) except RateLimitError: # ๋์ฒด LLM์ผ๋ก ํด๋ฐฑ try: return await self.fallback_llm.agenerate([prompt]) except Exception: # ์ต์ข
ํด๋ฐฑ return await self.emergency_llm.agenerate([prompt]) ๐ ๋์ ๋ฐ ๋ณํ
์คํ์์ค ์ฐ์ ์คํ
| ์ปดํฌ๋ํธ | ์์ฉ | ์คํ์์ค ๋์ | |----------|------|---------------| | LLM | OpenAI | Llama 2, Mistral | | Vector DB | Pinecone | Weaviate, Qdrant | | ๋ชจ๋ํฐ๋ง | Datadog | Prometheus + Grafana | | ์ค์ผ์คํธ๋ ์ด์ | LangChain | LlamaIndex, Haystack |
ํด๋ผ์ฐ๋๋ณ ์ต์ ํ
# AWS ์คํ aws: llm: "Bedrock (Claude, Llama)" vector_db: "OpenSearch" compute: "EKS" storage: "S3" monitoring: "CloudWatch" # Azure ์คํ azure: llm: "Azure OpenAI Service" vector_db: "Azure Cognitive Search" compute: "AKS" storage: "Blob Storage" monitoring: "Application Insights" # GCP ์คํ gcp: llm: "Vertex AI (PaLM, Gemini)" vector_db: "Vertex AI Matching Engine" compute: "GKE" storage: "Cloud Storage" monitoring: "Cloud Monitoring" ๐ ์ฑ๊ณต ์ฌ๋ก
Perplexity AI
- ์คํ: Custom LLM + Proprietary Vector Search
- ํน์ง: ์ค์๊ฐ ์น ๊ฒ์ ํตํฉ
- ์ฑ๊ณผ: ์ 1000๋ง+ ์ฟผ๋ฆฌ ์ฒ๋ฆฌ
Jasper AI
- ์คํ: GPT-3/4 + Custom Fine-tuning
- ํน์ง: ๋ง์ผํ ์ฝํ ์ธ ํนํ
- ์ฑ๊ณผ: $1.5B ๋ฐธ๋ฅ์์ด์
Glean
- ์คํ: ๋ฉํฐ LLM + Enterprise Search
- ํน์ง: ๊ธฐ์ ๋ด๋ถ ์ง์ ๊ฒ์
- ์ฑ๊ณผ: $2.2B ๋ฐธ๋ฅ์์ด์
๐ ํ์ ๋ฆฌ์์ค
ํ๋ ์์ํฌ & ๋๊ตฌ
ํ์ต ์๋ฃ
์ปค๋ฎค๋ํฐ
๐ฌ ์ค๋ฌด์ ์กฐ์ธ
"ํ๋ก๋์ ์์ ๊ฐ์ฅ ์ค์ํ ๊ฑด ์๋ฌ ์ฒ๋ฆฌ์์. LLM์ ์์ธก ๋ถ๊ฐ๋ฅํ๊ฒ ์คํจํ๋ฏ๋ก ํญ์ ํด๋ฐฑ ์ ๋ต์ ์ค๋นํ์ธ์." - @aieng_pro
"๋ฒกํฐ DB ์ ํ์ด ์ ๋ง ์ค์ํฉ๋๋ค. ์ด๊ธฐ์ Pinecone์ด ํธํ์ง๋ง, ์ค์ผ์ผ ์ปค์ง๋ฉด ์์ฒด ํธ์คํ ๊ณ ๋ คํ์ธ์." - @vectordb_expert
"ํ ํฐ ๋น์ฉ์ด ์ง์ง ๋ฌธ์ . ์บ์ฑ ์ ๋ต ์์ด๋ ์ ์์ฒ ๋ฌ๋ฌ ๋๊ฐ๋๋ค. Redis๋ก ์๋ฒ ๋ฉ ์บ์ฑํ๋ 70% ์ ๊ฐํ์ด์." - @frugal_ai_dev
"ํ๋กฌํํธ ๋ฒ์ ๊ด๋ฆฌ ๊ผญ ํ์ธ์. Git์ผ๋ก ๊ด๋ฆฌํ๊ณ A/B ํ ์คํธํ๋ฉด ์ฑ๋ฅ ์ฐจ์ด๊ฐ ๋๋์ต๋๋ค." - @prompt_engineer
โก ์ฑ๋ฅ ์ต์ ํ
์๋ต ์๊ฐ ๋จ์ถ
# 1. ์คํธ๋ฆฌ๋ฐ ์๋ต @app.get("/stream") async def stream_response(query: str): async def generate(): async for chunk in llm.astream(query): yield f"data: {chunk}\n\n" return StreamingResponse(generate(), media_type="text/event-stream") # 2. ๋ณ๋ ฌ ์ฒ๋ฆฌ async def parallel_search(query: str): tasks = [ vector_store.asimilarity_search(query), keyword_search(query), cache.get(query) ] results = await asyncio.gather(*tasks) return merge_results(results) ๋น์ฉ ์ต์ ํ
# 3. ์ค๋งํธ ์บ์ฑ class SmartCache: def __init__(self, ttl_matrix): self.ttl_matrix = ttl_matrix # ์ฟผ๋ฆฌ ํ์
๋ณ TTL async def get_or_compute(self, key: str, compute_fn, query_type: str): cached = await redis.get(key) if cached: return json.loads(cached) result = await compute_fn() ttl = self.ttl_matrix.get(query_type, 3600) await redis.setex(key, ttl, json.dumps(result)) return result ๐จ ์ฃผ์์ฌํญ
๋ณด์
- API ํค ๋ ธ์ถ ๋ฐฉ์ง (ํ๊ฒฝ ๋ณ์ ์ฌ์ฉ)
- ํ๋กฌํํธ ์ธ์ ์ ๋ฐฉ์ด
- PII ๋ฐ์ดํฐ ํํฐ๋ง
- Rate limiting ๊ตฌํ
๊ท์ ์ค์
- GDPR/CCPA ๋ฐ์ดํฐ ์ฒ๋ฆฌ
- ์๋ฃ/๊ธ์ต ๋ถ์ผ ๊ท์
- AI ์ค๋ฆฌ ๊ฐ์ด๋๋ผ์ธ
- ๋ฐ์ดํฐ ๋ณด์กด ์ ์ฑ
๋ชจ๋ํฐ๋ง ํ์ ์งํ
- Token ์ฌ์ฉ๋ ๋ฐ ๋น์ฉ
- ์๋ต ์๊ฐ (P50, P90, P99)
- ์๋ฌ์จ ๋ฐ ํด๋ฐฑ ๋น๋
- ์ฌ์ฉ์ ๋ง์กฑ๋ (ํผ๋๋ฐฑ ์์ง)
๋ง์ง๋ง ์ ๋ฐ์ดํธ: 2025-01-28
๊ธฐ์ฌํ๊ธฐ: GitHub์์ ํธ์ง