[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
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# 설계서: ChatScreen LLM warm-up (#311)
|
||||
|
||||
> **상태**: Draft
|
||||
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
|
||||
> **추적성** — Redmine: #311 · 관련 ADR: 없음 (Backward-compatible 추가)
|
||||
> **상태**: Approved (v0.4.1, 2026-06-15)
|
||||
> **작성**: [AI] Architect · **최종수정**: 2026-06-15 (08-Documenter 마감)
|
||||
> **추적성** — Redmine: #311 · 관련 ADR: 없음 (Backward-compatible 추가) · Release: v0.4.1 (`121108f`)
|
||||
> · 구현 파일: `app/lib/state/chat_warmup_provider.dart` (신규) · `app/lib/data/ai/llm_service.dart` (수정) · `app/lib/data/ai/model_lifecycle.dart` (`quickCheck` 추가) · `app/lib/data/ai/gemma_llm_service.dart` (concurrent load guard) · `app/lib/ui/screens/chat_screen.dart` (warmup binding)
|
||||
> · 테스트: `app/test/state/chat_warmup_test.dart` (신규) · `app/test/data/ai/model_lifecycle_test.dart` (quickCheck 케이스 추가) · `app/test/ui/chat_screen_test.dart` (warmup 라벨/disabled 케이스 추가)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 함수 설계서: `ChatWarmupController.start` (#311)
|
||||
|
||||
> **부모 설계서**: ./README.md · **상태**: Draft
|
||||
> **부모 설계서**: ./README.md · **상태**: Approved (v0.4.1, 2026-06-15)
|
||||
> **작성**: [AI] Architect · **구현**: `app/lib/state/chat_warmup_provider.dart:start` · **테스트**: `app/test/state/chat_warmup_test.dart`
|
||||
|
||||
## 1. 시그니처
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 함수 설계서: `GemmaLlmService.load` concurrent guard (#311)
|
||||
|
||||
> **부모 설계서**: ./README.md · **상태**: Draft
|
||||
> **부모 설계서**: ./README.md · **상태**: Approved (v0.4.1, 2026-06-15)
|
||||
> **작성**: [AI] Architect · **구현**: `app/lib/data/ai/gemma_llm_service.dart:load` (수정) · **테스트**: `app/test/data/ai/gemma_llm_service_test.dart` (concurrent 케이스 추가) / `chat_warmup_test.dart` (시뮬)
|
||||
|
||||
## 1. 시그니처
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AI 코치와 대화하기 (사용자 가이드)
|
||||
|
||||
> 적용 버전: **v0.4.0 이상** · Redmine #260 · 관련 레퍼런스: [docs/reference/260-in-app-tool-calling.md](../reference/260-in-app-tool-calling.md)
|
||||
> 적용 버전: **v0.4.0 이상** (warm-up 은 **v0.4.1+**) · Redmine #260 / #311 · 관련 레퍼런스: [도구 호출](../reference/260-in-app-tool-calling.md) · [Warm-up](../reference/311-llm-warmup.md)
|
||||
>
|
||||
> 본 가이드는 v0.3.0 의 [AI 도움 켜기·끄기](ai-help-onboarding.md) 가 끝났다는 전제에서 시작합니다. 모델이 단말에 준비되어 있어야 합니다.
|
||||
|
||||
@@ -52,6 +52,12 @@ A. 4 회를 넘기면 자동으로 멈추고 "도구 호출 루프가 너무 길
|
||||
**Q. 응답이 너무 느린 것 같아요.**
|
||||
A. 단말 모델이라 1턴 평균 2~5초가 정상입니다. AI 가 도구를 호출하면 round trip 이 한 번 더 들어가 6~10초가 될 수 있어요.
|
||||
|
||||
**Q. ChatScreen 진입 시 입력창에 "AI 준비 중…" 이 떠요.**
|
||||
A. v0.4.1 부터 모델을 백그라운드로 미리 시동합니다 (warm-up). 첫 진입 시 한 번만 보이고 평균 2~8초 안에 사라집니다. 그동안 메시지를 미리 타이핑해도 되고, send 버튼만 비활성 상태로 기다립니다. 다시 진입하면 즉시 사용 가능합니다.
|
||||
|
||||
**Q. "AI 모델 파일을 찾을 수 없어요." / "AI 를 시작하지 못했어요." 가 떠요.**
|
||||
A. 파일을 못 찾는 경우는 [설정으로 가기] 로 이동해 재다운로드, 일시적 시작 실패는 [다시 시도] 로 회복합니다. 설정에서 다운로드를 완료하고 돌아오면 자동으로 다시 시도합니다.
|
||||
|
||||
**Q. 도구 결과가 잘렸어요.**
|
||||
A. 모델 컨텍스트 보호를 위해 도구 결과는 2KB 로 제한됩니다. 잘림이 발생하면 AI 가 자동으로 단건 조회 (`프로토콜 상세`) 도구를 다시 부르도록 안내됩니다.
|
||||
|
||||
|
||||
103
docs/reference/311-llm-warmup.md
Normal file
103
docs/reference/311-llm-warmup.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# LLM warm-up (#311)
|
||||
|
||||
> 적용 버전: **v0.4.1 이상** · Redmine #311 · 설계서: [docs/design/311-llm-warmup/](../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`. 회복 = `SettingsScreen` push (재다운로드).
|
||||
- `runtime` — 기타. 회복 = `retry()`.
|
||||
|
||||
## API
|
||||
|
||||
### `ChatWarmupController`
|
||||
|
||||
```dart
|
||||
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()`
|
||||
|
||||
```dart
|
||||
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` 가드 공유:
|
||||
|
||||
```dart
|
||||
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 한계.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- 설계서: [docs/design/311-llm-warmup/](../design/311-llm-warmup/)
|
||||
- 사용자 가이드: [AI 코치와 대화하기](../guides/ai-chat-using.md)
|
||||
- 변경 이력: [CHANGELOG.md](../../CHANGELOG.md) [0.4.1]
|
||||
Reference in New Issue
Block a user