feat(timeout): LLM超时预算统一收敛到bridge._resolve_budget单点(2026-09-16用户裁定:禁散落各产线各角色)——timeout参数语义=一次调用总预算(0=平台缺省chat300/gen900,封顶900下限60),payload._timeout恒传+客户端=预算+60恒覆盖上游;四个公开入口(llm_call/llm_call_msgs/llm_call_msgs_native/llm_infer)废弃各自为政的等待公式(330缺省/+60/原值-60/min+60)全走统一解析;调用方忘传timeout也结构性安全——消灭09-05提取/09-14develop/09-16QC三次'忘传→端点按供应商120s掐断长生成'同款事故;顺带修llm_infer潜伏bug(不传timeout客户端缺省330<异步轮询预算600客户端先断);agent_loop._LLM_CLIENT_TIMEOUT语义同步为总预算510(510→客户端570→wait_for600三层对齐),v2引擎_MAIN_TIMEOUT=180在新语义下总时长受预算约束顺带修复旧3x放大超客户端的潜伏bug

This commit is contained in:
ymq 2026-09-16 17:58:14 +08:00
parent 9e9886a6f1
commit 5f98cd79a7
2 changed files with 74 additions and 35 deletions

View File

@ -2309,16 +2309,13 @@ async def _exec_agent_tool(tool, params, workspace_dir, ctx=None):
# 30 轮全耗在 list_files/read_file/run_shell 反复「了解现状 + 验证已有内容」,从不 write_file/deliver。
_FORCE_PRODUCE_TURN = 5
# LLM 调用硬超时(外层 asyncio.wait_for 兜底。llm_bridge 内部 300s×3 重试最坏 15 分钟,
# 期间 role_agent_run 心跳不更新(心跳在调用前 touch观察上像任务挂起/僵尸。
# 外层硬上限:① 防 aiohttp total 超时因 DNS 等场景失效导致的真正无限挂起 ② 给 llm_bridge 至少 2 轮完整重试机会
# 420s 只够 ~1.4 轮,网关瞬时挂起时重试被提前掐断,可自愈的瞬时故障也 TimeoutError 超限暂停任务链)。
# LLM 调用超时预算2026-09-16 统一收敛到 bridge._resolve_budget 单点后,
# 本常量语义 = 一次调用的【总预算】bridge 自动派生客户端等待 = 预算+60
# 预算 510 → 客户端 570 → 外层硬超时 600恒覆盖防真正无限挂起
# 历史2026-09-14 前此处是「客户端超时」且只挂 develop native 路径QC/PM/
# 复盘漏传 → 端点按供应商配置掐断(三次事故);现忘传也走平台缺省,不再掉洞。
_LLM_HARD_TIMEOUT = 600
# 单次 LLM 调用客户端超时aiohttp total略小于外层硬超时让长生成用满外层
# 预算而不被缺省 330s 提前掐断2026-09-14 pbls需求分析生成 ~368s > 330s 缺省
# → 客户端 TimeoutError旧代码误报「端点不可达」触发任务链暂停
_LLM_CLIENT_TIMEOUT = 570
_LLM_CLIENT_TIMEOUT = 510 # 总预算inference deadline 约束含内部重试)
_FORCE_PRODUCE_HINT = (
"⚠️ 你已经探索了足够多轮(已超过 5 轮)。现在必须立即产出并交付:\n"

View File

@ -18,6 +18,43 @@ import time
logger = logging.getLogger("pipeline.llm_bridge")
# ── 平台统一超时预算单点治理2026-09-16 用户裁定收敛)─────────────────
# 背景:超时逻辑曾散落三层——①各调用方自传 timeout忘传 → payload 无
# _timeout → inference 按供应商端点配置(百炼 120s掐断长生成2026-09-05
# 提取、2026-09-14 develop、2026-09-16 QC 三次事故同一根因②bridge 四个
# 公开函数客户端等待公式各不相同330 缺省 / +60 / 原值 / min+60
# inference 3 attempt × 单attempt超时最坏 3× 总预算,超过客户端等待 → 客户端
# 先断连报模糊 TimeoutError分类器再误判永久错误
#
# 统一语义(只有这一个函数 + inference._post_upstream 的 deadline 两处):
# timeout 参数 = 一次调用的【总预算】秒数(含 inference 内部重试)。
# 不传 → 平台缺省chat 类 300 / 生成类 900结构性消除「忘传掉洞」。
# 三层预算对齐公式:
# payload._timeout = total inference 端 deadline 约束最坏≈total
# client aiohttp = total + 60 (恒覆盖上游:断连只可能是端点侧
# 结构化错误,永不模糊 TimeoutError
# 调用方外层 wait_for若有须 ≥ clientagent_loop 600 > 510+60 ✓)
_DEFAULT_CHAT_BUDGET = 300 # 对话类缺省总预算(对齐 inference _TOTAL_TIMEOUT
_DEFAULT_GEN_BUDGET = 900 # 生成类llm_infert2i/t2v/tts 等慢任务)缺省
_BUDGET_CAP = 900 # inference 端 _timeout 封顶(一致)
_BUDGET_FLOOR = 60
_CLIENT_MARGIN = 60
def _resolve_budget(timeout, default):
"""统一预算解析 → (payload._timeout 总预算, 客户端 aiohttp total)。
全平台唯一公式任何产线/角色/模块经本 bridge 调用都走这里
调用方不需要也不应该自己算客户端等待
"""
try:
t = int(timeout or 0)
except (TypeError, ValueError):
t = 0
total = t if t > 0 else default
total = max(_BUDGET_FLOOR, min(total, _BUDGET_CAP))
return total, total + _CLIENT_MARGIN
# 内部自调用 token 缓存:(org_id, user_id, model_name) -> {token, calls, expires_at}
# 每次 LLM 调用都签发新 token 会让 tokens 表膨胀,故按上下文缓存复用;
# 接近调用上限或临近过期时换新(上限/过期由签发侧强制,本地计数只是提前量)。
@ -74,8 +111,8 @@ async def _http_chat(payload, org_id, user_id, model_name, timeout: int = 0,
project_id: str = ''):
"""POST 本进程推理端点。返回上游响应 dict失败抛 ValueError消息真实可行动
timeout客户端等待秒数0=缺省 330异步生成模型视频等端点侧最长
900 调用方须同步放大客户端超时否则客户端先断连
timeout客户端等待秒数0=缺省 chat 预算+60 余量公开入口llm_call*
一律经 _resolve_budget 算好后传入调用方无需自己算单点治理
"""
import aiohttp
@ -85,7 +122,8 @@ async def _http_chat(payload, org_id, user_id, model_name, timeout: int = 0,
# 2026-09-04 实测Authorization 头变成 "***plk-..." 致端点校验失败)
_BEARER = 'Bea' + 'rer '
headers = {"Authorization": _BEARER + token, "Content-Type": "application/json"}
_total = int(timeout) if timeout and int(timeout) > 0 else 330
_total = int(timeout) if timeout and int(timeout) > 0 \
else _DEFAULT_CHAT_BUDGET + _CLIENT_MARGIN
try:
async with aiohttp.ClientSession() as session:
async with session.post(
@ -139,6 +177,9 @@ async def llm_call(prompt: str, model: str = None, temperature: float = 0.7,
org_id 为空 = 系统级'0'与旧语义不过滤机构等价
purpose='utility'辅助任务分类/选择/摘要治理层按用途选模型链
机构策略配置辅助模型优先 payload _purpose 键透传到端点
timeout一次调用的总预算秒数0=平台缺省 chat 300上限 900
预算分配/客户端等待由 _resolve_budget 单点计算2026-09-16 统一收敛
调用方忘传也结构性安全不再按供应商端点小配置被掐断
session_id2026-09-11 用户定夺会话粘性键同会话固定同一账号
上游 KV/前缀缓存按账号隔离固定账号命中缓存省钱 payload._session_id
透传到端点不进 token 缓存键token 与账号选择解耦
@ -163,12 +204,15 @@ async def llm_call(prompt: str, model: str = None, temperature: float = 0.7,
}
if purpose:
payload["_purpose"] = purpose
if timeout:
payload["_timeout"] = int(timeout)
if session_id:
payload["_session_id"] = session_id
# 统一预算单点payload._timeout 恒传 → inference 端按总预算 deadline
# 约束重试;客户端 = 预算+60 恒覆盖上游。不传 timeout 走平台缺省,结构性
# 消除「忘传 → 端点按供应商小配置掐断」2026-09-05/09-14/09-16 三事故根因)。
_budget, _client = _resolve_budget(timeout, _DEFAULT_CHAT_BUDGET)
payload["_timeout"] = _budget
data = await _http_chat(payload, org_id or '0', user_id or '', model or '',
project_id=project_id or '')
timeout=_client, project_id=project_id or '')
try:
return data["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as e:
@ -187,20 +231,20 @@ async def llm_call_msgs(messages: list, model: str = None, temperature: float =
"""Call LLM with full message array (system/user/assistant).
purpose='utility'辅助任务分类/选择/摘要治理层按用途选模型链
timeout单次上游调用超时秒数0=用端点默认上限 900超长文本提取用
timeout一次调用的总预算秒数0=平台缺省 chat 300上限 900
预算分配/客户端等待由 _resolve_budget 单点计算调用方只管声明预算
session_id2026-09-11会话粘性账号键 payload._session_id 透传
"""
payload = {"model": model or '', "messages": messages, "temperature": temperature}
if purpose:
payload["_purpose"] = purpose
if timeout:
payload["_timeout"] = int(timeout)
if session_id:
payload["_session_id"] = session_id
# 客户端等待须覆盖端点侧预算 + 余量(同 llm_infer 模式),否则客户端先断连、
# 报模糊 TimeoutError 而非端点的结构化错误
# 统一预算(单点,同 llm_call
_budget, _client = _resolve_budget(timeout, _DEFAULT_CHAT_BUDGET)
payload["_timeout"] = _budget
data = await _http_chat(payload, org_id or '0', user_id or '', model or '',
timeout=(int(timeout) + 60 if timeout else 0),
timeout=_client,
project_id=project_id or '')
try:
return data["choices"][0]["message"]["content"]
@ -221,6 +265,9 @@ async def llm_call_msgs_native(messages: list, tools: list = None, model: str =
当模型返回 tool_calls content 通常为空字符串
purpose='utility'辅助任务分类/选择/摘要治理层按用途选模型链
session_id2026-09-11会话粘性账号键 payload._session_id 透传
timeout一次调用的总预算秒数0=平台缺省 chat 300预算分配/客户端
等待由 _resolve_budget 单点计算2026-09-15 pbls design 三连超时
2026-09-16 QC 事故后统一收敛调用方不再自己算 -60/+60
"""
payload = {"model": model or '', "messages": messages, "temperature": temperature}
if tools:
@ -228,18 +275,12 @@ async def llm_call_msgs_native(messages: list, tools: list = None, model: str =
payload["tool_choice"] = "auto"
if purpose:
payload["_purpose"] = purpose
if timeout:
# 端点侧上游预算必须随调用方透传2026-09-15 pbls design 三连超时根因):
# 原来 native 只把 timeout 用作客户端等待payload 不带 _timeout → 端点按
# 供应商端点配置(百炼 timeout=120掐断上游而 design 收尾大请求天然
# 72~115s一半概率 120s 被杀 → 内部 3 重试全灭 → 任务 failed。
# 端点预算 = 客户端等待 - 60s 余量:让端点先掐并返回结构化错误(可分类
# 重试),而不是客户端 aiohttp 断连报模糊 TimeoutError。
payload["_timeout"] = max(int(timeout) - 60, 60)
if session_id:
payload["_session_id"] = session_id
_budget, _client = _resolve_budget(timeout, _DEFAULT_CHAT_BUDGET)
payload["_timeout"] = _budget
data = await _http_chat(payload, org_id or '0', user_id or '', model or '',
timeout=timeout, project_id=project_id or '')
timeout=_client, project_id=project_id or '')
try:
msg = data["choices"][0]["message"]
except (KeyError, IndexError, TypeError) as e:
@ -269,11 +310,12 @@ async def llm_infer(payload: dict, model: str = None, org_id: str = None,
body = dict(payload or {})
if model:
body["model"] = model
if timeout:
body["_timeout"] = int(timeout)
if session_id and not body.get('_session_id'):
body["_session_id"] = session_id
# 客户端等待须覆盖端点侧预算_timeout 端点封顶 900+ 余量,否则客户端先断连
client_timeout = min(int(timeout or 0), 900) + 60 if timeout else 0
# 统一预算(单点,同 llm_call*timeout=总预算0=生成类缺省 900
# payload._timeout 恒传 + 客户端=预算+60。旧实现不传 timeout 时客户端缺省
# 330 而端点异步轮询预算 600 → 客户端先断连(潜伏 bug一并根治
_budget, _client = _resolve_budget(timeout, _DEFAULT_GEN_BUDGET)
body["_timeout"] = _budget
return await _http_chat(body, org_id or '0', user_id or '', model or '',
timeout=client_timeout, project_id=project_id or '')
timeout=_client, project_id=project_id or '')