Files
life-helper/docs/reference/311-llm-warmup.md
joungmin 41457ab96e [08-Documenter] #311 설계서 Approved + reference + 사용자 가이드 FAQ
- 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
2026-06-15 13:25:38 +09:00

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.dartsealed class ChatWarmupState.

상태 진입 조건 UI 동작
Idle 초기값 평상
Loading quickCheck = ready + llm.isLoaded = falsellm.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:

  • fileMissingFileSystemException. 회복 = SettingsScreen push (재다운로드).
  • runtime — 기타. 회복 = retry().

API

ChatWarmupController

final chatWarmupProvider =
    StateNotifierProvider.autoDispose<ChatWarmupController, ChatWarmupState>(...);
  • Future<void> start() — quickCheck → load → state 전이. 재진입 가드 (Loading 중 호출은 no-op). ChatScreen.initStatepostFrameCallback 에서 호출.
  • 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 ↔ pumpAndSettle race 가 framework-level 한계.

관련 문서