問題重現(xiàn)
在調(diào)用 ChatGPT API 并使用流式輸出時,我們經(jīng)常會遇到網(wǎng)絡(luò)問題導(dǎo)致的超時情況。有趣的是,筆者發(fā)現(xiàn)在本地調(diào)試遇到的超時,會在 10 分鐘后自動恢復(fù)(為什么是 10 分鐘?我們留到后面解釋),但是在服務(wù)器上等待一會兒卻會失敗,報出超時異常(錯誤代碼 502)。
筆者認為,本地能恢復(fù)的原因可能是自動重試,只是重試的時間有點久(ChatGPT API 沒有重試功能,這是項目加入的)。服務(wù)器返回「502」是因為內(nèi)容從后臺返回到前端需要經(jīng)過網(wǎng)關(guān)層,而網(wǎng)關(guān)層超時校驗的時間比自動重試的時間(10 分鐘)更短,所以撐不到重試就會報超時異常。
基于以上場景,本文著手解決 ChatGPT API 調(diào)用超時問題。
優(yōu)化訴求
不向用戶展示超時的報錯信息。
縮短超時后重試的時間間隔。
解決思路
筆者考慮了兩種方案。
一是徹底解決網(wǎng)絡(luò)問題,但難度有點大。這屬于 OpenAI 服務(wù)器問題,即使是部署在國外的服務(wù)器也會出現(xiàn)超時的情況。
二是利用自動重試解決問題。通過調(diào)整超時的時間,提升響應(yīng)速度,方案可行。
實施解決方案
解決過程中,筆者分兩步由淺至深地調(diào)整了超時時間;如果想直接了解最終方案,請移步「解決方案二」~
運行環(huán)境:
Python: 3.10.7
openai: 0.27.6
調(diào)用方法:?
openai.api_resources.chat_completion.ChatCompletion.acreate
( 這是異步調(diào)用 ChatGPT 的方法。)
方法調(diào)用鏈路:
超時參數(shù) ClientTimeout,一共有 4 個屬性 total、connect、sock_read 和 sock_connect。
#?方法?->?超時相關(guān)參數(shù) openai.api_resources.chat_completion.ChatCompletion.acreate?->?kwargs openai.api_resources.abstract.engine_api_resource.EngineAPIResource.acreate?->?params openai.api_requestor.APIRequestor.arequest?->?request_timeout #?request_timeout?在這一步變成了?timeout,因此,只需要傳參?request_timeout?即可 openai.api_requestor.APIRequestor.arequest_raw?->?request_timeout aiohttp.client.ClientSession.request?->?kwargs aiohttp.client.ClientSession._request?->?timeout ????tm?=?TimeoutHandle(self._loop,?real_timeout.total)?->?ClientTimeout.total ????async?with?ceil_timeout(real_timeout.connect):?->?ClientTimeout.connect #?子分支1 aiohttp.connector.BaseConnector.connect?->?timeout aiohttp.connector.TCPConnector._create_connection?->?timeout aiohttp.connector.TCPConnector._create_direct_connection?->?timeout aiohttp.connector.TCPConnector._wrap_create_connection?->?timeout ????async?with?ceil_timeout(timeout.sock_connect):?->?ClientTimeout.sock_connect #?子分支2 aiohttp.client_reqrep.ClientRequest.send?->?timeout aiohttp.client_proto.ResponseHandler.set_response_params?->?read_timeout aiohttp.client_proto.ResponseHandler._reschedule_timeout?->?self._read_timeout ????if?timeout: ????self._read_timeout_handle?=?self._loop.call_later( ????????timeout,?self._on_read_timeout ????)?->?ClientTimeout.sock_read
解決方案一
openai.api_requestor.APIRequestor.arequest_raw?方法中的?request_timeout?參數(shù)可以傳遞 connect 和 total?參數(shù),因此可以在調(diào)用openai.api_resources.chat_completion.ChatCompletion.acreate時,設(shè)置 request_time(10, 300)。
# async?def?arequest_raw( ????self, ????method, ????url, ????session, ????*, ????params=None, ????supplied_headers:?Optional[Dict[str,?str]]?=?None, ????files=None, ????request_id:?Optional[str]?=?None, ????request_timeout:?Optional[Union[float,?Tuple[float,?float]]]?=?None, )?->?aiohttp.ClientResponse: ????abs_url,?headers,?data?=?self._prepare_request_raw( ????????url,?supplied_headers,?method,?params,?files,?request_id ????) ???? ????if?isinstance(request_timeout,?tuple): ????????timeout?=?aiohttp.ClientTimeout( ????????????connect=request_timeout[0], ????????????total=request_timeout[1], ????????)else: ????????????timeout?=?aiohttp.ClientTimeout( ????????????????total=request_timeout?if?request_timeout?else?TIMEOUT_SECS ????????????) ????...
該方案有效,但沒有完全生效:它可以控制連接時間和請求的全部時間,但沒有徹底解決超時異常,因為「請求連接時間」和「第一個字符讀取時間」是兩碼事?!刚埱筮B接時間」基于 total 時間重試(300s),而網(wǎng)關(guān)時間并沒有設(shè)置這么久。
于是,筆者繼續(xù)提出「解決方案二」。
解決方案二
使用 monkey_patch?方式重寫openai.api_requestor.APIRequestor.arequest_raw?方法,重點在于重寫 request_timeout 參數(shù),讓其支持原生的 aiohttp.client.ClientTimeout 參數(shù)。
1. 新建 api_requestor_mp.py 文件,并寫入以下代碼。
#?注意?request_timeout?參數(shù)已經(jīng)換了,Optional[Union[float,?Tuple[float,?float]]]?->?Optional[Union[float,?tuple]] async?def?arequest_raw( ????????self, ????????method, ????????url, ????????session, ????????*, ????????params=None, ????????supplied_headers:?Optional[Dict[str,?str]]?=?None, ????????files=None, ????????request_id:?Optional[str]?=?None, ????????request_timeout:?Optional[Union[float,?tuple]]?=?None, )?->?aiohttp.ClientResponse: ????abs_url,?headers,?data?=?self._prepare_request_raw( ????????url,?supplied_headers,?method,?params,?files,?request_id ????) ????#?判斷?request_timeout?的類型,按需設(shè)置?sock_read?和?sock_connect?屬性 ????if?isinstance(request_timeout,?tuple): ????????timeout?=?aiohttp.ClientTimeout( ????????????connect=request_timeout[0], ????????????total=request_timeout[1], ????????????sock_read=None?if?len(request_timeout)?3?else?request_timeout[2], ????????????sock_connect=None?if?len(request_timeout)?4?else?request_timeout[3], ????????) ????else: ????????timeout?=?aiohttp.ClientTimeout( ????????????total=request_timeout?if?request_timeout?else?TIMEOUT_SECS ????????) ????if?files: ????????#?TODO:?Use?aiohttp.MultipartWriter?to?create?the?multipart?form?data?here. ????????#?For?now?we?use?the?private?requests?method?that?is?known?to?have?worked?so?far. ????????data,?content_type?=?requests.models.RequestEncodingMixin._encode_files(??#?type:?ignore ????????????files,?data ????????) ????????headers["Content-Type"]?=?content_type ????request_kwargs?=?{ ????????"method":?method, ????????"url":?abs_url, ????????"headers":?headers, ????????"data":?data, ????????"proxy":?_aiohttp_proxies_arg(openai.proxy), ????????"timeout":?timeout, ????} ????try: ????????result?=?await?session.request(**request_kwargs) ????????util.log_info( ????????????"OpenAI?API?response", ????????????path=abs_url, ????????????response_code=result.status, ????????????processing_ms=result.headers.get("OpenAI-Processing-Ms"), ????????????request_id=result.headers.get("X-Request-Id"), ????????) ????????#?Don't?read?the?whole?stream?for?debug?logging?unless?necessary. ????????if?openai.log?==?"debug": ????????????util.log_debug( ????????????????"API?response?body",?body=result.content,?headers=result.headers ????????????) ????????????return?result ????????except?(aiohttp.ServerTimeoutError,?asyncio.TimeoutError)?as?e: ????????????raise?error.Timeout("Request?timed?out")?from?e ????????except?aiohttp.ClientError?as?e: ????????????raise?error.APIConnectionError("Error?communicating?with?OpenAI")?from?e def?monkey_patch(): ????APIRequestor.arequest_raw?=?arequest_raw
2. 在初始化 ChatGPT API 的文件頭部補充:
from?*.*.api_requestor_mp?import?monkey_patch do_api_requestor?=?monkey_patch
設(shè)置參數(shù) request_timeout=(10, 300, 15, 10)?后,再調(diào)試就沒什么問題了。
交付測試,通過。
經(jīng)驗總結(jié)
直接看代碼、看方法調(diào)用鏈路會有點困難,可以通過異常堆棧來找調(diào)用鏈路,這樣更方便。
ChatGPT API 暴露的?request_timeout?參數(shù)不夠用,需要重寫;搜索了一下重寫方案,了解到 monkey_patch,非常實用。
項目過程中,筆者發(fā)現(xiàn)改代碼本身不難,難的是知道「改哪里」「怎么改」以及「為什么」。
審核編輯:黃飛
?
評論
查看更多