- docs/design/311-llm-warmup/ 3파일 상태 Draft → Approved (v0.4.1) - docs/reference/311-llm-warmup.md 신규 (상태 머신, API, UI binding, 마이크로카피, 테스트) - docs/guides/ai-chat-using.md 헤더 + FAQ 2건 추가 (warmup 인지/회복) Refs #311
4.4 KiB
4.4 KiB
LLM warm-up (#311)
적용 버전: v0.4.1 이상 · Redmine #311 · 설계서: docs/design/311-llm-warmup/
ChatScreen 진입 시 LLM 모델을 백그라운드로 warm-up 하여 첫 send 의 cold native-init 비용 (수 초) 을 제거한다.
상태 머신
app/lib/state/chat_warmup_provider.dart — sealed class ChatWarmupState.
| 상태 | 진입 조건 | UI 동작 |
|---|---|---|
Idle |
초기값 | 평상 |
Loading |
quickCheck = ready + llm.isLoaded = false → llm.load() in-flight |
hintText 교체, send 자리 spinner |
Ready |
load() 성공 또는 llm.isLoaded = true fast path |
평상, send 즉시 활성 |
Failed(kind) |
load() throw |
_WarmupErrorBanner 표시 |
Unavailable |
quickCheck != ready (opt-out / downloading / missing) |
평상 (warmup 라벨 X, 기존 lazy 경로 fallback) |
ChatWarmupFailureKind:
fileMissing—FileSystemException. 회복 =SettingsScreenpush (재다운로드).runtime— 기타. 회복 =retry().
API
ChatWarmupController
final chatWarmupProvider =
StateNotifierProvider.autoDispose<ChatWarmupController, ChatWarmupState>(...);
Future<void> start()— quickCheck → load → state 전이. 재진입 가드 (Loading 중 호출은 no-op).ChatScreen.initState의postFrameCallback에서 호출.Future<void> retry()— Idle reset 후 start() 재호출.- dispose 시
_disposed = true→_safeSet가 후속 state 변경 무시.
ModelLifecycle.quickCheck()
Future<ModelAvailability> quickCheck();
checkAvailability() 의 lightweight 버전. SHA-256 재해싱 (~2.4GB) 을 건너뛰고 meta_kv + 파일 존재만으로 ready 추정. tamper 감지는 checkAvailability() 의 cold path (SettingsScreen) 에 위임.
| meta_kv 상태 | quickCheck 반환 |
|---|---|
ai_opt_in != 'true' |
missing |
ai_download_state in (downloading, paused) |
downloading |
ai_model_path null |
missing |
ai_model_sha256 null |
corrupt |
| 파일 부재 | missing |
| 그 외 | ready |
| 내부 throw | corrupt (보수적 fallback) |
Concurrent load guard
GemmaLlmService.load() + MockLlmService.load() 가 _loadingFuture 가드 공유:
Future<void> load() {
if (_loaded) return Future.value();
final existing = _loadingFuture;
if (existing != null) return existing;
final future = _doLoad();
_loadingFuture = future;
return future.whenComplete(() { _loadingFuture = null; });
}
ChatScreen warm-up + userTurn lazy load 가 race 해도 native init 1회만 실행. _doLoad throw 시 whenComplete 가 _loadingFuture = null 처리 → 다음 caller 가 새 시도.
UI binding (chat_screen.dart)
TextField.enabled = !state.isStreaming— warmup state 와 무관 (타이핑 미리 작성 가능, UX R1+R2).hintText분기 — warmup loading 시AI 준비 중… 첫 시작은 몇 초 걸려요, 그 외 평상.- send 영역 —
state.isStreaming || isWarming이면CircularProgressIndicator(strokeWidth: 2), 그 외IconButton.filled. _WarmupErrorBanner(Failed일 때만 표시):- 메시지 본문 = 상태 기술만 (명령형 X, AC12).
- 우측 정렬
OutlinedButton:fileMissing→[설정으로 가기]+Navigator.push(SettingsScreen).then((_) => retry())(pop 후 자동 retry).runtime→[다시 시도]+retry()(즉시).
마이크로카피
| 상황 | 한국어 |
|---|---|
| warmup 중 hintText | AI 준비 중… 첫 시작은 몇 초 걸려요 |
| 평상 hintText | 습관 추가, 기록, 카탈로그 질문… |
| Failed(fileMissing) | AI 모델 파일을 찾을 수 없어요. |
| Failed(runtime) | AI 를 시작하지 못했어요. |
명령형 ("다시 시도해주세요" 등) 은 본문 금지 — 행동은 버튼이 담당.
테스트
app/test/state/chat_warmup_test.dart— 8 unit (AC1-2, 5-7, 11-12 + retry + 재진입 가드).app/test/data/ai/model_lifecycle_test.dart— quickCheck 4 신규.app/test/ui/chat_screen_test.dart— widget E2E 보류 (NOTE comment 사유).CircularProgressIndicator무한 ticker ↔pumpAndSettlerace 가 framework-level 한계.
관련 문서
- 설계서: docs/design/311-llm-warmup/
- 사용자 가이드: AI 코치와 대화하기
- 변경 이력: CHANGELOG.md [0.4.1]