fix(llm): 终审 I-1/I-2+M-1/2/3/5——传输异常纳入重试链出口 LLMError(502 契约)/sleep 改 asyncio.sleep(事件循环不冻结)/save sentence 上限/坏 YAML fail-soft/删死脚手架 [vps] [no-doc]
This commit is contained in:
@@ -46,12 +46,12 @@ _SYSTEM_TEMPLATE = """你是A股量化投研假设卡设计专家。用户会给
|
||||
|
||||
|
||||
def load_domains() -> list[str]:
|
||||
"""读 data_manifest.yaml domains 键清单;文件缺失 fail-soft 返回 []."""
|
||||
"""读 data_manifest.yaml domains 键清单;文件缺失/坏 YAML fail-soft 返回 []."""
|
||||
path = os.environ.get("SANGUO_DATA_MANIFEST", "config/data_manifest.yaml")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
manifest = yaml.safe_load(f) or {}
|
||||
except OSError:
|
||||
except (OSError, yaml.YAMLError):
|
||||
return []
|
||||
return sorted((manifest.get("domains") or {}).keys())
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Callable
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -29,10 +28,10 @@ _STRICT_NOTE = "Strictly output valid JSON. No extra text."
|
||||
class LLMClient:
|
||||
def __init__(self, config: LLMConfig,
|
||||
transport: httpx.AsyncBaseTransport | None = None,
|
||||
sleep: Callable[[float], None] = time.sleep) -> None:
|
||||
sleep: Callable[[float], Awaitable[None]] | None = None) -> None:
|
||||
self._cfg = config
|
||||
self._transport = transport
|
||||
self._sleep = sleep
|
||||
self._sleep = sleep or asyncio.sleep
|
||||
|
||||
async def chat_json(self, messages: list[dict], *, temperature: float = 0.2,
|
||||
max_tokens: int = 2000) -> dict:
|
||||
@@ -48,7 +47,14 @@ class LLMClient:
|
||||
attempts = 0
|
||||
while True:
|
||||
attempts += 1
|
||||
status, text = await self._post(payload)
|
||||
try:
|
||||
status, text = await self._post(payload)
|
||||
except httpx.TransportError as e:
|
||||
# 传输级故障(连接拒绝/超时/断连)视作一次失败 attempt,同款退避
|
||||
if attempts >= _MAX_ATTEMPTS:
|
||||
raise LLMError(f"LLM 连接失败: {e}") from e
|
||||
await self._sleep(_BACKOFF_SECONDS[min(attempts - 1, 1)])
|
||||
continue
|
||||
if status == 200:
|
||||
content = self._content(text)
|
||||
usage = json.loads(text).get("usage") or {}
|
||||
@@ -76,14 +82,17 @@ class LLMClient:
|
||||
if status == 429 or status >= 500:
|
||||
if attempts >= _MAX_ATTEMPTS:
|
||||
raise LLMError(f"LLM 重试耗尽(HTTP {status}): {text[:200]}")
|
||||
self._sleep(_BACKOFF_SECONDS[min(attempts - 1, 1)])
|
||||
await self._sleep(_BACKOFF_SECONDS[min(attempts - 1, 1)])
|
||||
continue
|
||||
raise LLMError(f"LLM 未预期状态 HTTP {status}: {text[:200]}")
|
||||
|
||||
async def _strict_retry(self, messages: list[dict], payload: dict) -> dict:
|
||||
payload = dict(payload, messages=messages + [
|
||||
{"role": "user", "content": _STRICT_NOTE}])
|
||||
status, text = await self._post(payload)
|
||||
try:
|
||||
status, text = await self._post(payload)
|
||||
except httpx.TransportError as e:
|
||||
raise LLMError(f"LLM strict 重试连接失败: {e}") from e
|
||||
if status != 200:
|
||||
raise LLMError(f"LLM strict 重试失败(HTTP {status}): {text[:200]}")
|
||||
try:
|
||||
|
||||
@@ -844,6 +844,9 @@ async def hypotheses_draft(body: dict) -> dict:
|
||||
|
||||
@router.post("/pipeline/hypotheses", status_code=201)
|
||||
def hypotheses_save(body: dict) -> dict:
|
||||
sentence = str(body.get("sentence") or "")
|
||||
if len(sentence) > _DRAFT_SENTENCE_MAX:
|
||||
raise HTTPException(400, f"sentence 超长(>{_DRAFT_SENTENCE_MAX} 字)")
|
||||
snake = {
|
||||
"title": body.get("title"),
|
||||
"logic": body.get("logic"),
|
||||
|
||||
Reference in New Issue
Block a user