| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- """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<T>(
- fn: () => Promise<T>,
- modelId: string,
- onSwitch: (nextModelId: string) => Promise<T>,
- callType: string = 'llm_chat',
- ): Promise<T> {
- 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<T>(fn: (modelId: string) => Promise<T>, modelId: string, callType: string = 'llm_chat'): Promise<T> {
- const tried = new Map<string, { model: string; error: string; attempts: number; errorType: 'network' | 'business' | 'unknown' }>();
- 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<string> {
- 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<string> {
- 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<string> {
- 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<string> {
- 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")
|