fix(inference): chat 每次调用新建 httpx client,修复同步门禁 Event loop is closed

This commit is contained in:
lhl
2026-08-14 00:09:29 +08:00
parent 752fd2df6d
commit 4da5f7087d
2 changed files with 43 additions and 37 deletions
+41 -36
View File
@@ -50,6 +50,7 @@ class HttpLLMClient:
self._api_key = api_key
self._timeout_sec = timeout_sec
self._retry_backoff = retry_backoff
self._transport = transport
self._client = httpx.AsyncClient(timeout=timeout_sec, transport=transport)
async def __aenter__(self) -> HttpLLMClient:
@@ -79,42 +80,46 @@ class HttpLLMClient:
"Content-Type": "application/json",
}
attempts = 1 + len(self._retry_backoff)
last_error: Exception | None = None
for attempt in range(attempts):
if attempt > 0:
await asyncio.sleep(self._retry_backoff[attempt - 1])
try:
resp = await self._client.post(url, json=payload, headers=headers)
except httpx.TimeoutException as exc:
last_error = exc
continue
except httpx.HTTPError as exc:
last_error = exc
continue
# 每次调用新建 httpx.AsyncClient,绑定到当前事件循环。
# 兼容「同步门禁中多次 asyncio.run 驱动 async chat_structured」场景,
# 避免复用 __init__ 中创建的 client 因首个循环关闭而报 Event loop is closed。
async with httpx.AsyncClient(timeout=self._timeout_sec, transport=self._transport) as client:
attempts = 1 + len(self._retry_backoff)
last_error: Exception | None = None
for attempt in range(attempts):
if attempt > 0:
await asyncio.sleep(self._retry_backoff[attempt - 1])
try:
resp = await client.post(url, json=payload, headers=headers)
except httpx.TimeoutException as exc:
last_error = exc
continue
except httpx.HTTPError as exc:
last_error = exc
continue
if resp.status_code >= 500:
last_error = LLMNetworkError(f"LLM 5xx: {resp.status_code}")
continue
if resp.status_code >= 400:
raise LLMNetworkError(f"LLM HTTP {resp.status_code}: {resp.text[:200]}")
if not (200 <= resp.status_code < 300):
# 3xx 重定向不自动跟随,不得误判为成功
raise LLMNetworkError(f"LLM HTTP {resp.status_code}: {resp.text[:200]}")
if resp.status_code >= 500:
last_error = LLMNetworkError(f"LLM 5xx: {resp.status_code}")
continue
if resp.status_code >= 400:
raise LLMNetworkError(f"LLM HTTP {resp.status_code}: {resp.text[:200]}")
if not (200 <= resp.status_code < 300):
# 3xx 重定向不自动跟随,不得误判为成功
raise LLMNetworkError(f"LLM HTTP {resp.status_code}: {resp.text[:200]}")
try:
data = resp.json()
content = data["choices"][0]["message"]["content"]
except (json.JSONDecodeError, KeyError, IndexError, TypeError) as exc:
# 2xx 但响应结构损坏(非 JSON / 缺字段)→ 结构化错误,不重试
raise LLMResponseError(f"LLM 响应结构损坏: {exc}") from exc
usage_raw = data.get("usage", {})
usage = TokenUsage(
input_tokens=usage_raw.get("prompt_tokens", 0),
output_tokens=usage_raw.get("completion_tokens", 0),
)
return content, usage
try:
data = resp.json()
content = data["choices"][0]["message"]["content"]
except (json.JSONDecodeError, KeyError, IndexError, TypeError) as exc:
# 2xx 但响应结构损坏(非 JSON / 缺字段)→ 结构化错误,不重试
raise LLMResponseError(f"LLM 响应结构损坏: {exc}") from exc
usage_raw = data.get("usage", {})
usage = TokenUsage(
input_tokens=usage_raw.get("prompt_tokens", 0),
output_tokens=usage_raw.get("completion_tokens", 0),
)
return content, usage
if isinstance(last_error, httpx.TimeoutException):
raise LLMTimeoutError(f"LLM 超时({self._timeout_sec}s") from last_error
raise LLMNetworkError(f"LLM 调用失败(重试耗尽): {last_error}") from last_error
if isinstance(last_error, httpx.TimeoutException):
raise LLMTimeoutError(f"LLM 超时({self._timeout_sec}s") from last_error
raise LLMNetworkError(f"LLM 调用失败(重试耗尽): {last_error}") from last_error