Feat: 아키텍쳐 설계 수정

This commit is contained in:
0914eagle 2025-07-24 14:09:36 +09:00
parent 42d6b0335f
commit 80edf25e9a

View File

@ -1,4 +1,4 @@
# 로빙(Robing) 슬랙 요약 스킬 아키텍처 설계
# 로빙(Robing) 경량 멀티테넌트 아키텍처 설계
## 1. 개요
@ -6,9 +6,11 @@
- 로빙은 스탯 기반 성장형 AI 에이전트로, 회사별로 독립적인 컨테이너로 배포됨
- 로빙 브레인은 스킬 라우팅과 결과 판단에 집중하며, 실제 처리는 공용 스킬 서버에서 수행
- 프롬프트용 기억은 컨테이너 외부의 공용 저장소에 보관
- 회사가 100개 이상으로 확장되어도 효율적으로 운영 가능한 구조 필요
### 1.2 설계 원칙
- **경량화**: 회사별 로빙 컨테이너는 최소한의 리소스(512MB)로 운영
- **무상태(Stateless)**: 컨테이너는 언제든 재시작 가능하며 상태는 외부에서 관리
- **확장성**: 스킬 서버는 독립적으로 스케일링 가능
- **비용 효율성**: LLM API는 공용 서버에서만 호출하여 비용 최적화
- **독립성**: 각 스킬은 마이크로서비스로 분리되어 독립적 배포/업데이트 가능
@ -21,95 +23,272 @@
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 회사 A 로빙 │ │ 회사 B 로빙 │ │ 회사 C 로빙 │
│ (가벼운 라우터) │ │ (가벼운 라우터) │ │ (가벼운 라우터) │
│ (Stateless) │ │ (Stateless) │ │ (Stateless) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┴───────────────────────┘
├───────────────────────┼───────────────────────┤
│ │ │
┌────▼─────┐ ┌─────▼──────┐ ┌──────▼─────┐
│ 상태 │ │ 공용 스킬 │ │ 인증 │
│ 서비스 │ │ 서버 │ │ 서비스 │
└────┬─────┘ └────────────┘ └────────────┘
┌────────────▼────────────┐
│ 공용 스킬 서버 클러스터 │
├─────────────────────────┤
│ • 슬랙 요약 서비스 │
│ • 이메일 파싱 서비스 │
│ • 일정 관리 서비스 │
│ • LLM 처리 (Gemini/GPT) │
└─────────────────────────┘
┌────────────▼────────────┐
│ 공용 데이터 저장소 │
├─────────────────────────┤
│ • ChromaDB (벡터 DB) │
│ • PostgreSQL (메타데이터) │
│ • Redis (캐싱) │
└─────────────────────────┘
┌────▼─────┐ ┌────────────┐
│PostgreSQL│ │ Redis │
│ (영구상태)│ │ (캐시) │
└──────────┘ └────────────┘
```
### 2.2 데이터 흐름
```
1. Slack Event → 회사별 로빙 컨테이너
2. 로빙 브레인이 의도 분석 (키워드 기반, LLM 없이)
3. 적절한 스킬 서버로 라우팅
4. 스킬 서버에서 LLM 처리 및 결과 생성
5. 결과를 로빙 컨테이너로 반환
6. 로빙이 Slack으로 응답 전송
2. 로빙이 State Service에서 사용자 상태 조회
3. 로빙 브레인이 의도 분석 (키워드 기반, LLM 없이)
4. 적절한 스킬 서버로 라우팅
5. 스킬 서버에서 LLM 처리 및 결과 생성
6. 결과를 로빙 컨테이너로 반환
7. 로빙이 Slack으로 응답 전송
8. 사용 기록을 State Service에 업데이트
```
## 3. 로빙 컨테이너 설계
## 3. 상태 관리 아키텍처
### 3.1 핵심 역할
### 3.1 State Service 설계
State Service는 모든 회사와 사용자의 상태를 중앙에서 관리하는 핵심 서비스입니다.
```python
# state-service/main.py
from fastapi import FastAPI
from sqlalchemy import create_engine
app = FastAPI()
@app.get("/company/{company_id}/state")
async def get_company_state(company_id: str):
"""회사별 전체 상태 조회 - 로빙 컨테이너 초기화용"""
return {
"company_id": company_id,
"slack_tokens": {
"bot_token": decrypt(company.slack_bot_token),
"signing_secret": decrypt(company.slack_signing_secret),
"app_token": decrypt(company.slack_app_token)
},
"users": await get_users_with_stats(company_id),
"skills": await get_company_skills(company_id),
"settings": await get_company_settings(company_id)
}
@app.get("/user/{user_id}/stats")
async def get_user_stats(user_id: str):
"""프론트엔드용 사용자 상태 API"""
return {
"user_id": user_id,
"stats": {
"연산": 15,
"기억": 20,
"공감": 10,
"통솔": 5,
"반응": 12
},
"skills": {
"slack_summary": {"level": 3, "usage": 67},
"email_parser": {"level": 2, "usage": 34}
},
"level": 12,
"exp": 3450
}
@app.post("/user/{user_id}/levelup")
async def level_up_user(user_id: str, stat: str, points: int):
"""사용자 레벨업 처리"""
# DB에 직접 업데이트
await update_user_stats(user_id, stat, points)
# 캐시 무효화
await redis.delete(f"user:{user_id}:stats")
# WebSocket으로 실시간 알림
await notify_user(user_id, {"type": "LEVEL_UP", "stat": stat})
return {"success": True}
```
### 3.2 데이터베이스 스키마
```sql
-- PostgreSQL 스키마
CREATE TABLE companies (
id UUID PRIMARY KEY,
name VARCHAR(255),
slack_workspace_id VARCHAR(50) UNIQUE,
slack_bot_token TEXT ENCRYPTED,
slack_signing_secret TEXT ENCRYPTED,
slack_app_token TEXT ENCRYPTED,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE users (
id UUID PRIMARY KEY,
company_id UUID REFERENCES companies(id),
slack_user_id VARCHAR(50),
stats JSONB DEFAULT '{"연산": 0, "기억": 0, "공감": 0, "통솔": 0, "반응": 0}',
level INTEGER DEFAULT 1,
exp INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(company_id, slack_user_id)
);
CREATE TABLE user_skills (
user_id UUID REFERENCES users(id),
skill_id VARCHAR(50),
level INTEGER DEFAULT 1,
usage_count INTEGER DEFAULT 0,
unlocked_at TIMESTAMP,
last_used_at TIMESTAMP,
PRIMARY KEY (user_id, skill_id)
);
CREATE TABLE skill_growth_logs (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
skill_id VARCHAR(50),
old_level INTEGER,
new_level INTEGER,
triggered_by VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW()
);
```
## 4. 로빙 컨테이너 설계 (Stateless)
### 4.1 핵심 역할
- **Slack 이벤트 게이트웨이**: 회사별 Slack 이벤트 수신 및 ACK 응답
- **상태 조회**: State Service에서 필요한 정보 조회
- **간단한 의도 분석**: 키워드 기반 빠른 분류 (LLM 없이)
- **스킬 라우팅**: 적절한 스킬 서버로 요청 전달
- **회사별 컨텍스트 관리**: 회사 설정, 사용자 스탯, 활성 스킬 관리
- **학습 욕구 기록**: 처리하지 못한 요청 로컬 기록
### 3.2 구조
```python
app/
├── core/
│ ├── config.py # 회사별 설정
│ ├── auth.py # 인증/인가
│ └── router.py # 스킬 라우팅
├── brain/
│ └── intent.py # 의도 분석 (경량)
├── api/
│ └── slack.py # Slack 이벤트 수신
└── main.py # FastAPI 앱
```
### 3.3 로빙 브레인 구현
### 4.2 초기화 및 상태 관리
```python
# app/services/robing_brain.py
class RobeingBrain:
"""스킬 라우터 + 의사결정자"""
def __init__(self, company_id: str):
self.company_id = company_id
self.state_service = StateServiceClient()
self.skill_registry = {
"요약": "http://skill-summary:8001",
"이메일": "http://skill-email:8002",
"일정": "http://skill-calendar:8003"
}
self._state_cache = {} # 메모리 캐시 (TTL 있음)
async def route_message(self, message: str, context: dict):
# 1. 간단한 의도 분석 (LLM 없이)
intent = self._quick_intent_check(message)
async def initialize(self):
"""컨테이너 시작 시 상태 로드"""
# 외부 서비스에서 상태 가져오기
company_state = await self.state_service.get_company_state(
self.company_id
)
# 2. 스킬 결정
skill_url = self.skill_registry.get(intent)
# Slack 토큰 설정
self.slack_token = company_state["slack_tokens"]["bot_token"]
self.slack_signing_secret = company_state["slack_tokens"]["signing_secret"]
self.slack_client = SlackClient(token=self.slack_token)
# 3. 스킬 서버로 전달
if skill_url:
return await self._call_skill(skill_url, message, context)
else:
self._record_learning_desire(message)
return "아직 그 기능은 없어요"
# 회사 설정 로드
self.settings = company_state["settings"]
print(f"로빙 초기화 완료: {self.company_id}")
async def get_user_stats(self, user_id: str):
"""사용자 스탯 조회 (캐시 활용)"""
cache_key = f"user:{user_id}:stats"
# 메모리 캐시 확인
if cache_key in self._state_cache:
if not self._is_cache_expired(cache_key):
return self._state_cache[cache_key]["data"]
# 외부 서비스에서 조회
stats = await self.state_service.get_user_stats(user_id)
# 캐시 저장 (5분 TTL)
self._state_cache[cache_key] = {
"data": stats,
"expires_at": time.time() + 300
}
return stats
```
## 4. 슬랙 요약 스킬 마이크로서비스
### 4.3 컨테이너 시작 흐름
### 4.1 프로젝트 구조
```python
# main.py
async def startup():
"""컨테이너 시작 시 실행"""
# 1. 환경변수에서 회사 ID 가져오기
company_id = os.getenv("COMPANY_ID")
if not company_id:
raise ValueError("COMPANY_ID is required")
# 2. 로빙 브레인 초기화
brain = RobeingBrain(company_id)
await brain.initialize()
# 3. FastAPI 앱에 brain 주입
app.state.brain = brain
# 4. 헬스체크 활성화
app.state.healthy = True
app = FastAPI()
app.add_event_handler("startup", startup)
```
## 5. 슬랙 요약 스킬 마이크로서비스
### 5.1 스킬 레벨 시스템
```python
class SkillLevels:
"""스킬 레벨별 기능 정의"""
LEVEL_1 = {
"name": "기본 요약",
"required_stats": {"연산": 5, "기억": 5},
"features": ["최근 1시간 대화 요약", "핵심 키워드 추출"]
}
LEVEL_2 = {
"name": "구조화 요약",
"required_stats": {"연산": 10, "기억": 10},
"features": ["주제별 그룹화", "중요도 판단", "결정사항 하이라이트"]
}
LEVEL_3 = {
"name": "맥락 인식 요약",
"required_stats": {"연산": 15, "기억": 15, "공감": 5},
"features": ["이전 회의와 연결", "진행률 추적", "분위기 분석"]
}
LEVEL_4 = {
"name": "액션 중심 요약",
"required_stats": {"연산": 20, "기억": 20, "통솔": 10},
"features": ["액션아이템 자동 추출", "담당자 자동 배정", "우선순위 설정"]
}
LEVEL_5 = {
"name": "예측적 요약",
"required_stats": {"연산": 25, "기억": 30, "공감": 15, "통솔": 15},
"features": ["패턴 학습", "리스크 예측", "선제적 제안"]
}
```
### 5.2 스킬 서버 구조
```
skill-slack-summary/
@ -133,66 +312,17 @@ skill-slack-summary/
└── main.py
```
### 4.2 API 엔드포인트
```python
from fastapi import FastAPI, Depends
from pydantic import BaseModel
## 6. 통신 프로토콜
app = FastAPI()
class SummaryRequest(BaseModel):
company_id: str
channel_id: str
time_range: str # "1h", "2h", "today", etc.
user_id: str
user_stats: Dict[str, int]
@app.post("/summarize")
async def summarize_messages(
request: SummaryRequest,
auth: str = Depends(verify_auth)
):
"""슬랙 메시지 요약"""
# 1. 메시지 수집
messages = await slack_client.fetch_messages(
request.channel_id,
request.time_range
)
# 2. LLM으로 요약
summary = await summarizer.process(messages)
# 3. 메모리 저장
await memory_service.save(request.company_id, summary)
return {
"summary": summary.content,
"key_points": summary.key_points,
"action_items": summary.action_items
}
```
### 4.3 스킬 메타데이터
```python
class SkillMetadata(BaseModel):
name: str = "slack_summary"
version: str = "1.0.0"
description: str = "슬랙 회의 내용 요약"
required_stats: Dict[str, int] = {"memory": 5}
triggers: List[str] = ["회의 요약", "미팅 정리", "summary"]
```
## 5. 통신 프로토콜
### 5.1 로빙 → 스킬 서비스 요청
### 6.1 로빙 → 스킬 서비스 요청
```json
{
"company_id": "company_a",
"user_id": "U123456",
"skill": "slack_summary",
"skill_level": 3,
"auth_token": "jwt_token",
"payload": {
"channel_id": "C789012",
@ -206,7 +336,7 @@ class SkillMetadata(BaseModel):
}
```
### 5.2 스킬 서비스 → 로빙 응답
### 6.2 스킬 서비스 → 로빙 응답
```json
{
@ -233,9 +363,9 @@ class SkillMetadata(BaseModel):
}
```
## 6. LLM 처리 및 비용 최적화
## 7. LLM 처리 및 비용 최적화
### 6.1 LLM 관리 전략
### 7.1 LLM 관리 전략
```python
class LLMManager:
@ -245,6 +375,7 @@ class LLMManager:
self.openai_client = OpenAIClient()
self.cache = Redis()
self.rate_limiter = RateLimiter()
self.api_key_pool = APIKeyPool()
async def summarize(self, messages: List[str], company_id: str):
# 1. 캐시 확인
@ -255,16 +386,22 @@ class LLMManager:
# 2. Rate limiting
await self.rate_limiter.check(company_id)
# 3. LLM 호출
summary = await self.gemini_client.summarize(messages)
# 3. API 키 로테이션
api_key = self.api_key_pool.get_available_key()
# 4. 캐시 저장
# 4. LLM 호출
summary = await self.gemini_client.summarize(
messages,
api_key=api_key
)
# 5. 캐시 저장
await self.cache.set(cache_key, summary, ttl=3600)
return summary
```
### 6.2 Rate Limit 대응
### 7.2 Rate Limit 대응
1. **다중 API 키 풀**: 여러 API 키를 로테이션하여 사용
2. **큐 기반 처리**: 우선순위 큐로 유료 고객 우선 처리
@ -272,9 +409,9 @@ class LLMManager:
4. **계층적 처리**: 부하에 따라 다른 모델 사용 (Pro vs Flash)
5. **스마트 캐싱**: 유사한 요청에 대한 캐시 재사용
## 7. 메모리 저장 구조
## 8. 메모리 저장 구조
### 7.1 ChromaDB Collection
### 8.1 ChromaDB Collection
```json
{
@ -291,27 +428,40 @@ class LLMManager:
"participants": ["user1", "user2", "user3"],
"key_points": ["포인트1", "포인트2"],
"action_items": ["할일1", "할일2"],
"skill_version": "1.0.0"
"skill_version": "1.0.0",
"skill_level": 3
}
}
]
}
```
## 8. 배포 구성
## 9. 배포 구성
### 8.1 Docker Compose - 공용 스킬 서버
### 9.1 Docker Compose - 공용 서비스
```yaml
# docker-compose.skills.yml
# docker-compose.shared.yml
version: '3.8'
services:
# 상태 관리 서비스
state-service:
build: ./state-service
environment:
- DATABASE_URL=postgresql://postgres:password@postgres:5432/robbing
- REDIS_URL=redis://redis:6379
- ENCRYPTION_KEY=${MASTER_ENCRYPTION_KEY}
- JWT_SECRET=${JWT_SECRET}
ports:
- "8000:8000"
depends_on:
- postgres
- redis
# 스킬 서버들
skill-slack-summary:
build: ./skills/slack-summary
image: robing-skills/slack-summary:latest
ports:
- "8001:8001"
environment:
- GEMINI_API_KEY=${GEMINI_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
@ -324,6 +474,15 @@ services:
limits:
memory: 2G
# 데이터베이스
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=robbing
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
@ -333,23 +492,27 @@ services:
image: chromadb/chroma
volumes:
- chroma_data:/chroma/chroma
volumes:
postgres_data:
redis_data:
chroma_data:
```
### 8.2 Docker Compose - 회사별 로빙
### 9.2 Docker Compose - 회사별 로빙
```yaml
# docker-compose.company-a.yml
version: '3.8'
services:
robing-company-a:
image: robing/core:latest
robbing-company-a:
image: robbing/core:latest
environment:
- COMPANY_ID=company_a
- SLACK_BOT_TOKEN=${COMPANY_A_SLACK_TOKEN}
- SLACK_SIGNING_SECRET=${COMPANY_A_SIGNING_SECRET}
- SKILL_SERVER_URL=https://skills.robing.ai
- JWT_SECRET=${JWT_SECRET}
- STATE_SERVICE_URL=http://state-service:8000
- SKILL_SUMMARY_URL=http://skill-slack-summary:8001
- SKILL_EMAIL_URL=http://skill-email:8002
ports:
- "10001:8000"
deploy:
@ -362,52 +525,78 @@ services:
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
```
## 9. 확장성 고려사항
### 9.1 수평 확장
- 로빙 컨테이너: 회사별로 독립 배포, 필요시 여러 인스턴스 가능
- 스킬 서버: 부하에 따라 레플리카 수 조정
- 데이터베이스: 읽기 전용 복제본 추가
### 9.2 지역별 배포
- 회사 위치에 따라 가까운 리전에 로빙 컨테이너 배포
- 스킬 서버는 중앙 또는 주요 리전에 배포
### 9.3 모니터링
- Prometheus + Grafana로 실시간 모니터링
- 회사별 사용량 추적
- API 사용량 및 비용 대시보드
## 10. 보안 고려사항
### 10.1 인증/인가
- 로빙-스킬 간 통신: JWT 토큰 기반 인증
- 로빙-State Service 간: 서비스 계정 + mTLS
- 로빙-스킬 서버 간: JWT 토큰 기반 인증
- 회사별 데이터 격리: company_id 기반 접근 제어
- API 키 관리: 환경 변수로 관리, 로빙 컨테이너에는 노출하지 않음
### 10.2 데이터 보호
- 전송 중 암호화: HTTPS 사용
- 저장 시 암호화: 민감한 데이터는 암호화하여 저장
- 전송 중 암호화: HTTPS/TLS 사용
- 저장 시 암호화: Slack 토큰 등 민감한 데이터는 암호화하여 저장
- 로그 마스킹: 개인정보가 로그에 노출되지 않도록 처리
- 접근 로깅: 모든 API 호출 감사 로그 기록
## 11. 성능 최적화
## 11. 모니터링 및 운영
### 11.1 캐싱 전략
### 11.1 메트릭 수집
```yaml
# 모니터링 스택
services:
prometheus:
image: prometheus/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
ports:
- "3000:3000"
```
### 11.2 주요 모니터링 지표
- 회사별 API 사용량
- 스킬별 응답 시간
- LLM API 사용량 및 비용
- 에러율 및 가용성
- 메모리/CPU 사용률
## 12. 상태 관리 FAQ
### Q1: 로빙 레벨업 시 컨테이너가 꺼졌다 켜지는데 상태값을 변화시킬 수 있는가?
**A**: 네, 가능합니다. 로빙 컨테이너는 시작할 때마다 외부 상태 관리 서비스(State Service)에서 최신 상태를 불러옵니다. 레벨업이나 스킬 해금 같은 상태 변경은 State Service의 API를 통해 직접 데이터베이스에 반영되므로, 컨테이너가 꺼져 있는 동안에도 상태 변경이 가능합니다. 컨테이너가 다시 시작되면 `await brain.initialize()`에서 변경된 상태를 자동으로 로드하여 적용합니다.
### Q2: 회사별 로빙의 상태는 프론트엔드에서 어떻게 조회하는가?
**A**: 프론트엔드는 State Service의 공개 API(`GET /user/{user_id}/stats`)를 호출하여 사용자 상태를 조회합니다. 로빙 컨테이너를 거치지 않고 직접 State Service와 통신하므로, 로빙 컨테이너가 꺼져 있어도 정상적으로 데이터를 볼 수 있습니다. State Service는 WebSocket도 제공하여 레벨업 같은 실시간 이벤트도 프론트엔드로 직접 전달할 수 있습니다.
### Q3: Slack 인증 정보는 어디서 관리하고 어떻게 불러오는가?
**A**: Slack 인증 정보는 State Service가 중앙에서 관리합니다. 각 회사의 Slack 토큰들은 암호화되어 PostgreSQL에 저장되고, 로빙 컨테이너는 시작할 때 State Service의 `GET /company/{company_id}/state` API를 호출하여 복호화된 토큰을 받아옵니다. 이 방식으로 토큰 갱신이 필요할 때 State Service에서만 업데이트하면 모든 컨테이너가 재시작 시 자동으로 새 토큰을 받게 됩니다.
## 13. 성능 최적화
### 13.1 캐싱 전략
- Redis를 통한 LLM 응답 캐싱
- 자주 요청되는 요약에 대한 사전 처리
- 유사 요청 감지 및 재사용
### 11.2 비동기 처리
### 13.2 비동기 처리
- 모든 I/O 작업은 비동기로 처리
- 백그라운드 태스크로 무거운 작업 분리
- 웹훅 방식으로 결과 전달 옵션
## 12. 향후 로드맵
## 14. 향후 로드맵
### Phase 1 (현재)
- 기본 슬랙 요약 기능 구현
- State Service 구축
- 단일 스킬 서버 운영
### Phase 2
@ -420,7 +609,7 @@ services:
- 사용자별 요약 스타일 학습
- 스킬 마켓플레이스 구축
## 13. 결론
## 15. 결론
이 아키텍처는 다음과 같은 이점을 제공합니다:
@ -429,5 +618,6 @@ services:
3. **독립성**: 각 컴포넌트가 독립적으로 배포/업데이트 가능
4. **안정성**: 장애 격리로 전체 시스템 안정성 향상
5. **성능**: 캐싱과 배치 처리로 응답 속도 향상
6. **무상태**: 컨테이너 재시작에 영향 받지 않는 안정적 서비스
이 설계를 통해 로빙은 수백 개의 회사를 효율적으로 서비스할 수 있는 확장 가능한 플랫폼으로 성장할 수 있습니다.