# @line_count 200 """大模型API客户端模块""" import os import yaml import json import time from pathlib import Path from typing import Dict, Optional, List, Any import httpx class APIClient: """大模型API客户端,支持多个提供商""" def __init__(self, config_path: Optional[str] = None): """ 初始化API客户端 Args: config_path: API配置文件路径,默认为config/api_config.yaml """ if config_path is None: current_dir = Path(__file__).parent.parent config_path = current_dir / "config" / "api_config.yaml" self.config_path = Path(config_path) self.config: Dict[str, Any] = {} self.current_provider: str = "" self.load_config() def load_config(self): """加载API配置""" if not self.config_path.exists(): raise FileNotFoundError(f"API配置文件不存在: {self.config_path}") with open(self.config_path, 'r', encoding='utf-8') as f: self.config = yaml.safe_load(f) self.current_provider = self.config.get('default_provider', 'deepseek') def set_provider(self, provider: str): """ 设置当前使用的API提供商 Args: provider: 提供商名称 (deepseek, qianwen, openai, openrouter) """ if provider not in self.config.get('providers', {}): raise ValueError(f"不支持的API提供商: {provider}") self.current_provider = provider def get_provider_config(self) -> Dict[str, Any]: """获取当前提供商的配置""" providers = self.config.get('providers', {}) if self.current_provider not in providers: raise ValueError(f"提供商配置不存在: {self.current_provider}") return providers[self.current_provider] def _call_deepseek_api(self, prompt: str, **kwargs) -> str: """调用DeepSeek API""" config = self.get_provider_config() api_key = config.get('api_key') or os.getenv('DEEPSEEK_API_KEY', '') if not api_key: raise ValueError("DeepSeek API密钥未配置") headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}' } data = { 'model': config.get('model', 'deepseek-chat'), 'messages': [ {'role': 'user', 'content': prompt} ], 'temperature': config.get('temperature', 0.7), 'max_tokens': config.get('max_tokens', 4000) } # 使用精细化超时配置:连接10秒,读取180秒 timeout_config = httpx.Timeout(connect=10.0, read=180.0, write=10.0, pool=10.0) with httpx.Client(timeout=timeout_config) as client: response = client.post( config.get('base_url'), headers=headers, json=data ) response.raise_for_status() result = response.json() return result['choices'][0]['message']['content'] # def _call_qianwen_api(self, prompt: str, **kwargs) -> str: # """调用通义千问API""" # config = self.get_provider_config() # api_key = config.get('api_key') or os.getenv('QIANWEN_API_KEY', '') # if not api_key: # raise ValueError("通义千问API密钥未配置") # headers = { # 'Content-Type': 'application/json', # 'Authorization': f'Bearer {api_key}' # } # # 通义千问API格式(DashScope) # data = { # 'model': config.get('model', 'qwen-turbo'), # 'input': { # 'messages': [ # {'role': 'user', 'content': prompt} # ] # }, # 'parameters': { # 'temperature': config.get('temperature', 0.7), # 'max_tokens': config.get('max_tokens', 4000) # } # } # with httpx.Client(timeout=60.0) as client: # response = client.post( # config.get('base_url'), # headers=headers, # json=data # ) # response.raise_for_status() # result = response.json() # # 通义千问的响应格式适配 # if 'output' in result: # output = result['output'] # if 'choices' in output and len(output['choices']) > 0: # return output['choices'][0]['message']['content'] # elif 'text' in output: # return output['text'] # elif 'choices' in result and len(result['choices']) > 0: # return result['choices'][0]['message']['content'] # # 如果都不匹配,返回整个结果(用于调试) # raise ValueError(f"无法解析通义千问API响应: {result}") def _call_qianwen_api(self, prompt: str, **kwargs) -> str: """调用通义千问API(兼容模式)""" config = self.get_provider_config() api_key = config.get('api_key') or os.getenv('QIANWEN_API_KEY', '') if not api_key: raise ValueError("通义千问API密钥未配置") headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}' } # 系统提示,提高生成质量(模拟Web端行为) # 针对航空航天软件测试的专业化system message system_message = """你是一位拥有20年经验的航空航天软件测试专家。你的任务是生成高质量、可执行的测试用例。 【强制要求 - 必须遵守】 你必须为以下8种黑盒测试方法各生成至少1个测试用例: 1. 等价类划分 - 有效/无效输入类测试 2. 边界值分析 - min-1, min, min+1, max-1, max, max+1 3. 错误推测法 - 字节序错误、超长帧、非法字符 4. 因果图法 - 多条件联动触发测试 5. 决策表测试 - 条件组合规则覆盖 6. 状态转换法 - 状态机路径覆盖 7. 场景法 - 端到端业务流程测试 8. 随机测试 - 随机合法输入测试 【输出要求】 - 每个test_case的name字段必须用方括号标注测试方法,如:正常指令解析-[等价类划分] - 必须生成至少8个测试用例,确保8种方法全覆盖 - 测试步骤要具体,包含CAN帧ID、指令编码、响应时间等 - 只输出JSON,不要任何解释性文字 - 严格遵循用户指定的JSON格式""" # 使用OpenAI兼容格式,包含system message data = { 'model': config.get('model', 'qwen-max'), 'messages': [ {'role': 'system', 'content': system_message}, {'role': 'user', 'content': prompt} ], 'temperature': config.get('temperature', 0.3), 'max_tokens': config.get('max_tokens', 8192) } print("\n发送Prompt: " + prompt) # 使用精细化超时配置:连接10秒,读取180秒(3分钟) # 测试用例生成需要较长时间,特别是复杂的结构化输出 timeout_config = httpx.Timeout( connect=10.0, # 连接超时:10秒 read=180.0, # 读取超时:180秒(适合复杂生成任务) write=10.0, # 写入超时:10秒 pool=10.0 # 连接池超时:10秒 ) with httpx.Client(timeout=timeout_config) as client: response = client.post( config.get('base_url'), headers=headers, json=data ) response.raise_for_status() result = response.json() # OpenAI兼容格式的响应 if 'choices' in result and len(result['choices']) > 0: content = result['choices'][0]['message']['content'] # 调试输出:显示API返回的完整内容 print("\n" + "="*60) print("[API响应] 完整返回内容:") print("="*60) print(f"响应长度: {len(content)} 字符") print("-"*60) # 显示前2000字符 if len(content) > 2000: print(content[:2000]) print(f"\n... [截断,还有 {len(content)-2000} 字符]") else: print(content) print("="*60 + "\n") return content raise ValueError(f"无法解析通义千问API响应: {result}") def _call_openai_api(self, prompt: str, **kwargs) -> str: """调用OpenAI API""" config = self.get_provider_config() api_key = config.get('api_key') or os.getenv('OPENAI_API_KEY', '') if not api_key: raise ValueError("OpenAI API密钥未配置") headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}' } data = { 'model': config.get('model', 'gpt-3.5-turbo'), 'messages': [ {'role': 'user', 'content': prompt} ], 'temperature': config.get('temperature', 0.7), 'max_tokens': config.get('max_tokens', 4000) } # 使用精细化超时配置:连接10秒,读取180秒 timeout_config = httpx.Timeout(connect=10.0, read=180.0, write=10.0, pool=10.0) with httpx.Client(timeout=timeout_config) as client: response = client.post( config.get('base_url'), headers=headers, json=data ) response.raise_for_status() result = response.json() return result['choices'][0]['message']['content'] def _call_openrouter_api(self, prompt: str, **kwargs) -> str: """调用OpenRouter API""" config = self.get_provider_config() api_key = config.get('api_key') or os.getenv('OPENROUTER_API_KEY', '') if not api_key: raise ValueError("OpenRouter API密钥未配置") headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}' } # 添加可选的 HTTP-Referer 和 X-Title headers http_referer = config.get('http_referer', '') if http_referer: headers['HTTP-Referer'] = http_referer x_title = config.get('x_title', '') if x_title: headers['X-Title'] = x_title data = { 'model': config.get('model', 'allenai/molmo-2-8b:free'), 'messages': [ {'role': 'user', 'content': prompt} ], 'temperature': config.get('temperature', 0.7), 'max_tokens': config.get('max_tokens', 4000) } # 使用精细化超时配置:连接10秒,读取180秒 timeout_config = httpx.Timeout(connect=10.0, read=180.0, write=10.0, pool=10.0) with httpx.Client(timeout=timeout_config) as client: response = client.post( config.get('base_url'), headers=headers, json=data ) response.raise_for_status() result = response.json() # OpenRouter 使用 OpenAI 兼容格式 if 'choices' in result and len(result['choices']) > 0: return result['choices'][0]['message']['content'] raise ValueError(f"无法解析OpenRouter API响应: {result}") def call_api(self, prompt: str, max_retries: int = 3, retry_delay: int = 2) -> str: """ 调用大模型API Args: prompt: 提示词 max_retries: 最大重试次数 retry_delay: 重试延迟(秒) Returns: API返回的文本内容 """ provider_methods = { 'deepseek': self._call_deepseek_api, 'qianwen': self._call_qianwen_api, 'openai': self._call_openai_api, 'openrouter': self._call_openrouter_api } method = provider_methods.get(self.current_provider) if not method: raise ValueError(f"不支持的API提供商: {self.current_provider}") last_error = None for attempt in range(max_retries): try: return method(prompt) except Exception as e: last_error = e if attempt < max_retries - 1: time.sleep(retry_delay * (attempt + 1)) else: raise Exception(f"API调用失败(重试{max_retries}次): {str(e)}") from last_error raise Exception(f"API调用失败: {str(last_error)}") def update_api_key(self, provider: str, api_key: str): """ 更新API密钥 Args: provider: 提供商名称 api_key: API密钥 """ if provider not in self.config.get('providers', {}): raise ValueError(f"不支持的API提供商: {provider}") self.config['providers'][provider]['api_key'] = api_key # 保存到配置文件 with open(self.config_path, 'w', encoding='utf-8') as f: yaml.dump(self.config, f, allow_unicode=True, default_flow_style=False)