From 680afb7f9dc3429e273caab22e611e2aabf80931 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 16 Sep 2026 17:58:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(timeout):=20=E7=BB=9F=E4=B8=80=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E9=A2=84=E7=AE=97=E5=8D=95=E7=82=B9=E6=B2=BB=E7=90=86?= =?UTF-8?q?(2026-09-16=E7=94=A8=E6=88=B7=E8=A3=81=E5=AE=9A:=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E9=80=BB=E8=BE=91=E7=A6=81=E6=95=A3=E8=90=BD=E5=90=84?= =?UTF-8?q?=E4=BA=A7=E7=BA=BF=E5=90=84=E8=A7=92=E8=89=B2)=E2=80=94?= =?UTF-8?q?=E2=80=94payload.=5Ftimeout=E8=AF=AD=E4=B9=89=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E4=B8=BA=E4=B8=80=E6=AC=A1=E8=B0=83=E7=94=A8=E6=80=BB=E9=A2=84?= =?UTF-8?q?=E7=AE=97=E8=90=BDctx.budget,=5Fpost=5Fupstream=E6=8C=89deadlin?= =?UTF-8?q?e=E7=BA=A6=E6=9D=9F=E9=87=8D=E8=AF=95(=E6=80=BB=E6=97=B6?= =?UTF-8?q?=E9=95=BF=E2=89=88=E9=A2=84=E7=AE=97,=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=3D=E9=A2=84=E7=AE=97+60=E6=81=92=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E4=B8=8A=E6=B8=B8,=E6=B6=88=E7=81=AD3x=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E6=94=BE=E5=A4=A7=E2=86=92=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=85=88?= =?UTF-8?q?=E6=96=AD=E8=BF=9E=E2=86=92=E6=A8=A1=E7=B3=8ATimeoutError?= =?UTF-8?q?=E2=86=92=E5=88=86=E7=B1=BB=E5=99=A8=E8=AF=AF=E5=88=A4=E6=B0=B8?= =?UTF-8?q?=E4=B9=85=E9=94=99=E8=AF=AF=E7=9A=84=E4=BA=8B=E6=95=85=E9=93=BE?= =?UTF-8?q?);attempt=E8=B6=85=E6=97=B6=3D=E5=89=A9=E4=BD=99=E9=A2=84?= =?UTF-8?q?=E7=AE=97(=E5=90=88=E6=B3=95=E9=95=BF=E7=94=9F=E6=88=90?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E7=94=A8=E6=BB=A1,373s=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?=E6=A0=B7=E6=9C=AC),=E7=AB=AF=E7=82=B9=E9=85=8D=E7=BD=AEtimeout?= =?UTF-8?q?(=E7=99=BE=E7=82=BC120)=E4=B8=8D=E5=86=8D=E6=8E=90=E6=96=ADatte?= =?UTF-8?q?mpt=E2=80=94=E2=80=94=E4=BE=9B=E5=BA=94=E5=95=86=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=B8=8D=E7=9F=A5=E9=81=93=E8=B0=83=E7=94=A8=E6=96=B9?= =?UTF-8?q?=E9=9C=80=E6=B1=82,=E6=AD=A3=E6=98=AF09-05=E6=8F=90=E5=8F=96/09?= =?UTF-8?q?-14develop/09-16QC=E4=B8=89=E6=AC=A1=E4=BA=8B=E6=95=85=E5=85=B1?= =?UTF-8?q?=E5=90=8C=E6=A0=B9=E5=9B=A0,=E4=BB=85=E5=9C=A8=E7=9B=B4?= =?UTF-8?q?=E8=BF=9E=E6=9C=AA=E4=BC=A0=5Ftimeout=E6=97=B6=E4=B8=8E=5FTOTAL?= =?UTF-8?q?=5FTIMEOUT=E5=8F=96=E5=A4=A7=E5=85=9C=E5=BA=95;=E9=A2=84?= =?UTF-8?q?=E7=AE=97=E8=80=97=E5=B0=BD(<15s)=E4=B8=8D=E5=86=8D=E5=8F=91?= =?UTF-8?q?=E8=B5=B7=E5=BF=85=E7=84=B6=E8=B6=85=E6=97=B6=E7=9A=84attempt,?= =?UTF-8?q?=E6=8A=9B=E7=BB=93=E6=9E=84=E5=8C=96=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_llm/inference.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/pipeline_llm/inference.py b/pipeline_llm/inference.py index 4e84d55..b2fd1ee 100644 --- a/pipeline_llm/inference.py +++ b/pipeline_llm/inference.py @@ -41,6 +41,9 @@ _RETRYABLE_STATUS = (429, 500, 502, 503, 504) _MAX_ATTEMPTS = 3 _TOTAL_TIMEOUT = 300 _CONNECT_TIMEOUT = 30 +# 单次 attempt 最少剩余预算(2026-09-16 统一预算 deadline 配套):剩余不足 +# 该值不再发起新 attempt(必然超时白烧),直接抛预算耗尽错误 +_MIN_ATTEMPT_SECONDS = 15 EST_TOKENS_PER_CHAR = 2 # 预估用量:中文约 1 字符≈0.5 token,保守按 2 字符 1 token @@ -426,15 +429,34 @@ async def _post_upstream(ctx, payload, require_choices=True): kind = 'chat' if require_choices else 'gen' # method 独立列(2026-09-06 用户定夺):默认 POST,profile 声明优先 method = (profile.get('method') or 'POST').upper() - timeout = int(_fnum(ctx.get('timeout')) or _TOTAL_TIMEOUT) + # 统一预算 deadline(单点治理,2026-09-16 用户裁定收敛): + # ctx['budget'] = 本次调用【总预算】(bridge._resolve_budget 经 payload. + # _timeout 恒传)。attempt 超时 = 剩余预算(合法长生成可一次用满, + # 373s 实测样本;快速失败的瞬时错误自然留出重试余量)。重试受 + # deadline 约束,总时长≈预算,客户端(预算+60s)恒覆盖上游。 + # 端点配置 timeout(百炼 120 等)不再掐断 attempt——它是供应商参数, + # 不知道调用方需求,正是 09-05/09-14/09-16 三次事故的根因;仅在 + # 直连端点且未传 _timeout 时参与兜底(与 _TOTAL_TIMEOUT 取大)。 + # 旧实现每 attempt 独享全额超时,最坏 3×预算+退避 > 客户端等待 → 客户端 + # 先断连报模糊 TimeoutError(分类器误判永久错误,2026-09-16 QC 事故链一环)。 + budget = int(_fnum(ctx.get('budget')) or 0) + if budget <= 0: + budget = max(int(_fnum(ctx.get('timeout')) or 0), _TOTAL_TIMEOUT) + deadline = time.time() + budget # 可诊断性:只记长度不记值(真 key 不落日志)。print 确保进应用日志 # (模块 logging.getLogger 的 handler 未接应用日志管道,2026-09-04 实测) - print("[inference] 组包 url=%s auth头长度=%d body字段=%s" % ( + print("[inference] 组包 url=%s auth头长度=%d body字段=%s 预算=%ds" % ( url.split('?')[0], len(headers.get('Authorization', '') or ''), - sorted(body.keys()))) + sorted(body.keys()), budget)) last = None for attempt in range(_MAX_ATTEMPTS): + remaining = deadline - time.time() + if remaining < _MIN_ATTEMPT_SECONDS: + raise ValueError('上游调用失败(总预算 %ds 耗尽,已试 %d/%d 次,%s): %s' % ( + budget, attempt, _MAX_ATTEMPTS, type(last).__name__ if last else '无', + (repr(last) if last else '')[:200])) + timeout = int(remaining) _t0 = time.time() try: async with aiohttp.ClientSession() as session: @@ -887,8 +909,14 @@ async def chat_inference(org_id, user_id, payload, model_name='', task_ref='', # llm_usage.call_id 同值 → 流水可反查全部原文(trace.record_roundtrip / llm_call_trace) from appPublic.uniqueID import getID ctx['call_id'] = getID() + # 统一预算(2026-09-16 收敛):payload._timeout = 本次调用【总预算】→ + # ctx['budget'](_post_upstream 按 deadline 约束重试,总时长≈预算)。 + # ctx['timeout'] 保持供应商端点配置语义 = 单 attempt 上限,不再被覆盖 + # (旧实现覆盖后每 attempt 独享全额预算,最坏 3× 超客户端等待)。 + # 未传 _timeout(直连端点的第三方调用)→ budget 走 _TOTAL_TIMEOUT 平台 + # 缺省,不再落到端点小配置掐断长生成。 if req_timeout > 0: - ctx['timeout'] = min(req_timeout, 900) + ctx['budget'] = min(req_timeout, 900) # 异步模型:提交→轮询→取结果(用量因子走 usages 记账) model_row = ctx.get('model_row') or {} if (model_row.get('sync_mode') or 'sync') == 'async':