fix(desktop): qwen3 reasoning 모드로 LLM refine 39초 + 빈 응답
증상:
- STT 426ms로 빠른데 LLM refine 단계가 39초 소요 후 빈 문자열 반환
- _completeSession('') → finalText.length === 0이라 paste 호출 자체 건너뜀
- 사용자에게는 '느리고 paste 안 됨'으로 보임
원인:
- 사용자가 ollama pull qwen3:4b 한 직후 첫 호출 (모델 cold start 일부 있음)
- qwen3는 reasoning model이라 응답에 <think>...</think> 블록을 길게 출력
- system prompt에 '/no_think' 토큰 없음 → reasoning mode ON
- generate()는 data.response.trim() 그대로 반환 → think 블록 + 빈 본문이면
trim 후 빈 문자열
- 빈 문자열에 대한 fallback이 없어서 그대로 _completeSession('')
수정:
- SYSTEM_PROMPTS 모두에 '/no_think' 헤더 추가
- qwen3 reasoning 비활성화 → 응답 속도 대폭 단축
- 다른 모델(llama, mistral, gemma)은 토큰 무시 → 호환성 OK
- stripReasoningBlocks() 추가
- <think>...</think> + <thinking>...</thinking> 블록 제거 (gi flag)
- /no_think를 무시하는 모델 + 응답에 think tag가 섞여 들어오는 케이스 안전망
- processText() 결과:
- stripReasoningBlocks(result.text)
- 빈 문자열이면 원본 transcript fallback + warn 로그
This commit is contained in:
parent
a41b8a4c4b
commit
9d12133cfe
1 changed files with 38 additions and 6 deletions
|
|
@ -70,25 +70,47 @@ interface LocalLLMEvents {
|
|||
// 시스템 프롬프트 (설계서 Phase 4 참조)
|
||||
// ============================================================
|
||||
|
||||
// 시스템 프롬프트.
|
||||
// `/no_think`는 qwen3 계열 reasoning model의 thinking mode를 비활성화하는 토큰.
|
||||
// 다른 모델에서는 무시되므로 호환성에 문제 없음.
|
||||
const NO_THINK = '/no_think'
|
||||
|
||||
const SYSTEM_PROMPTS: Record<string, string> = {
|
||||
refine: `다음 음성 전사 텍스트를 자연스럽고 격식 있는 문어체로 다듬어주세요.
|
||||
refine: `${NO_THINK}
|
||||
다음 음성 전사 텍스트를 자연스럽고 격식 있는 문어체로 다듬어주세요.
|
||||
원래 의미를 유지하면서 문법 오류를 수정하고, 불필요한 반복이나 필러를 제거하세요.
|
||||
다듬어진 텍스트만 출력하세요. 설명이나 부가 문구를 붙이지 마세요.`,
|
||||
|
||||
translate: `다음 텍스트를 {{targetLanguage}}로 번역해주세요.
|
||||
translate: `${NO_THINK}
|
||||
다음 텍스트를 {{targetLanguage}}로 번역해주세요.
|
||||
자연스럽고 정확한 번역만 출력하세요. 원문이나 설명을 붙이지 마세요.`,
|
||||
|
||||
summarize: `다음 텍스트의 핵심 내용을 3줄 이내로 요약해주세요.
|
||||
summarize: `${NO_THINK}
|
||||
다음 텍스트의 핵심 내용을 3줄 이내로 요약해주세요.
|
||||
요약문만 출력하세요.`,
|
||||
|
||||
grammar: `다음 텍스트의 문법 오류만 수정해주세요.
|
||||
grammar: `${NO_THINK}
|
||||
다음 텍스트의 문법 오류만 수정해주세요.
|
||||
원래 의미와 톤을 유지하면서 문법 오류만 수정하세요.
|
||||
수정된 텍스트만 출력하세요.`,
|
||||
|
||||
expand: `다음 텍스트를 더 자세하고 풍부하게 확장해주세요.
|
||||
expand: `${NO_THINK}
|
||||
다음 텍스트를 더 자세하고 풍부하게 확장해주세요.
|
||||
확장된 텍스트만 출력하세요.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Reasoning model(qwen3, deepseek-r1 등)이 응답에 포함하는
|
||||
* <think>...</think> 블록을 제거한다. /no_think 토큰을 무시하는
|
||||
* 모델에서도 안전하게 동작하도록.
|
||||
*/
|
||||
function stripReasoningBlocks(text: string): string {
|
||||
return text
|
||||
.replace(/<think>[\s\S]*?<\/think>\s*/gi, '')
|
||||
.replace(/<thinking>[\s\S]*?<\/thinking>\s*/gi, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LocalLLMService
|
||||
// ============================================================
|
||||
|
|
@ -437,7 +459,17 @@ class LocalLLMService extends EventEmitter {
|
|||
}
|
||||
|
||||
const result = await this.generate(text, { systemPrompt })
|
||||
return result.text.trim()
|
||||
const cleaned = stripReasoningBlocks(result.text)
|
||||
// reasoning 블록 제거 후 빈 응답이면 원본 텍스트 폴백
|
||||
// (모델이 thinking만 하고 출력은 안 한 경우 / 응답 파싱 실패 케이스)
|
||||
if (cleaned.length === 0) {
|
||||
logger.warn(
|
||||
`LLM returned empty after reasoning strip — falling back to original transcript ` +
|
||||
`(raw length=${result.text.length})`
|
||||
)
|
||||
return text
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
cancelGeneration(): void {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue