|
7 | 7 | 错误:data: {"error": "..."}\n\n |
8 | 8 |
|
9 | 9 | @ai-context: 流式输出路由(SSE):以 Server-Sent Events 流式返回全量 AI 功能结果。 |
| 10 | +@ai-context: P2-13 超时保护——event_generator 以手动 __anext__ + asyncio.wait_for |
| 11 | + enforce 首 token 超时与 chunk 间空闲超时,避免 provider 挂起导致流无限等待; |
| 12 | + 超时后下发 error 事件并结束,客户端据此降级非流式。 |
10 | 13 | """ |
11 | 14 |
|
| 15 | +import asyncio |
12 | 16 | import json |
13 | 17 | import logging |
14 | 18 | import time |
|
24 | 28 | logger = logging.getLogger(__name__) |
25 | 29 | router = APIRouter(prefix="/api/v1/ai", tags=["流式输出"]) |
26 | 30 |
|
| 31 | +# ============================================================ |
| 32 | +# P2-13 流式超时保护(秒) |
| 33 | +# ============================================================ |
| 34 | +# 首 token 超时:从开始到第一个 chunk 的最长等待(含模型冷启动/排队) |
| 35 | +_FIRST_TOKEN_TIMEOUT = 30.0 |
| 36 | +# chunk 间空闲超时:相邻 chunk 的最长间隔(防中途挂起) |
| 37 | +_CHUNK_IDLE_TIMEOUT = 30.0 |
| 38 | + |
27 | 39 | # ============================================================ |
28 | 40 | # Prompt 模板目录 |
29 | 41 | # ============================================================ |
@@ -284,10 +296,26 @@ def _stream_fn(provider, model_name): |
284 | 296 | raise HTTPException(status_code=503, detail="所有 AI 服务暂时不可用,请稍后重试") |
285 | 297 |
|
286 | 298 | async def event_generator() -> AsyncGenerator[str, None]: |
287 | | - """将 provider 的 async generator 包装为 SSE 事件流""" |
| 299 | + """将 provider 的 async generator 包装为 SSE 事件流(P2-13:带首 token/空闲超时)""" |
288 | 300 | start_time = time.monotonic() |
| 301 | + agen = gen.__aiter__() |
| 302 | + is_first = True |
289 | 303 | try: |
290 | | - async for chunk_text in gen: |
| 304 | + while True: |
| 305 | + timeout = _FIRST_TOKEN_TIMEOUT if is_first else _CHUNK_IDLE_TIMEOUT |
| 306 | + try: |
| 307 | + chunk_text = await asyncio.wait_for(agen.__anext__(), timeout=timeout) |
| 308 | + except StopAsyncIteration: |
| 309 | + break |
| 310 | + except asyncio.TimeoutError: |
| 311 | + phase = "首 token" if is_first else "chunk 间隔" |
| 312 | + logger.error( |
| 313 | + "流式超时: feature=%s, provider=%s, %s 超时=%.1fs", |
| 314 | + feature, used_provider, phase, timeout, |
| 315 | + ) |
| 316 | + yield _sse_error(f"AI 响应超时({phase} {timeout:.0f}s)") |
| 317 | + break |
| 318 | + is_first = False |
291 | 319 | if chunk_text: |
292 | 320 | yield _sse_chunk(chunk_text) |
293 | 321 | yield _sse_done() |
|
0 commit comments