代码改变世界

解决 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,不错,很好用。

  • 改代码不难,难的是改哪里、怎么改、为什么。