feat(desktop): LLM 기본 모델 qwen3:4b → gemma4:e4b 전면 전환 + think:false 안전장치

qwen3:4b가 reasoning 모델이라 <think>...</think> 블록을 길게 생성 →
stripReasoningBlocks 후 빈 문자열 → 원본 transcript fallback으로 끝나면서
LLM refine이 42초 걸리는 병목 발견. Google Gemma 4 e4b(4.5B effective
params, 2026-04-02 릴리스)로 교체. non-reasoning 기본 + Ollama v0.20+
think: false 파라미터로 2중 방어.

실측 결과: 받아쓰기 한 사이클 51.4s → 5.5s (9.3배 빠름).
  STT 500ms + LLM 3,925ms + insert 1,092ms.
refine 품질 정상 동작 확인: "테스트하는 중입니다" → "테스트하고 있습니다".

- LocalLLMService: 3개 fallback 기본값 변경(generate / streamGenerate /
  chatStream) + Ollama 요청 body에 think: false 명시 추가. non-reasoning
  모델은 무시, reasoning 모델은 thinking 토큰 차단. NO_THINK 주석을
  legacy 설명으로 업데이트 — qwen3/deepseek-r1 수동 선택자를 위한 3중
  방어(/no_think + think:false + stripReasoningBlocks) 명시.
- OnboardingModal / OllamaGuideModal: pull 명령어 갱신
- 테스트 fixture 갱신
- 12개 i18n locale JSON: settings.ollamaHint / ollama.step2.alt 키 업데이트
  (qwen3:4b → gemma4:e4b, qwen3:8b → gemma4:26b)
- 10개 site i18n locale TS + HowItWorks.tsx 파이프라인 시각화 — detail
  문자열 'qwen3 / llama3 / gemma3' → 'gemma4 / llama3.2 / phi4',
  파이프라인 라벨 'qwen3:4b @ localhost' → 'gemma4:e4b @ localhost'
- 설계서 00 LLMConfig 기본값 + CONFIG_DEFAULTS
- 설계서 05: 6개 API 스키마 예시, 2개 OllamaClient 코드 예시, LLM 모델
  추천 표 재정렬(gemma4:e4b 최상위, qwen3는 reasoning 경고와 함께 후순위),
  권장 JSON 설정에 think:false 추가
- phase-14 meeting mode 컨텍스트 윈도우 표 갱신
- V2-5 Mac 부트스트랩 가이드 pull 커맨드 갱신
- project_status.md Part 7 전체 섹션 추가
This commit is contained in:
윤찬 2026-04-12 09:47:08 +09:00
parent a744551442
commit d397bcbf57
32 changed files with 153 additions and 65 deletions

View file

@ -71,8 +71,12 @@ interface LocalLLMEvents {
// ============================================================
// 시스템 프롬프트.
// `/no_think`는 qwen3 계열 reasoning model의 thinking mode를 비활성화하는 토큰.
// 다른 모델에서는 무시되므로 호환성에 문제 없음.
// 기본 권장 모델은 `gemma4:e4b` (non-reasoning). 기본값으로 thinking mode가
// 꺼져 있어 추가 토큰이 필요 없지만, 사용자가 수동으로 qwen3/deepseek-r1 등
// reasoning 모델로 교체했을 때를 대비한 2중 방어:
// (1) 아래 `/no_think` 시스템 프롬프트 토큰 (qwen3 계열 전용 힌트, 타 모델은 무시)
// (2) Ollama 요청 body의 `think: false` 파라미터 (Ollama v0.20.0+)
// (3) `stripReasoningBlocks()` 출력 가드
const NO_THINK = '/no_think'
const SYSTEM_PROMPTS: Record<string, string> = {
@ -269,7 +273,7 @@ class LocalLLMService extends EventEmitter {
}
const serverUrl = configGet('ollamaServerUrl')
const model = options?.model ?? configGet('llmModelId') ?? 'qwen3:4b'
const model = options?.model ?? configGet('llmModelId') ?? 'gemma4:e4b'
this._state = LLMState.Generating
@ -282,6 +286,9 @@ class LocalLLMService extends EventEmitter {
prompt,
system: options?.systemPrompt,
stream: false,
// Ollama v0.20+ think 파라미터: reasoning 모델에서 thinking 토큰 생성 중단.
// gemma4/llama3.2 등 non-reasoning 모델에서는 무시됨.
think: false,
options: {
temperature: options?.temperature ?? 0.3,
num_predict: options?.maxTokens ?? 2048
@ -333,7 +340,7 @@ class LocalLLMService extends EventEmitter {
}
const serverUrl = configGet('ollamaServerUrl')
const model = options?.model ?? configGet('llmModelId') ?? 'qwen3:4b'
const model = options?.model ?? configGet('llmModelId') ?? 'gemma4:e4b'
this._state = LLMState.Generating
this._abortController = new AbortController()
@ -347,6 +354,7 @@ class LocalLLMService extends EventEmitter {
prompt,
system: options?.systemPrompt,
stream: true,
think: false,
options: {
temperature: options?.temperature ?? 0.3,
num_predict: options?.maxTokens ?? 2048
@ -541,7 +549,7 @@ class LocalLLMService extends EventEmitter {
}
const serverUrl = configGet('ollamaServerUrl')
const model = options?.model ?? configGet('llmModelId') ?? 'qwen3:4b'
const model = options?.model ?? configGet('llmModelId') ?? 'gemma4:e4b'
this._abortController = new AbortController()
this._state = LLMState.Generating
@ -554,6 +562,7 @@ class LocalLLMService extends EventEmitter {
model,
messages,
stream: true,
think: false,
options: {
temperature: options?.temperature ?? 0.7,
},

View file

@ -130,7 +130,7 @@ export function OllamaGuideModal({ open, onClose }: OllamaGuideModalProps): Reac
{t('ollama.step2.desc')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<CodeBlock>ollama pull qwen3:4b</CodeBlock>
<CodeBlock>ollama pull gemma4:e4b</CodeBlock>
<Typography variant="caption" sx={{ color: d3roPalette.text.inactive }}>
{t('ollama.step2.alt')}
</Typography>

View file

@ -243,7 +243,7 @@ export function OnboardingModal({ open, onClose }: OnboardingModalProps): React.
</Button>
<Box sx={{ p: 1.5, borderRadius: '8px', bgcolor: d3roPalette.bg.inset, boxShadow: d3roShadow.inset, mb: 3 }}>
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '11px', color: d3roPalette.accent.amber }}>
$ ollama pull qwen3:4b
$ ollama pull gemma4:e4b
</Typography>
<Typography sx={{ fontSize: '10px', color: d3roPalette.text.inactive, mt: 0.5 }}>
{t('onboarding.ollama.modelHint')}

View file

@ -55,7 +55,7 @@ vi.mock('../../../src/main/services/ConfigService', () => ({
sttModelId: 'base',
defaultLLMAction: 'refine',
ollamaServerUrl: 'http://localhost:11434',
llmModelId: 'qwen3:4b'
llmModelId: 'gemma4:e4b'
}
return defaults[key]
})