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:
2026-10-02 00:06:03 +08:00
parent 780d0dc786
commit 1e3579b64e
6 changed files with 59 additions and 11 deletions
+2 -2
View File
@@ -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())
+16 -7
View File
@@ -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:
+3
View File
@@ -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"),