StackUp AI 서버. Python 3.13 + FastAPI + LangChain. RabbitMQ consumer로 동작하며 LLM 호출, RAG, 음성 분석을 담당.
상위 컨텍스트: /CLAUDE.md · 횡단 관심사: /docs/
| 영역 | 기술 |
|---|---|
| Language | Python 3.13 (.python-version 고정) |
| 패키지 매니저 | uv (uv.lock 락파일) |
| Framework | FastAPI 0.135+ |
| ASGI | uvicorn (standard) |
| 스키마 | Pydantic 2.x + pydantic-settings |
| HTTP | httpx |
| MQ | aio-pika (async AMQP) |
| 객체 스토리지 | boto3 (S3 호환) |
| LLM | LangChain 1.x (core + community) |
| 로깅 | structlog |
| 빌드 | hatchling |
| 테스트 | pytest + pytest-asyncio |
| 포맷/린트 | black, flake8, pylint |
ai/
├── pyproject.toml
├── uv.lock
├── .python-version
├── Dockerfile
├── .env.example
├── src/
│ └── ai_server/
│ ├── __init__.py
│ ├── main.py # FastAPI 앱 팩토리
│ ├── api/ # FastAPI 라우터 (health, internal endpoints)
│ │ └── health.py
│ ├── config/
│ │ └── settings.py # pydantic-settings (env)
│ ├── chain/ # (계획) LangChain 체인 정의
│ ├── rag/ # (계획) 청킹·임베딩·검색
│ ├── analyzer/ # (계획) 이력서·레포 분석
│ ├── voice/ # (계획) STT/TTS, 음성 분석
│ ├── messaging/ # (계획) RabbitMQ consumer/publisher
│ ├── storage/ # (계획) S3 client
│ └── model/ # (계획) Pydantic 모델 (메시지 envelope, 도메인)
└── tests/
현재는
api/,config/만 존재. 기능 추가 시 위 골격대로 디렉토리 생성.
| 모듈 | 책임 |
|---|---|
main.py |
FastAPI 부트스트랩, lifespan에서 RabbitMQ consumer 시작 |
config/settings.py |
환경변수 → 타입 안전 설정 객체 |
api/ |
헬스체크 + (필요 시) 내부 디버그 API |
messaging/ |
aio-pika consumer (큐별), publisher |
analyzer/ |
이력서/레포 분석 use case (PDF 추출, GitHub fetch, 마크다운 생성) |
chain/ |
LangChain prompt template + chain composition |
rag/ |
청킹, 임베딩 생성, pgvector 검색 호출 (Core API 경유) |
voice/ |
STT/TTS 어댑터, WPM/filler/silence 분석 |
storage/ |
S3 GET/PUT 래퍼 |
model/ |
RabbitMQ envelope, request/response Pydantic 모델 |
- ❌ PostgreSQL 직접 접근 — Core 서버 API 경유 또는 RabbitMQ 메시지에 데이터 동봉
- ❌ JWT 발급·검증 — 인증은 Core
- ❌ REST CRUD API 노출 — 외부 트리거는 RabbitMQ만
- ❌ 사용자 인증 (내부 통신만) —
api/는 헬스체크 / 내부 도구
본 서버는 RabbitMQ consumer로 작동.
| Queue | Bind |
|---|---|
q.ai.resume |
ai.request.resume.* |
q.ai.repo |
ai.request.repo.* |
q.ai.session |
ai.request.session.* |
콜백 발행: ai.callback.{type} 익스체인지.
상세 envelope/스키마/재시도: /docs/messaging.md.
async def consume_resume_analyze(message: AbstractIncomingMessage) -> None:
async with message.process(requeue=False): # auto ack on exit
envelope = parse_envelope(message)
with trace_context(envelope.trace_id):
await resume_analyzer.handle(envelope.payload)- envelope의
messageId를 PostgreSQLprocessed_messages테이블 (Core API 경유) 또는 AI 프로세스 인메모리 LRU 캐시에 기록 - 이미 존재하면 skip + ACK
- (Redis 미사용 — DB 1쿼리 부담을 감수하거나, 인메모리만 쓰고 재시작 시 RabbitMQ delivery_tag로 보조)
| 시점 | 모델 | 용도 |
|---|---|---|
| 세션 시작 | Pro (Gemini 3.1 Pro 기본) | 질문 풀 (품질) |
| 세션 중 | Flash (Gemini 3.1 Flash) | 꼬리질문 (저지연 < 3s) |
| 분석 (이력서/레포) | Pro | 마크다운 구조화 |
설정은 settings.py + 환경변수로 모델명 주입 (코드에 하드코딩 금지).
from langchain_core.prompts import ChatPromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 면접관입니다..."),
("human", "이력서: {resume}\n질문 후보: ..."),
])
llm = ChatGoogleGenerativeAI(model=settings.llm_pro_model)
chain = prompt | llm | StructuredOutputParser(schema=...)- 모든 프롬프트는
chain/prompts/{name}.py단일 위치 - 프롬프트 변경은 PR로 (커밋 메시지에 의도 명시)
- 응답은 반드시 schema validation (Pydantic) — LLM 결과를 그대로 신뢰 X
호출 시작/완료/실패를 Core 서버에 RabbitMQ로 보내 ai_request_logs에 기록 — 또는 자체 endpoint POST.
필드: request_type, model_name, input_tokens, output_tokens, latency_ms, status.
상세: /docs/observability.md §3.
- 마크다운 입력
- 청킹 (LangChain
RecursiveCharacterTextSplitter, chunk_size=1000, overlap=200) - 임베딩 생성 (Gemini
text-embedding-004또는 OpenAItext-embedding-3-small) - Core API 호출 → pgvector INSERT
- 쿼리 텍스트 → 임베딩
- Core API:
POST /api/internal/embeddings/search(top_k 검색) - 검색 결과 + 추가 메타데이터 → 프롬프트에 주입
Core가 pgvector 단일 진입점을 제공하므로 AI는 직접 pg 호출 X.
- STT: OpenAI Whisper API 채택 (한국어 + 개발 영어 혼용 환경 정확도 우수)
- 비용: $0.006 / 분 (1시간 면접 ≈ ₩500 / USD ≈ $0.36)
- 셀프호스팅 옵션:
whisper.cpp또는faster-whisper(GPU 권장, 비용 ↓ but 운영 부담 ↑) - 브라우저 내장 SpeechRecognition API는 정확도 부족으로 채택 안 함
- TTS 제공자 미정 → 도입 시 본 섹션 갱신
- 추상화 계층 두기:
voice/stt/base.py(interface),voice/stt/whisper_api.py,voice/tts/{provider}.py - 분석:
- WPM = words / minutes
- 간투어: 한국어 정규식
r"\b(음+|어+|그+)\b"카운트 - 침묵: VAD (Voice Activity Detection) 라이브러리 결과 합산
config/settings.py의 Settings 클래스가 진실 공급원 (single source of truth).
필수 변수는 default 없음 → 부팅 실패로 누락 감지.
class Settings(BaseSettings):
rabbitmq_url: str
s3_endpoint_url: str
s3_access_key: str
s3_secret_key: str
s3_bucket_name: str
openai_api_key: str = ""
google_api_key: str = ""
llm_pro_model: str = "gemini-3.1-pro"
llm_flash_model: str = "gemini-3.1-flash"
embedding_model: str = "text-embedding-004"
embedding_dim: int = 768
core_server_base_url: str = "http://core:8080"전체 환경변수 카탈로그: /docs/environment.md §4.
import structlog
log = structlog.get_logger()
log.info("resume.analyze.start", resume_id=42, trace_id=trace_id)
log.error("resume.analyze.failed", resume_id=42, error_code="PDF_PARSE_FAILED", exc_info=True)- JSON 출력 (운영) / human pretty (로컬)
- 컨텍스트 변수로
trace_id,user_id자동 주입 - 민감정보 (이력서 본문, 답변 본문) 절대 X
- 자세한 정책:
/docs/observability.md
uv run pytest # 전체
uv run pytest tests/test_rag.py # 특정 파일
uv run pytest -k "embedding" # 키워드- 비동기 테스트는
pytest-asyncio(@pytest.mark.asyncio) - LLM 호출은 mock (LangChain
FakeListLLM) - RabbitMQ는 Testcontainer 또는
aio-pikamock - 자세한 전략:
/docs/testing-strategy.md
- black (line-length 88)
- pyproject.toml 기준
- 함수/변수:
snake_case - 상수:
UPPER_SNAKE_CASE - 클래스:
PascalCase - 타입 힌트 필수 (
from __future__ import annotations없이 PEP 604 unionint | None) - async first — sync IO 사용 시 명시적 이유
상세 공통 규약: /docs/coding-conventions.md.
uv sync # 의존성 설치
uv run uvicorn ai_server.main:app --reload # 개발 실행
uv run python -m ai_server.messaging.runner # consumer 단독 실행 (도입 후)
# Docker
docker build -t stackup-ai .
docker run --env-file .env -p 8000:8000 stackup-aidocker-compose.yml에 ai 서비스 추가는 추후 (현재는 Core/PG/MQ/MinIO만 있음).
- 메시지 스키마 정의 →
model/messages.py - RabbitMQ routing key 결정 →
/docs/messaging.md갱신 - consumer 작성 →
messaging/{name}_consumer.py - 비즈니스 로직 →
analyzer//chain//voice/적절한 모듈 - 프롬프트 (LLM 호출 시) →
chain/prompts/{name}.py - 단위 테스트 (LLM mock) + 통합 테스트 (Testcontainer)
- 본 문서 §3, §5 갱신
- ❌ 프롬프트를 코드 안 곳곳에 산재시키기 →
chain/prompts/로 모은다 - ❌ LLM 응답을 파싱 없이 그대로 사용 → 항상 Pydantic schema validation
- ❌ 동기 라이브러리(
requests,pika) 사용 → async 라이브러리(httpx,aio-pika) - ❌ 트랜잭션이 필요한 작업 (PG 직접 접근) → Core API 호출
- ❌ messageId 멱등 체크 누락 → 중복 처리 위험
- ❌ 프롬프트에 사용자 답변을 그대로 system message로 → injection 가능. 반드시 user message로.
- FastAPI 부트스트랩 + 헬스체크만 구현
- RabbitMQ consumer 미구현 → US-09(이력서 분석) 작업 시 도입
- LangChain 기본 import만 있음, 체인·프롬프트 정의 0
- pgvector 연동 미구현
- 음성 모듈은 Phase 2에서 본격 작성
각 도입 시 본 문서 갱신.