解决 ChatGPT api 经常调用超时的问题
2023-11-20 20:07 BruceBoron 阅读(5345) 评论(2) 收藏 举报前言
如题,解决 ChatGPT api 经常调用超时的问题。
场景重现
在调用 ChatGPT 的 api 并且使用流式输出时,经常会因为网络问题遇到超时的情况。
神奇的是,在本地调试的时候,即使遇到了超时的情况,过个10分钟(没错,就是10分钟,为什么是10分钟,后面会说),它会自动恢复。但是在服务器上,过个一会就会失败,报出超时异常(错误码502)。
后面仔细一想,本地能恢复,估计是自动重试(ChatGPT 的 api 没有重试功能,是我们的项目中加自动重试)了,只不过重试的时间有点久。服务器返回502,是因为内容从后台返回到前端时,需要经过网关层,在网关层有超时校验,它的时间比自动重试的时间(10分钟)短,所以撑不到重试,就会报超时异常。
用户诉求
-
不要展示出超时的错误信息给到用户。
-
减少超时后重试的时间间隔。
如何解决
-
1.彻底解决网络问题:
难,这是openai的服务器问题,即使部署在国外的服务器,也会出现超时的情况。
-
2.利用自动重试解决问题:
可行,调整超时时间,提升响应速度。
实施解决方案
笔者分了两步,由浅至深的去调整超时时间。如果只看最终方案,请直接移步解决方案二。
运行环境:
Python: 3.10.7
openai: 0.27.6
调用方法:
openai.api_resources.chat_completion.ChatCompletion.acreate(这是个异步调用 ChatGPT 的方法)
方法调用链路:
超时参数 ClientTimeout,一共有 4 个属性(total, connect, sock_read, sock_connect)
# 方法 -> 超时相关参数
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,我们能传递 connect 和 total 参数。
因此,在调用 openai.api_resources.chat_completion.ChatCompletion.acreate 时,设置 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
)
...
该方案生效了,但没有完全生效,它可以控制连接时间,也能控制请求的全部时间。但是,还是会出现超时异常,因为请求的连接时间和第一个字符的读取时间是两码事,它是基于total的时间进行重试的(300s),然而我们的网关时间并没有设置这么久(貌似只有120s)。
然后就有了方案二。
解决方案二
使用 monkey_patch 的方式重写 openai.api_requestor.APIRequestor.arequest_raw 方法,最主要是重写 request_timeout 参数,让其支持原生的 aiohttp.client.ClientTimeout 参数。
- 新建文件,api_requestor_mp.py,并写入以下代码。
# 注意 request_timeout 参数已经换了,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 的类型,按需设置 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
- 在初始化 ChatGPT api 的文件头部,补充以下代码。
from *.*.api_requestor_mp import monkey_patch
do_api_requestor = monkey_patch
设置参数 request_timeout=(10, 300, 15, 10) 后,再进行调试,没啥问题了。交付测试后,通过。
总结
-
直接看代码,看方法调用链路,有点难。但通过异常堆栈来找调用链路,挺方便的。
-
ChatGPT api 暴露的 request_timeout 参数不够用,需要重写,然后上网搜索了一下重写方案,了解到了 monkey_patch,不错,很好用。
-
改代码不难,难的是改哪里、怎么改、为什么。
浙公网安备 33010602011771号