Character Runtime MVP
상세 개발 계획
12개 모듈, 100+ 태스크, 평가셋, 스키마, 의존관계를 정리한 실행 계획
1개발 원칙
1.1 설계 원칙
| 원칙 | 설명 |
| Package-first | Character Package 스펙을 먼저 확정하고, 모든 모듈이 이 스펙에 맞춰 구현 |
| Schema-driven | TurnPlan, ActionPlan, Memory 등 핵심 데이터는 JSON Schema로 선정의 |
| LLM은 제안자 | LLM은 TurnPlan을 제안할 뿐, 직접 DB 쓰기·파라미터 접근 불가 |
| Adapter 분리 | LLM, TTS, Vector DB, MCP 프로토콜은 모두 Adapter 패턴으로 교체 가능 |
| 의미 기반 제어 | Live2D 파라미터를 직접 다루지 않고 semantic control을 통해 간접 제어 |
| 테넌트 격리 필수 | character_instance_id 기반 격리, 사용자 간 기억 누출 0건이 통과 조건 |
| Idempotency | 모든 쓰기 API에 idempotency_key 필수 |
1.2 기술 결정 가이드
| 결정 항목 | 권장안 | 대안 | 결정 기준 |
| 모노레포 도구 | Turborepo | Nx | 설정 단순성, TS 생태계 |
| API 프레임워크 | Fastify | NestJS | MVP 속도 vs 구조화 |
| ORM/쿼리 | Drizzle ORM | Prisma, Kysely | 타입 안전성 + SQL 제어력 |
| 벡터 검색 | pgvector | Qdrant, Pinecone | 인프라 단순화 (PG 하나로) |
| 작업 큐 | BullMQ (Redis) | pg-boss | Redis를 캐시·큐 양쪽 활용 |
| 실시간 | ws (raw WebSocket) | Socket.io | 프로토콜 제어력 |
| 프론트엔드 | React + Vite | Next.js | Player는 SPA |
| 인증 | Lucia Auth + OAuth | Clerk, Auth.js | 자체 구현 제어력 |
| 오브젝트 저장 | Cloudflare R2 | S3 | 기존 CF 계정, egress 무료 |
| 컨테이너 배포 | Fly.io / Railway | AWS ECS | MVP 비용 최소화 |
2모노레포 구조
character-platform/
├─ apps/
│ ├─ api/ # Fastify — Character Runtime API
│ ├─ web-player/ # React+Vite — Live2D Player
│ ├─ mcp-server/ # MCP TS SDK — Gateway
│ ├─ admin-console/ # 관리자 콘솔
│ └─ worker/ # BullMQ 비동기 Worker
├─ packages/
│ ├─ character-schema/ # JSON Schema + TS 타입
│ ├─ character-package/ # Package 로더/검증
│ ├─ llm-adapter/ # LLM Provider Adapter
│ ├─ tts-adapter/ # TTS Provider Adapter
│ ├─ memory-engine/ # 기억 검색/저장/갱신
│ ├─ action-engine/ # ActionPlan → Live2D 이벤트
│ ├─ db/ # Drizzle 스키마 + 마이그레이션
│ └─ observability/ # 로깅/메트릭
├─ characters/
│ └─ luna-v1/ # MVP 캐릭터 패키지
├─ tests/
│ ├─ fixtures/ # 평가셋 (기억 30개, 동작 20개)
│ ├─ e2e/
│ ├─ contract/ # MCP 계약 테스트
│ └─ security/
└─ infra/
초기 세팅 태스크
| # | 태스크 | 산출물 | 완료 기준 |
| S-1 | 모노레포 초기화 (Turborepo + pnpm) | turbo.json, pnpm-workspace.yaml | turbo build 성공 |
| S-2 | TypeScript 공통 설정 | tsconfig.base.json | strict 모드, path alias |
| S-3 | ESLint + Prettier | 설정 파일 | lint/format 동작 |
| S-4 | Docker Compose (PG16+pgvector, Redis 7) | docker-compose.yml | DB 접속 확인 |
| S-5 | 환경변수 관리 | .env.example | env 검증 |
| S-6 | CI 파이프라인 | GitHub Actions | push 시 자동 실행 |
3Phase 0 — 기술 검증 스파이크
전체 MVP를 만들기 전에 가장 위험한 8개 연결을 최소한으로 검증한다. 완성도 있는 코드가 아니라 연결이 가능한지만 확인하는 throwaway prototype이다.
| # | 스파이크 | 검증 대상 | Go 기준 |
| SP-1 | Live2D Web SDK 모델 로딩 | 브라우저에서 .moc3 렌더링 | 모델 표시 + 표정 3종 + 모션 3종 |
| SP-2 | LLM Structured Output → TurnPlan | Claude/GPT → JSON TurnPlan | 10회 중 9회+ 유효 Schema 통과 |
| SP-3 | ActionPlan Validator | SP-2 출력을 capability manifest 대비 검증 | 범위 초과·미지원 모두 차단 |
| SP-4 | TTS → 볼륨 립싱크 | Web Audio API 분석, 입 파라미터 반영 | 발화 중 입 움직임, 묵음 시 닫힘 |
| SP-5 | WebSocket 이벤트 전달 | 서버 → Player 이벤트 수신 | 순서 보존 확인 |
| SP-6 | pgvector 기억 저장/검색 | 메모리 3개 저장, 유사도 검색 | 다른 표현으로 재호출 성공 |
| SP-7 | MCP character.turn | MCP Inspector에서 Tool 호출 | 호출 성공, Schema 일치 |
| SP-8 | 사용자 격리 | A/B 교차 검색 | A의 기억이 B에게 0건 노출 |
스파이크 통합 시나리오
- 브라우저에서 Live2D 모델 로딩 (SP-1)
- 텍스트 입력 → API → LLM → TurnPlan (SP-2)
- Validator가 TurnPlan 검증 (SP-3)
- WebSocket으로 Player에 이벤트 전달 (SP-5)
- Player가 Expression + Motion 실행 (SP-1)
- TTS 오디오 재생 + 립싱크 (SP-4)
- 대화 내용에서 기억 저장 (SP-6)
- 새 세션에서 기억 재호출 (SP-6)
- MCP Inspector에서 같은 호출 (SP-7)
- 다른 사용자로 같은 질문 → 기억 격리 (SP-8)
Go 판정: 10단계 중 10단계 모두 성공해야 Phase 1 진행.
스파이크 산출물 (Phase 1으로 이관)
- LLM 프롬프트 초안 (TurnPlan 생성용)
- TurnPlan / ActionPlan JSON Schema v0.1
- Capability Manifest v0.1 (MVP 캐릭터)
- WebSocket 이벤트 프로토콜 초안
- pgvector 임베딩 모델 선정
- 립싱크 볼륨 매핑 파라미터 초안
- MCP Tool Schema 초안
4Phase 1 — MVP 본개발
모듈 개요
| 모듈 | 앱/패키지 | 핵심 의존 |
| M1. 기반 인프라 | packages/db | 없음 |
| M2. Package & Schema | packages/character-schema, character-package | 없음 |
| M3. Live2D Player | apps/web-player | M2 |
| M4. Runtime API | apps/api | M1, M2 |
| M5. Orchestrator | apps/api/services | M2, M4, M6, M7 |
| M6. Memory Engine | packages/memory-engine | M1 |
| M7. Action Engine | packages/action-engine | M2 |
| M8. TTS & Lip-sync | packages/tts-adapter | M4 |
| M9. Realtime Gateway | apps/api WS | M4, M3 |
| M10. MCP Server | apps/mcp-server | M4, M5, M6 |
| M11. Admin Console | apps/admin-console | M4, M6 |
| M12. Observability | packages/observability | M4 |
M1. 기반 인프라
| # | 태스크 | 산출물 | 완료 기준 |
| M1-1 | Drizzle ORM + PG 연결 | packages/db/src/client.ts | 연결 + 쿼리 동작 |
| M1-2 | pgvector 확장 | 마이그레이션 SQL | CREATE EXTENSION vector 성공 |
| M1-3 | 스키마 — 사용자 | users, sessions | up/down 동작 |
| M1-4 | 스키마 — 캐릭터 | character_templates, instances 등 4개 테이블 | FK 관계 정상 |
| M1-5 | 스키마 — 대화 | conversations, messages, turns, turn_context_snapshots | 인덱스 포함 |
| M1-6 | 스키마 — 상태 | character_state, relationship_state, state_events | instance_id 인덱스 |
| M1-7 | 스키마 — 기억 | memories, memory_embeddings (vector column) | 벡터 인덱스 (HNSW) |
| M1-8 | 스키마 — 애니메이션 | expressions, motion_assets, action_events | — |
| M1-9 | 스키마 — 감사 | audit_logs, mcp_tokens | — |
| M1-10 | Redis + BullMQ 큐 | Worker 큐 초기화 | enqueue/dequeue 동작 |
| M1-11 | R2 오브젝트 저장 | S3 호환 클라이언트 | 업로드/서명 URL 생성 |
| M1-12 | 인증 (Lucia + Google OAuth) | 로그인/세션 | OAuth 콜백, 쿠키 발급 |
| M1-13 | Idempotency 미들웨어 | middleware/idempotency.ts | 중복 키 → 같은 응답 |
M2. Character Package & Schema
| # | 태스크 | 완료 기준 |
| M2-1 | TurnPlan JSON Schema 확정 | ajv로 유효/무효 각 10개 검증 |
| M2-2 | ActionPlan JSON Schema 확정 | 유효/무효 검증 |
| M2-3 | CapabilityManifest Schema 확정 | — |
| M2-4 | Memory Schema 확정 | — |
| M2-5 | CharacterContextBundle Schema 확정 | — |
| M2-6 | Character Event Schema 확정 | WS 이벤트 타입 정의 |
| M2-7 | TypeScript 타입 자동 생성 | 스키마↔타입 동기화 |
| M2-8 | Character Package Loader | manifest.yaml 읽고 해석 |
| M2-9 | Character Package Validator | 필수 필드·참조 파일 검출 |
| M2-10 | MVP 캐릭터 (Luna) 패키지 작성 | Validator 통과 |
M3. Live2D Web Player
| # | 태스크 | 완료 기준 |
| M3-1 | Vite + React 프로젝트 | dev server 기동 |
| M3-2 | Live2D Cubism SDK 통합 | Canvas 렌더링 |
| M3-3 | Model Loader | model3.json 경로로 로딩 |
| M3-4 | Expression Controller | setExpression('happy') API |
| M3-5 | Motion Controller | playMotion('greet') API |
| M3-6 | Parameter Overlay Controller | semantic → live2d parameter 오버레이 |
| M3-7 | Capability Manifest 연동 | manifest에 없는 제어 → 무시 |
| M3-8 | Idle / Eye Blink / Breathing | 입력 없이도 자연스러운 상태 |
| M3-9 | Event Queue (우선순위) | 높은 우선순위 선점 |
| M3-10 | 채팅 UI | 입력, 스트리밍 표시 |
| M3-11 | 연결 끊김 → 안전한 Idle | 끊겨도 모델 정상 |
M4. Character Runtime API
| # | 태스크 | 완료 기준 |
| M4-1 | Fastify 서버 + 플러그인 | health check 동작 |
| M4-2 | 인증 미들웨어 | 비인증 → 401 |
| M4-3 | POST /v1/characters/{id}/turns | Orchestrator 호출 → 응답 |
| M4-4 | GET /v1/characters/{id}/state | 상태 조회 |
| M4-5 | GET /v1/characters/{id}/memories | 기억 목록 (페이징) |
| M4-6 | POST /v1/characters/{id}/memories | 수동 기억 추가 |
| M4-7 | DELETE /v1/characters/{id}/memories/{mid} | soft delete |
| M4-8 | GET /v1/characters/{id}/capabilities | Manifest 반환 |
| M4-9 | POST /v1/characters/{id}/players | Player Session 생성 |
| M4-10 | Idempotency 적용 | POST/DELETE에 적용 |
| M4-11 | 에러 핸들링 (RFC 7807) | 표준 에러 응답 |
| M4-12 | 인스턴스 소유권 검사 | 타 사용자 → 403 |
M5. 대화 Orchestrator
| # | 태스크 | 완료 기준 |
| M5-1 | LLM Adapter 인터페이스 | generateTurnPlan(bundle): TurnPlan |
| M5-2 | Claude Adapter | Structured Output → TurnPlan |
| M5-3 | OpenAI Adapter (대안) | function calling → TurnPlan |
| M5-4 | Prompt Builder | ContextBundle → System/User 메시지 |
| M5-5 | Context Builder | Identity+Persona+Memories+Capabilities 조립 |
| M5-6 | Turn Validator | Schema + Capability 범위 검증 |
| M5-7 | State Engine | state_updates 적용, revision++ |
| M5-8 | Orchestrator 메인 로직 | 전체 턴 파이프라인 |
| M5-9 | 텍스트 스트리밍 (SSE/WS) | 첫 토큰 빠르게 전달 |
| M5-10 | 오류 재시도 정책 | LLM 실패 → 1회 재시도, TurnPlan 무효 → fallback |
| M5-11 | Context Budget | 토큰 초과 시 기억/lore 축소 |
M6. Memory Engine
| # | 태스크 | 완료 기준 |
| M6-1 | 임베딩 생성 Adapter | OpenAI embedding API 호출 |
| M6-2 | Memory Retrieval (복합 점수) | semantic×0.45 + recency×0.15 + importance×0.20 + relationship×0.10 + pin×0.10 |
| M6-3 | Memory Extraction (LLM 기반) | 대화에서 사실 추출 |
| M6-4 | Memory Proposal → 저장 | 중복 감지, confidence 부여 |
| M6-5 | 기억 정정 (Supersede) | 모순 시 기존 superseded + 새 active |
| M6-6 | 기억 삭제 | soft delete, 검색 제외 |
| M6-7 | Session Summary 생성 | N턴마다 요약 |
| M6-8 | 기억 격리 테스트 | 다른 instance_id 기억 반환 0건 |
기억 검색 SQL
WITH scored AS (
SELECT m.*, me.embedding,
1 - (me.embedding <=> $1::vector) AS semantic_sim,
1.0 / (1.0 + EXTRACT(EPOCH FROM (NOW() - m.created_at)) / 86400) AS recency,
CASE WHEN m.type = 'pinned' THEN 1.0 ELSE 0.0 END AS pin_score
FROM memories m
JOIN memory_embeddings me ON me.memory_id = m.id
WHERE m.character_instance_id = $2 AND m.status = 'active'
)
SELECT *,
(semantic_sim * 0.45 + recency * 0.15 + importance * 0.20
+ confidence * 0.10 + pin_score * 0.10) AS total_score
FROM scored ORDER BY total_score DESC LIMIT $3;
M7. Action Engine & Validator
| # | 태스크 | 완료 기준 |
| M7-1 | ActionPlan Validator | expression/motion manifest 존재 확인, overlay 범위 검사 |
| M7-2 | Semantic → Live2D Mapper | head_yaw → ParamAngleX |
| M7-3 | Priority Resolver | 충돌 파라미터 우선순위 결정 |
| M7-4 | Velocity Limiter | max_velocity 초과 시 클램핑 |
| M7-5 | ActionPlan → Player Events 변환 | WebSocket 이벤트 배열 생성 |
M8. TTS & Lip-sync
| # | 태스크 | 완료 기준 |
| M8-1 | TTS Adapter 인터페이스 | synthesize(text, config): AudioResult |
| M8-2 | ElevenLabs Adapter | 텍스트 → mp3/wav |
| M8-3 | Google TTS Adapter (대안) | — |
| M8-4 | TTS Worker (BullMQ) | 큐 → TTS → R2 → 서명 URL → WS audio.ready |
| M8-5 | Web Audio 볼륨 분석기 | AudioContext + AnalyserNode → 0~1 |
| M8-6 | Lip-sync Controller | 볼륨 → ParamMouthOpenY |
| M8-7 | 오디오 + 립싱크 동기화 | 재생 시작·종료 연동 |
M9. Realtime Event Gateway
| # | 태스크 | 완료 기준 |
| M9-1 | WebSocket 서버 | 연결/인증/해제 |
| M9-2 | Player Session 인증 | 무효 세션 거부 |
| M9-3 | 이벤트 전송 인터페이스 | send(sessionId, events) |
| M9-4 | 이벤트 타입별 처리 | 모든 이벤트 타입 수신 확인 |
| M9-5 | 재연결 + 놓친 이벤트 버퍼 | 재연결 후 마지막 턴 수신 |
| M9-6 | Heartbeat / Ping-pong | 좀비 연결 정리 |
이벤트 타입
turn.started │ text.delta │ emotion.changed
expression.play │ motion.play │ parameter.overlay
audio.ready │ audio.sync │ state.updated
turn.completed │ turn.failed
M10. MCP Server
| # | 태스크 | 완료 기준 |
| M10-1 | MCP TS SDK 통합 | server/discover 응답 |
| M10-2 | Streamable HTTP Transport | MCP Inspector 연결 |
| M10-3 | OAuth 2.1 + PKCE | 비인증 거부 |
| M10-4 | Tool: character.turn | 입출력 Schema 통과 |
| M10-5 | Tool: character.get_state | — |
| M10-6 | Tool: character.get_context | — |
| M10-7 | Tool: character.search_memory | — |
| M10-8 | Tool: character.remember | — |
| M10-9 | Tool: character.forget | — |
| M10-10 | Tool: character.play_motion | Player 이벤트 발생 |
| M10-11 | Tool: character.set_expression | — |
| M10-12 | Tool: character.list_capabilities | — |
| M10-13~15 | Resources (profile, state, memory/index) | — |
| M10-16 | 최신/레거시 호환 Adapter | 두 버전 Inspector 통과 |
| M10-17 | Scope 기반 권한 | Scope 부족 시 거부 |
| M10-18 | MCP Inspector 계약 테스트 | CI 자동 실행 |
M11. Admin Console
| # | 태스크 | 완료 기준 |
| M11-1 | 프로젝트 세팅 | dev server |
| M11-2 | 캐릭터 설정 조회/편집 | Persona 읽기/수정 |
| M11-3 | 기억 관리 | 목록/검색/삭제 |
| M11-4 | 상태 모니터 | 감정, 관계, revision |
| M11-5 | 테스트 채팅 | 관리자용 대화 |
| M11-6 | 모션 매핑 편집 | semantic ↔ parameter 수정 |
| M11-7 | 비용 대시보드 | LLM/TTS/턴 집계 |
M12. Observability
| # | 태스크 | 완료 기준 |
| M12-1 | 구조화 로거 (pino) | JSON 로그, request_id |
| M12-2 | 턴별 비용 기록 | usage_ledger 테이블 기록 |
| M12-3 | 응답 지연 메트릭 | p50/p95/p99 |
| M12-4 | 기억 적중률 메트릭 | 로그 기반 추정 |
| M12-5 | ActionPlan 유효율 | 통과/실패 비율 |
5모듈 의존관계 & 권장 구현 순서
M2 (Schema/Package) ───────────────────────────────────┐
│ │
├── M3 (Live2D Player) ◄── M9 (Realtime Gateway) │
│ │ │ │
│ └──────── M8 (TTS/Lipsync) ◄──┐ │
│ │ │
M1 (인프라/DB) ──┬── M4 (API) ──── M5 (Orchestrator) │
│ │ │ │ │ │
│ │ │ │ └─ M7 (Action Engine) ◄─ M2
│ │ │ │
│ │ │ └── M6 (Memory Engine) ◄── M1
│ │ │
│ │ └── M8 (TTS Worker)
│ │
│ ├── M10 (MCP Server)
│ └── M11 (Admin Console)
│
└── M12 (Observability)
권장 구현 순서
| Layer | 모듈 | 비고 |
| 0 (병렬) | M1 + M2 | 스키마와 인프라가 모든 것의 기반 |
| 1 (병렬) | M3 + M6 + M7 + M12 | 서로 독립 개발 가능 |
| 2 | M4 | Layer 1 패키지를 조합 |
| 3 (병렬) | M5 + M8 + M9 | 모든 엔진 결합, TTS와 WS |
| 4 (병렬) | M10 + M11 | API 완성 후 |
6DB 상세 설계
RLS (Row Level Security) — 기억 누출 방지
ALTER TABLE memories ENABLE ROW LEVEL SECURITY;
CREATE POLICY memories_instance_isolation ON memories
USING (character_instance_id = current_setting('app.current_instance_id')::text);
-- 애플리케이션에서 매 요청마다:
-- SET LOCAL app.current_instance_id = 'ci_abc123';
인덱스 전략
-- 기억 검색
CREATE INDEX idx_memories_active ON memories (character_instance_id, status)
WHERE status = 'active';
-- 벡터 검색 (HNSW)
CREATE INDEX idx_memory_embeddings_hnsw ON memory_embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 최근 대화
CREATE INDEX idx_messages_conv ON messages (conversation_id, created_at DESC);
-- 상태 이벤트
CREATE INDEX idx_state_events ON state_events (character_instance_id, created_at DESC);
-- 감사 로그
CREATE INDEX idx_audit ON audit_logs (character_instance_id, created_at DESC);
7API 계약 상세
POST /v1/characters/{instanceId}/turns
Request
{
"conversation_id": "conv_...",
"message": "오늘 기분 어때?",
"player_session_id": "player_...",
"locale": "ko-KR",
"response_mode": "text_audio_action",
"idempotency_key": "idem_..."
}
Response (200)
{
"turn_id": "turn_...",
"text": "오늘은 네가 와서 꽤 좋아졌어.",
"emotion": { "primary": "joy", "intensity": 0.7 },
"action_plan": {
"expression": { "id": "happy", "weight": 0.85, "fade_ms": 250 },
"base_motion": { "id": "greet", "priority": 60 },
"overlays": []
},
"state_revision": 143,
"audio_pending": true
}
response_mode 옵션: text_only | text_action | text_audio_action
8MCP Tool/Resource 계약
character.turn
{
"name": "character.turn",
"description": "캐릭터에게 대화 메시지를 보내고 응답을 받는다.",
"inputSchema": {
"type": "object",
"required": ["character_instance_id", "message"],
"properties": {
"character_instance_id": { "type": "string" },
"conversation_id": { "type": "string" },
"message": { "type": "string", "maxLength": 4000 },
"player_session_id": { "type": "string" },
"locale": { "type": "string", "default": "ko-KR" },
"response_mode": { "enum": ["text_only","text_action","text_audio_action"] }
}
}
}
character.search_memory
{
"name": "character.search_memory",
"description": "캐릭터 인스턴스의 기억을 검색한다.",
"inputSchema": {
"type": "object",
"required": ["character_instance_id", "query"],
"properties": {
"character_instance_id": { "type": "string" },
"query": { "type": "string" },
"limit": { "type": "integer", "default": 5, "maximum": 20 },
"type_filter": { "enum": ["all","user_preference","episodic","semantic"] }
}
}
}
MCP Resources
character://instances/{id}/profile
character://instances/{id}/state
character://instances/{id}/relationship
character://instances/{id}/capabilities
character://instances/{id}/memory/index
character://instances/{id}/memory/{memory_id}
character://instances/{id}/motion-library
character://templates/{template_id}/lore/{page}
Auth Scopes
character:profile:read character:state:read
character:chat character:memory:read
character:memory:write character:animation:control
character:instance:export character:admin
9Character Package v0.1
MVP Expressions (8종)
| ID | 용도 |
neutral | 기본 |
happy | 기쁨/만족 |
sad | 슬픔 |
angry | 화남 |
surprised | 놀람 |
shy | 수줍음 |
thinking | 생각 중 |
tired | 피곤 |
MVP Motions (10종)
| ID | 용도 | 우선순위 |
idle_01 | 기본 대기 | 10 |
idle_02 | 대기 변형 | 10 |
greet | 인사 | 60 |
nod | 끄덕임 | 60 |
shake_head | 고개 젓기 | 60 |
wave | 손 흔들기 | 70 |
laugh | 웃음 | 60 |
think | 생각 포즈 | 50 |
look_away | 시선 회피 | 60 |
celebrate | 기뻐하기 | 70 |
Semantic Controls
| Control | Live2D Param | Min | Max | Max Vel |
head_yaw | ParamAngleX | -30 | 30 | 60/s |
head_pitch | ParamAngleY | -20 | 20 | 40/s |
head_roll | ParamAngleZ | -15 | 15 | 30/s |
gaze_x | ParamEyeBallX | -1 | 1 | 3/s |
gaze_y | ParamEyeBallY | -1 | 1 | 3/s |
body_yaw | ParamBodyAngleX | -10 | 10 | 20/s |
body_pitch | ParamBodyAngleY | -5 | 5 | 10/s |
10테스트 계획
기억 평가셋 (30개)
| # | 카테고리 | 예시 |
| 1-5 | 직접 사실 회상 | "내가 좋아하는 음료가 뭐야?" |
| 6-10 | 표현 변형 질문 | "내 취미 알아?" (게임 좋아한다고 저장 후) |
| 11-15 | 오래된 기억 | 20턴 전 내용 회상 |
| 16-18 | 정정된 기억 | "서울" → "부산으로 이사" 후 질문 |
| 19-21 | 삭제된 기억 | 삭제 정보를 확정 사실로 안 써야 |
| 22-24 | 교차 사용자 | A 기억이 B에게 0건 노출 |
| 25-27 | 유사 정보 구분 | A "고양이" vs B "강아지" |
| 28-30 | Lore vs 사용자 기억 | 설정과 사용자 말 충돌 시 |
자연어 동작 평가셋 (20개, 발췌)
| # | 입력 | 기대 action_plan |
| 1 | "웃어줘" | expression: happy, motion: laugh |
| 3 | "삐진 것처럼 고개 돌려" | expression: angry, motion: look_away, head_yaw -16 |
| 11 | "좌측을 봐" | gaze_x -0.8, head_yaw -10 |
| 19 | "천천히 오른쪽을 봐" | head_yaw 15 duration 1000ms |
테스트 디렉터리 구성
tests/
├─ unit/ # 순수 로직
├─ integration/ # DB/Redis 포함
├─ contract/ # MCP 계약
├─ e2e/ # 브라우저 E2E
├─ eval/ # LLM 평가 (비결정적)
└─ security/ # 인젝션, 격리
11배포 전략
| 컴포넌트 | 배포 대상 |
| API Server | Fly.io / Railway (컨테이너) |
| Worker | 같은 플랫폼, 별도 프로세스 |
| MCP Server | 같은 플랫폼, 별도 포트 |
| Web Player | Cloudflare Pages |
| Admin Console | Cloudflare Pages |
| PostgreSQL | Neon (Serverless, pgvector) |
| Redis | Upstash (Serverless) |
| 오브젝트 저장 | Cloudflare R2 |
| CDN | Cloudflare |
12Go/No-Go 체크리스트
Phase 0 → Phase 1
- Live2D Web SDK — 모델 로딩 + Expression + Motion
- LLM → TurnPlan structured output 성공률 90%+
- ActionPlan Validator — 범위 초과 차단
- TTS + 볼륨 립싱크 동작
- WebSocket 이벤트 송수신
- pgvector 기억 저장/검색
- MCP Inspector — character.turn 동작
- 사용자 간 기억 격리 0건 누출
MVP 완료
- 로그인 후 대화 가능
- TTS + 립싱크 정상
- 8 표정 + 10 모션 동작
- 자연어 동작 20개 중 16개+ 정확 (80%+)
- 브라우저 재접속 후 핵심 기억 유지
- Recall@5 ≥ 85%, 사실 회상 ≥ 90%
- 사용자 간 기억 누출 0건
- 기억 확인·삭제 가능
- MCP 동일 Instance 호출 성공
- ActionPlan 통과율 ≥ 99%
- 무효 파라미터 실행 0건
- p95 첫 텍스트 ≤ 3초, 첫 음성 ≤ 5초
- 이벤트 유실 < 1%
- 기억 삭제 반영 ≤ 1분
- 데모 시나리오 11단계 전체 통과
13선행 과제 (비기술)
| # | 과제 | 담당 | 의존 |
| BIZ-1 | Live2D Expandable Application 사전 문의 | 사업/법무 | Phase 0 시작 전 |
| BIZ-2 | MVP 비공개 검증과 상용 공개의 라이선스 경계 | 사업/법무 | BIZ-1 |
| BIZ-3 | MVP용 Live2D 모델 확보 | 기획/아트 | BIZ-1 후 |
| BIZ-4 | TTS 상업 이용 라이선스 | 사업 | TTS 공급자 선정 후 |
| BIZ-5 | 이용약관 초안 (기억 저장 동의, 삭제 권리) | 법무 | M6 설계 후 |
| BIZ-6 | 개인정보 처리방침 초안 | 법무 | BIZ-5 |
Character Runtime MVP — 상세 개발 계획
2026-08-22