feat(inference): LLM 客户端全异步化(T8 架构审查整改)
- Issue9: client.py 由同步 httpx.Client 全异步化 - LLMClient Protocol / HttpLLMClient.chat → async;httpx.AsyncClient + asyncio.sleep 退避 - __enter__/__exit__ → __aenter__/__aexit__(async with 生命周期闭环) - engine.py chat/chat_structured/_call 全部 async + await - FakeLLMClient.chat → async;测试用 anyio pytest 插件转换(engine 32 + client 10 用例) - 同步 inference-engine spec 与 milestone3 review 的 httpx 描述 - 全量 182 passed / 100.00%(987 stmts/252 br)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import Protocol, Sequence
|
||||
|
||||
import httpx
|
||||
@@ -16,9 +16,9 @@ from .types import ChatMessage, TokenUsage
|
||||
|
||||
|
||||
class LLMClient(Protocol):
|
||||
"""LLM 调用适配器(可注入替换为 Fake)。"""
|
||||
"""LLM 调用适配器(可注入替换为 Fake)。T8 起为 async 接口。"""
|
||||
|
||||
def chat(
|
||||
async def chat(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
@@ -29,7 +29,11 @@ class LLMClient(Protocol):
|
||||
|
||||
|
||||
class HttpLLMClient:
|
||||
"""OpenAI Chat Completions 兼容的 httpx 实现;支持重试(指数退避)。"""
|
||||
"""OpenAI Chat Completions 兼容的 httpx 异步实现;支持重试(指数退避)。
|
||||
|
||||
T8(架构审查整改):由同步 httpx.Client 全异步化——async def chat、
|
||||
httpx.AsyncClient、asyncio.sleep 退避、__aenter__/__aexit__ 生命周期闭环。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -46,16 +50,16 @@ class HttpLLMClient:
|
||||
self._api_key = api_key
|
||||
self._timeout_sec = timeout_sec
|
||||
self._retry_backoff = retry_backoff
|
||||
self._client = httpx.Client(timeout=timeout_sec, transport=transport)
|
||||
self._client = httpx.AsyncClient(timeout=timeout_sec, transport=transport)
|
||||
|
||||
def __enter__(self) -> HttpLLMClient:
|
||||
"""支持 with 块:退出时自动关闭底层连接。"""
|
||||
async def __aenter__(self) -> HttpLLMClient:
|
||||
"""支持 async with 块:退出时自动关闭底层连接。"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
self._client.close()
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
await self._client.aclose()
|
||||
|
||||
def chat(
|
||||
async def chat(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
@@ -79,9 +83,9 @@ class HttpLLMClient:
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(attempts):
|
||||
if attempt > 0:
|
||||
time.sleep(self._retry_backoff[attempt - 1])
|
||||
await asyncio.sleep(self._retry_backoff[attempt - 1])
|
||||
try:
|
||||
resp = self._client.post(url, json=payload, headers=headers)
|
||||
resp = await self._client.post(url, json=payload, headers=headers)
|
||||
except httpx.TimeoutException as exc:
|
||||
last_error = exc
|
||||
continue
|
||||
@@ -113,4 +117,4 @@ class HttpLLMClient:
|
||||
|
||||
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
|
||||
raise LLMNetworkError(f"LLM 调用失败(重试耗尽): {last_error}") from last_error
|
||||
|
||||
@@ -87,7 +87,7 @@ class InferenceEngine:
|
||||
return names
|
||||
return ["deepseek-chat"]
|
||||
|
||||
def _call(
|
||||
async def _call(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
@@ -100,7 +100,7 @@ class InferenceEngine:
|
||||
ChatMessage(role="system", content=self._system_instruction),
|
||||
ChatMessage(role="user", content=self._wrap_user_data(rendered)),
|
||||
]
|
||||
return self._client.chat(
|
||||
return await self._client.chat(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
@@ -109,7 +109,7 @@ class InferenceEngine:
|
||||
|
||||
# ---------- 公开 ----------
|
||||
|
||||
def chat(
|
||||
async def chat(
|
||||
self,
|
||||
*,
|
||||
session_id: str,
|
||||
@@ -129,7 +129,7 @@ class InferenceEngine:
|
||||
last_error_code: str | None = None
|
||||
for idx, name in enumerate(self._model_names(model)):
|
||||
try:
|
||||
text, usage = self._call(
|
||||
text, usage = await self._call(
|
||||
model=name, rendered=rendered,
|
||||
temperature=temperature, max_tokens=max_tokens,
|
||||
)
|
||||
@@ -150,7 +150,7 @@ class InferenceEngine:
|
||||
status="failed", error=last_error, error_code=last_error_code,
|
||||
)
|
||||
|
||||
def chat_structured(
|
||||
async def chat_structured(
|
||||
self,
|
||||
*,
|
||||
session_id: str,
|
||||
@@ -179,7 +179,7 @@ class InferenceEngine:
|
||||
attempts += 1
|
||||
for idx, name in enumerate(names):
|
||||
try:
|
||||
text, usage = self._call(
|
||||
text, usage = await self._call(
|
||||
model=name,
|
||||
rendered=base_rendered,
|
||||
temperature=0.0, max_tokens=4096,
|
||||
|
||||
Reference in New Issue
Block a user