const axios = require('axios'); const { llmConfig, getHeaders } = require('../config/llm'); class LLMApiClient { constructor() { this.baseURL = llmConfig.baseURL; this.model = llmConfig.model; this.timeout = llmConfig.timeout; this.maxRetries = llmConfig.maxRetries; this.retryDelay = llmConfig.retryDelay; // 创建axios实例 this.client = axios.create({ baseURL: this.baseURL, timeout: this.timeout, headers: getHeaders() }); // 配置请求拦截器 this.client.interceptors.request.use( (config) => { console.log(`LLM API请求: ${config.method?.toUpperCase()} ${config.url}`); return config; }, (error) => { console.error('LLM API请求拦截器错误:', error); return Promise.reject(error); } ); // 配置响应拦截器 this.client.interceptors.response.use( (response) => { console.log(`LLM API响应: ${response.status} ${response.config.url}`); return response; }, (error) => { console.error('LLM API响应错误:', error.response?.data || error.message); return Promise.reject(error); } ); } /** * 调用聊天完成API */ async createChatCompletion(messages, options = {}) { try { const requestBody = { model: options.model || this.model, messages: this.formatMessages(messages), ...llmConfig.defaultOptions, ...options }; // 移除undefined值 Object.keys(requestBody).forEach(key => { if (requestBody[key] === undefined) { delete requestBody[key]; } }); console.log('发送LLM请求:', JSON.stringify(requestBody, null, 2)); const response = await this.makeRequestWithRetry('/chat/completions', requestBody); // 返回标准格式的响应 return this.formatResponse(response.data); } catch (error) { console.error('LLM API调用失败:', error); throw this.handleError(error); } } /** * 创建流式聊天完成 */ async createStreamChatCompletion(messages, options = {}) { try { const requestBody = { model: options.model || this.model, messages: this.formatMessages(messages), stream: true, ...llmConfig.defaultOptions, ...options }; // 移除undefined值 Object.keys(requestBody).forEach(key => { if (requestBody[key] === undefined) { delete requestBody[key]; } }); const response = await this.client.post('/chat/completions', requestBody, { responseType: 'stream' }); return response.data; } catch (error) { console.error('流式LLM API调用失败:', error); throw this.handleError(error); } } /** * 带重试的请求方法 */ async makeRequestWithRetry(endpoint, data, retryCount = 0) { try { const response = await this.client.post(endpoint, data); return response; } catch (error) { // 检查是否应该重试 if (this.shouldRetry(error, retryCount)) { console.log(`请求失败,${this.retryDelay}ms后进行第${retryCount + 1}次重试...`); await this.delay(this.retryDelay); return this.makeRequestWithRetry(endpoint, data, retryCount + 1); } throw error; } } /** * 判断是否应该重试 */ shouldRetry(error, retryCount) { // 超过最大重试次数 if (retryCount >= this.maxRetries) { return false; } // 网络错误或服务器错误可以重试 if (error.code === 'ECONNABORTED' || error.code === 'ENOTFOUND') { return true; } // HTTP状态码500+可以重试 if (error.response && error.response.status >= 500) { return true; } // 429 Too Many Requests 可以重试 if (error.response && error.response.status === 429) { return true; } return false; } /** * 延迟函数 */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 格式化消息数组 */ formatMessages(messages) { // 确保有系统消息 const formattedMessages = [...messages]; // 检查是否已有系统消息 const hasSystemMessage = formattedMessages.some(msg => msg.role === 'system'); if (!hasSystemMessage && llmConfig.systemPrompt) { formattedMessages.unshift({ role: 'system', content: llmConfig.systemPrompt }); } return formattedMessages; } /** * 格式化API响应 */ formatResponse(data) { if (!data || !data.choices || data.choices.length === 0) { throw new Error('LLM API返回无效响应'); } const choice = data.choices[0]; const message = choice.message; return { id: data.id, model: data.model, content: message.content, role: message.role, finishReason: choice.finish_reason, usage: data.usage, created: data.created }; } /** * 处理API错误 */ handleError(error) { if (error.response) { // API返回错误响应 const status = error.response.status; const data = error.response.data; switch (status) { case 400: return new Error(`请求参数错误: ${data.error?.message || '无效请求'}`); case 401: return new Error('API密钥无效或未提供'); case 403: return new Error('API访问被拒绝,请检查权限'); case 404: return new Error('API端点不存在'); case 429: return new Error('请求频率过高,请稍后重试'); case 500: return new Error('LLM服务内部错误'); case 503: return new Error('LLM服务暂时不可用'); default: return new Error(`LLM API错误 (${status}): ${data.error?.message || '未知错误'}`); } } else if (error.request) { // 网络错误 if (error.code === 'ECONNABORTED') { return new Error('请求超时,请检查网络连接'); } else if (error.code === 'ENOTFOUND') { return new Error('无法连接到LLM服务,请检查网络和API地址'); } else { return new Error(`网络错误: ${error.message}`); } } else { // 其他错误 return new Error(`未知错误: ${error.message}`); } } /** * 检查API连接状态 */ async checkConnection() { try { const testMessages = [ { role: 'user', content: 'Hello' } ]; await this.createChatCompletion(testMessages, { max_tokens: 10, temperature: 0 }); return { status: 'connected', message: 'LLM API连接正常' }; } catch (error) { return { status: 'error', message: error.message, error: error }; } } /** * 获取可用模型列表(如果API支持) */ async getModels() { try { const response = await this.client.get('/models'); return response.data; } catch (error) { console.warn('获取模型列表失败:', error.message); return null; } } /** * 计算tokens估算(简单实现) */ estimateTokens(text) { // 简单估算:英文按空格分割,中文按字符数 const englishWords = (text.match(/[a-zA-Z]+/g) || []).length; const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length; const otherChars = text.replace(/[a-zA-Z\u4e00-\u9fff\s]/g, '').length; return englishWords + chineseChars + Math.ceil(otherChars / 2); } /** * 截断消息以适应token限制 */ truncateMessages(messages, maxTokens = 4000) { let totalTokens = 0; const truncatedMessages = []; // 从最后一条消息开始,向前累加 for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; const messageTokens = this.estimateTokens(message.content); if (totalTokens + messageTokens > maxTokens && truncatedMessages.length > 0) { break; } truncatedMessages.unshift(message); totalTokens += messageTokens; } return truncatedMessages; } } module.exports = LLMApiClient;