OpenAIService.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Http;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Http\Client\ConnectionException;
  6. use Illuminate\Http\Client\RequestException;
  7. class OpenAIService
  8. {
  9. protected int $retries = 3;
  10. protected int $delayMs = 2000;
  11. protected string $model = 'gpt-4-1106-preview';
  12. protected string $apiUrl = 'https://api.openai.com/v1/chat/completions';
  13. protected string $apiKey;
  14. protected string $systemPrompt = '你是一个有帮助的助手。';
  15. protected float $temperature = 0.7;
  16. protected bool $stream = false;
  17. protected int $timeout = 600;
  18. protected int $maxTokens = 0;
  19. public static function withRetry(int $retries = 3, int $delayMs = 2000): static
  20. {
  21. return (new static())->setRetry($retries, $delayMs);
  22. }
  23. public function setRetry(int $retries, int $delayMs): static
  24. {
  25. $this->retries = $retries;
  26. $this->delayMs = $delayMs;
  27. return $this;
  28. }
  29. public function setModel(string $model): static
  30. {
  31. $this->model = $model;
  32. return $this;
  33. }
  34. public function setApiUrl(string $url): static
  35. {
  36. $this->apiUrl = $url;
  37. return $this;
  38. }
  39. public function setApiKey(string $key): static
  40. {
  41. $this->apiKey = $key;
  42. return $this;
  43. }
  44. public function setSystemPrompt(string $prompt): static
  45. {
  46. $this->systemPrompt = $prompt;
  47. return $this;
  48. }
  49. public function setTemperature(float $temperature): static
  50. {
  51. $this->temperature = $temperature;
  52. return $this;
  53. }
  54. public function setStream(bool $stream): static
  55. {
  56. $this->stream = $stream;
  57. // 流式时需要无限超时
  58. if ($stream) {
  59. $this->timeout = 0;
  60. }
  61. return $this;
  62. }
  63. public function setMaxToken(int $maxTokens): static
  64. {
  65. $this->maxTokens = $maxTokens;
  66. return $this;
  67. }
  68. /**
  69. * 发送 GPT 请求(支持流式与非流式)
  70. */
  71. public function send(string $question): string|array
  72. {
  73. $lastException = null;
  74. for ($attempt = 1; $attempt <= $this->retries; $attempt++) {
  75. try {
  76. if ($this->stream === false) {
  77. return $this->sendNormal($question);
  78. }
  79. return $this->sendStreaming($question);
  80. } catch (RateLimitException $e) {
  81. // 429 速率限制,等待后重试
  82. $retryAfter = $e->getRetryAfter();
  83. Log::warning("请求被限流(429),等待 {$retryAfter} 秒后重试...(第 {$attempt} 次)");
  84. sleep($retryAfter);
  85. $lastException = $e;
  86. continue;
  87. } catch (ServerErrorException $e) {
  88. // 5xx 服务器错误,使用指数退避重试
  89. Log::warning("服务器错误({$e->getStatusCode()}):{$e->getMessage()},准备重试...(第 {$attempt} 次)");
  90. if ($attempt < $this->retries) {
  91. usleep($this->delayMs * 1000 * pow(2, $attempt - 1));
  92. }
  93. $lastException = $e;
  94. continue;
  95. } catch (ConnectionException $e) {
  96. // 网络连接错误,使用指数退避重试
  97. Log::warning("第 {$attempt} 次连接超时:{$e->getMessage()},准备重试...");
  98. if ($attempt < $this->retries) {
  99. usleep($this->delayMs * 1000 * pow(2, $attempt - 1));
  100. }
  101. $lastException = $e;
  102. continue;
  103. } catch (NetworkException $e) {
  104. // 其他网络错误,使用指数退避重试
  105. Log::warning("网络错误:{$e->getMessage()},准备重试...(第 {$attempt} 次)");
  106. if ($attempt < $this->retries) {
  107. usleep($this->delayMs * 1000 * pow(2, $attempt - 1));
  108. }
  109. $lastException = $e;
  110. continue;
  111. } catch (ClientErrorException $e) {
  112. // 4xx 客户端错误(除429外)不重试,直接抛出
  113. Log::error("客户端错误({$e->getStatusCode()}):{$e->getMessage()}");
  114. throw $e;
  115. } catch (\Exception $e) {
  116. // 其他未知异常,不重试,直接抛出
  117. Log::error("GPT 请求异常:" . $e->getMessage());
  118. throw $e;
  119. }
  120. }
  121. // 所有重试都失败了
  122. Log::error("请求多次失败,已重试 {$this->retries} 次");
  123. throw new \RuntimeException(
  124. '请求多次失败或超时,请稍后再试。原因: ' . ($lastException ? $lastException->getMessage() : '未知'),
  125. 504,
  126. $lastException
  127. );
  128. }
  129. /**
  130. * 普通非流式请求
  131. */
  132. protected function sendNormal(string $question): array
  133. {
  134. $data = [
  135. 'model' => $this->model,
  136. 'messages' => [
  137. ['role' => 'system', 'content' => $this->systemPrompt],
  138. ['role' => 'user', 'content' => $question],
  139. ],
  140. 'temperature' => $this->temperature,
  141. 'stream' => false,
  142. ];
  143. if ($this->maxTokens > 0) {
  144. $data['max_tokens'] = $this->maxTokens;
  145. }
  146. $response = Http::withToken($this->apiKey)
  147. ->timeout($this->timeout)
  148. ->post($this->apiUrl, $data);
  149. $status = $response->status();
  150. $body = $response->json();
  151. // 处理 429 速率限制
  152. if ($status === 429) {
  153. $retryAfter = (int)($response->header('Retry-After') ?? 20);
  154. throw new RateLimitException(
  155. $body['error']['message'] ?? '请求被限流',
  156. $status,
  157. $retryAfter
  158. );
  159. }
  160. // 处理 5xx 服务器错误
  161. if ($status >= 500 && $status < 600) {
  162. throw new ServerErrorException(
  163. $body['error']['message'] ?? '服务器错误',
  164. $status
  165. );
  166. }
  167. // 处理 4xx 客户端错误
  168. if ($status >= 400 && $status < 500) {
  169. throw new ClientErrorException(
  170. $body['error']['message'] ?? '客户端请求错误',
  171. $status
  172. );
  173. }
  174. // 处理成功响应
  175. if ($response->successful()) {
  176. return $body;
  177. }
  178. // 其他未知错误
  179. throw new \RuntimeException(
  180. $body['error']['message'] ?? '请求失败',
  181. $status
  182. );
  183. }
  184. /**
  185. * 流式请求(使用原生 CURL SSE)
  186. */
  187. protected function sendStreaming(string $question): string
  188. {
  189. $payload = [
  190. 'model' => $this->model,
  191. 'messages' => [
  192. ['role' => 'system', 'content' => $this->systemPrompt],
  193. ['role' => 'user', 'content' => $question],
  194. ],
  195. 'temperature' => $this->temperature,
  196. 'stream' => true,
  197. ];
  198. if ($this->maxTokens > 0) {
  199. $payload['max_tokens'] = $this->maxTokens;
  200. }
  201. $ch = curl_init($this->apiUrl);
  202. $fullContent = '';
  203. $httpCode = 0;
  204. $errorMessage = '';
  205. curl_setopt_array($ch, [
  206. CURLOPT_POST => true,
  207. CURLOPT_HTTPHEADER => [
  208. "Authorization: Bearer {$this->apiKey}",
  209. "Content-Type: application/json",
  210. "Accept: text/event-stream",
  211. ],
  212. CURLOPT_POSTFIELDS => json_encode($payload),
  213. CURLOPT_RETURNTRANSFER => false,
  214. CURLOPT_TIMEOUT => 0,
  215. CURLOPT_HEADER => false,
  216. CURLOPT_FOLLOWLOCATION => true,
  217. CURLOPT_WRITEFUNCTION => function (\CurlHandle $curl, string $data) use (&$fullContent, &$errorMessage): int {
  218. $lines = explode("\n", $data);
  219. foreach ($lines as $line) {
  220. $line = trim($line);
  221. if (!str_starts_with($line, 'data: ')) {
  222. continue;
  223. }
  224. $json = substr($line, 6);
  225. if ($json === '[DONE]') {
  226. continue;
  227. }
  228. $obj = json_decode($json, true);
  229. if (!is_array($obj)) {
  230. continue;
  231. }
  232. // 检查是否有错误
  233. if (isset($obj['error'])) {
  234. $errorMessage = $obj['error']['message'] ?? 'Stream error';
  235. return 0; // 停止接收
  236. }
  237. $delta = $obj['choices'][0]['delta']['content'] ?? '';
  238. if ($delta !== '') {
  239. $fullContent .= $delta;
  240. }
  241. }
  242. return strlen($data);
  243. },
  244. ]);
  245. curl_exec($ch);
  246. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  247. if ($curlError = curl_error($ch)) {
  248. curl_close($ch);
  249. throw new NetworkException("CURL 错误: {$curlError}");
  250. }
  251. curl_close($ch);
  252. // 检查流式响应中的错误
  253. if ($errorMessage) {
  254. if ($httpCode === 429) {
  255. throw new RateLimitException($errorMessage, $httpCode);
  256. } elseif ($httpCode >= 500) {
  257. throw new ServerErrorException($errorMessage, $httpCode);
  258. } elseif ($httpCode >= 400) {
  259. throw new ClientErrorException($errorMessage, $httpCode);
  260. } else {
  261. throw new \RuntimeException($errorMessage, $httpCode);
  262. }
  263. }
  264. // 检查 HTTP 状态码
  265. if ($httpCode === 429) {
  266. throw new RateLimitException('请求被限流', $httpCode);
  267. } elseif ($httpCode >= 500) {
  268. throw new ServerErrorException('服务器错误', $httpCode);
  269. } elseif ($httpCode >= 400) {
  270. throw new ClientErrorException('客户端请求错误', $httpCode);
  271. } elseif ($httpCode < 200 || $httpCode >= 300) {
  272. throw new \RuntimeException("HTTP 错误: {$httpCode}");
  273. }
  274. return $fullContent;
  275. }
  276. }
  277. /**
  278. * 速率限制异常(429)
  279. */
  280. class RateLimitException extends \RuntimeException
  281. {
  282. protected int $retryAfter;
  283. public function __construct(string $message, int $code = 429, int $retryAfter = 20)
  284. {
  285. parent::__construct($message, $code);
  286. $this->retryAfter = $retryAfter;
  287. }
  288. public function getRetryAfter(): int
  289. {
  290. return $this->retryAfter;
  291. }
  292. public function getStatusCode(): int
  293. {
  294. return $this->code;
  295. }
  296. }
  297. /**
  298. * 服务器错误异常(5xx)
  299. */
  300. class ServerErrorException extends \RuntimeException
  301. {
  302. public function __construct(string $message, int $code = 500)
  303. {
  304. parent::__construct($message, $code);
  305. }
  306. public function getStatusCode(): int
  307. {
  308. return $this->code;
  309. }
  310. }
  311. /**
  312. * 客户端错误异常(4xx,除429外)
  313. */
  314. class ClientErrorException extends \RuntimeException
  315. {
  316. public function __construct(string $message, int $code = 400)
  317. {
  318. parent::__construct($message, $code);
  319. }
  320. public function getStatusCode(): int
  321. {
  322. return $this->code;
  323. }
  324. }
  325. /**
  326. * 网络错误异常
  327. */
  328. class NetworkException extends \RuntimeException
  329. {
  330. public function __construct(string $message, int $code = 0, ?\Throwable $previous = null)
  331. {
  332. parent::__construct($message, $code, $previous);
  333. }
  334. }