Multi-LLM Agent 기반 비동기 분산 클러스터로
서비스 수준의 제품 완성도를 목표로 한 고도화 여정
Evolution journey toward production-ready quality
with Multi-LLM Agent based async distributed cluster
Vision LLM과 Multi-Agent로 환경과 일상을 연결하는 에이전트입니다. An agent connecting environment and daily life through Vision LLM and Multi-Agent.
8개의 도메인 API, 6개의 Worker, 3개의 인프라 피쳐를 설계, 구현, 배포, 운영 중입니다. Designing, implementing, deploying, and operating 8 domain APIs, 6 Workers, and 3 infrastructure features.
ADR·장애 로그·도메인 규칙을 Knowledge Base로 축적 → 자체 RAG + 27개 커스텀 Skills로 에이전트 온보딩 가속. Sub agent에 Skills를 주입하여 Task 도구 7,500+ 호출이 관측되며, 병렬 포함 멀티파트 작업을 수행합니다. Anthropic·Vercel 등 공식 벤더 Skills도 적극 활용하며, 설계-구현-테스트-관측-검증 피드백 루프를 구성해 사이클을 운용합니다. Accumulates ADRs, incident logs, and domain rules as Knowledge Base → accelerates agent onboarding with self-RAG + 27 custom Skills. Injects Skills into sub agents with 7,500+ Task tool invocations observed, including parallel multi-part execution. Actively leverages official vendor Skills from Anthropic, Vercel, and others. Operates a design-implement-test-observe-verify feedback loop.
docs/는 ADR·장애 로그·도메인 규칙을 담은 Knowledge Base이고,
.claude/skills/는 27개 커스텀 Skills + Anthropic·Vercel 벤더 Skills를 Sub agent에 주입하여 온보딩을 가속합니다.
Task 도구 7,500+ 호출(8,251 세션)이 관측되며, 병렬 포함 멀티파트 작업을 수행합니다. 설계-구현-테스트-관측-검증 피드백 루프를 구성해 사이클을 운용합니다.
이 체계는 Anthropic Engineering의 Effective Harness 패턴과 구조적으로 대응합니다.
📊 Anthropic Insights →
docs/ is a Knowledge Base of ADRs, incident logs, and domain rules.
.claude/skills/ injects 27 custom Skills + vendor Skills from Anthropic and Vercel into sub agents to accelerate onboarding.
7,500+ Task tool invocations observed across 8,251 sessions, including parallel multi-part execution. Operates a design-implement-test-observe-verify feedback loop.
This system structurally corresponds to Anthropic Engineering's Effective Harness pattern.
📊 Anthropic Insights →
LangGraph Send API로 11종 서브에이전트를 동적 병렬 라우팅하는 멀티에이전트 시스템입니다. 9분류 Intent 분석 후 Multi-Intent Fanout을 통해 "이 페트병 어떻게 분리해? 근처 수거함도 알려줘" 같은 복합 질의를 단일 요청으로 병렬 처리합니다. Eco² 캐릭터 13종과 대한민국 폐기물 분류체계를 도메인 지식으로 주입하고, Nano Banana Pro 기반 이미지 생성, 사용자 위치 연동 실시간 날씨·수거함·재활용센터 검색, 네이티브 웹 검색까지 지원합니다. 3-Tier Memory(Redis hot + PostgreSQL persistent) 위에 Token v2 스트리밍을 구현하여 연결 단절 후 재접속 시에도 토큰 catch-up이 가능합니다. A multi-agent system that dynamically routes across 11 sub-agents in parallel via LangGraph Send API. After 9-class intent classification, Multi-Intent Fanout decomposes compound queries like "How do I separate this PET bottle? Find nearby collection points too" into parallel branches within a single request. Injects domain knowledge (13 Eco² characters, Korean waste classification system), supports Nano Banana Pro-powered image generation, real-time weather and recycling center search linked to user location, and native web search. Built on 3-Tier Memory (Redis hot + PostgreSQL persistent) with Token v2 streaming, enabling token catch-up even after connection drops.
8개 도메인 API(Auth, Character, Chat, Scan, Location, Users, Image, Info)를 개발하고, 장애 격리와 독립 스케일링을 위해 DOMA 원칙 기반 노드 분리를 설계했습니다. Bridge Ingress 단일 진입점으로 ALB → Istio Gateway → VirtualService 토폴로지를 구성하고, North-South(NodePort)와 East-West(ClusterIP/Calico VXLAN) 트래픽을 분리합니다. ArgoCD App-of-Apps + Sync Wave(00~63)로 CRD → Operator → Instance 순서의 선언적 배포를 자동화하고, Helm Chart + Kustomize Overlay로 환경별 오버레이를 관리합니다. Developed 8 domain APIs (Auth, Character, Chat, Scan, Location, Users, Image, Info) and designed DOMA-based node separation for fault isolation and independent scaling. Configured Bridge Ingress single entry point with ALB → Istio Gateway → VirtualService topology, separating North-South (NodePort) and East-West (ClusterIP/Calico VXLAN) traffic. Automated declarative deployment with ArgoCD App-of-Apps + Sync Wave (00~63) following CRD → Operator → Instance order, managing environment overlays with Helm Chart + Kustomize Overlay.
모든 요청의 인증/인가가 통과하는 Global Choke Point를 별도 서버로 분리해 API 서버 부하를 제거했습니다. Python 대비 동시성 처리에 유리한 Go + gRPC를 선택하고, Redis 병목 해결 후 Local Cache + Fanout으로 클러스터 처리량 Baseline을 확보했습니다. Separated Global Choke Point (auth/authz for all requests) into a dedicated server, eliminating API server load. Chose Go + gRPC for better concurrency over Python, established cluster throughput baseline via Local Cache + Fanout after resolving Redis bottleneck.
Agent-Driven Development의 Recursive Self-Improvement를 위해 로그-트레이스-메트릭 통합이 필수였습니다. Helm Chart 대신 ECK Operator를 선택해 ES 클러스터 관리 복잡도를 낮추고, ECS 스키마로 8개 도메인의 로그 포맷을 표준화했습니다. Log-trace-metric integration was essential for Agent-Driven Development's Recursive Self-Improvement. Chose ECK Operator over Helm Charts to reduce ES cluster management complexity, standardized log format across 8 domains with ECS schema.
LLM API 호출(~12초/req)의 HTTP 타임아웃 회피와 동시 접속 부하 분산을 위해 비동기 Job Queue를 도입했습니다. Kafka 대비 운영 복잡도가 낮은 RabbitMQ를 선택하고, I/O-bound 워크로드에 Gevent, CPU-bound에 Thread Pool로 동시성 전략을 분리했습니다. Introduced async Job Queue to avoid HTTP timeouts from LLM API calls (~12s/req) and distribute concurrent load. Chose RabbitMQ over Kafka for lower operational complexity, separated concurrency strategies: Gevent for I/O-bound, Thread Pool for CPU-bound workloads.
LLM 파이프라인을 동기 요청으로 배포한 초기에는, 10+초 API 지연이 scan-api 스레드를 점유해 동시 처리에 한계가 있었습니다. SSE 전환 후에도 Celery Events 구조(SSE당 RabbitMQ 21연결)는 50 VU에서 연결 341개로 폭증, 503 에러가 발생했습니다. Redis Streams 기반 이벤트 릴레이로 연결 복잡도를 O(n×m) → O(n)으로 개선하고, Streams(영속) + Pub/Sub(실시간) + State KV(복구)로 책임을 분리해 500 VU 100% 완료율을 달성했습니다. When initially deploying the LLM pipeline as synchronous requests, 10+s API latency occupied scan-api threads, limiting concurrent processing. After SSE transition, Celery Events structure (21 RabbitMQ connections per SSE) exploded to 341 connections at 50 VU, causing 503 errors. Switched to Redis Streams-based event relay, improved connection complexity from O(n×m) → O(n), separated responsibilities: Streams (persistence) + Pub/Sub (realtime) + State KV (recovery), achieving 500 VU 100% completion.
강결합과 즉시 응답이 필요한 로직(OAuth 플로우 등)을 제외한 모든 영역에 Eventual Consistency를 적용했습니다. Strong Consistency는 Latency를 늘리고, Fire-and-forget은 데이터 유실 위험이 있어, at-least-once + 멱등성 키로 Exactly-once Semantics를 구현했습니다. Application Layer가 Persistence를 Worker에 Offloading하면서 DB 병목 없이 수평 확장이 가능해졌고, API Pod는 빠른 응답만 담당하여 처리량(Throughput)이 크게 향상되었습니다. Applied Eventual Consistency to all areas except tightly-coupled, immediate-response logic (e.g., OAuth flow). Strong consistency increases latency, fire-and-forget risks data loss. Implemented Exactly-once Semantics via at-least-once + idempotency keys. Application Layer offloads persistence to Workers, enabling horizontal scaling without DB bottleneck. API Pods focus solely on fast responses, significantly improving throughput.
Fallback Outbox 패턴 도입 후 Integration+Persistence 레이어가 중첩되며 레이어드 아키텍처의 한계에 직면했습니다. Redis 직접 의존 등 DI 위반을 제거하고, Port/Adapter 기반으로 인프라 교체 용이성과 역할 명확화를 확보했습니다. 도메인별 응집으로 비즈니스 요구사항 변경 시 수정 범위가 단일 도메인으로 제한되어, 사이드 이펙트 추적과 코드 리뷰 부담이 크게 줄었습니다. Port 인터페이스만 Mock하면 외부 인프라 없이 도메인 로직을 독립 검증할 수 있어, 테스트 작성 비용이 낮아지고 병렬 개발이 자연스럽게 가능해졌습니다. 인프라 의존성이 Adapter로 캡슐화되어 특정 컴포넌트 장애가 도메인 경계를 넘어 전파되지 않습니다. After introducing Fallback Outbox pattern, Integration+Persistence layers overlapped, revealing layered architecture limits. Eliminated DI violations (e.g., direct Redis dependency), achieved infrastructure swappability and clear role separation via Port/Adapter. Domain-level cohesion limits change scope to single domain, reducing side-effect tracking and code review burden. Mocking only Port interfaces enables independent domain logic verification without external infra, lowering test costs and enabling parallel development. Infrastructure dependencies encapsulated in Adapters ensure component failures don't propagate beyond domain boundaries.
PWA 기반 프론트엔드와 연동되는 24-Node 분산 클러스터 아키텍처입니다. 24-Node distributed cluster architecture integrated with PWA-based frontend.
| 흐름Flow | 경로Path | 프로토콜Protocol |
|---|---|---|
| 🌐 N-S Traffic | User → Route53 → ALB → Istio GW → ext-authz → APIs | HTTPS, gRPC |
| 🔄 E-W Sync | auth ↔ users, character → users (mTLS Envoy Sidecar) | gRPC |
| 🤖 Scan AI | Scan API → RabbitMQ → scan-worker (Vision→Rule→Answer→Reward) → OpenAI API | AMQP, Celery Chain |
| 💬 Chat Agent | Chat API → chat-worker (Intent→TagRetriever→EvalAgent→Fallback) → YAML inject + OpenAI API |
Taskiq, LangGraph |
| 🎭 Character Batch | scan-worker → character-match → character-worker (batch) → users-worker (UPSERT) → PostgreSQL | AMQP, Batch Insert |
| 📡 SSE Event | Workers → Redis Streams → Event Router → Pub/Sub → SSE GW → Client | XADD, PUBLISH, SSE |
| 🔔 Auth Relay | Auth API → Redis Fallback Outbox → auth-relay → RabbitMQ Fanout → auth-worker → Redis Blacklist | Fallback Outbox |
| 🔄 Cache Broadcast | RabbitMQ Fanout → ext-authz Blacklist + character-match Catalog (all replicas) Local Cache Sync | AMQP Fanout |
| 📊 Metrics | Envoy Sidecars → Prometheus → Grafana / KEDA Autoscaling | Prometheus scrape |
| 📝 Logs | All Pods (stdout) → Fluent Bit DaemonSet → Elasticsearch → Kibana | HTTP (ECS format) |
| 🔍 Traces | Envoy → OTel Collector → Jaeger (trace_id correlation, Kiali viz) | OTLP, Zipkin |
프로젝트에서 활용한 주요 기술들입니다. Key technologies used in the project.
GET /catalog — 캐릭터 카탈로그POST /internal/rewards — 보상 평가GetCharacterReward() — 보상 판정GetDefaultCharacter() — 기본 캐릭터GetCharacterByMatch() — 분류 매칭| 패턴Pattern | 구현Implementation | 설명Description |
|---|---|---|
| Decorator | LocalCachedCatalogReader |
Cache 래핑 → SqlaCharacterReader delegateCache wrapper → SqlaCharacterReader delegate |
| Singleton | CharacterLocalCache |
Double-Checked Locking으로 thread-safeThread-safe via Double-Checked Locking |
| Eager Loading | FastAPI Lifespan | 서버 시작 전 DB에서 캐시 워밍업Cache warmup from DB before server start |
| Eventual Consistency | RabbitMQ Fanout | 수 초 지연 허용, Redis 의존성 제거Accept seconds latency, Redis-independent |
intent:{query_hash}chat:tokens:{job_id}
chat:state:{job_id}┌─────────────────────────────────────────────────────────────────────────────┐ │ Taskiq Worker Architecture │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Command: │ │ taskiq worker chat_worker.main:broker --workers 4 --max-async-tasks 10 │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Worker Pool (4 processes × 10 async = 40 concurrent tasks) │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ │ │ │ │ Worker 1 Worker 2 Worker 3 Worker 4 │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │asyncio │ │asyncio │ │asyncio │ │asyncio │ │ │ │ │ │loop │ │loop │ │loop │ │loop │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 10 tasks│ │ 10 tasks│ │ 10 tasks│ │ 10 tasks│ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ Task Definition: │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ @broker.task(task_name="chat.process", timeout=120, max_retries=2) │ │ │ │ async def process_chat(job_id, session_id, message, image_url=None):│ │ │ │ async with LangGraphApp(...) as app: │ │ │ │ await app.ainvoke(...) │ │ │ │ await event_bus.publish(...) │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
| 항목Aspect | Celery (Scan Worker) | Taskiq (Chat Worker) |
|---|---|---|
| 동시성 모델Concurrency Model | gevent (greenlet) | asyncio (native) |
| LangGraph 호환LangGraph Compat | △ (래핑 필요Wrapping needed) | ✅ Native async |
| BrokerBroker | RabbitMQ | RabbitMQ (AioPikaBroker) |
| 결과 반환Result Return | AsyncResult | TaskiqResult |
| 사용처Use Case | Scan Worker (Vision) | Chat Worker (LangGraph) |
┌─────────────────────────────────────────────────────────────────────────────┐ │ RabbitMQ Job Submission Flow │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Dual-Path Messaging Architecture │ │ ════════════════════════════════════════════════════════════════════ │ │ Path 1: RabbitMQ (Job Execution) │ Path 2: Redis (Event Streaming) │ │ ───────────────────────────────── │ ───────────────────────────── │ │ DIRECT exchange: chat_tasks │ Streams + Pub/Sub (3-Tier) │ │ Queue: chat.process │ Durable → Router → Volatile │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ chat-api chat-worker │ │ │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ │ │ AioPikaBroker │ AMQP │ @broker.task │ │ │ │ │ │ .kiq(job_id, │ ──────────▶ │ async def process() │ │ │ │ │ │ session_id, │ JSON │ → LangGraph │ │ │ │ │ │ message, │ │ → Event Publish │ │ │ │ │ │ image_url?) │ └─────────────────────┘ │ │ │ │ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
| 구성 요소Component | 설정Config | 설명Description |
|---|---|---|
| Broker | AioPikaBroker |
RabbitMQ asyncio 클라이언트RabbitMQ asyncio client |
| Exchange | chat_tasks (direct) |
Topology CR로 미리 생성Pre-created by Topology CR |
| Queue | chat.process |
DLX, TTL 설정 포함With DLX, TTL settings |
| Routing Key | chat.process |
Direct 라우팅Direct routing |
broker.kiq(job_id, session_id, message)
| 트리거Trigger | 메트릭Metric | 임계값Threshold | 스케일Scale |
|---|---|---|---|
| RabbitMQ | Queue Length | 5 messages | 1 → 4 replicas |
┌─────────────────────────────────────────────────────────────────────────────┐
│ SSE Gateway Architecture │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────┐ ┌──────────────────────────┐ │
│ │ Event Router │ PUBLISH │ SSE Gateway │ │
│ │ (Consumer Group) │ ────────────────────▶│ │ │
│ └───────────────────┘ │ ┌──────────────────┐ │ │
│ │ │ In-memory fan-out│ │──▶│ SSE
│ ┌───────────────────┐ │ └──────────────────┘ │ │
│ │ Redis Streams │ XRANGE │ ┌──────────────────┐ │ │
│ │ chat:events:{id} │ ◀────────────────────│ │ State recovery │ │ │
│ └───────────────────┘ (catch-up) │ │ (Redis KV) │ │ │
│ │ └──────────────────┘ │ │
│ ┌───────────────────┐ │ ┌──────────────────┐ │ │
│ │ Redis Pub/Sub │ SUBSCRIBE │ │ Last-Event-ID │ │ │
│ │ chat:{job_id} │ ────────────────────▶│ │ header │ │ │
│ └───────────────────┘ │ └──────────────────┘ │ │
│ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 기능Feature | 구현Implementation | 설정값Config |
|---|---|---|
| Stale Detection | 3초 타임아웃 시 자동 재연결Auto-reconnect on 3s timeout | 3s threshold |
| Max Reconnections | 지수 간격으로 최대 3회 시도3 attempts with exponential spacing | 3 attempts |
| Fallback Polling | SSE 실패 시 폴링 전환Polling fallback if SSE fails | 3s interval, 120s max |
| Seq Encoding | Stage: STAGE_ORDER×10, Token: 1000+Stage: STAGE_ORDER×10, Token: 1000+ | Last-Event-ID |
| Catch-up | XREVRANGE로 누락 이벤트 복구XREVRANGE for missed events | State KV + Streams |
| 트리거Trigger | 메트릭Metric | 임계값Threshold | 스케일Scale |
|---|---|---|---|
| Prometheus | sse_active_connections | 100/pod | 1 → 3 replicas |
| Node | Intent | Data Source | Protocol |
|---|---|---|---|
| waste_rag | waste | JSON 규정 + Feedback Loop | In-Memory |
| character | character | Character Service | gRPC |
| location | location | Kakao Local API | HTTP |
| bulk_waste | bulk_waste | 행정안전부 MOIS API | HTTP |
| recyclable_price | recyclable_price | 한국환경공단 KECO | HTTP |
| collection_point | collection_point | 한국환경공단 KECO | HTTP |
| weather | weather / enrichment | 기상청 KMA API | HTTP |
| web_search | web_search | DuckDuckGo/Tavily | HTTP |
| image_generation | image_generation | gemini-3-pro-image + Images gRPC | gRPC |
| feedback | (after waste_rag) | LLM Evaluator + Fallback | Internal |
| general | general | Passthrough (LLM Only) | - |
@broker.task("chat.process")는
LangGraph StateGraph 전체를 하나의 Task 단위로 큐잉합니다.
개별 Task로 분리해 큐잉하지 않은 이유는 아래와 같습니다.additional_intents와 Enrichment Rules를 평가해 병렬 노드를 결정.
큐잉 시점에 어떤 서브에이전트가 실행될지 예측 불가.route_after_feedback처럼
노드 결과에 따라 다음 경로가 결정되는 조건부 엣지는 DAG를 정적으로 분할할 수 없음.ChatState는
모든 노드가 공유하는 단일 상태 객체. 분산 Task 간 상태 동기화는 복잡도와 오버헤드를 증가시킴.grpc.aio)는 asyncio 네이티브로 Graph 내부에서 ~1-100ms 응답.
Celery는 Fire & Forget에 적합하나, await 기반 결과 대기에는 부적합.
# contracts.py - Single Source of Truth INTENT_REQUIRED_FIELDS = { "waste": {"disposal_rules"}, "character": {"character_context"}, "location": {"location_context"}, "bulk_waste": {"bulk_waste_context"}, ... } # Aggregator 검증 로직 missing_required, missing_optional = validate_missing_fields( intent=intent, collected_fields=collected_fields, ) needs_fallback = len(missing_required) > 0
needs_fallback=True를 설정하여
Answer Node에서 Fallback Chain(Web Search → General LLM)을 실행합니다.
| Model | Context Window | Max Output | Trigger Threshold | Summary Tokens |
|---|---|---|---|---|
| gpt-5.2 | 400,000 | 128,000 | 272,000 | 60,000 (15%) |
| gemini-3-flash-preview | 1,000,000 | 64,000 | 936,000 | 65,536 (cap) |
Plan → Auto-Accept → Normal)하며 운용합니다. Plan Mode에서 코드베이스를 read-only 분석한 뒤 방향을 확정하고, Auto-Accept로 전환해 무중단 실행합니다.
| Company | Period | Title / Role | Description |
|---|---|---|---|
| Rakuten Symphony Korea 정규직 · Cloud BU |
2024.12 – 2025.08 | Cloud Engineer Jr. Storage Developer (Server) / Full-time |
Rakuten CNP v5.5.0 — CNP 분산 스토리지 서버 개발, Rakuten Mobile(JP) + 1&1(DE) 20M 글로벌 사용자
Rakuten OStore v1.0.0 — Object Storage 분산 게이트웨이 개발, MinIO/S3 대체 엔터프라이즈 프로덕션
|
| Eco² Project SeSACTHON 2025 |
2025.10.31 – 2026.01.28 | Backend/Infra Lead Agent-Driven Workflow 설계 · 백엔드/인프라 개발 |
24노드 K8s 분산 비동기 클러스터 (8 APIs, 6 Workers, 3 Infra) · 설계 → 개발 → 배포 → 운영
📐 Performance Improvement
• Auth Offloading — Istio EnvoyFilter + ext-authz(Go) JWT 검증 분리, 48 → 1,500 RPS (31×)
• Event Bus Layer — Redis Streams + Pub/Sub 기반 실시간 전달, 동시 부하 600% 개선
⚙️ Implementation
• LangGraph StateGraph — Send API 기반 동적 병렬 라우팅, Multi-Agent 오케스트레이션
• 4-Stage Celery Pipeline — Vision/Rule/Answer/Reward, KEDA 이벤트 기반 오토스케일링
• 3-Tier Memory Architecture — Redis Hot + PostgreSQL Persistent + Summary Compression
|
XADD (seq-based)PUBLISH (실시간)Last-Event-ID 헤더 전송XRANGE catch-upXREAD BLOCK 재개| Node | Timeout | Retry | CB Threshold | FailMode | Rationale |
|---|---|---|---|---|---|
| waste_rag | 1000ms | 1 | 5 | FALLBACK | 로컬 파일 검색 1초 이내 |
| bulk_waste | 10000ms | 2 | 5 | FALLBACK | MOIS API DEFAULT_TIMEOUT=15s |
| location | 3000ms | 2 | 5 | FALLBACK | gRPC PostGIS ~100ms |
| general | 30000ms | 2 | 3 | CLOSE | LLM HTTP_TIMEOUT.read=60s |
| character | 3000ms | 1 | 3 | OPEN | gRPC LocalCache ~1-3ms |
| collection_point | 10000ms | 2 | 5 | FALLBACK | KECO API DEFAULT_TIMEOUT=15s |
| weather | 5000ms | 1 | 3 | OPEN | KMA API, 보조 정보 |
| web_search | 10000ms | 2 | 5 | FALLBACK | DuckDuckGo timeout=10s |
| recyclable_price | 10000ms | 2 | 5 | FALLBACK | KECO API, 시세 정보 |
| image_generation | 30000ms | 1 | 3 | OPEN | DALL-E 10-30초 소요 |
James Reason(1990)의 Swiss Cheese Model을 LLM Agent 평가에 적용. 사고는 단일 실패가 아닌 '다층 방어의 구멍이 동시에 정렬될 때' 발생합니다. Anthropic의 Responsible Scaling Policy와 동일한 Defense-in-Depth 전략을 따릅니다. Applied James Reason's Swiss Cheese Model (1990) to LLM Agent evaluation. Accidents occur not from single failures but when 'multiple defense layers' holes align simultaneously.' Following Anthropic's Responsible Scaling Policy Defense-in-Depth strategy.
graph TD
__start__([__start__]) --> eval_entry
eval_entry -.-> calibration_check
eval_entry -.-> code_grader
eval_entry -.-> llm_grader
calibration_check --> eval_aggregator
code_grader --> eval_aggregator
llm_grader --> eval_aggregator
eval_aggregator --> eval_decision
eval_decision --> __end__([__end__])
| Axis | Weight | Description |
|---|---|---|
| Faithfulness | 0.30 | Factual grounding in context |
| Relevance | 0.25 | Direct answer to query |
| Completeness | 0.20 | TREC Nugget-based coverage |
| Safety | 0.15→0.25 | Risk mitigation (hazmat boost) |
| Communication | 0.10 | Clarity and tone |
| Total | 5^5=3,125 | → 4 Grades (S/A/B/C), ~9.61 bits info loss |
| Layer | Component | Nodes | Purpose |
|---|---|---|---|
| AWS External | Route53, ALB, CloudFront, S3, ACM | - | DNS, L7 LB, CDN, Storage, SSL |
| Edge | Istio Ingress, ext-authz | k8s-ingress | 트래픽 라우팅, JWT 인증 |
| Service | 7 APIs + 2 Gateways | k8s-api-* | 비즈니스 로직 |
| Integration | RabbitMQ, Redis, gRPC | k8s-worker-* | 비동기/이벤트/동기 통신 |
| Persistence | PostgreSQL, Redis (4종) | k8s-data | 데이터 영속화 |
| Platform | ArgoCD, KEDA, Observability | k8s-platform | 오케스트레이션 |
| Category | Node Name | Instance | vCPU | RAM | Storage | Purpose |
|---|---|---|---|---|---|---|
| Control Plane | k8s-master | t3.xlarge | 4 | 16GB | 80GB | API Server, etcd, Scheduler, Controller |
| API Nodes | k8s-api-auth | t3.small | 2 | 2GB | 20GB | JWT 인증/인가, OAuth |
| k8s-api-users | t3.small | 2 | 2GB | 20GB | 사용자/마이페이지 | |
| k8s-api-scan | t3.medium | 2 | 4GB | 30GB | 폐기물 스캔 (AI) | |
| k8s-api-chat | t3.medium | 2 | 4GB | 30GB | 챗봇 (LangGraph Agent 🔄) | |
| k8s-api-character | t3.small | 2 | 2GB | 20GB | 캐릭터 카탈로그 | |
| k8s-api-location | t3.small | 2 | 2GB | 20GB | 위치/수거함 (PostGIS) | |
| k8s-api-image | t3.small | 2 | 2GB | 20GB | 이미지 업로드 (S3) | |
| k8s-api-info | t3.small | 2 | 2GB | 20GB | 뉴스 피드 (CQRS + Cache Aside) | |
| Worker Nodes | k8s-worker-storage | t3.medium | 2 | 4GB | 40GB | auth/users/character/info Worker |
| k8s-worker-storage-2 | t3.medium | 2 | 4GB | 40GB | Storage Worker HA | |
| k8s-worker-ai | t3.medium | 2 | 4GB | 40GB | scan/chat Worker (GPT) | |
| k8s-worker-ai-2 | t3.medium | 2 | 4GB | 40GB | AI Worker HA | |
| Data Nodes | k8s-postgresql | t3.large | 2 | 8GB | 80GB | PostgreSQL (7 DBs) |
| k8s-redis-auth | t3.medium | 2 | 4GB | 20GB | Blacklist, OAuth State | |
| k8s-redis-streams | t3.small | 2 | 2GB | 10GB | SSE Events (4 shards) | |
| k8s-redis-cache | t3.small | 2 | 2GB | 10GB | Celery Result, LRU | |
| k8s-redis-pubsub | t3.small | 2 | 2GB | 10GB | Realtime Broadcast | |
| Platform Nodes | k8s-rabbitmq | t3.medium | 2 | 4GB | 40GB | AMQP Message Broker |
| k8s-monitoring | t3.large | 2 | 8GB | 60GB | Prometheus, Grafana | |
| k8s-logging | t3.xlarge | 4 | 16GB | 100GB | EFK Stack (ES 8GB Heap) | |
| k8s-ingress-gateway | t3.medium | 2 | 4GB | 20GB | Istio Ingress, ext-authz | |
| k8s-sse-gateway | t3.small | 2 | 2GB | 20GB | SSE Gateway (Long-lived) | |
| k8s-event-router | t3.small | 2 | 2GB | 20GB | Streams → Pub/Sub Fan-out |
terraform/main.tfterraform/modules/ec2/| Layer | tier Label | 구성 요소 |
|---|---|---|
| 1 | network | Istio Gateway, ext-authz, Envoy, Cert-Manager |
| 2 | business-logic | auth, scan, chat, character, location, users, images, sse-gw + 8 Workers |
| 3 | integration | RabbitMQ, gRPC, Redis Streams/Pub/Sub, Event Router |
| 4 | data | PostgreSQL, Redis (Cache, Auth, Streams, PubSub) |
| 5 | platform | K8s, ArgoCD, Prometheus, KEDA, Jaeger, Kiali, ECK |
| Taint | Effect | 스케줄되는 Pod |
|---|---|---|
| domain=auth:NoSchedule | auth 노드 격리 | auth-api, ext-authz만 배치 |
| domain=scan:NoSchedule | scan 노드 격리 | scan-api만 배치 |
| domain=worker-storage:NoSchedule | Storage Worker 격리 | auth/users/character/info Worker |
| domain=worker-ai:NoSchedule | AI Worker 격리 | scan-worker, chat-worker (GPT) |
| domain=data:NoSchedule | Data 노드 격리 | PostgreSQL, Redis StatefulSet |
| domain=observability:NoSchedule | Monitoring 격리 | Prometheus, Grafana, EFK |
terraform/main.tf의 kubelet_profiles에서 노드별 Labels & Taints를 user-data로 주입:--node-labels=role=api,domain=auth,tier=business-logic --register-with-taints=domain=auth:NoSchedule
| Wave | Category | Resources |
|---|---|---|
| 0 | Foundation | CRDs |
| 4-6 | Service Mesh | Istio, ArgoCD Image Updater |
| 7-8 | Network/Cert | NetworkPolicy, Cert-Manager |
| 10-11 | Secrets | External Secrets Operator → CRs |
| 20-23 | Monitoring | Prometheus, Grafana, Alerting |
| 24-25 | Database | PostgreSQL, Prometheus Adapter |
| 27-28 | Cache | Redis Operator → CRs |
| 29-32 | Message Queue | RabbitMQ Operator → Topology → CRs |
| 35-36 | Autoscaling | KEDA, ScaledObjects |
| 40-43 | Application | APIs, Workers, SSE Gateway, Event Router |
| 50 | Routing | Istio VirtualService |
| 60-63 | Observability | Kiali, Jaeger, ECK |
| 대상Target | 워크플로우Workflow | 검증Checks | 트리거/특징Trigger/Notes |
|---|---|---|---|
| apps/* API | ci-services.yml | black==24.4.2, ruff==0.6.9, pytest==8.3.3 | PR에선 품질 게이트만, push/수동 실행에서만 이미지 빌드/푸시 PR runs quality only, build/push only on push or manual |
| apps/* Workers | ci-workers.yml | black/ruff + pytest(unit) (integration은 기본 ignore) | Redis 환경변수 주입으로 단위 테스트 중심(빠른 피드백) Unit-test focused for fast feedback (Redis env injected) |
| SSE Components | ci-sse-components.yml | apps/sse_gateway, apps/event_router: black/ruff/pytest | apps/를 PYTHONPATH로 잡아 모듈 import 정합성 강제 Sets PYTHONPATH=apps to enforce import correctness |
| Infra | ci-infra.yml | Terraform validate/plan 등 IaC 검증 IaC checks (Terraform validate/plan, etc.) | docs 타입 커밋은 skip 가능(인프라 CI 비용 절감) Can skip on docs-only commits to reduce CI cost |
| Coverage (리포트) | ci-sonarcloud.yml | pytest-cov → coverage.xml 생성 | SonarCloud 연동용(현재는 수동 트리거) For SonarCloud integration (manual trigger currently) |
| Type | Name | Version | 용도 |
|---|---|---|---|
| Operator | RabbitMQ Cluster | v2.11.0 | MQ 클러스터 |
| Operator | Messaging Topology | v1.15.0 | Exchange/Queue CRD |
| Operator | Redis (Spotahome) | v3.3.0 | Sentinel HA |
| Operator | External Secrets | v0.9.11 | Secret 동기화 |
| Operator | ECK | v2.11.0 | Elasticsearch |
| Operator | Cert-Manager | v1.16.2 | TLS 인증서 |
| Helm | kube-prometheus-stack | v56.21.1 | 모니터링 |
| Helm | KEDA | v2.16.0 | 이벤트 기반 스케일링 |
| Helm | Istio | v1.24.1 | Service Mesh |
| Helm | Grafana | v8.5.9 | 대시보드 |
| Helm | Prometheus Adapter | v4.10.0 | Custom Metrics |
| Helm | PostgreSQL | v18.1.11 | 데이터베이스 |
| 메트릭 | AS-IS (PoolSize=20) | 1차 (PoolSize=100) | 2차 (PoolSize=500+HPA) | 개선율 |
|---|---|---|---|---|
| Peak RPS | ~884 (→42 포화) | ~900 | ~1,200 | 28배 |
| Avg Latency | 57-80ms (→125ms) | 16-22ms | 10-24ms | 84% 감소 |
| Success Rate | 90-94% (→86%) | 98.25-99.5% | 99.55-100% | +13.8%p |
| redis_error | 11-54 req/s | 3-8 req/s | 0-3.5 req/s | 93% 감소 |
| Pod Count | 1 (CPU 병목) | 1 | 3-5 (HPA) | 수평 확장 |
| 컴포넌트 | 용도 | 주요 설정 |
|---|---|---|
| Route 53 | DNS 관리 | Hosted Zone: growbin.app, DNS Validation for ACM |
| AWS ALB | L7 로드밸런서 | SSL/TLS Termination, Target: Istio Ingress |
| CloudFront | 이미지 CDN | OAI, 24h/7d TTL, PriceClass_200 |
| S3 Bucket | 이미지 저장 | Versioning, 30d→IA, 90d 삭제, Pre-signed URL |
| ACM | SSL 인증서 | Wildcard *.growbin.app, DNS Validation |
terraform/ 디렉토리에서 선언적으로 관리됩니다:cloudfront.tf, s3.tf, route53.tf, acm.tf, alb-controller-iam.tf
| 자동화 도구 | ServiceAccount | IAM 권한 | 동작 |
|---|---|---|---|
| ExternalSecrets | platform-system:external-secrets-sa | SSM GetParameter, SecretsManager GetSecretValue | AWS SSM → K8s Secret 자동 동기화 |
| ExternalDNS | platform-system:external-dns | Route53 ChangeResourceRecordSets, ListHostedZones | K8s Ingress/Service → Route53 A 레코드 |
| ALB Controller | kube-system:aws-load-balancer-controller | ELB Create/Modify, EC2 Describe, ACM List | K8s Ingress → AWS ALB 자동 프로비저닝 |
terraform/irsa-roles.tf에서 IAM Role 선언적 관리
┌─────────────────────────────────────────────────────────────────────────────────┐ │ Istio Service Mesh Traffic Flow │ ├─────────────────────────────────────────────────────────────────────────────────┤ │ │ │ [User/Client] │ │ │ │ │ │ HTTPS Request │ │ ▼ │ │ ┌─────────────────┐ CNAME/A Record ┌─────────────────┐ │ │ │ Route53 DNS │◀──────────────────────▶│ ExternalDNS │ │ │ │ (Global) │ │ Controller │ │ │ └────────┬────────┘ └─────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────┐ Target Group ┌─────────────────┐ │ │ │ AWS ALB │◀──────────────────────▶│ ALB Controller │ │ │ │ (HTTPS :443) │ Instance Mode │ (IRSA) │ │ │ └────────┬────────┘ └─────────────────┘ │ │ │ │ │ │ Forward to NodePort (30xxx) │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Ingress Gateway Node (k8s-ingress-gateway) │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Istio Ingress Gateway Pod (:80) │ │ │ │ │ │ ├── Gateway CR (hosts, port binding) │ │ │ │ │ │ └── VirtualService (path-based routing) │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └────────┬────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ xDS Config (Istiod Control Plane) │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Worker Node │ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ │ │ ┌─────────────┐ ┌─────────────────────────────────┐ │ │ │ │ │ │ │ Envoy │ mTLS │ Application Container │ │ │ │ │ │ │ │ Sidecar │────────▶│ (scan-api, chat-api, etc) │ │ │ │ │ │ │ │ (:15001) │ localhost│ │ │ │ │ │ │ │ └─────────────┘ └─────────────────────────────────┘ │ │ │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ Before: AWS ALB → K8s Ingress → NodePort → Pod │ │ After: AWS ALB → Istio Gateway → VirtualService → Envoy Sidecar → App │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘
| 고려 사항Consideration | Sidecar | Ambient | 선택Choice |
|---|---|---|---|
| 네임스페이스 격리Namespace Isolation | 도메인별 Pod 단위Per-pod per domain | Waypoint per NS | ✅ Sidecar |
| 노드 밀도Node Density | 노드당 적은 PodFew pods/node | ztunnel 효율 낮음inefficient | ✅ Sidecar |
| Calico CNI | 충돌 없음No conflict | CNI 레벨 충돌 위험CNI-level overlap risk | ✅ Sidecar |
| 운영 성숙도Operational Maturity | 검증됨Proven | Beta (v1.24) | ✅ Sidecar |
| Wave | 컴포넌트Component | 노드Node | 설명Description |
|---|---|---|---|
| 04 | istio-base, istiod | k8s-master | CRD + Control PlaneCRDs + Control Plane |
| 05 | Istio Ingress Gateway | k8s-ingress-gateway | 단일 진입점 (t3.medium)Single entry point (t3.medium) |
| 50 | VirtualService, DestinationRule | - | 앱 배포 후 라우팅 규칙Routing rules after app deployment |
| 문제Issue | 원인Cause | 해결Solution |
|---|---|---|
| ExternalDNS가 Bridge Ingress 감지 못함ExternalDNS not detecting Bridge Ingress | Istio Gateway 기반 Ingress 미인식Istio Gateway-based Ingress not recognized | external-dns.alpha.kubernetes.io/managed 라벨 추가label added |
| ALB Health Check 실패ALB Health Check failing | Gateway hosts에 ALB 헬스체크 경로 없음Gateway hosts missing ALB healthcheck path | Gateway에 와일드카드 호스트 추가Added wildcard host to Gateway |
| Envoy-istiod 통신 차단Envoy-istiod communication blocked | Calico NetworkPolicy가 xDS 연결 차단Calico NetworkPolicy blocking xDS | istio-system egress 명시적 허용Explicit egress allow to istio-system |
로그아웃 시 auth-api가 Redis Fallback Outbox에 이벤트를 적재하고, auth_relay가 이를 폴링하여 RabbitMQ Fanout Exchange로 발행합니다.
모든 ext-authz Pod가 브로드캐스트를 수신해 Local Cache(sync.Map)를 즉시 갱신합니다.
병렬로 auth_worker가 Redis에 영속 저장하여 신규 Pod 부트스트랩 시 일관성을 보장합니다.
On logout, auth-api pushes event to Redis Fallback Outbox, auth_relay polls and publishes to RabbitMQ Fanout Exchange.
All ext-authz Pods receive broadcast and immediately update Local Cache (sync.Map).
In parallel, auth_worker persists to Redis for new Pod bootstrap consistency.
| 컴포넌트 | 역할 | 구현 |
|---|---|---|
| auth_relay | Fallback Outbox → MQ 재발행 | Redis List 폴링 → RabbitMQ Publish |
| Fanout Exchange | 브로드캐스트 | blacklist.events, type=fanout |
| Local Cache | O(1) 조회 | Go sync.Map, cleanupInterval 60s |
| auth_worker | Redis 영속화 | blacklist:{jti}, TTL = exp - now |
| 정책 | 값 | 이유 |
|---|---|---|
| Fail Mode | fail-closed | 블랙리스트 우회 방지, 보안 우선 |
| Blacklist TTL | min(exp-now, 24h) | Refresh Token 만료 주기 커버 |
| Cleanup Interval | 60s | 메모리 회수 주기 |
| Pool Size | 500 (min idle 200) | 고동시성, Cold start 방지 |
apps/ext_authz/internal/server/server.go - fail-closed 정책apps/ext_authz/internal/cache/blacklist.go - Local Cache 구현apps/ext_authz/internal/config/config.go - Pool 설정apps/auth_worker/infrastructure/persistence_redis/blacklist_store_redis.py - TTL 계산
| Integration | Source | Target | Agent Feedback 활용 |
|---|---|---|---|
| trace.id Correlation | Kibana logs | Jaeger spans | 분산 트랜잭션 에러 추적 |
| Grafana Exemplars | Prometheus metrics | Jaeger traces | p99 이상치 원인 분석 |
| Alertmanager | PrometheusRule | Slack #eco_sre | 실시간 장애 컨텍스트 |
| Kiali Service Graph | Istio Telemetry | Traffic visualization | 서비스 의존성 분석 |
| Layer | 구성요소Components | 역할Role | 핵심 기술Key Tech |
|---|---|---|---|
| L1. Edge | Istio Ingress, ext-authz | 트래픽 라우팅, JWT 인증/인가Traffic routing, JWT auth | Istio 1.24, Go gRPC, Redis Blacklist |
| L2. Service | 8 Domain APIs + 2 Gateways | 비즈니스 로직, SSE 실시간 통신Business logic, SSE realtime | FastAPI, Clean Architecture, CQRS |
| L3. Integration | RabbitMQ, Workers (8), Event Router | 비동기 처리, 이벤트 스트리밍Async processing, event streaming | Celery/Taskiq, Redis Streams, Pub/Sub |
| L4. Persistence | PostgreSQL, Redis ×4 | 데이터 영속화, 캐싱, 상태 관리Data persistence, caching, state | 7 schemas, Auth/Cache/Streams/Pub/Sub |
| L5. Platform | ArgoCD, KEDA, Observability Stack | GitOps 배포, 오토스케일링, 모니터링GitOps deploy, autoscaling, monitoring | App-of-Apps, Prometheus/Jaeger/EFK |
| 단계Phase | 흐름Flow | 설명Description |
|---|---|---|
| 1. Request | Client → API → RabbitMQ | API가 작업을 MQ에 발행 (Quorum Queue)API publishes task to MQ (Quorum Queue) |
| 2. Process | Worker → Business Logic | Celery/Taskiq 워커가 비동기 처리Celery/Taskiq worker async processing |
| 3. Event | Worker → Redis Streams | 처리 완료 이벤트를 스트림에 기록Record completion event to stream |
| 4. Fan-out | Event Router → Redis Pub/Sub | Consumer Group이 이벤트를 Pub/Sub로 전파Consumer Group propagates to Pub/Sub |
| 5. Realtime | SSE Gateway → Client | Pub/Sub 구독 → SSE로 클라이언트에 푸시Subscribe Pub/Sub → Push to client via SSE |
Anthropic은 Claude Code로 Claude를 개발하며 90% 이상의 코드를 AI가 작성합니다. 핵심은 CLAUDE.md 자동 주입과 Skills의 Progressive Disclosure입니다. Eco²는 이 방식을 적용하여 문서 기반 Self-RAG 컨텍스트를 축적하며, GitOps(ArgoCD)로 클러스터와 코드베이스가 동기화되어 코드에 대한 이해가 곧 배포 환경에 대한 이해로 이어집니다. Anthropic develops Claude using Claude Code, with over 90% of code written by AI. The key is CLAUDE.md auto-injection and Skills' Progressive Disclosure. Eco² applies this approach, accumulating document-based Self-RAG context. With GitOps(ArgoCD) syncing the cluster with codebase, understanding the code directly translates to understanding the deployment environment.
| 개념Concept | Eco² 적용Implementation | 설명Description |
|---|---|---|
| CLAUDE.md | CLAUDE.md |
세션 시작 시 프로젝트 컨텍스트 자동 주입Auto-inject project context at session start |
| Skills | /.claude/skills/ (27개) |
k8s-debug, load-test, pr-review 등 반복 작업 자동화Automate k8s-debug, load-test, pr-review, etc. |
| Progressive Disclosure | foundations → plans → reports | 필요한 문서만 점진적 로드 (토큰 효율)Load only needed docs progressively (token efficient) |
| Bash 선호 | kubectl, terraform, helm | MCP 50K-134K 토큰 vs Bash 직접 접근MCP 50K-134K tokens vs Bash direct access |
Self-RAG Context가 Agent의 판단 근거를 형성하고, Skills가 반복 작업을 자동화하며
Runtime Environment에서 Deploy → Observe → Report 사이클이 순환
Self-RAG Context forms Agent's decision basis, Skills automate repetitive tasks
Deploy → Observe → Report cycle loops in Runtime Environment
reports/에 축적되고, 이는 다음 세션의 foundations/가 되어 Agent의 판단 근거로 작용skills/로 정제되어 코딩 에이전트(세션) 온보딩 비용 없이 즉시 재사용reports/, becoming foundations/ for next session as Agent's decision basisskills/, instantly reusable without coding agent (session) onboarding cost| Component | Version | 역할 | 설정 |
|---|---|---|---|
| ECK Operator | v2.11.0 | ES/Kibana 라이프사이클 관리 | Helm (helm.elastic.co), elastic-system ns |
| Fluent Bit | v2.2.0 | 로그 수집 (DaemonSet) | CRI Parser, K8s Filter, ECS Lua |
| Elasticsearch | v8.11.0 | 로그 저장/검색 (CR) | elasticsearch.k8s.elastic.co/v1, 50GB gp3 |
| Kibana | v8.11.0 | 로그 시각화 (CR) | kibana.k8s.elastic.co/v1, elasticsearchRef |
logs-YYYY.MM.DD (Logstash format, 1 shard, 0 replica)eco2-logs-ecs with subobjects: false (dot notation 보존: trace.id, span.id)service.name 필드로 8개 도메인 구분 (cross-service trace.id 추적 용이)
| Service | Language | Tracing SDK | trace_id 주입 방식 |
|---|---|---|---|
| auth-api, scan-api, chat-api... | Python | OpenTelemetry | ECSJsonFormatter + trace.get_current_span() |
| ext-authz | Go 1.21+ | OpenTelemetry | slog + gRPC B3 metadata extraction |
| Istio Sidecar | Envoy | Built-in | EnvoyFilter %TRACE_ID% (99.8%) |
| Jaeger Collector | - | OTLP | gRPC 4317 → Memory (dev) |
| Severity | Receiver | Color | 예시 Alert |
|---|---|---|---|
| 🔴 critical | slack-critical | #ff0000 | PodCrashLooping, NodeNotReady |
| 🟠 warning | slack-warning | #ffa500 | CPUThrottlingHigh, TargetDown |
| 🟡 info | slack-info | #ffff00 | KubeHpaMaxedOut |
| - Watchdog | null | - | 무시 (heartbeat) |
| Tool | URL | Protocol | 기능 |
|---|---|---|---|
| Kiali | kiali.dev.growbin.app | - | Service Graph, Traffic Flow, Health Status |
| Jaeger | jaeger.dev.growbin.app | Zipkin 9411, OTLP 4317 | Distributed Tracing, Span Analysis |
| Grafana | grafana.dev.growbin.app | - | Metrics Dashboard, Alerting |
| Stage | 평균Avg | p99 | 역할Role |
|---|---|---|---|
| vision | 4.5s | 6~10s | OpenAI Vision API (이미지 분석)OpenAI Vision API (image analysis) |
| rule | 0.3s | 2~3s | 로컬 캐시 Rule-Based RetrievalLocal Cache Rule-Based Retrieval |
| answer | 4.8s | 8~15s | OpenAI Chat API (답변 생성)OpenAI Chat API (answer generation) |
| reward | 1.7s | 3~5s | 캐릭터 매칭 + DB 저장Character matching + DB save |
| 항목Item | prefork (Celery) | Gevent (Celery) | asyncio (Taskiq) |
|---|---|---|---|
| 실행 단위Execution Unit | OS Process | Greenlet | Coroutine |
| 메모리/단위Memory/Unit | ~10-50MB | ~4-8KB | ~2-4KB |
| 컨텍스트 스위치Context Switch | ~1-10ms | ~1-10µs | ~100ns-1µs |
| 동시 처리Concurrency | 6-9 (메모리 제한)6-9 (memory limited) | 100+ | 수천+ (이벤트 루프)1000s+ (event loop) |
| 적용 워커Applied Worker | character, info, users | scan-worker | chat-worker |
| 선택 근거Rationale | 단순 DB 태스크, 프로세스 격리Simple DB tasks, process isolation | 기존 동기 코드 + 고 I/O (65% OpenAI)Legacy sync code + high I/O (65% OpenAI) | LangGraph async native 극대화LangGraph async native maximization |
| RPS | 충분 (경량 태스크)Sufficient (lightweight tasks) | ~4 (OpenAI rate limit)~4 (OpenAI rate limit) | LLM 응답 대기 중 다른 요청 처리Processes other requests during LLM wait |
-P prefork -c 8-P gevent -c 100| Worker | 동시성Concurrency | Engine | Broker | 파이프라인Pipeline |
|---|---|---|---|---|
| scan-worker | Gevent (100) | Celery | RabbitMQ | Vision→Retrieval→Answer→Reward |
| chat-worker | asyncio | Taskiq | RabbitMQ (aio-pika) | LangGraph Multi-Agent astream() |
| character-worker | prefork | Celery | RabbitMQ | DB UPSERT, cache sync |
| info-worker | prefork | Celery | RabbitMQ | Beat: news collection (5m/30m) |
| users-worker | prefork | Celery | RabbitMQ | save_character UPSERT |
| 전략 | 설명 | 효과 |
|---|---|---|
| at-least-once | 실패 시 자동 재시도 (최대 5회) | 메시지 유실 방지 |
| Exponential Backoff | 1s → 2s → 4s → 8s → 16s | 일시 장애 복원력 |
| Idempotency Key | user_id + scan_id 기반 | 중복 처리 방지 |
| DLQ | 최종 실패 메시지 보관 | celery-beat 재처리 |
{domain}:events:{shard}sse:events:{shard}{domain}:state:{job_id}router:published:{job_id}:{seq}sse:events:{job_id}sse:events:{shard} (shard = hash(job_id) % 4)| 동시 접속 수Concurrent Users | Streams | Pub/Sub (Before) | Pub/Sub (After) | 절감률Reduction |
|---|---|---|---|---|
| 100명 | 4개 (고정) | 100개 | 4개 | 96% |
| 1,000명 | 4개 (고정) | 1,000개 | 4개 | 99.6% |
| 10,000명 | 4개 (고정) | 10,000개 ⚠️ | 4개 | 99.96% |
| 구분Phase | 연결 구조Connection Structure | 50 VU 연결 수50 VU Connections | 스케일링Scaling | 결과Result |
|---|---|---|---|---|
| Celery Events | SSE × RabbitMQ = O(n×m) | 341개 (1:21 비율) | ❌ 불가 | 503 Error |
| Connection-per-XREAD | SSE × Coroutine = O(n) | 50개 | ⚠️ CPU 85% | 62% 완료율 |
| Event Bus Layer | Router(1) + SSE(SUB) = O(n) | ≈20개 | ✅ HPA/KEDA | 100% @ 500VU |
| VU | 완료율Completion | 처리량Throughput | E2E p95 | Scan API p95 | 스냅샷Snapshot |
|---|---|---|---|---|---|
| 500 | 100% | 367.9 req/m | 83.3s | 232ms | SLA → |
| 600 | 99.7% | 358.6 req/m | 108.3s | 360ms | → |
| 700 | 99.2% | 329.1 req/m | 122.3s | 444ms | Live → |
| 800 | 99.7% | 367.3 req/m | 144.6s | 734ms | → |
| 900 | 99.7% | 405.5 req/m | 149.6s | 635ms | → |
| 1000 | 97.8% | 373.4 req/m | 173.3s | 787ms | → |
| Stage | 평균 (VU 10)Avg (VU 10) | 비율Ratio | 병목 원인Bottleneck |
|---|---|---|---|
| vision | 4.5초 | 40% | OpenAI Vision API (gpt-5.2) |
| answer | 4.8초 | 42% | OpenAI Chat API (gpt-5.2) |
| reward | 1.7초 | 15% | Character Match gRPC |
| rule | 0.3초 | 3% | PostgreSQL 쿼리 |
| Total | ~11.3초 | 100% | 🎯 SLA: 500 VU p95 83.3초 |
| 저장소 | 용도 | TTL |
|---|---|---|
| Redis Streams | 이벤트 영속 저장 (4개 샤드) | maxlen 50 |
| Redis Pub/Sub | 실시간 브로드캐스트 | Fire-and-forget |
| Redis State KV | 현재 상태 스냅샷 | 30분 |
| 항목 | Before | After |
|---|---|---|
| 클라이언트 응답 | 판정 + DB 저장 완료 후 | 판정 즉시 |
| DB 저장 | 순차 (character → users gRPC) | 병렬 (Fire & Forget) |
| users 도메인 연동 | gRPC 호출 | 직접 DB INSERT |
| 실패 시 영향 | 전체 실패 | 각자 독립 재시도 |
| 계층 | 방어 메커니즘 | 효과 |
|---|---|---|
| Application | Deterministic UUID (uuid5) | 동일 입력 → 동일 키 |
| Cache | Redis SETEX (idempotency 마킹) | 빠른 중복 체크 |
| Database | Unique Constraint | 최종 중복 방지 |
| Worker | IntegrityError 핸들링 | Race condition 방어 |
| Task | 큐 | 대상 DB | 재시도 |
|---|---|---|---|
| persist_reward_task | reward.persist | dispatcher | 3회 |
| save_ownership_task | reward.persist | character.character_ownerships | 5회 |
| save_user_character_task | users.sync | users.user_characters | 5회 |
| 전략 | 메커니즘 | 지연 시간 |
|---|---|---|
| Fanout Broadcast | Exchange → 모든 구독 Worker | ~10ms |
| TTL 기반 만료 | Local Cache TTL 10초 | 최대 10초 |
| 신규 Worker 워밍 | 부팅 시 DB 로드 + 구독 | 즉시 |
async def _collect_batch(self) -> list[SyncEvent]:
"""BRPOP + drain → batch 수집."""
batch = []
first = await self._redis.brpop(SYNC_QUEUE_KEY, timeout=5)
if first:
batch.append(parse(first))
# drain: 추가 메시지를 2초간 수집
while len(batch) < 50:
item = await self._redis.rpop(SYNC_QUEUE_KEY)
if not item: break
batch.append(parse(item))
return batch
# Dedup: 동일 (thread_id, checkpoint_ns)는 최신만 유지
dedup = {(e.thread_id, e.ns): e for e in batch}
| 패턴 | Write 경로 | Consistency 보장 |
|---|---|---|
| CheckpointSyncService | Redis → Queue → Batch → PG | DLQ fallback, Redis TTL 24h |
| ReadThroughCheckpointer | PG miss → Redis promote | Temporal Locality, LRU |
| Persistence Consumer | Streams → Batch flush → PG | Consumer Group, XACK |
| 설정 | 값 | 설명 |
|---|---|---|
| minReplicaCount | 1 | 유휴 시 최소 Pod |
| maxReplicaCount | 10 | 최대 스케일 한계 |
| QueueLength threshold | 10 | 스케일업 트리거 |
| cooldownPeriod | 30초 | 스케일다운 대기 |
| pollingInterval | 10초 | 메트릭 체크 주기 |
UPDATE_STATE_SCRIPT = """
local state_key = KEYS[1] -- {domain}:state:{job_id}
local publish_key = KEYS[2] -- router:published:{job_id}:{seq}
-- 멱등성: 이미 처리했으면 스킵
if redis.call('EXISTS', publish_key) == 1 then
return 0
end
-- State 조건부 갱신 (더 큰 seq만)
local current = redis.call('GET', state_key)
if current then
local cur_seq = tonumber(cjson.decode(current).seq) or 0
if new_seq <= cur_seq then should_update_state = false end
end
if should_update_state then
redis.call('SETEX', state_key, state_ttl, event_data)
end
-- 처리 마킹 + Pub/Sub 발행 허용
redis.call('SETEX', publish_key, published_ttl, '1')
return 1
"""
| 컴포넌트 | 역할 | 핵심 메커니즘 |
|---|---|---|
| Consumer | XREADGROUP 기반 이벤트 소비 | Multi-domain 8 streams 병렬 |
| Processor | State 갱신 + Pub/Sub 발행 | Lua Script 원자적 처리 |
| Reclaimer | Pending 메시지 복구 | XAUTOCLAIM (5분 idle) |
| Operator | 버전 | 역할 |
|---|---|---|
| RabbitMQ Cluster Operator | v2.12.0 | RabbitmqCluster CR → 3-node Quorum 클러스터 |
| Messaging Topology Operator | v1.16.0 | Exchange, Queue, Binding CR 관리 |
broker = AioPikaBroker(
url=settings.rabbitmq_url,
declare_exchange=not _is_production, # 운영: Topology Operator 사용
exchange_name="chat_tasks",
exchange_type=ExchangeType.DIRECT,
routing_key="chat.process",
queue_name="chat.process",
)
| 비교 | scan-worker (Celery) | chat-worker (Taskiq) |
|---|---|---|
| 런타임 | Gevent (몽키패칭) | asyncio-native |
| LLM 프레임워크 | 동기 OpenAI SDK | LangGraph (async) |
| Exchange | scan.events (direct) | chat_tasks (direct) |
| 이벤트 발행 | Redis Streams (scan:events) | Redis Streams (chat:events) |
| 트레이싱 | - | aio-pika + OpenAI + Gemini OTEL |
| 레이어Layer | 책임Responsibility | 구성 요소Components |
|---|---|---|
| Presentation | 외부 인터페이스External Interface | HTTP Router, gRPC Servicer, Schema |
| Application | 유스케이스 오케스트레이션Use Case Orchestration | Command, Query, DTO, Port |
| Domain | 핵심 비즈니스 로직Core Business Logic | Entity, Value Object, Domain Service |
| Infrastructure | 기술 구현 (Port 구현체)Tech Implementation (Port Impl) | SQLAlchemy Adapter, Redis Adapter, gRPC Client |
| 유형Type | 역할Role | 특징Characteristics | 예시Example |
|---|---|---|---|
| Command | 상태 변경 (Write)State Change (Write) | Side Effect, Transaction | OAuthCallback, Logout |
| Query | 조회 (Read)Read Only | No Side Effect, Cacheable | ValidateToken, GetProfile |
| Port | 인터페이스 정의Interface Definition | Protocol/ABC, DI | UserCommandGateway |
| Service | 복잡 로직 캡슐화Complex Logic Encapsulation | Facade or Pure | OAuthFlowService |
| Port (인터페이스)Port (Interface) | 현재 AdapterCurrent Adapter | 교체 가능 옵션Swappable Options |
|---|---|---|
| UserQueryGateway | SQLAlchemy (PostgreSQL) | MySQL, MongoDB, DynamoDB |
| TokenBlacklistStore | Redis Adapter | Memcached, In-Memory |
| LLMAdapter (chat) | OpenAI GPT-5.2 | Anthropic, Gemini, Local LLM |
| MessagePublisher | RabbitMQ | Kafka, Redis Streams, AWS SQS |
| ProgressNotifier | Redis Streams | Kafka, RabbitMQ, WebSocket |
| 단계Stage | 지연Latency | LLMLLM | 복원력 전략Resilience Strategy |
|---|---|---|---|
| Vision | ~3s | GPT-5.2 | Fallback → GPT-5.2, Checkpoint 저장 |
| Rule | ~0.5s | - | Rule Engine, 빠른 매칭 |
| Answer | ~8s | GPT-5.2 | Fallback → GPT-5.2, Streaming |
| Reward | ~0.5s | - | Batch Persist, Idempotency |
| 도메인Domain | ConfigMapConfigMap | SecretSecret | 특수 설정Special Config |
|---|---|---|---|
| auth | auth-config | auth-secret (JWT, OAuth) | OAuth Provider별 설정 |
| character | character-config | character-secret | gRPC 포트, 매칭 임계값 |
| chat | chat-config | chat-secret (OpenAI) | LLM 모델, 컨텍스트 길이 |
| scan | scan-config | scan-secret (OpenAI) | Vision/Answer 모델 |
| users | users-config | users-secret | 동기화 설정 |
| 등급Grade | 범위Range | 의미Meaning | 비율Ratio |
|---|---|---|---|
| A | 1-5 | 단순, 저위험Simple, Low Risk | 92.3% |
| B | 6-10 | 복잡, 중위험Complex, Moderate Risk | 5.8% |
| C | 11-20 | 고복잡High Complexity | 1.4% |
| D-F | 21+ | 리팩토링 필요Refactor Required | 0.5% |
radon cc apps/ -a -s --total-average radon cc apps/ -a -nc # A등급만 제외하고 표시
| 컴포넌트Component | 테스트 파일Test Files | 테스트 함수Test Functions | 주요 대상Target |
|---|---|---|---|
| chat_worker | 28 | 394 | LangGraph Nodes, Adapters, Commands |
| auth_worker | 11 | 69 | OAuth, JWT, Commands |
| auth | 10 | 67 | Controllers, Services |
| auth_relay | 10 | 64 | Token Relay, Middleware |
| location | 4 | 65 | gRPC, Queries |
| character | 6 | 42 | Domain, gRPC |
| sse_gateway | 4 | 31 | Event Streaming |
| scan | 3 | 27 | Submit API |
| users | 3 | 26 | Domain, Controllers |
| scan_worker | 4 | 15 | Celery Tasks |
| event_router | 3 | 13 | Redis Streams |
| Total | 86 | 813 | - |
| 단계Stage | 도구Tool | 설정Config | 출력Output |
|---|---|---|---|
| 커버리지 측정Coverage | pytest-cov | --cov=apps/ |
coverage.xml |
| 품질 분석Quality | SonarCloud | sonar.python.coverage.reportPaths |
대시보드Dashboard |
| 제외 패턴Exclusions | pytest.ini | --ignore=tests/integration |
Unit만 CI 실행Unit-only in CI |
┌─────────────────────────────────────────────────────────────────────┐
│ Vision LLM Stage │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ 📷 Image │ ──► │ 🧠 GPT-5.2 / │ ──► │ 📝 분류 │ │
│ │ (Base64) │ │ Gemini 3.0 Pro │ │ 결과 │ │
│ └──────────────┘ └────────┬─────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 📋 System Prompt │ │
│ │ ────────────────────│ │
│ │ • 재활용 가능 여부 │ │
│ │ • 분류 기준 제시 │ │
│ │ • 출력 포맷 정의 │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
| 항목Item | 설명Description |
|---|---|
| 모델Model | gpt-5.2 (default) / gemini-3-pro-preview |
| 입력Input | 사용자가 촬영한 이미지 (Base64 인코딩)User-captured image (Base64 encoded) |
| 출력Output | 품목 분류 결과 (대분류, 소분류, 재질)Item classification (category, subcategory, material) |
| 프롬프트Prompt | 분류 기준과 판단 근거를 명시한 시스템 프롬프트System prompt with classification criteria and reasoning guidelines |
| 조건Condition | Avg | 비고Note |
|---|---|---|
| 단일 요청Single request | 6.9s | 기준값 (워밍업 후)Baseline (warmed up) |
| 저부하Low Load (k6 VU 10) | 4.5s | 비동기 Gevent 전환 후After Gevent migration |
| 🎯 SLA (k6 VU 500) | E2E p95: 83.3s (전체 Chain 기준)E2E p95: 83.3s (total Chain) | |
┌─────────────────────────────────────────────────────────────────────┐
│ Rule Engine Stage │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ 📝 분류 │ ──► │ 🔍 Rule Search │ ──► │ 📄 관련 │ │
│ │ 결과 │ │ (Rule-based RAG) │ │ 규정 │ │
│ └──────────────┘ └────────┬─────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 📁 YAML Knowledge Base │ │
│ │ ─────────────────────────────│ │
│ │ item_class_list.yaml │ │
│ │ ├── 대분류: 플라스틱/종이/... │ │
│ │ ├── 소분류: PET/PP/PE/... │ │
│ │ └── 규정: 세척/라벨제거/... │ │
│ │ │ │
│ │ 📊 167개 품목 규정 수록 │ │
│ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
| 항목Item | 설명Description |
|---|---|
| 데이터 소스Data Source | 온누리 재활용 품목별 규정 (YAML 구조화)On-Nuri recycling regulations per item (YAML structured) |
| 검색 방식Search Method | 분류 결과 기반 키워드 매칭 → 관련 규정 추출Keyword matching based on classification → Extract relevant rules |
| 규정 항목Rule Items | 세척 여부, 라벨 제거, 분리배출 방법, 특이사항Washing, label removal, disposal method, special notes |
| 품목 수Item Count | 200+ 품목items |
| 조건Condition | Avg | 비고Note |
|---|---|---|
| 단일 요청Single request | 0.5ms | 기준값 (워밍업 후)Baseline (warmed up) |
| 저부하Low Load (k6 VU 10) | 0.3s | 비동기 Gevent 전환 후After Gevent migration |
| 🎯 SLA (k6 VU 500) | E2E p95: 83.3s (전체 Chain 기준)E2E p95: 83.3s (total Chain) | |
┌─────────────────────────────────────────────────────────────────────┐
│ Answer LLM Stage │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ 📄 관련 │ ──► │ 🧠 gpt-5.2-mini │ ──► │ 📤 JSON │ │
│ │ 규정 │ │ / Gemini 3 Flash│ │ Output │ │
│ └──────────────┘ └────────┬─────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 📋 Structured Output Schema │ │
│ │ ────────────────────────────────── │ │
│ │ { │ │
│ │ "category": "플라스틱", │ │
│ │ "subcategory": "PET", │ │
│ │ "recyclable": true, │ │
│ │ "instructions": ["세척", "라벨제거"],│ │
│ │ "reward_points": 10, │ │
│ │ "explanation": "..." │ │
│ │ } │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
| 항목Item | 설명Description |
|---|---|
| 모델Model | gpt-5.2 (default) / gemini-3-flash-preview |
| 입력Input | Vision 분류 결과 + Rule Engine 검색 규정Vision classification + Rule Engine retrieved regulations |
| 출력Output | JSON Structured Output (스키마 강제)JSON Structured Output (schema enforced) |
| 조건Condition | Avg | 비고Note |
|---|---|---|
| 단일 요청Single request | 4.8s | 기준값 (워밍업 후)Baseline (warmed up) |
| 저부하Low Load (k6 VU 10) | 4.8s | 비동기 Gevent 전환 후After Gevent migration |
| VU | 완료율Completion | E2E p95 | 상태Status |
|---|---|---|---|
| 🎯 500 (SLA) | 100% | 83.3s | ✅ SLA 기준SLA baseline |
| 700 | 99.2% | 122.3s | ✅ |
| 900 | 99.7% | 149.6s | ✅ |
| 1000 | 97.8% | 173.3s | ⚠️ 포화 지점Saturation |
┌────────────────────────────────────────────────────────────────────────────────────┐
│ 4-Stage Celery Chain Pipeline │
├────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ 👁️ Vision │───▶│ 📋 Rule │───▶│ 💬 Answer │───▶│ 🎁 Reward │ │
│ │ ~4.5s │ │ ~0.3s │ │ ~4.8s │ │ ~1.7s │ │
│ │ (40%) │ │ (5%) │ │ (45%) │ │ (10%) │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ seq:10-11 │ seq:20-21 │ seq:30-31 │ seq:40-41 │
│ ▼ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Redis Event Bus (XADD) │ │
│ │ scan:events:{shard} (4 shards) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Total: ~11.3s (LLM I/O-bound 85%) │
└────────────────────────────────────────────────────────────────────────────────────┘
| Stage | Queue | 처리 내용Processing | 외부 의존성External Dependency | Latency |
|---|---|---|---|---|
| 👁️ Vision | scan.vision |
이미지 분류 (GPT Vision)Image Classification (GPT Vision) | OpenAI API (gpt-5.2) | ~4.5s (40%) |
| 📋 Rule | scan.rule |
규정 검색 (Lite RAG)Regulation Search (Lite RAG) | JSON File (In-Memory) | ~0.3s (5%) |
| 💬 Answer | scan.answer |
응답 생성 (Structured Output)Response Generation (Structured Output) | OpenAI API (gpt-5.2) | ~4.8s (45%) |
| 🎁 Reward | scan.reward |
캐릭터 매칭 + 포인트 지급Character Matching + Points | Character gRPC Service | ~1.7s (10%) |
# Celery Chain: 순차 실행 보장 chain = ( vision_task.s(task_id, image_url, model) | rule_task.s() # vision 결과 자동 전달 | answer_task.s() # rule 결과 자동 전달 | reward_task.s() # answer 결과 자동 전달 ) # 실행: apply_async로 비동기 발행 chain.apply_async(task_id=task_id) # Gevent Pool: 100 coroutines per worker celery -A scan_worker worker --pool=gevent --concurrency=100
| 설정Setting | 값Value | 설명Description |
|---|---|---|
max_retries |
3 | 최대 재시도 횟수Maximum retry attempts |
default_retry_delay |
5s | 재시도 간격 (exponential backoff)Retry interval (exponential backoff) |
acks_late |
true | 작업 완료 후 ACK (재시도 보장)ACK after completion (retry guarantee) |
reject_on_worker_lost |
true | 워커 실패 시 큐 재전달Requeue on worker failure |
task_time_limit |
300s | 하드 타임아웃 (강제 종료)Hard timeout (force kill) |
task_soft_time_limit |
240s | 소프트 타임아웃 (SoftTimeLimitExceeded)Soft timeout (SoftTimeLimitExceeded) |
┌─────────────────────────────────────────────────────────────────────┐
│ Multi-intent Classification │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ 💬 User │ ──► │ 🧠 LLM │ ──► │ 🏷️ Intent │ │
│ │ Query │ │ Classifier │ │ Tags │ │
│ └──────────────┘ └────────┬─────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 📋 intent.txt (System Prompt) │ │
│ │ ───────────────────────────────────────────────────── │ │
│ │ 4-Class Routing: │ │
│ │ ┌────────────┬────────────┬────────────┬────────────┐ │ │
│ │ │ 🗑️ waste │ 🎭 character│ 📍 location│ 💭 general │ │ │
│ │ │ 분리배출 │ 캐릭터 │ 수거장소 │ 일반대화 │ │ │
│ │ └────────────┴────────────┴────────────┴────────────┘ │ │
│ │ │ │
│ │ 복합 질문 분해: │ │
│ │ "플라스틱 버리는 곳 알려줘" → [waste, location] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
| 분류Class | 설명Description | 예시Example |
|---|---|---|
| 🗑️ waste | 분리배출 방법 질문Recycling method questions | "페트병 어떻게 버려?""How to dispose PET bottles?" |
| 🎭 character | 캐릭터 관련 질문Character-related questions | "초록이가 좋아하는 건?""What does Choroki like?" |
| 📍 location | 수거 장소 질문Collection point questions | "근처 재활용 센터""Nearby recycling center" |
| 💭 general | 일반 대화/인사General chat/greetings | "안녕!""Hello!" |
┌──────────────────────────────────────────────────────────────────────────┐
│ TagBasedRetriever Architecture │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ 💬 Query │──────────────────────────│ 🔍 Vector Search │ │
│ │ + Intent │ │ (Embedding Similarity) │ │
│ └──────┬───────┘ └───────────┬────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 📁 Structured Data Injection (YAML → Context) │ │
│ │ ────────────────────────────────────────────────────────────────│ │
│ │ │ │
│ │ item_class_list.yaml situation_tags.yaml │ │
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
│ │ │ 167개 품목 │ │ 80개 상황 태그 │ │ │
│ │ │ ├── 플라스틱 │ │ ├── 청소 │ │ │
│ │ │ │ ├── PET │ │ ├── 분리수거 │ │ │
│ │ │ │ ├── PP │ │ ├── 재활용 │ │ │
│ │ │ │ └── PE │ │ └── 환경보호 │ │ │
│ │ │ ├── 종이 │ └─────────────────────┘ │ │
│ │ │ └── 금속 │ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 📄 Enriched Context │ │
│ │ (Query + YAML data) │ │
│ └─────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
| 파일File | 내용Content | 용도Purpose |
|---|---|---|
item_class_list.yaml |
167개 품목 분류 체계200+ item classification | 분리배출 질문 시 품목 매칭Item matching for disposal questions |
situation_tags.yaml |
80개 상황 태그100+ situation tags | 컨텍스트 기반 검색 강화Contextual search enhancement |
┌──────────────────────────────────────────────────────────────────────────┐
│ Eval Agent - 4 Phase Evaluation │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 1: Citation (근거 검증) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • 응답이 검색된 문서에 기반하는지 확인 │ │
│ │ • Rule-based 빠른 판단 (정규식, 키워드 매칭) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 2: Nugget (완전성 평가) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • 핵심 정보 포함 여부 체크 │ │
│ │ • 누락된 정보 식별 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 3: Groundedness (검증) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • LLM 기반 정밀 평가 │ │
│ │ • Hallucination 탐지 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 4: Just-in-Time (다음 액션) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • 품질 점수 기반 분기 결정 │ │
│ │ • Pass / Retry / Fallback 판단 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
| Phase | 평가 항목Evaluation | 방식Method | 기준Threshold |
|---|---|---|---|
| 1. Citation | 근거 존재Evidence exists | Rule-based | ≥1 참조reference |
| 2. Nugget | 정보 완전성Info completeness | Rule + LLM | ≥70% |
| 3. Groundedness | 사실 검증Fact verification | LLM | ≥0.8 score |
| 4. Just-in-Time | 다음 액션Next action | Decision Tree | Pass/Retry/Fallback |
┌──────────────────────────────────────────────────────────────────────────┐
│ Fallback Chain Strategy │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ Eval Agent 저품질 판정 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Tier 1: RAG Retry (검색 재시도) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • 쿼리 리라이팅 후 재검색 │ │
│ │ • 다른 검색 전략 시도 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ 실패 시 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Tier 2: Web Search (외부 검색) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • Tavily/Serper API로 웹 검색 │ │
│ │ • 최신 정보 보완 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ 실패 시 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Tier 3: General LLM (일반 응답) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • 컨텍스트 없이 LLM 직접 응답 │ │
│ │ • "정확한 정보 확인 필요" 안내 포함 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ 의도 불명확 시 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Clarify Template (명확화 요청) │ │
│ │ ───────────────────────────────────────────────────────────────│ │
│ │ • "좀 더 구체적으로 질문해 주세요" │ │
│ │ • 예시 질문 제안 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
| Tier | 전략Strategy | 조건Condition | 비용Cost |
|---|---|---|---|
| 1 | RAG Retry | 품질 점수 < 임계값Quality score < threshold | 낮음Low |
| 2 | Web Search | RAG 재시도 실패RAG retry failed | 중간Medium |
| 3 | General LLM | Web 검색도 실패Web search also failed | 높음High |
| - | Clarify | 의도 파악 불가Intent unclear | 없음None |
┌─────────────────────────────────────────────────────────────────────────────────┐ │ Scan Worker Infrastructure (Production) │ ├─────────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │ │ Celery Worker (gevent pool=100) │ │ │ │ ─────────────────────────────────────────────────────────────────── │ │ │ │ celery -A scan_worker.main worker --pool=gevent --concurrency=100 │ │ │ │ -Q scan.vision,scan.rule,scan.answer,scan.reward │ │ │ └────────────────────────────────────────────────────────────────────────┘ │ │ │ │ 4-Stage Chain + Sequence Events (순차 의존성) │ │ ──────────────────────────────────────────────────────────────────────── │ │ vision (5.16s) → rule (0.05s) → answer (3.69s) → reward (0.22s) → done │ │ seq:10-11 seq:20-21 seq:30-31 seq:40-41 seq:51 │ │ ↓ ↓ ↓ ↓ │ │ OpenAI Vision Lite RAG OpenAI Chat Character gRPC │ │ (PostgreSQL) (완벽 분류 시에만 지급) │ │ │ │ 2-Redis Event Bus Layer (Idempotent Fan-out) │ │ ──────────────────────────────────────────────────────────────────────── │ │ │ │ Worker ──XADD──▶ Streams Redis ──────────────▶ Event Router ──PUBLISH──▶ │ │ (4 shards: scan:events:0~3) │ │ │ (Durable Buffer) │ ┌──────────────────┐ │ │ ├──▶│ State KV Update │ │ │ ┌──────────────────────────────────────────────────┘ │ (Lua Atomic) │ │ │ │ └──────────────────┘ │ │ │ StreamConsumer EventProcessor PendingReclaimer │ │ │ ─────────────── ────────────── ──────────────── │ │ │ XREADGROUP Lua Script XAUTOCLAIM │ │ │ eventrouter seq 중복 검사 5min idle 재처리 │ │ │ batch count published marker (idempotent safe) │ │ │ TTL 2h │ │ │ │ │ └──────────▶ Pub/Sub Redis ──────────────────────▶ SSE Gateway ──▶ Client │ │ (Volatile Broadcast) (5s timeout → catch-up) │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘
| VU | 완료율Completion | E2E p95 | 처리량Throughput | 상태Status |
|---|---|---|---|---|
| 🎯 500 (SLA) | 100% | 83.3s | 367.9 req/m | ✅ SLA 기준SLA baseline |
| 600 | 99.7% | 108.3s | 358.6 req/m | ✅ |
| 700 | 99.2% | 122.3s | 329.1 req/m | ✅ |
| 800 | 99.7% | 144.6s | 367.3 req/m | ✅ |
| 900 | 99.7% | 149.6s | 405.5 req/m | ✅ |
| 1000 | 97.8% | 173.3s | 373.4 req/m | ⚠️ 포화 지점Saturation |
| Stage | Avg | 비율Ratio | 병목 원인Bottleneck |
|---|---|---|---|
| vision (seq:10-11) | 5.16s | 57% | OpenAI Vision API (gpt-5.2) |
| answer (seq:30-31) | 3.69s | 40% | OpenAI Chat API (gpt-5.2) |
| reward (seq:40-41) | 0.22s | 2% | Character gRPC (완벽 분류 시에만)Character gRPC (perfect only) |
| rule (seq:20-21) | 0.05s | 1% | Lite RAG (Memory, JSON) |
| Total | ~9.12s | 100% | 🎯 SLA 500 VU E2E p95 83.3s |
| 구성 요소Component | 설정Config | 설명Description |
|---|---|---|
| Celery Pool | gevent 100 |
100% I/O-bound → greenlet 100개 동시 처리100% I/O-bound → 100 concurrent greenlets |
| Queues | scan.vision, rule, answer, reward |
스테이지별 개별 큐 (Topology CR)Per-stage queues (Topology CR) |
| Redis Streams | 4 shards |
이벤트 샤딩 (균등 분배)Event sharding (balanced) |
| KEDA Scaling | 1-5 replicas |
RabbitMQ 큐 길이 기반RabbitMQ queue length based |
| 컴포넌트Component | 역할Role | 상세Details |
|---|---|---|
| Streams Redis | 내구성 버퍼Durable Buffer | scan:events:0~3 4 샤드, State KV 저장4 shards, State KV storage |
| Pub/Sub Redis | 실시간 브로드캐스트Realtime Broadcast | Volatile, 장애 격리 (State 복구 가능)Volatile, fault-isolated (State recoverable) |
| StreamConsumer | XREADGROUP |
Consumer Group: eventrouter, batch 소비Consumer Group: eventrouter, batch consume |
| EventProcessor | Lua Script | seq 중복 검사 + State + published marker 원자적seq dedup + State + published marker atomic |
| PendingReclaimer | XAUTOCLAIM |
5분 idle 메시지 재처리 (idempotent safe)5min idle reclaim (idempotent safe) |
| Published Marker | router:published:{job_id}:{seq} |
TTL 2h (중복 발행 방지)(prevent duplicate publish) |
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Chat Worker Infrastructure (Production) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Dual-Path Messaging Architecture │
│ ════════════════════════════════════════════════════════════════════════ │
│ Path 1: RabbitMQ (Job Execution) │ Path 2: Redis (Event Streaming) │
│ ───────────────────────────────── │ ────────────────────────────────── │
│ DIRECT exchange: chat_tasks │ Streams + Pub/Sub (3-Tier) │
│ Queue: chat.process │ Durable → Router → Volatile │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Taskiq Worker (asyncio-native) │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ taskiq worker chat_worker.main:broker --workers 4 --max-async-tasks 10 │ │
│ │ @broker.task(task_name="chat.process", timeout=120, max_retries=2) │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ Job Submission Flow (chat-api → chat-worker) │
│ ──────────────────────────────────────────────────────────────────────── │
│ chat-api chat-worker │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ AioPikaBroker │ AMQP │ @broker.task │ │
│ │ .kiq(job_id, │ ──────────▶ │ async def process() │ │
│ │ session_id, │ JSON │ → LangGraph │ │
│ │ message, │ │ → Event Publish │ │
│ │ image_url?) │ └─────────────────────┘ │
│ └─────────────────────┘ │
│ │
│ Event Bus Layer (Idempotent Fan-out) │
│ ──────────────────────────────────────────────────────────────────────── │
│ Worker ──XADD──▶ Redis Streams ──▶ Event Router ──PUBLISH──▶ Pub/Sub │
│ (chat:events:*) │ │
│ │ Lua Script (Atomic) │
│ ├─ seq 중복 검사 │
│ ├─ State KV Update │
│ └─ router:published:{job}:{seq} │
│ │ │
│ ▼ │
│ SSE Gateway ──▶ Client │
│ ├─ In-memory fan-out │
│ ├─ State recovery (Redis KV) │
│ └─ Catch-up (XRANGE) │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
| 지표Metric | 결과Result | 상세Detail |
|---|---|---|
| 동시 처리Concurrency | 40 tasks | 4 workers × 10 async-tasks (--workers 4 --max-async-tasks 10) |
| Connection PoolConnection Pool | 212→33 (84%↓) | ReadThroughCheckpointer로 PostgreSQL 직접 접근 제거Removed direct PostgreSQL access via ReadThroughCheckpointer |
| 체크포인트 접근Checkpoint Access | ~1ms (Redis) | Cold: 10-50ms (PG) → Warm: ~1ms (Redis Cache)Cold: 10-50ms (PG) → Warm: ~1ms (Redis Cache) |
| 멀티턴 검증Multi-turn Verified | 5턴 38 steps | 40 checkpoints, 45 blobs, 162 writes 검증Verified 40 checkpoints, 45 blobs, 162 writes |
| 항목Aspect | Celery (Scan) | Taskiq (Chat) |
|---|---|---|
| 동시성 모델Concurrency | gevent (greenlet) | asyncio (native) |
| LangGraph 호환LangGraph | △ (래핑 필요Wrapping needed) | ✅ Native async |
| BrokerBroker | RabbitMQ | RabbitMQ (재사용) |
| 결과 반환Result | AsyncResult | TaskiqResult |
| 구성 요소Component | 설정Config | 설명Description |
|---|---|---|
| Broker | AioPikaBroker |
RabbitMQ asyncio 클라이언트RabbitMQ asyncio client |
| Exchange | chat_tasks (direct) |
Topology CR로 미리 생성Pre-created by Topology CR |
| Queue | chat.process |
DLX, TTL 설정 포함With DLX, TTL settings |
| Checkpointer | ReadThroughCheckpointer |
Redis Primary + PostgreSQL Async Sync (3-Tier Memory)Redis Primary + PostgreSQL Async Sync (3-Tier Memory) |
| LLM Models | gpt-5.2, gemini-3-flash |
Fallback 체인Fallback chain |
| Tier | 컴포넌트Component | 역할Role |
|---|---|---|
| 1. Streams | chat:events:{shard} |
Durable Buffer, Worker → XADDDurable Buffer, Worker → XADD |
| 2. Event Router | Lua Script |
Idempotent 처리, seq 중복검사, State KVIdempotent processing, seq dedup, State KV |
| 3. Pub/Sub | chat:{job_id} |
Volatile Broadcast → SSE GatewayVolatile Broadcast → SSE Gateway |
| 기능Feature | 구현Implementation | 설정값Config |
|---|---|---|
| Stale Detection | 3초 타임아웃 시 자동 재연결Auto-reconnect on 3s timeout | 3s threshold |
| Max Reconnections | 지수 간격으로 최대 3회 시도3 attempts with exponential spacing | 3 attempts |
| Fallback Polling | SSE 실패 시 폴링 전환Polling fallback if SSE fails | 3s interval, 120s max |
| Seq Encoding | Stage: STAGE_ORDER×10, Token: 1000+Stage: STAGE_ORDER×10, Token: 1000+ | Last-Event-ID |
| Catch-up | XREVRANGE로 누락 이벤트 복구XREVRANGE for missed events | State KV + Streams |
┌─────────────────────────────────────────────────────────────────────────────────┐ │ Chat Observability Architecture │ ├─────────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ │ │ LangSmith (Feature-level Analysis) │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ • Per-node latency tracking │ │ │ │ • Token usage & cost monitoring │ │ │ │ • Error tracking & debugging │ │ │ │ • Run metadata (user_id, job_id, intent tags) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ LANGSMITH_OTEL_ENABLED=true │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ │ │ Jaeger (Distributed Tracing) │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ chat-api ───▶ RabbitMQ ───▶ chat-worker ───▶ LangGraph Pipeline │ │ │ │ │ │ │ │ │ │ │ └─ trace_id propagation ─────┴──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ │ │ Prometheus + Grafana (Metrics) │ │ │ │ ───────────────────────────────────────────────────────────────── │ │ │ │ chat_stream_token_latency_seconds │ Redis XADD delay detection │ │ │ │ chat_stream_duration_seconds │ P95 SLO target: 30s │ │ │ │ chat_stream_active │ Concurrent stream count │ │ │ │ chat_stream_tokens_total │ Per-node throughput │ │ │ └─────────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐ │ Application Layer (Ports) │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ TelemetryConfigPort │ │ │ │ ├── create_run_config(tags, metadata) → RunnableConfig │ │ │ │ └── Abstract: LangSmith 의존성 분리 │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Infrastructure Layer (Adapters) │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ LangSmithTelemetryAdapter │ NoOpTelemetryAdapter │ │ │ │ (Production) │ (Testing) │ │ │ └───────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘
| Metric | Type | 용도Purpose | Alert |
|---|---|---|---|
chat_stream_token_latency_seconds |
Histogram | Redis XADD 지연 감지Redis XADD delay detection | P95 > 50ms |
chat_stream_duration_seconds |
Histogram | 전체 스트림 완료 시간Total stream completion | P95 > 30s |
chat_stream_active |
Gauge | 동시 스트림 수Concurrent streams | > 100 |
chat_stream_tokens_total |
Counter | 노드별 처리량Per-node throughput | - |
| 필드Field | 값Value | 용도Purpose |
|---|---|---|
| tags | ["intent:disposal", "env:prod"] |
UI 필터링UI filtering |
| metadata.user_id | UUID |
사용자 추적User tracking |
| metadata.job_id | UUID |
작업 상관관계Job correlation |
| metadata.session_id | UUID |
대화 세션Conversation session |
userLocation이 비동기 geolocation API에서 로드되는데, 클로저가 초기값(undefined)을 캡처
useState)만 사용하여 브라우저 새로고침 시 모든 메시지 손실
Timeline: ───────────────────────────────────────────────────────────────────── T0: User sends message T1: Frontend Optimistic Update (즉시) T2: SSE streaming starts (0.5s) T3: SSE done event (3s) T4: Backend DB write starts (3.1s) ← Eventual Consistency T5: Backend DB write completes (3.3s) T6: User scrolls up → API call (4s) ← Race Condition! ─────────────────────────────────────────────────────────────────────────────── Gap: T1~T5 사이에 프론트엔드는 메시지를 가지고 있지만, 백엔드 DB에는 아직 없음
┌─────────────────────────────────────────────────────────────────────────────┐ │ Frontend-Backend Integration │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Frontend (Browser) │ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │ │ React State (Optimistic) │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ │ │ │ │ Messages │───▶│ Reconciler │───▶│ IndexedDB │ │ │ │ │ │ (client_id) │ │ (30s buffer) │ │ (Persistent)│ │ │ │ │ └─────────────┘ └──────────────┘ └─────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────────┘ │ │ │ ▲ │ │ POST /send │ │ GET /messages │ │ ▼ │ │ │ ────────────────────────────────────────────────────────────────────────── │ │ │ │ Backend (Kubernetes) │ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │ │ chat-api → RabbitMQ → chat-worker (LangGraph) │ │ │ │ │ │ │ │ │ ┌───────────────┼───────────────┐ │ │ │ │ ▼ ▼ ▼ │ │ │ │ Redis Streams SSE Gateway PostgreSQL │ │ │ │ (chat:events) (Real-time) (Eventual Write) │ │ │ │ │ ▲ ▲ │ │ │ │ └───────────────┘ │ │ │ │ │ event-router chat-consumer │ │ │ │ (Async Write) │ │ │ └────────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘
committed 상태이지만 server_id 없는 메시지를 30초간 유지client_id: 프론트엔드 UUID (불변, idempotency key)server_id: 백엔드 DB PK (done 이벤트 후 할당)useState는 클로저 캡처 → 비동기 완료 전 값 고정useRef는 항상 최신 값 참조 → geolocation 완료 후 반영| Consumer Group | Consumer | 용도Purpose | Latency |
|---|---|---|---|
eventrouter |
event-router |
SSE 실시간 전송SSE Real-time | ~10ms |
chat-persistence |
chat-consumer |
PostgreSQL 저장 (5초 Batch)PostgreSQL Write (5s Batch) | ~200ms |
type MessageStatus = 'pending' | 'streaming' | 'committed' | 'failed';
interface AgentMessage {
client_id: string; // UUID (프론트엔드 생성, 불변)
server_id?: string; // DB PK (백엔드 할당)
id: string; // Legacy compat (server_id || client_id)
role: 'user' | 'assistant';
content: string;
created_at: string;
status: MessageStatus;
}
User Message: pending ────────────────▶ committed
│ ▲
└───────────────────────────┘
(실패 시) failed
Assistant Message: streaming ──────────────▶ committed
| Message Status | Server_ID | Keep? | 이유Reason |
|---|---|---|---|
pending |
❌ | ✅ 항상 | 전송 중Sending |
streaming |
❌ | ✅ 항상 | SSE 수신 중SSE Receiving |
committed |
✅ | ❌ | 서버 버전으로 대체Replace with server |
committed |
❌ | ✅ 30초 내 | Eventual Consistency 버퍼Eventual Consistency buffer |
failed |
❌ | ✅ 항상 | 재시도 가능Retry available |
saveMessages(chatId, messages)getMessages(chatId)cleanup(chatId, options)
Timeline Frontend Backend
────────────────────────────────────────────────────────────────────────────────
T0: User clicks createUserMessage()
└─ client_id: uuid-1
└─ status: 'pending'
setMessages([...prev, userMsg])
IndexedDB.save(userMsg)
API.sendMessage(chatId, {...}) ───▶ POST /chat/:id/messages
T1: 0.1s RabbitMQ.publish()
└─ queue: chat.process
T2~T5: Streaming SSE: onmessage ◀─── XADD chat:events:0
appendStreamingText(token) LangGraph Pipeline
T6: done event handleSSEComplete() ◀─── {stage: "done", ...}
├─ updateUserMessage(uuid-1)
│ └─ status: 'committed'
│ └─ server_id: 'srv-uuid-1'
└─ IndexedDB.save(both)
T7~T8: Async chat-consumer
└─ PostgreSQL INSERT
T9: Pagination loadMoreMessages()
API.getChatDetail() ───▶ GET /chat/:id/messages
reconcileMessages(local, server) ◀─── {messages: [...]}
└─ 중복 제거 + 30s 버퍼 유지
────────────────────────────────────────────────────────────────────────────────
| Layer | Technology | 용도Purpose |
|---|---|---|
| Frontend State | React useState | Optimistic UI 상태Optimistic UI state |
| Frontend Cache | IndexedDB (idb) | 영구 저장 + 새로고침 복구Persistent + refresh recovery |
| Frontend Sync | Reconcile Algorithm | 로컬/서버 데이터 병합Local/server merge |
| Backend Real-time | Redis Streams → Pub/Sub → SSE | SSE 이벤트 전송SSE event delivery |
| Backend Persistence | PostgreSQL | Eventual WriteEventual Write |
| Backend Worker | LangGraph + TaskIQ + RabbitMQ | 비동기 메시지 처리Async message processing |
| Intent | Node | 방식 Method |
|---|---|---|
| 🗑️ waste | waste_rag | RAG (Tag-Based) |
| 🌍 character | character | gRPC |
| 📍 location | location | Tool Calling (Kakao) |
| 🛋️ bulk_waste | bulk_waste | Tool Calling (MOIS) |
| 💰 recyclable_price | recyclable_price | Tool Calling (KECO) |
| 📦 collection_point | collection_point | Tool Calling (KECO) |
| 🌤️ weather | weather | Tool Calling (KMA) |
| 🎨 image_generation | image_generation | Gemini Image API |
| 🔍 web_search | web_search | Native Tool (OpenAI/Gemini) |
| 💬 general | general | LLM Direct |
tools=[{"type": "function"}]google.genai.types.Tool@function_tooltool_start →
tool_progress →
tool_result →
answer_stream
Tool execution results stream in real-time via Event Bus:tool_start →
tool_progress →
tool_result →
answer_stream
┌─────────────────────────────────────────────────────────────────────────────────┐ │ Data Consistency Architecture │ ├─────────────────────────────────────────────────────────────────────────────────┤ │ │ │ Frontend (React) Backend │ │ ┌─────────────────────┐ ┌──────────────────────────────────┐ │ │ │ useState (Optimistic)│ ──POST──────► │ API → RabbitMQ → LangGraph │ │ │ │ IndexedDB (Persist) │ │ │ │ │ │ │ Reconcile Algorithm │ ◄──SSE─────── │ ▼ │ │ │ └─────────────────────┘ │ Redis Streams (Fan-out) │ │ │ │ │ ├─ eventrouter → SSE (~10ms) │ │ │ │ 500ms throttle │ └─ persistence → PG (~200ms) │ │ │ ▼ └──────────────────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ IndexedDB │ TTL: 7일 (일반) / 30초 (synced) │ │ │ + Auto Cleanup (1m) │ Compound Index: [chat_id, timestamp] │ │ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────────┘
| Consumer Group | 목적Purpose | 지연시간Latency |
|---|---|---|
eventrouter |
SSE 실시간 전송SSE Real-time delivery | ~10ms |
chat-persistence |
PostgreSQL 저장PostgreSQL persistence | ~200ms |
┌─────────────────────────────────────────────────────────────────────┐ │ Multi-Model LLM Architecture (Port/Adapter) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ LLMClientPort (ABC) │ │ │ │ ─────────────────────────────────────────────────────────── │ │ │ │ • generate(prompt, system_prompt) → str │ │ │ │ • generate_stream(prompt) → AsyncIterator[str] │ │ │ │ • generate_structured(prompt, schema) → T │ │ │ │ • generate_with_tools(prompt, tools) → AsyncIterator[str] │ │ │ │ • generate_function_call(prompt, functions) → (name, args) │ │ │ └───────────────────────┬───────────────────────────────────────┘ │ │ │ │ │ ┌──────────────┼──────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ │ │ OpenAI │ │ Gemini │ │ LangChain │ │ │ │ Adapter │ │ Adapter │ │ Adapter │ │ │ ├─────────────┤ ├─────────────┤ ├──────────────┤ │ │ │ gpt-5.2 │ │ gemini-3- │ │ OpenAI SDK │ │ │ │ Agents SDK │ │ flash/pro │ │ → Runnable │ │ │ │ (Primary) │ │ Gemini SDK │ │ → astream() │ │ │ │ +Responses │ │ (FC, Image) │ │ Token capture│ │ │ │ (Fallback) │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └──────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ Model Registry (provider.py) │ │ │ │ ─────────────────────────────────────────────────────────── │ │ │ │ openai/gpt-5.2 │ 400K ctx │ tools, vision │ │ │ │ google/gemini-3-flash-preview │ 1M ctx │ tools, vision │ │ │ │ google/gemini-3-pro-image │ - │ image_gen │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ │ Runtime Selection: create_llm_client(provider, model) │ │ Auto-Inference: "gemini-*" → google, "gpt-*" → openai │ │ LLMPolicyPort: ModelTier(FAST/STANDARD/PREMIUM) × TaskType │ │ │ └─────────────────────────────────────────────────────────────────────┘
generate_with_tools() — Agents SDK Primary + Responses Fallbackgenerate_structured() — Agent output_type (Pydantic 자동 파싱)generate_function_call() — Chat Completions functionsgpt-image-1.5 toolresponse.output_text.delta 이벤트
generate_content() — 텍스트 생성generate_content_stream() — 스트리밍response_mime_type + schemaresponse_modalities=["TEXT","IMAGE"]| Model ID | Provider | Context | Capabilities |
|---|---|---|---|
openai/gpt-5.2 | OpenAI | 400K | tools, vision, web_search |
google/gemini-3-flash-preview | 1M | tools, vision | |
google/gemini-3-pro-image | - | image_gen, char_ref |
create_llm_client(provider, model) 팩토리에서 모델명 prefix로 Provider 자동 추론.LLMPolicyPort.select_model(task_type, tier)로 태스크별 최적 모델 선택.create_llm_client(provider, model) factory auto-infers provider from model name prefix.LLMPolicyPort.select_model(task_type, tier) selects optimal model per task.stream_mode="messages"로 토큰 캡처 시:OpenAI SDK → LangChainOpenAIRunnable(BaseChatModel) → AIMessageChunkLLMClientPort.generate_with_tools() → notify_token_v2() 직접 발행
For LangGraph stream_mode="messages" token capture:OpenAI SDK → LangChainOpenAIRunnable(BaseChatModel) → AIMessageChunkLLMClientPort.generate_with_tools() → notify_token_v2() direct emit
┌─────────────────────────────────────────────────────────────────────┐ │ Intent Classification Pipeline │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User Message ──► intent_node ──► dynamic_router ──► subagents │ │ │ │ │ ┌─────────────┴─────────────┐ │ │ │ ClassifyIntentCommand │ (UseCase) │ │ │ ┌───────────────────────┐ │ │ │ │ │ 1. Cache 조회 │ │ SHA256(message) → Redis │ │ │ │ TTL: 3600s (1시간) │ │ │ │ │ ├───────────────────────┤ │ │ │ │ │ 2. LLM 호출 │ │ │ │ │ │ intent.txt 프롬프트 │ │ │ │ │ │ → JSON 응답 파싱 │ │ │ │ │ ├───────────────────────┤ │ │ │ │ │ 3. IntentClassifier │ │ (Service) │ │ │ │ Service │ │ │ │ │ │ • Keyword Boost +0.2 │ │ │ │ │ │ • Chain-of-Intent │ │ │ │ │ │ Transition +0.15 │ │ │ │ │ │ • Length Penalty -0.1 │ │ │ │ │ │ • THRESHOLD: 0.6 │ │ │ │ │ ├───────────────────────┤ │ │ │ │ │ 4. Multi-Intent 감지 │ │ │ │ │ │ "그리고","또","같이" │ │ │ │ │ │ → Query Decompose │ │ │ │ │ ├───────────────────────┤ │ │ │ │ │ 5. 캐릭터 이름 감지 │ │ │ │ │ │ "페티","이코" 등 │ │ │ │ │ │ → image_gen 참조용 │ │ │ │ │ ├───────────────────────┤ │ │ │ │ │ 6. Cache 저장 │ │ │ │ │ └───────────────────────┘ │ │ │ └────────────────────────────┘ │ │ │ │ Output → state: │ │ intent, confidence, is_complex, has_multi_intent, │ │ additional_intents, decomposed_queries, intent_history, │ │ detected_character │ │ │ └─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ │ waste Intent Pipeline (RAG) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User Message ──► rag_node ──► answer_node ──► SSE Stream │ │ │ │ │ ┌───────┴───────┐ │ │ │ RetrieverPort │ (Vector DB Search) │ │ │ ─────────────│ │ │ │ • Query 임베딩 │ │ │ • Similarity Search │ │ │ • Top-K 규정 문서 반환 │ │ └───────────────┘ │ │ │ │ │ ┌───────┴───────┐ │ │ │ SearchRAGCommand│ │ │ │ (UseCase) │ │ │ │ • disposal_rules │ │ │ • classification │ │ │ • situation_tags │ │ └───────────────┘ │ │ │ │ Policy: NodeExecutor (FAIL_OPEN, timeout: 3000ms, retry: 1) │ │ │ └─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ │ character Intent Pipeline (gRPC) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User Message ──► character_node ──► answer_node ──► SSE Stream │ │ │ │ │ ┌─────────┴─────────┐ │ │ │ LLM: 카테고리 추출 │ (CategoryExtractorService) │ │ │ "이코가 누구야?" │ │ │ │ → category: "이코" │ │ │ └─────────┬─────────┘ │ │ │ │ │ ┌─────────┴─────────┐ │ │ │ CharacterClientPort│ (gRPC Call) │ │ │ ──────────────────│ │ │ │ • GetCharacter() │ │ │ │ • 13 캐릭터 데이터 │ │ │ │ • 이미지 에셋 로드 │ │ │ └───────────────────┘ │ │ │ │ Policy: NodeExecutor (FAIL_OPEN, timeout: 3000ms, retry: 1) │ │ ※ 선택적 컨텍스트 - 실패해도 파이프라인 진행 │ │ │ └─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ location Intent Pipeline (Function Calling) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ User Message ──► kakao_place_node ──► answer_node ──► SSE Stream │
│ │ │
│ ┌─────────┴──────────┐ │
│ │ LLM Function Calling│ │
│ │ ───────────────────│ │
│ │ search_kakao_place: │ │
│ │ • query: "재활용센터"│ │
│ │ • search_type: keyword│ │
│ │ • radius: 5000 (m) │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌─────────┴──────────┐ │
│ │ SearchKakaoPlace │ │
│ │ Command (UseCase) │ │
│ │ ───────────────────│ │
│ │ KakaoLocalClientPort│ → REST API │
│ │ • Keyword Search │ │
│ │ • Category Search │ │
│ └────────────────────┘ │
│ │
│ HITL: user_location 없으면 → notify_needs_input("location") │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ │ bulk_waste Intent Pipeline (LLM Agent + MOIS API) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User ──► bulk_waste_agent_node ──► answer_node ──► SSE Stream │ │ │ │ │ ┌──────────┴──────────┐ │ │ │ LLM Agent Loop │ (max 5 iterations) │ │ │ ┌───────────────┐ │ │ │ │ │ GPT-5.2 Strict│ │ or │ Gemini 3 │ │ │ │ └───────┬───────┘ │ │ │ │ │ │ │ │ │ tool_choice: auto│ │ │ │ │ │ │ │ │ ┌───────┴───────┐ │ │ │ │ │ Tool Registry │ │ │ │ │ │───────────────│ │ │ │ │ │• get_collection│ │ → 수거 방법/신청 URL/전화번호 │ │ │ │ _info(sigungu)│ │ │ │ │ │• search_fee │ │ → 품목별 수수료 검색 │ │ │ │ (sigungu,item)│ │ │ │ │ └───────────────┘ │ │ │ └─────────────────────┘ │ │ │ │ 병렬 Tool 실행: asyncio.gather(*tool_calls) │ │ Provider: OpenAI (Strict Mode) / Gemini (AUTO mode) │ │ │ └─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ │ recyclable_price Intent Pipeline (LLM Agent + KECO API) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User ──► recyclable_price_agent_node ──► answer_node ──► SSE │ │ │ │ │ ┌──────────┴──────────┐ │ │ │ LLM Agent Loop │ (max 5 iterations) │ │ │ ┌───────────────┐ │ │ │ │ │ GPT-5.2/Gemini│ │ │ │ │ └───────┬───────┘ │ │ │ │ │ │ │ │ │ ┌───────┴───────┐ │ │ │ │ │ Tool Registry │ │ │ │ │ │───────────────│ │ │ │ │ │• search_price │ │ → 품목명 검색 (부분 매칭) │ │ │ │ (item_name, │ │ │ │ │ │ region?) │ │ │ │ │ │• get_category │ │ → 카테고리별 전체 조회 │ │ │ │ _prices │ │ │ │ │ │ (category, │ │ │ │ │ │ region?) │ │ │ │ │ └───────────────┘ │ │ │ └─────────────────────┘ │ │ │ │ 8 Regions: capital, gangwon, chungbuk, chungnam, │ │ jeonbuk, jeonnam, gyeongbuk, gyeongnam + national │ │ 5 Categories: paper, plastic, glass, metal, tire │ │ │ └─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ │ collection_point Intent Pipeline (LLM Agent + KECO API) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User ──► collection_point_agent_node ──► answer_node ──► SSE │ │ │ │ │ ┌──────────┴──────────┐ │ │ │ LLM Agent Loop │ (max 5 iterations) │ │ │ ┌───────────────┐ │ │ │ │ │ GPT-5.2/Gemini│ │ │ │ │ └───────┬───────┘ │ │ │ │ │ │ │ │ │ ┌───────┴──────────────┐ │ │ │ │ Tool Registry │ │ │ │ │─────────────────────│ │ │ │ │• search_collection │ → 주소/상호명 검색 │ │ │ │ _points │ │ │ │ │• get_nearby │ → 좌표 기반 주변 검색 │ │ │ │ _collection_points │ │ │ │ │• geocode │ → 장소명→좌표 변환 (Kakao) │ │ │ └──────────────────────┘ │ │ └─────────────────────────┘ │ │ │ │ Multi-Step Pattern: │ │ "[지역명] 근처 수거함" → geocode(지역) → get_nearby(lat,lon) │ │ │ │ 수거 품목: 폐전자제품, 폐건전지, 폐형광등 │ │ │ └─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ │ web_search Intent Pipeline (Agents SDK + Fallback) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ AS-IS (Responses API raw) TO-BE (Agents SDK Primary) │ │ ───────────────────────── ────────────────────────── │ │ Intent → /v1/responses (timeout빈발) Intent → Agents SDK Runner │ │ → FC 2회 호출 (재확인) → WebSearchTool (재시도 내장) │ │ → 검색결과 미반영 (FAIL_OPEN) → 1회 호출 (Router 신뢰) │ │ │ │ ┌─ Primary: Agents SDK ─────────────────────────────────────────────────┐ │ │ │ │ │ │ │ agent = Agent( │ │ │ │ model=OpenAIResponsesModel(model="gpt-5.2"), │ │ │ │ tools=[WebSearchTool()], # SDK 내장 재시도 │ │ │ │ output_type=response_schema, # Pydantic 자동 파싱 │ │ │ │ ) │ │ │ │ result = await Runner.run(agent, input=prompt) │ │ │ │ → result.final_output # 이미 구조화된 응답 │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ │ │ Exception 발생 시 │ │ ┌─ Fallback: Responses API ─────────────────────────────────────────────┐ │ │ │ generate_with_responses_api(tools=["web_search"]) │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ │ │ │ ※ DuckDuckGo web_search_node.py는 DEPRECATED (Feedback Fallback용 잔존) │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
| 기능Feature | AS-IS (Responses API raw) | TO-BE (Agents SDK) |
|---|---|---|
| Web Search | Responses API raw 호출 (타임아웃 빈발)Responses API raw call (frequent timeout) | Agents SDK Primary + Fallback |
| 구조화 출력Structured Output | json_schema 수동 파싱Manual json_schema parsing | Agent output_type 자동화 (Pydantic)Agent output_type auto (Pydantic) |
| 결정 프로세스Decision Process | FC로 재확인 (2회 호출)FC re-confirmation (2 calls) | Router 신뢰 (1회 호출, ~500ms↓)Router trust (1 call, ~500ms↓) |
| 에러 핸들링Error Handling | 미적용 (FAIL_OPEN 동작)Not applied (FAIL_OPEN behavior) | SDK 내장 재시도 + Fallback 체인SDK built-in retry + Fallback chain |
| 검증Verification | — | 760 passed, 5 skipped |
┌─────────────────────────────────────────────────────────────────────┐ │ image_generation Intent Pipeline (Gemini SDK Native) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User Message ──► image_generation_node ──► answer_node ──► SSE │ │ │ │ │ ┌──────────┴──────────┐ │ │ │ Character Reference │ (CharacterAssetPort) │ │ │ ────────────────────│ │ │ │ • 13 캐릭터 에셋 로드│ │ │ │ • 참조 이미지 base64 │ │ │ └──────────┬──────────┘ │ │ │ │ │ ┌──────────┴──────────┐ │ │ │ Gemini SDK Native │ │ │ │ ────────────────────│ │ │ │ • gemini-3-pro-image │ │ │ │ • Native 이미지 생성 │ │ │ │ • response_modalities│ │ │ │ : ["TEXT","IMAGE"] │ │ │ └──────────┬──────────┘ │ │ │ │ │ ┌──────────┴──────────┐ │ │ │ ImageStoragePort │ (gRPC → Images API) │ │ │ ────────────────────│ │ │ │ • base64 → bytes │ │ │ │ • gRPC UploadImage() │ │ │ │ • CDN URL 반환 │ │ │ └─────────────────────┘ │ │ │ │ Timeout: 30000ms | System Prompt: Character Fidelity 원칙 │ │ │ └─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ │ general Intent Pipeline (Answer Generation) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ User Message ──► answer_node ──► SSE Token Stream │ │ │ │ │ ┌────────┴────────┐ │ │ │ Context Assembly │ │ │ │ ────────────────│ │ │ │ • disposal_rules │ (waste 결과) │ │ │ • character_ctx │ (character 결과) │ │ │ • location_ctx │ (location 결과) │ │ │ • price_ctx │ (recyclable_price 결과) │ │ │ • bulk_waste_ctx │ (bulk_waste 결과) │ │ │ • weather_ctx │ (weather 결과) │ │ │ • collection_ctx │ (collection_point 결과) │ │ │ • image_gen_ctx │ (image_generation 결과) │ │ │ • web_search_ctx │ (web_search 결과) │ │ └────────┬────────┘ │ │ │ │ │ ┌────────┴────────┐ │ │ │ PromptBuilder │ │ │ │ ────────────────│ │ │ │ • System Prompt │ (eco_character.txt) │ │ │ • Intent Prompt │ (general_instruction.txt) │ │ │ • Context Inject │ (위 모든 컨텍스트) │ │ │ • History (10) │ (Multi-turn 대화) │ │ └────────┬────────┘ │ │ │ │ │ ┌────────┴────────┐ │ │ │ LLM Streaming │ │ │ │ ────────────────│ │ │ │ • LangChain astream() │ │ │ │ • or generate_stream()│ │ │ │ → notify_token_v2() │ → Redis → SSE │ │ └─────────────────┘ │ │ │ │ Lamport Clock: cleanup_sequence(job_id) at pipeline end │ │ │ └─────────────────────────────────────────────────────────────────────┘
model: 미지정model: "gpt-5.2"model: "gemini-3-pro-preview"LangChainLLMAdapterget_langchain_llm() 메서드 제공method providedlangchain_llm.astream() 스트리밍streamingGeminiLLMClientgenerate_stream() 직접 스트리밍direct streaminggoogle-generativeai SDKhasattr 분기로 호환성 확보branching for compatibility| 시나리오Scenario | 요청Request | Provider | LLM Client | 결과Result |
|---|---|---|---|---|
| 1 | model: 미지정 |
openai (env) | LangChainLLMAdapter | ✅ |
| 2 | model: "gpt-5.2" |
openai (auto) | LangChainLLMAdapter | ✅ |
| 3 | model: "gemini-3-pro-preview" |
google (auto) | GeminiLLMClient | ✅ |
| VU | 완료율Success | RPM | E2E p95 | Submit p95 | Snapshot |
|---|---|---|---|---|---|
| 500 | 100% | 367.9 | 83.3s | 232ms | → |
| 600 | 99.7% | 358.6 | 108.3s | 360ms | → |
| 700 | 99.2% | 329.1 | 122.3s | 444ms | Live |
| 800 | 99.7% | 367.3 | 144.6s | 734ms | → |
| 900 | 99.7% | 405.5 | 149.6s | 635ms | → |
| 1000 | 97.8% | 373.4 | 173.3s | 787ms | → |
| Category | Nodes | Instance Types | Total vCPU | Total Memory | Total Storage |
|---|---|---|---|---|---|
| Control Plane | 1 | t3.xlarge | 4 | 16GB | 80GB |
| API Nodes | 8 | t3.small(6), t3.medium(2) | 16 | 20GB | 180GB |
| Workers | 4 | t3.medium | 8 | 16GB | 160GB |
| Data Layer | 6 | t3.small(4), t3.medium(2), t3.large(1) | 12 | 22GB | 180GB |
| Observability | 2 | t3.large, t3.xlarge | 6 | 24GB | 160GB |
| Network | 3 | t3.small(2), t3.medium(1) | 6 | 8GB | 60GB |
| Total | 24 | - | 52 vCPU | 106GB | 820GB |
terraform/main.tf
— IaC로 관리되는 AWS EC2 기반 Kubernetes 클러스터
| Agent | NodePolicy | Circuit Breaker | Timeout | Rationale |
|---|---|---|---|---|
intent_classifier |
FAIL_CLOSE | 5회/60s | 10s | Intent 없이 라우팅 불가 |
weather_agent |
FALLBACK | 5회/60s | 15s | 외부 API 의존, 대체 응답 가능 |
eco_guide_agent |
FALLBACK | 5회/60s | 20s | RAG + Web Search 체인 |
summarize |
FAIL_OPEN | 3회/30s | 10s | 선택적 압축, 원본 전달 가능 |
aggregator |
FAIL_CLOSE | - | 30s | 모든 결과 수집 필수 |
# Fallback Chain Implementation
async def run_with_fallback(self, query: str) -> AgentResponse:
# 1. RAG 시도
rag_response = await self.rag_retriever.query(query)
if self._evaluate_quality(rag_response) >= 0.7:
return rag_response
# 2. Web Search 시도
web_response = await self.web_search.search(query)
if self._evaluate_quality(web_response) >= 0.7:
return web_response
# 3. General LLM Fallback
return await self.general_llm.generate_fallback(query)