"""Phase 1 HA: upgrade services/llm/index.ts""" PATH = "/data/ai/audio/server/src/services/llm/index.ts" with open(PATH, 'r', encoding='utf-8') as f: src = f.read() # 替换 invokeWithRetry OLD = """async function invokeWithRetry( fn: () => Promise, modelId: string, onSwitch: (nextModelId: string) => Promise, callType: string = 'llm_chat', ): Promise { const provider = resolveProviderKey(modelId); // 第1次尝试 try { return await withAiLog(fn, { callType, provider, model: modelId }); } catch (error: any) { // 不可切换的错误,直接抛出 if (!config.models.shouldSwitchModel(error?.message || '')) { throw error; } // 可切换错误:先重试1次(可能是瞬时波动) console.log(`[LLM] ${modelId} 调用失败,2s后重试...`); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); try { // 第2次尝试(同模型重试,记录日志) const retryResult = await withAiLog( fn, { callType: `${callType}_retry1`, provider, model: modelId } ); return retryResult; } catch (retryError: any) { // 重试仍失败,切换供应商 const nextModel = trySwitchModel(modelId, retryError); if (nextModel) { return onSwitch(nextModel); } throw retryError; } } }""" NEW = """// ============ 高可用 LLM 调度(HA - High Availability)============ export const HA_RETRY_CONFIG = { maxRetriesPerProvider: 3, maxFallbackRounds: 4, baseDelayMs: 1000, maxDelayMs: 8000, jitterPct: 0.2, }; const NET_ERR = ['timed out','timeout','request timeout','etimedout','econnreset','econnrefused','esockettimedout','enotfound','fetch failed','aborted','eai_again','connection reset','connection error','econn']; const BIZ_ERR = ['json parse error','invalid request','schema','unauthorized','forbidden','context length','token limit','input too long','invalid api key']; function backoffWithJitter(attempt: number): number { const base = Math.min(HA_RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt), HA_RETRY_CONFIG.maxDelayMs); const jitter = base * HA_RETRY_CONFIG.jitterPct * (Math.random() * 2 - 1); return Math.max(100, Math.floor(base + jitter)); } function isNetworkError(msg: string): boolean { const m = msg.toLowerCase(); return NET_ERR.some(p => m.includes(p)); } function isBusinessError(msg: string): boolean { const m = msg.toLowerCase(); return BIZ_ERR.some(p => m.includes(p)); } export class AllProvidersFailedError extends Error { readonly name = 'AllProvidersFailedError'; readonly attempts: Array<{ provider: string; model: string; error: string; attempts: number; errorType: 'network' | 'business' | 'unknown' }>; constructor(attempts: Array<{ provider: string; model: string; error: string; attempts: number; errorType: 'network' | 'business' | 'unknown' }>) { const summary = attempts.map(a => `${a.provider}/${a.model} (${a.attempts}次, ${a.errorType}): ${a.error.substring(0, 80)}`).join('; '); super(`All ${attempts.length} LLM provider(s) failed. ${summary}`); this.attempts = attempts; } } async function invokeWithRetry(fn: (modelId: string) => Promise, modelId: string, callType: string = 'llm_chat'): Promise { const tried = new Map(); let cur = modelId, lastErr: any = null; for (let round = 0; round < HA_RETRY_CONFIG.maxFallbackRounds; round++) { const provider = resolveProviderKey(cur); if (tried.has(provider)) break; let providerError: any = null; for (let attempt = 0; attempt < HA_RETRY_CONFIG.maxRetriesPerProvider; attempt++) { try { return await fn(cur); } catch (error: any) { const em = error?.message || String(error); lastErr = error; providerError = error; if (isBusinessError(em) && !isNetworkError(em)) { console.error(`[LLM-HA] ${provider}/${cur} 业务错误,立即抛出: ${em.substring(0, 100)}`); throw error; } if (isNetworkError(em)) { console.warn(`[LLM-HA] ${provider}/${cur} 网络错误,立即切换: ${em.substring(0, 100)}`); break; } if (attempt < HA_RETRY_CONFIG.maxRetriesPerProvider - 1) { const delay = backoffWithJitter(attempt); console.log(`[LLM-HA] ${provider}/${cur} 第${attempt+1}次失败: ${em.substring(0, 100)}, ${delay}ms后重试`); await new Promise(r => setTimeout(r, delay)); } } } const em = providerError?.message || 'unknown'; const et: 'network' | 'business' | 'unknown' = isNetworkError(em) ? 'network' : (isBusinessError(em) ? 'business' : 'unknown'); tried.set(provider, { model: cur, error: em, attempts: HA_RETRY_CONFIG.maxRetriesPerProvider, errorType: et }); console.log(`[LLM-HA] ${provider}/${cur} 失败 (${et}),尝试切换供应商`); const next = trySwitchModel(cur, providerError); if (!next) break; if (tried.has(resolveProviderKey(next))) { console.warn(`[LLM-HA] 下一供应商已尝试过,停止 fallback`); break; } cur = next; console.log(`[LLM-HA] 切换到 ${resolveProviderKey(cur)}/${cur}`); } throw new AllProvidersFailedError(Array.from(tried.entries()).map(([provider, info]) => ({ provider, ...info }))); }""" assert OLD in src, "OLD invokeWithRetry not found" src = src.replace(OLD, NEW) # 改 callLLM(去掉 onSwitch) OLD_CALL = """export async function callLLM(prompt: string, modelId?: string): Promise { const id = modelId || getDefaultModelId(); return invokeWithRetry( async () => { const llm = getLLM(id); const response = await llm.invoke(prompt); return cleanLlmResponse(response.content as string); }, id, (nextModel) => callLLM(prompt, nextModel), ); }""" NEW_CALL = """export async function callLLM(prompt: string, modelId?: string): Promise { const id = modelId || getDefaultModelId(); return invokeWithRetry( async (useModelId) => { const llm = getLLM(useModelId); const response = await llm.invoke(prompt); return cleanLlmResponse(response.content as string); }, id, ); }""" assert OLD_CALL in src, "OLD callLLM not found" src = src.replace(OLD_CALL, NEW_CALL) # 改 callLLMWithMessages(去掉手写重试,统一走 invokeWithRetry) OLD_MSG = """export async function callLLMWithMessages( messages: ChatMessage[], modelId?: string, maxTokens?: number ): Promise { const id = modelId || getDefaultModelId(); // 请求日志 console.log('[AI请求] ===== 开始 ====='); console.log('[AI请求] 模型:', id, maxTokens !== undefined ? `(maxTokens=${maxTokens})` : ''); console.log('[AI请求] 消息数:', messages.length); const systemMsg = messages.find(m => m.role === 'system'); if (systemMsg) { console.log('[AI请求] System提示词 (前200字):', systemMsg.content.substring(0, 200)); } // 解析 provider const provider = resolveProviderKey(id); try { const llm = getLLM(id, maxTokens); const baseMessages = toBaseMessages(messages); const prompt = messages.map(m => `${m.role}: ${m.content}`).join('\\n'); const response = await withAiLog( () => llm.invoke(baseMessages as any), { callType: 'llm_chat', provider, model: id, textLen: messages.reduce((s, m) => s + (m.content?.length || 0), 0), prompt } ); const responseContent = cleanLlmResponse(response.content as string); console.log('[AI响应] 内容 (前1000字):', responseContent.substring(0, 1000)); if (responseContent.length > 1000) { console.log('[AI响应] ... (总长度:', responseContent.length, '字)'); } console.log('[AI响应] ===== 结束 ====='); return responseContent; } catch (error: any) { // 不可切换的错误,直接抛出 if (!config.models.shouldSwitchModel(error?.message || '')) { throw error; } // 可切换错误:先重试1次 console.log(`[LLM] ${id} 消息调用失败,2s后重试...`); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); try { const llm = getLLM(id, maxTokens); const baseMessages = toBaseMessages(messages); const prompt = messages.map(m => `${m.role}: ${m.content}`).join('\\n'); const response = await withAiLog( () => llm.invoke(baseMessages as any), { callType: 'llm_chat_retry', provider, model: id, textLen: messages.reduce((s, m) => s + (m.content?.length || 0), 0), prompt } ); const responseContent = cleanLlmResponse(response.content as string); console.log('[AI响应] 重试成功 (前1000字):', responseContent.substring(0, 1000)); console.log('[AI响应] ===== 结束 ====='); return responseContent; } catch (retryError: any) { const nextModel = trySwitchModel(id, retryError); if (nextModel) { console.log(`[LLM] ${id} 重试仍失败,切换到 ${nextModel}`); return callLLMWithMessages(messages, nextModel, maxTokens); } throw retryError; } } }""" NEW_MSG = """export async function callLLMWithMessages( messages: ChatMessage[], modelId?: string, maxTokens?: number ): Promise { const id = modelId || getDefaultModelId(); const baseMessages = toBaseMessages(messages); const prompt = messages.map(m => `${m.role}: ${m.content}`).join('\\n'); const textLen = messages.reduce((s, m) => s + (m.content?.length || 0), 0); const systemMsg = messages.find(m => m.role === 'system'); return invokeWithRetry( async (useModelId) => { // 请求日志 console.log('[AI请求] ===== 开始 ====='); console.log('[AI请求] 模型:', useModelId, maxTokens !== undefined ? `(maxTokens=${maxTokens})` : ''); console.log('[AI请求] 消息数:', messages.length); if (systemMsg) { console.log('[AI请求] System提示词 (前200字):', systemMsg.content.substring(0, 200)); } const provider = resolveProviderKey(useModelId); const llm = getLLM(useModelId, maxTokens); const response = await withAiLog( () => llm.invoke(baseMessages as any), { callType: 'llm_chat', provider, model: useModelId, textLen, prompt } ); const responseContent = cleanLlmResponse(response.content as string); console.log('[AI响应] 内容 (前1000字):', responseContent.substring(0, 1000)); if (responseContent.length > 1000) { console.log('[AI响应] ... (总长度:', responseContent.length, '字)'); } console.log('[AI响应] ===== 结束 ====='); return responseContent; }, id, ); }""" assert OLD_MSG in src, "OLD callLLMWithMessages not found" src = src.replace(OLD_MSG, NEW_MSG) with open(PATH, 'w', encoding='utf-8') as f: f.write(src) print("OK: llm/index.ts patched")