diff --git "a/, i.get(script,)[:80])\n for v in w.values(): walk(v)\n elif isinstance(w, list):\n for v in w: walk(v)\nwalk(d)\n\\\"\"" "b/, i.get(script,)[:80])\n for v in w.values(): walk(v)\n elif isinstance(w, list):\n for v in w: walk(v)\nwalk(d)\n\\\"\"" new file mode 100644 index 0000000..e69de29 diff --git a/pipeline_service/agent_loop_v2.py b/pipeline_service/agent_loop_v2.py index 7bb768f..8c16617 100644 --- a/pipeline_service/agent_loop_v2.py +++ b/pipeline_service/agent_loop_v2.py @@ -198,7 +198,14 @@ class AgentExecutor: await self._maybe_compress() # 5b. LLM 调用(native function calling,返回 dict) - resp = await self._call_llm() + # 模型不可用等配置错误必须真实报给用户(error 事件), + # 绝不能让异常打断流——前端会停在"思考中..."干等。 + try: + resp = await self._call_llm() + except Exception as e: + logger.error(f"run: LLM 调用失败 turn={turn+1}: {e}") + yield json.dumps({"type": "error", "message": str(e)}, ensure_ascii=False) + "\n" + return # 5c. 原生 function calling:优先处理 tool_calls native_calls = (resp.get("tool_calls") or []) if isinstance(resp, dict) else [] diff --git a/pipeline_service/llm_bridge.py b/pipeline_service/llm_bridge.py index f333856..53ed366 100644 --- a/pipeline_service/llm_bridge.py +++ b/pipeline_service/llm_bridge.py @@ -50,10 +50,14 @@ async def _get_model_config(model_name: str = None, org_id: str = None) -> dict: dbname = "pipeline" async with db.sqlorContext(dbname) as sor: if model_name: - sql = "SELECT api_base, api_key, model_id FROM llm WHERE name=${name}$ AND status='active'" + # name/model_id 双匹配:与 gateway._resolve_and_persist_model 语义一致—— + # 调用方传 llm.name 或 API 模型名(model_id)都能解析,避免语义漂移。 + sql = ("SELECT api_base, api_key, model_id FROM llm " + "WHERE status='active' AND (name=${name}$ OR model_id=${name}$)") params = {"name": model_name} if org_id: - sql += " AND org_id=${org}$" + # 与 gateway 一致:本机构模型 + 系统级共享(org_id 为空/0) + sql += " AND (org_id=${org}$ OR org_id='' OR org_id='0')" params["org"] = org_id sql += " LIMIT 1" recs = await sor.sqlExe(sql, params) @@ -80,6 +84,24 @@ async def _get_model_config(model_name: str = None, org_id: str = None) -> dict: return {} +def _no_llm_error(model_name=None, org_id=None) -> ValueError: + """模型不可用的错误必须真实可行动:写明模型名与原因,前端原样展示、运维据此处理。 + + 禁止笼统的 'No LLM API configured'——用户看到它只会卡住干等。 + """ + if model_name: + return ValueError( + f"模型「{model_name}」不可用:llm 表中未找到可用配置" + f"(name/model_id 不匹配、status 非 active" + + (f",或不属于当前机构 org={org_id}" if org_id else "") + + ")。请在模型下拉里改选可用模型,或联系管理员在模型管理中补齐配置。" + ) + return ValueError( + "没有可用模型:当前会话未指定模型,且 llm 表无启用模型。" + "请在模型下拉中选择模型,或联系管理员配置模型。" + ) + + # 瞬时错误重试:超时/连接错误/限流/服务端5xx 均重试;配置错误(无key)、鉴权/参数4xx 不重试 _RETRYABLE_STATUS = (429, 500, 502, 503, 504) _LLM_MAX_ATTEMPTS = 3 @@ -168,7 +190,7 @@ async def llm_call(prompt: str, model: str = None, temperature: float = 0.7, org model_id = model or os.environ.get("LLM_MODEL", "gpt-4o-mini") if not api_key: - raise ValueError("No LLM API configured. Please add a model in the llm table or set LLM_API_KEY env var.") + raise _no_llm_error(model, org_id) import aiohttp @@ -207,7 +229,7 @@ async def llm_call_msgs(messages: list, model: str = None, temperature: float = model_id = model or os.environ.get("LLM_MODEL", "gpt-4o-mini") if not api_key: - raise ValueError("No LLM API configured") + raise _no_llm_error(model, org_id) headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} payload = {"model": model_id, "messages": messages, "temperature": temperature} @@ -237,7 +259,7 @@ async def llm_call_msgs_native(messages: list, tools: list = None, model: str = model_id = model or os.environ.get("LLM_MODEL", "gpt-4o-mini") if not api_key: - raise ValueError("No LLM API configured") + raise _no_llm_error(model, org_id) headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} payload = {"model": model_id, "messages": messages, "temperature": temperature}