개요
개요
Lumie AI는 학원 교육 플랫폼을 지원하는 4개의 독립적인 FastAPI 마이크로서비스로 구성됩니다. 각 서비스는 단일 책임 원칙에 따라 설계되었으며, Kubernetes 환경에서 독립적으로 배포·운영됩니다. 백엔드(lumie-backend)가 RabbitMQ 또는 HTTP를 통해 각 서비스를 오케스트레이션하며, 서비스 간 직접 호출은 없습니다.
기술 스택
| 항목 | 버전 / 값 |
|---|---|
| 언어 | Python 3.11+ |
| 웹 프레임워크 | FastAPI 0.115 |
| ASGI 서버 | Uvicorn |
| 메시지 브로커 | RabbitMQ (aio_pika) |
| DB 연결 풀 | psycopg AsyncConnectionPool (pgbouncer 트랜잭션 모드) |
| 컨테이너 레지스트리 | zot.lumie-infra.com |
| 개발 환경 | Tilt (hot reload) |
모든 서비스는 공통적으로 다음을 제공합니다.
GET /health— 헬스체크 엔드포인트- OpenTelemetry 트레이싱 (OTLP gRPC)
- Prometheus 메트릭 (
/metrics) - Uvicorn을 통한 ASGI 실행
프로젝트 구조
lumie-worker/
├── services/
│ ├── grading/ # OMR 자동 채점 (OpenCV, NumPy, RabbitMQ 소비자)
│ ├── report/ # 학생 리포트 생성 (Playwright, Jinja2, RabbitMQ 소비자)
│ ├── analysis/ # AI 피드백 생성 (OpenAI GPT-4o-mini, HTTP API)
│ └── chatbot/ # LangGraph 챗봇 (Gemini 2.5 Flash, SSE 스트리밍, HITL)
├── libs/
│ └── common/ # 공통 유틸리티 (mq.universal_process, HMAC 서명, 로깅)
└── Tiltfile # Tilt 개발 환경 설정
서비스 목록
채점 서비스 (grading-svc)
OMR(광학 마킹 인식) 방식으로 시험지 이미지를 분석하여 자동 채점을 수행합니다. RabbitMQ grading.omr-request 큐를 소비하고, 결과를 grading.omr.callback 라우팅 키로 백엔드에 콜백합니다.
- 포트:
8000 - 주요 라이브러리: OpenCV 4.8, NumPy, Pillow
- 상세 문서: 채점 서비스
리포트 서비스 (report-svc)
학생별 시험 결과를 Jinja2 템플릿으로 렌더링한 뒤 Playwright Chromium을 통해 JPG 이미지로 출력합니다. RabbitMQ report.generation-request 큐를 소비하고, 결과를 report.generation.callback 라우팅 키로 백엔드에 콜백합니다.
- 포트:
8000 - 주요 라이브러리: Playwright 1.50.0, Jinja2 3.1.3, httpx 0.27.0
- 상세 문서: 리포트 서비스
분석 서비스 (analysis-svc)
OpenAI GPT-4o-mini를 활용해 시험 전체 해설과 학생 개별 피드백을 한국어로 생성합니다. RabbitMQ 소비자 없이 HTTP API만 제공하며, lumie-backend 및 joossameng FE가 직접 호출합니다.
- 포트:
8000 - 주요 라이브러리: OpenAI SDK 1.82.0, Pydantic 2.10.0
- 상세 문서: 분석 서비스
챗봇 서비스 (chatbot-svc)
LangGraph 에이전트 기반의 학원 운영 어시스턴트입니다. 2026-05-26 lumie-backend의 Spring AI 챗봇을 대체하여 추가되었습니다. Gemini 2.5 Flash 모델을 사용하며, SSE 스트리밍과 Human-in-the-Loop(HITL) 도구 승인을 지원합니다. RabbitMQ를 사용하지 않으며, lumie-backend가 HTTP로 직접 프록시합니다.
- 포트:
8000 - 주요 라이브러리: LangGraph 0.3.5, langchain-openai 0.3.23, psycopg-pool 3.2.4
- DB: LangGraph 체크포인터 전용 Postgres (스키마
langgraph, 롤lumie_langgraph)
챗봇 서비스 상세
아키텍처
lumie-backend가 프론트엔드 요청을 받아 chatbot-svc에 프록시합니다. chatbot-svc는 LangGraph 그래프를 실행하여 백엔드의 /internal/chatbot/** 엔드포인트를 통해 테넌트 데이터를 조회·조작합니다. chatbot-svc는 테넌트 테이블에 직접 접근하지 않으며, 모든 실행과 영속성은 모노리스에서 처리됩니다.
LangGraph 그래프 구조
- scope_check: 프롬프트 인젝션 및 도메인 외 요청 사전 차단
- agent: Gemini 2.5 Flash에 도구를 바인딩하여 응답 생성
- tools: 읽기 도구(
general_query)는 즉시 실행; 쓰기 도구는interrupt()로 HITL 일시 중단 - give_up: SQL 재시도 횟수(
sql_max_retries=3) 초과 시 안내 메시지 반환 - cancel: 사용자가 HITL에서 거부한 경우 취소 메시지 반환
HTTP API 엔드포인트
POST /api/chatbot/stream — SSE 스트리밍
LangGraph 에이전트 실행을 Server-Sent Events로 스트리밍합니다. 클라이언트가 연결을 끊어도 그래프 실행은 계속되어 결과가 백엔드에 저장됩니다(단절 안전 내구성).
요청 헤더
| 헤더 | 설명 |
|---|---|
X-Tenant-Slug | 테넌트 식별자 (바디의 tenantSlug와 일치해야 함) |
요청 바디 (JSON)
{
"tenantSlug": "academy-a",
"tenantId": 1,
"userId": 42,
"conversationId": null,
"message": "이번 달 결석 학생 목록 보여줘"
}
conversationId가 null이면 새 대화를 생성합니다.
SSE 이벤트 유형
| 이벤트 | 데이터 형식 | 설명 |
|---|---|---|
token | \{"content": "..."}\ | 어시스턴트 토큰 스트림 |
pending | \{"messageId": 1, "toolName": "...", "description": "...", "arguments": \{\}\} | HITL 승인 대기 |
done | \{"conversationId": 1, "messageId": 5\} | 스트림 완료 |
error | \{"message": "..."\} | 오류 발생 |
POST /api/chatbot/confirm — HITL 도구 승인
HITL로 일시 중단된 그래프를 재개합니다.
요청 바디 (JSON)
{
"tenantSlug": "academy-a",
"tenantId": 1,
"userId": 42,
"conversationId": 1,
"messageId": 5,
"confirmed": true
}
에이전트 도구 목록
| 도구 | 유형 | 설명 |
|---|---|---|
general_query | 읽기 | SELECT SQL을 백엔드 SqlValidator+RLS를 통해 실행 |
create_announcement | 쓰기 (HITL) | 공지사항 생성 |
send_sms | 쓰기 (HITL) | SMS 발송 |
send_telegram | 쓰기 (HITL) | 텔레그램 발송 |
schedule_task | 쓰기 (HITL) | 반복·일회성 작업 예약 |
쓰기 도구는 interrupt()로 실행을 일시 중단하여 사용자 승인 후에만 실행됩니다.
DB 연결 풀 (AsyncConnectionPool)
LangGraph AsyncPostgresSaver 체크포인터는 단일 커넥션이 아닌 AsyncConnectionPool을 사용합니다. CNPG Pooler(pgbouncer 트랜잭션 모드)의 server_lifetime 순환·유휴 타임아웃·NAT 리셋으로 단일 커넥션이 끊어지면 재연결이 불가능하기 때문입니다.
pool = AsyncConnectionPool(
conninfo=settings.langgraph_db_dsn,
min_size=1,
max_size=5,
timeout=30.0,
max_idle=180.0, # pgbouncer server_idle_timeout(~600s)보다 낮게 설정
check=AsyncConnectionPool.check_connection, # 핸드아웃 전 SELECT 1 검증
kwargs={
"autocommit": True, # langgraph가 트랜잭션 직접 관리
"prepare_threshold": 0, # pgbouncer txn-mode: prepared statement 비활성화
"row_factory": dict_row, # AsyncPostgresSaver 요구사항
},
)
환경 변수
| 변수 | 기본값 | 설명 |
|---|---|---|
LLM_API_KEY | — ( 필수) | Gemini API 키 |
LLM_BASE_URL | https://generativelanguage.googleapis.com/v1beta/openai | LLM 엔드포인트 |
LLM_MODEL | gemini-2.5-flash | 사용할 LLM 모델 |
LUMIE_BACKEND_URL | — (필수) | 백엔드 URL |
LUMIE_INTERNAL_HMAC_SECRET | — (필수) | HMAC 서명 비밀키 |
LANGGRAPH_DB_DSN | — (필수) | LangGraph 체크포인터 Postgres DSN |
RECURSION_LIMIT | 10 | LangGraph 최대 재귀 스텝 수 |
SQL_MAX_RETRIES | 3 | SQL 생성→검증→실행 재시도 최대 횟수 |
공통 라이브러리 (libs/common)
libs/common 패키지는 서비스 간 공통으로 사용하는 유틸리티를 제공합니다.
| 모듈 | 역할 |
|---|---|
mq.universal_process | RabbitMQ DLQ + 재시도 + 콜백 생명주기 |
callback_mq.CallbackMqPublisher | 라우팅 키 사전 바인딩된 콜백 퍼블리셔 |
sign_request | /internal/** 호출용 HMAC 서명 |
logging_config | 구조화된 JSON 로깅 (Loki json-stage 파싱 호환) |
observability | OTel 트레이싱 + Prometheus 메트릭 헬퍼 |
Kubernetes 리소스 현황
| 서비스 | 메모리 요청/한도 | CPU 요청/한도 | K8s 배포 여부 |
|---|---|---|---|
| grading-svc | 1Gi / 2Gi | 100m / 1000m | O |
| report-svc | 512Mi / 2Gi | 100m / — | O |
| chatbot-svc | 256Mi / 512Mi | 50m / — | O |
| analysis-svc | — | — | O (joossameng용) |
채점 서비스는 OpenCV CPU 집약적 연산을 위해 CPU 한도가 높게 설정되어 있습니다. 챗봇 서비스는 LangGraph 그래프와 Postgres 연결 풀만 유지하므로 상대적으로 경량입니다.
DB 연결 풀 패턴
장기 실행 워커는 단일 psycopg.AsyncConnection 대신 반드시 AsyncConnectionPool과 check=check_connection을 사용해야 합니다. CNPG Pooler(pgbouncer 트랜잭션 모드)는 server_lifetime 주기, 유휴 시간 초과, NAT 리셋 등으로 연결을 끊을 수 있으며, 단일 커넥션은 재연결되지 않습니다. 풀의 check_connection은 각 핸드아웃 전 SELECT 1로 유효성을 검증하여 끊어진 연결을 자동 교체합니다.
chatbot-svc가 이 패턴을 채택하고 있으며, max_idle=180s를 pgbouncer의 server_idle_timeout(약 600s)보다 낮게 설정해 클라이언트 측이 먼저 유휴 연결을 닫도록 합니다.
테넌트 전파
모든 서비스는 인바운드 RabbitMQ 메시지나 HTTP 요청에서 받은 X-Tenant-Slug를 백엔드 콜백 및 /internal/** 호출 시 그대로 전달합니다. chatbot-svc는 요청 헤더의 X-Tenant-Slug와 바디의 tenantSlug가 일치하지 않으면 400을 반환합니다.
개발 환경
모든 AI 서비스는 Tiltfile을 통해 로컬 Kubernetes 클러스터(lumie-dev 네임스페이스)에서 개발 서버를 실행합니다. 파일 변경 시 자동으로 컨테이너가 재빌드·재시작되는 hot reload를 지원합니다.
# 루트 디렉토리에서 Tilt 실행
.github/tilt-up.sh
포트 포워딩
개발 환경에서는 다음 포트로 각 서비스에 접근할 수 있습니다.
| 서비스 | 로컬 포트 | 클러스터 포트 |
|---|---|---|
| grading-svc | 18000 | 8000 |
| report-svc | 18001 | 8000 |
| chatbot-svc | 18002 | 8000 |