patch_llm_ha.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. """Phase 1 HA: upgrade services/llm/index.ts"""
  2. PATH = "/data/ai/audio/server/src/services/llm/index.ts"
  3. with open(PATH, 'r', encoding='utf-8') as f:
  4. src = f.read()
  5. # 替换 invokeWithRetry
  6. OLD = """async function invokeWithRetry<T>(
  7. fn: () => Promise<T>,
  8. modelId: string,
  9. onSwitch: (nextModelId: string) => Promise<T>,
  10. callType: string = 'llm_chat',
  11. ): Promise<T> {
  12. const provider = resolveProviderKey(modelId);
  13. // 第1次尝试
  14. try {
  15. return await withAiLog(fn, { callType, provider, model: modelId });
  16. } catch (error: any) {
  17. // 不可切换的错误,直接抛出
  18. if (!config.models.shouldSwitchModel(error?.message || '')) {
  19. throw error;
  20. }
  21. // 可切换错误:先重试1次(可能是瞬时波动)
  22. console.log(`[LLM] ${modelId} 调用失败,2s后重试...`);
  23. await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
  24. try {
  25. // 第2次尝试(同模型重试,记录日志)
  26. const retryResult = await withAiLog(
  27. fn,
  28. { callType: `${callType}_retry1`, provider, model: modelId }
  29. );
  30. return retryResult;
  31. } catch (retryError: any) {
  32. // 重试仍失败,切换供应商
  33. const nextModel = trySwitchModel(modelId, retryError);
  34. if (nextModel) {
  35. return onSwitch(nextModel);
  36. }
  37. throw retryError;
  38. }
  39. }
  40. }"""
  41. NEW = """// ============ 高可用 LLM 调度(HA - High Availability)============
  42. export const HA_RETRY_CONFIG = {
  43. maxRetriesPerProvider: 3, maxFallbackRounds: 4,
  44. baseDelayMs: 1000, maxDelayMs: 8000, jitterPct: 0.2,
  45. };
  46. const NET_ERR = ['timed out','timeout','request timeout','etimedout','econnreset','econnrefused','esockettimedout','enotfound','fetch failed','aborted','eai_again','connection reset','connection error','econn'];
  47. const BIZ_ERR = ['json parse error','invalid request','schema','unauthorized','forbidden','context length','token limit','input too long','invalid api key'];
  48. function backoffWithJitter(attempt: number): number {
  49. const base = Math.min(HA_RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt), HA_RETRY_CONFIG.maxDelayMs);
  50. const jitter = base * HA_RETRY_CONFIG.jitterPct * (Math.random() * 2 - 1);
  51. return Math.max(100, Math.floor(base + jitter));
  52. }
  53. function isNetworkError(msg: string): boolean { const m = msg.toLowerCase(); return NET_ERR.some(p => m.includes(p)); }
  54. function isBusinessError(msg: string): boolean { const m = msg.toLowerCase(); return BIZ_ERR.some(p => m.includes(p)); }
  55. export class AllProvidersFailedError extends Error {
  56. readonly name = 'AllProvidersFailedError';
  57. readonly attempts: Array<{ provider: string; model: string; error: string; attempts: number; errorType: 'network' | 'business' | 'unknown' }>;
  58. constructor(attempts: Array<{ provider: string; model: string; error: string; attempts: number; errorType: 'network' | 'business' | 'unknown' }>) {
  59. const summary = attempts.map(a => `${a.provider}/${a.model} (${a.attempts}次, ${a.errorType}): ${a.error.substring(0, 80)}`).join('; ');
  60. super(`All ${attempts.length} LLM provider(s) failed. ${summary}`);
  61. this.attempts = attempts;
  62. }
  63. }
  64. async function invokeWithRetry<T>(fn: (modelId: string) => Promise<T>, modelId: string, callType: string = 'llm_chat'): Promise<T> {
  65. const tried = new Map<string, { model: string; error: string; attempts: number; errorType: 'network' | 'business' | 'unknown' }>();
  66. let cur = modelId, lastErr: any = null;
  67. for (let round = 0; round < HA_RETRY_CONFIG.maxFallbackRounds; round++) {
  68. const provider = resolveProviderKey(cur);
  69. if (tried.has(provider)) break;
  70. let providerError: any = null;
  71. for (let attempt = 0; attempt < HA_RETRY_CONFIG.maxRetriesPerProvider; attempt++) {
  72. try {
  73. return await fn(cur);
  74. } catch (error: any) {
  75. const em = error?.message || String(error);
  76. lastErr = error; providerError = error;
  77. if (isBusinessError(em) && !isNetworkError(em)) {
  78. console.error(`[LLM-HA] ${provider}/${cur} 业务错误,立即抛出: ${em.substring(0, 100)}`);
  79. throw error;
  80. }
  81. if (isNetworkError(em)) {
  82. console.warn(`[LLM-HA] ${provider}/${cur} 网络错误,立即切换: ${em.substring(0, 100)}`);
  83. break;
  84. }
  85. if (attempt < HA_RETRY_CONFIG.maxRetriesPerProvider - 1) {
  86. const delay = backoffWithJitter(attempt);
  87. console.log(`[LLM-HA] ${provider}/${cur} 第${attempt+1}次失败: ${em.substring(0, 100)}, ${delay}ms后重试`);
  88. await new Promise(r => setTimeout(r, delay));
  89. }
  90. }
  91. }
  92. const em = providerError?.message || 'unknown';
  93. const et: 'network' | 'business' | 'unknown' = isNetworkError(em) ? 'network' : (isBusinessError(em) ? 'business' : 'unknown');
  94. tried.set(provider, { model: cur, error: em, attempts: HA_RETRY_CONFIG.maxRetriesPerProvider, errorType: et });
  95. console.log(`[LLM-HA] ${provider}/${cur} 失败 (${et}),尝试切换供应商`);
  96. const next = trySwitchModel(cur, providerError);
  97. if (!next) break;
  98. if (tried.has(resolveProviderKey(next))) { console.warn(`[LLM-HA] 下一供应商已尝试过,停止 fallback`); break; }
  99. cur = next;
  100. console.log(`[LLM-HA] 切换到 ${resolveProviderKey(cur)}/${cur}`);
  101. }
  102. throw new AllProvidersFailedError(Array.from(tried.entries()).map(([provider, info]) => ({ provider, ...info })));
  103. }"""
  104. assert OLD in src, "OLD invokeWithRetry not found"
  105. src = src.replace(OLD, NEW)
  106. # 改 callLLM(去掉 onSwitch)
  107. OLD_CALL = """export async function callLLM(prompt: string, modelId?: string): Promise<string> {
  108. const id = modelId || getDefaultModelId();
  109. return invokeWithRetry(
  110. async () => {
  111. const llm = getLLM(id);
  112. const response = await llm.invoke(prompt);
  113. return cleanLlmResponse(response.content as string);
  114. },
  115. id,
  116. (nextModel) => callLLM(prompt, nextModel),
  117. );
  118. }"""
  119. NEW_CALL = """export async function callLLM(prompt: string, modelId?: string): Promise<string> {
  120. const id = modelId || getDefaultModelId();
  121. return invokeWithRetry(
  122. async (useModelId) => {
  123. const llm = getLLM(useModelId);
  124. const response = await llm.invoke(prompt);
  125. return cleanLlmResponse(response.content as string);
  126. },
  127. id,
  128. );
  129. }"""
  130. assert OLD_CALL in src, "OLD callLLM not found"
  131. src = src.replace(OLD_CALL, NEW_CALL)
  132. # 改 callLLMWithMessages(去掉手写重试,统一走 invokeWithRetry)
  133. OLD_MSG = """export async function callLLMWithMessages(
  134. messages: ChatMessage[],
  135. modelId?: string,
  136. maxTokens?: number
  137. ): Promise<string> {
  138. const id = modelId || getDefaultModelId();
  139. // 请求日志
  140. console.log('[AI请求] ===== 开始 =====');
  141. console.log('[AI请求] 模型:', id, maxTokens !== undefined ? `(maxTokens=${maxTokens})` : '');
  142. console.log('[AI请求] 消息数:', messages.length);
  143. const systemMsg = messages.find(m => m.role === 'system');
  144. if (systemMsg) {
  145. console.log('[AI请求] System提示词 (前200字):', systemMsg.content.substring(0, 200));
  146. }
  147. // 解析 provider
  148. const provider = resolveProviderKey(id);
  149. try {
  150. const llm = getLLM(id, maxTokens);
  151. const baseMessages = toBaseMessages(messages);
  152. const prompt = messages.map(m => `${m.role}: ${m.content}`).join('\\n');
  153. const response = await withAiLog(
  154. () => llm.invoke(baseMessages as any),
  155. { callType: 'llm_chat', provider, model: id, textLen: messages.reduce((s, m) => s + (m.content?.length || 0), 0), prompt }
  156. );
  157. const responseContent = cleanLlmResponse(response.content as string);
  158. console.log('[AI响应] 内容 (前1000字):', responseContent.substring(0, 1000));
  159. if (responseContent.length > 1000) {
  160. console.log('[AI响应] ... (总长度:', responseContent.length, '字)');
  161. }
  162. console.log('[AI响应] ===== 结束 =====');
  163. return responseContent;
  164. } catch (error: any) {
  165. // 不可切换的错误,直接抛出
  166. if (!config.models.shouldSwitchModel(error?.message || '')) {
  167. throw error;
  168. }
  169. // 可切换错误:先重试1次
  170. console.log(`[LLM] ${id} 消息调用失败,2s后重试...`);
  171. await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
  172. try {
  173. const llm = getLLM(id, maxTokens);
  174. const baseMessages = toBaseMessages(messages);
  175. const prompt = messages.map(m => `${m.role}: ${m.content}`).join('\\n');
  176. const response = await withAiLog(
  177. () => llm.invoke(baseMessages as any),
  178. { callType: 'llm_chat_retry', provider, model: id, textLen: messages.reduce((s, m) => s + (m.content?.length || 0), 0), prompt }
  179. );
  180. const responseContent = cleanLlmResponse(response.content as string);
  181. console.log('[AI响应] 重试成功 (前1000字):', responseContent.substring(0, 1000));
  182. console.log('[AI响应] ===== 结束 =====');
  183. return responseContent;
  184. } catch (retryError: any) {
  185. const nextModel = trySwitchModel(id, retryError);
  186. if (nextModel) {
  187. console.log(`[LLM] ${id} 重试仍失败,切换到 ${nextModel}`);
  188. return callLLMWithMessages(messages, nextModel, maxTokens);
  189. }
  190. throw retryError;
  191. }
  192. }
  193. }"""
  194. NEW_MSG = """export async function callLLMWithMessages(
  195. messages: ChatMessage[],
  196. modelId?: string,
  197. maxTokens?: number
  198. ): Promise<string> {
  199. const id = modelId || getDefaultModelId();
  200. const baseMessages = toBaseMessages(messages);
  201. const prompt = messages.map(m => `${m.role}: ${m.content}`).join('\\n');
  202. const textLen = messages.reduce((s, m) => s + (m.content?.length || 0), 0);
  203. const systemMsg = messages.find(m => m.role === 'system');
  204. return invokeWithRetry(
  205. async (useModelId) => {
  206. // 请求日志
  207. console.log('[AI请求] ===== 开始 =====');
  208. console.log('[AI请求] 模型:', useModelId, maxTokens !== undefined ? `(maxTokens=${maxTokens})` : '');
  209. console.log('[AI请求] 消息数:', messages.length);
  210. if (systemMsg) {
  211. console.log('[AI请求] System提示词 (前200字):', systemMsg.content.substring(0, 200));
  212. }
  213. const provider = resolveProviderKey(useModelId);
  214. const llm = getLLM(useModelId, maxTokens);
  215. const response = await withAiLog(
  216. () => llm.invoke(baseMessages as any),
  217. { callType: 'llm_chat', provider, model: useModelId, textLen, prompt }
  218. );
  219. const responseContent = cleanLlmResponse(response.content as string);
  220. console.log('[AI响应] 内容 (前1000字):', responseContent.substring(0, 1000));
  221. if (responseContent.length > 1000) {
  222. console.log('[AI响应] ... (总长度:', responseContent.length, '字)');
  223. }
  224. console.log('[AI响应] ===== 结束 =====');
  225. return responseContent;
  226. },
  227. id,
  228. );
  229. }"""
  230. assert OLD_MSG in src, "OLD callLLMWithMessages not found"
  231. src = src.replace(OLD_MSG, NEW_MSG)
  232. with open(PATH, 'w', encoding='utf-8') as f:
  233. f.write(src)
  234. print("OK: llm/index.ts patched")