feat: distinguish DAU from concurrent users in Server Expert AI

- Add expectedDau and expectedConcurrent fields to ServerSession
- Update system prompts to explain DAU vs concurrent users concept
- AI now asks for clarification when users mention visitor counts
- Use concurrent users (5-10% of DAU) for server recommendations
- Update inference rules: personal=10, business=50 concurrent users

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-27 11:15:22 +09:00
parent 20b4139dd4
commit 8815654137
2 changed files with 49 additions and 14 deletions

View File

@@ -162,10 +162,12 @@ function inferTechStack(useCase: string): string[] {
} }
// Expected users inference from scale // Expected users inference from scale
// Returns concurrent users (not DAU)
function inferExpectedUsers(scale: string): number { function inferExpectedUsers(scale: string): number {
if (scale === 'personal') return 100; // DAU → 동시접속자 변환 (5-10% 비율 적용)
if (scale === 'business') return 500; if (scale === 'personal') return 10; // DAU 100명 → 동접 10명
return 100; // Default to personal if (scale === 'business') return 50; // DAU 500명 → 동접 50명
return 10; // Default to personal
} }
// OpenAI API 응답 타입 // OpenAI API 응답 타입
@@ -254,14 +256,22 @@ ${JSON.stringify(recommendationData?.recommendations, null, 2)}
## 사용자 요구사항 ## 사용자 요구사항
- 용도: ${session.collectedInfo.useCase || '웹 서비스'} - 용도: ${session.collectedInfo.useCase || '웹 서비스'}
- 규모: ${session.collectedInfo.scale === 'business' ? '사업용' : '개인용'} - 규모: ${session.collectedInfo.scale === 'business' ? '사업용' : '개인용'}
${session.collectedInfo.expectedDau ? `- 일일 방문자(DAU): ${session.collectedInfo.expectedDau}` : ''}
${session.collectedInfo.expectedConcurrent ? `- 동시접속자: ${session.collectedInfo.expectedConcurrent}` : ''}
${session.collectedInfo.budgetLimit ? `- 예산: ${session.collectedInfo.budgetLimit}` : ''} ${session.collectedInfo.budgetLimit ? `- 예산: ${session.collectedInfo.budgetLimit}` : ''}
## 사용자 수 관련 참고사항
- DAU(일일 활성 사용자)와 동시접속자는 다른 개념입니다
- 일반적으로 동시접속자는 DAU의 5-10% 수준입니다
- 서버 스펙은 동시접속자 기준으로 계산됩니다
## 검토 작업 ## 검토 작업
다음을 검토하고 간결하게 2-3문장으로 코멘트해주세요: 다음을 검토하고 간결하게 2-3문장으로 코멘트해주세요:
1. 추천된 서버가 용도와 규모에 적합한지 1. 추천된 서버가 용도와 규모에 적합한지
2. 스펙이 충분한지 (RAM, CPU, 스토리지) 2. 스펙이 충분한지 (RAM, CPU, 스토리지)
3. 대역폭 경고(overage)가 있다면 언급 3. DAU/동시접속자 기준이 적절한지
4. 더 적합한 스펙이 필요하다면 제안 4. 대역폭 경고(overage)가 있다면 언급
5. 더 적합한 스펙이 필요하다면 제안
## 응답 형식 (반드시 JSON만 반환) ## 응답 형식 (반드시 JSON만 반환)
{ {
@@ -300,21 +310,35 @@ ${session.collectedInfo.budgetLimit ? `- 예산: ${session.collectedInfo.budgetL
## 대화 흐름 ## 대화 흐름
1. 용도 파악: "어떤 서비스를 운영하실 건가요? (예: 블로그, 쇼핑몰, 커뮤니티)" 1. 용도 파악: "어떤 서비스를 운영하실 건가요? (예: 블로그, 쇼핑몰, 커뮤니티)"
2. 규모 파악: "개인용인가요, 사업용인가요?" 2. 규모 파악: "개인용인가요, 사업용인가요?"
3. 정보가 충분하면 즉시 추천 (추가 질문 없이) 3. 사용자 수 확인 (필요 시): "방문자나 사용자 수는 어느 정도 예상하시나요?"
4. 정보가 충분하면 즉시 추천 (추가 질문 없이)
## 핵심 규칙 (반드시 준수) ## 핵심 규칙 (반드시 준수)
- 기술 스택, 동시접속자 수, 트래픽 패턴은 절대 묻지 않음 (30년 경험으로 알아서 추론) - 기술 스택, 트래픽 패턴은 절대 묻지 않음 (30년 경험으로 알아서 추론)
- 사용자 수를 언급하면 DAU인지 동시접속자인지 반드시 한 번 확인
- "방문자 1000명", "유저 500명" 등 언급 시 → "말씀하신 방문자는 일일 방문자(DAU)인가요, 동시접속자인가요?"
- DAU와 동시접속자를 구분해서 설명: "일반적으로 동시접속자는 일일 방문자의 5-10% 정도입니다"
- "모르겠어요", "아무거나", "글쎄요" → 즉시 action="recommend" (기본값: 개인용 웹서비스) - "모르겠어요", "아무거나", "글쎄요" → 즉시 action="recommend" (기본값: 개인용 웹서비스)
- 용도+규모 한번에 말하면 → 즉시 action="recommend" - 용도+규모 한번에 말하면 → 즉시 action="recommend"
- 용도만 말해도 → 개인용으로 가정하고 action="recommend" 가능 - 용도만 말해도 → 개인용으로 가정하고 action="recommend" 가능
- 질문은 최대 2번까지, 그 이후는 무조건 action="recommend" - 질문은 최대 2번까지, 그 이후는 무조건 action="recommend"
## 사용자 수 관련 용어 정리
- **DAU (일일 활성 사용자)**: 하루 동안 서비스를 사용하는 전체 사용자 수
- **동시접속자 (Concurrent Users)**: 같은 시간에 동시에 접속해 있는 사용자 수
- **중요**: 서버 스펙은 동시접속자를 기준으로 계산해야 합니다
- **일반 공식**: 동시접속자 = DAU × 5-10%
예시:
- "하루 방문자 1000명" → DAU 1000명 → 동시접속자 50-100명
- "동시 접속 100명" → 그대로 동시접속자 100명 사용
## 추론 규칙 (30년 경험 기반) ## 추론 규칙 (30년 경험 기반)
- 블로그 → WordPress, 1GB RAM이면 충분 - 블로그 → WordPress, 1GB RAM이면 충분, DAU 100명 (동시접속자 10명)
- 쇼핑몰 → 2GB+ RAM, DB 분리 고려 - 쇼핑몰 → 2GB+ RAM, DB 분리 고려, DAU 500명 (동시접속자 50명)
- 커뮤니티 → PHP+MySQL, 트래픽에 따라 2~4GB - 커뮤니티 → PHP+MySQL, 트래픽에 따라 2~4GB
- 게임서버 → 고사양 CPU, 낮은 레이턴시 리전 - 게임서버 → 고사양 CPU, 낮은 레이턴시 리전
- 규모: personal→100명, business→500명 - 규모: personal→DAU 100명 (동접 10명), business→DAU 500명 (동접 50명)
## 현재 수집된 정보 ## 현재 수집된 정보
${JSON.stringify(session.collectedInfo, null, 2)} ${JSON.stringify(session.collectedInfo, null, 2)}
@@ -325,7 +349,9 @@ ${JSON.stringify(session.collectedInfo, null, 2)}
"message": "사용자에게 보여줄 메시지 (도구에서 얻은 정보를 자연스럽게 포함)", "message": "사용자에게 보여줄 메시지 (도구에서 얻은 정보를 자연스럽게 포함)",
"collectedInfo": { "collectedInfo": {
"useCase": "용도 (없으면 '웹서비스')", "useCase": "용도 (없으면 '웹서비스')",
"scale": "personal 또는 business (없으면 'personal')" "scale": "personal 또는 business (없으면 'personal')",
"expectedDau": "일일 방문자 수 (사용자가 명시한 경우)",
"expectedConcurrent": "동시접속자 수 (사용자가 명시하거나 DAU에서 계산)"
} }
} }
@@ -537,9 +563,16 @@ export async function processServerConsultation(
? inferTechStack(session.collectedInfo.useCase) ? inferTechStack(session.collectedInfo.useCase)
: ['web']; : ['web'];
const expectedUsers = session.collectedInfo.scale // 동시접속자 우선 사용, 없으면 scale 기반 추론
? inferExpectedUsers(session.collectedInfo.scale) let expectedUsers = 10; // Default
: 100; if (session.collectedInfo.expectedConcurrent) {
expectedUsers = session.collectedInfo.expectedConcurrent;
} else if (session.collectedInfo.expectedDau) {
// DAU가 있으면 10% 비율로 동시접속자 계산
expectedUsers = Math.ceil(session.collectedInfo.expectedDau * 0.1);
} else if (session.collectedInfo.scale) {
expectedUsers = inferExpectedUsers(session.collectedInfo.scale);
}
const recommendationData = await getRecommendationData( const recommendationData = await getRecommendationData(
{ {

View File

@@ -230,6 +230,8 @@ export interface ServerSession {
scale?: 'personal' | 'business'; scale?: 'personal' | 'business';
budgetLimit?: number; budgetLimit?: number;
regionPreference?: string[]; regionPreference?: string[];
expectedDau?: number; // 일일 활성 사용자 (Daily Active Users)
expectedConcurrent?: number; // 동시접속자 (Concurrent Users)
}; };
messages: Array<{ role: 'user' | 'assistant'; content: string }>; messages: Array<{ role: 'user' | 'assistant'; content: string }>;
createdAt: number; createdAt: number;