feat(config): align LLM timeout/retry SLA with FastGPT client (60s/3)
Replaces the unequal SLA (LLM 30s/2 retries vs FastGPT client 60s/3) that made the LangGraph backend look less stable under gray release. - core/fastgpt_client: FASTGPT_TIMEOUT_SECONDS / FASTGPT_MAX_RETRIES module constants (were inline literals in lifespan) - agent/llm: OpenAILLMService defaults 60.0s / 3 retries - core/config: named-profile fallback defaults 60/3 (prefixed and JSON-dict forms) - contract test test/backends/test_sla_alignment.py pins both sides to the same constants so the SLA cannot drift apart again - retry backoff intentionally differs (FastGPT fixed 1s vs LLM exponential) and is documented; aligned parameters are per-call timeout and max retry count Verified: 215 unit tests (3 new SLA contract tests)
This commit is contained in:
@@ -17,6 +17,8 @@ VOICE_CONFIG=config/voice.json
|
|||||||
# 每组 LLM_<名>_BASE_URL / _API_KEY / _MODEL 必填;
|
# 每组 LLM_<名>_BASE_URL / _API_KEY / _MODEL 必填;
|
||||||
# 可选 _TIMEOUT_SECONDS / _MAX_RETRIES / _TEMPERATURE / _EXTRA_BODY(JSON 字符串,
|
# 可选 _TIMEOUT_SECONDS / _MAX_RETRIES / _TEMPERATURE / _EXTRA_BODY(JSON 字符串,
|
||||||
# 如思考开关 KIMI 网关为 {"reasoning_effort":"none"})。
|
# 如思考开关 KIMI 网关为 {"reasoning_effort":"none"})。
|
||||||
|
# 超时/重试缺省 60 秒 / 3 次,与 FastGPT 客户端 SLA 对齐
|
||||||
|
# (契约测试 test/backends/test_sla_alignment.py)。
|
||||||
# LLM_PROVIDERS=deepseek,kimi,gpt6luna
|
# LLM_PROVIDERS=deepseek,kimi,gpt6luna
|
||||||
# LLM_PROVIDER=deepseek
|
# LLM_PROVIDER=deepseek
|
||||||
# LLM_DEEPSEEK_BASE_URL=https://api.openai.com/v1
|
# LLM_DEEPSEEK_BASE_URL=https://api.openai.com/v1
|
||||||
|
|||||||
+2
-2
@@ -100,8 +100,8 @@ class OpenAILLMService:
|
|||||||
model: str,
|
model: str,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
base_url: str = "",
|
base_url: str = "",
|
||||||
timeout_seconds: float = 30.0,
|
timeout_seconds: float = 60.0,
|
||||||
max_retries: int = 2,
|
max_retries: int = 3,
|
||||||
temperature: float | None = None,
|
temperature: float | None = None,
|
||||||
extra_body: dict | None = None,
|
extra_body: dict | None = None,
|
||||||
):
|
):
|
||||||
|
|||||||
+4
-4
@@ -91,8 +91,8 @@ class Config:
|
|||||||
"base_url": base_url,
|
"base_url": base_url,
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
"model": model,
|
"model": model,
|
||||||
"timeout_seconds": float(spec.get("timeout_seconds", 30)),
|
"timeout_seconds": float(spec.get("timeout_seconds", 60)),
|
||||||
"max_retries": int(spec.get("max_retries", 2)),
|
"max_retries": int(spec.get("max_retries", 3)),
|
||||||
"temperature": float(temperature) if temperature is not None else None,
|
"temperature": float(temperature) if temperature is not None else None,
|
||||||
"extra_body": str(extra_body),
|
"extra_body": str(extra_body),
|
||||||
}
|
}
|
||||||
@@ -113,8 +113,8 @@ class Config:
|
|||||||
"base_url": base_url,
|
"base_url": base_url,
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
"model": model,
|
"model": model,
|
||||||
"timeout_seconds": float(os.getenv(prefix + "TIMEOUT_SECONDS", "30")),
|
"timeout_seconds": float(os.getenv(prefix + "TIMEOUT_SECONDS", "60")),
|
||||||
"max_retries": int(os.getenv(prefix + "MAX_RETRIES", "2")),
|
"max_retries": int(os.getenv(prefix + "MAX_RETRIES", "3")),
|
||||||
"temperature": float(temperature) if temperature else None,
|
"temperature": float(temperature) if temperature else None,
|
||||||
"extra_body": os.getenv(prefix + "EXTRA_BODY", ""),
|
"extra_body": os.getenv(prefix + "EXTRA_BODY", ""),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ _chat_backend: ChatBackend | None = None
|
|||||||
_langgraph_graph = None
|
_langgraph_graph = None
|
||||||
_app_service: AccidentAppService | None = None
|
_app_service: AccidentAppService | None = None
|
||||||
|
|
||||||
|
# 后端超时/重试 SLA(两后端对齐;契约测试防止再漂移)。
|
||||||
|
# FastGPT 侧:客户端每次请求总超时 + 重试次数;
|
||||||
|
# LangGraph 侧:LLM 每次调用总超时 + 重试次数(退避为指数式,
|
||||||
|
# 与 FastGPT 固定 1s 不同——总尝试次数与单次超时可比,见
|
||||||
|
# test/backends/test_sla_alignment.py)。
|
||||||
|
FASTGPT_TIMEOUT_SECONDS = 60.0
|
||||||
|
FASTGPT_MAX_RETRIES = 3
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
@@ -39,8 +47,8 @@ async def lifespan(app: FastAPI):
|
|||||||
_fastgpt_client = AsyncChatClient(
|
_fastgpt_client = AsyncChatClient(
|
||||||
api_key=Config.FASTGPT_API_KEY,
|
api_key=Config.FASTGPT_API_KEY,
|
||||||
base_url=Config.FASTGPT_BASE_URL,
|
base_url=Config.FASTGPT_BASE_URL,
|
||||||
timeout=60.0,
|
timeout=FASTGPT_TIMEOUT_SECONDS,
|
||||||
max_retries=3,
|
max_retries=FASTGPT_MAX_RETRIES,
|
||||||
retry_delay=1.0,
|
retry_delay=1.0,
|
||||||
enable_logging=Config.DEBUG,
|
enable_logging=Config.DEBUG,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""后端超时/重试 SLA 对齐契约测试。
|
||||||
|
|
||||||
|
背景(可替代性清单):FastGPT 客户端为 60s/3 次,LangGraph LLM 侧
|
||||||
|
曾是 30s/2 次,客户端感知到的超时行为不等价。本测试把两端常量
|
||||||
|
钉在一起,防止再次漂移。
|
||||||
|
|
||||||
|
注:退避策略有意不同——FastGPT 固定 1s,LLM 指数退避(限流场景
|
||||||
|
更友好);对齐的是"单次调用总超时"与"最大重试次数"两个 SLA 参数。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from src.agent.llm import OpenAILLMService
|
||||||
|
from src.core.fastgpt_client import FASTGPT_MAX_RETRIES, FASTGPT_TIMEOUT_SECONDS
|
||||||
|
|
||||||
|
|
||||||
|
def test_llm_default_timeout_matches_fastgpt_client():
|
||||||
|
signature = inspect.signature(OpenAILLMService.__init__)
|
||||||
|
default_timeout = signature.parameters["timeout_seconds"].default
|
||||||
|
assert default_timeout == FASTGPT_TIMEOUT_SECONDS == 60.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_llm_default_max_retries_matches_fastgpt_client():
|
||||||
|
signature = inspect.signature(OpenAILLMService.__init__)
|
||||||
|
default_retries = signature.parameters["max_retries"].default
|
||||||
|
assert default_retries == FASTGPT_MAX_RETRIES == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_named_profile_defaults_match_fastgpt_client(monkeypatch):
|
||||||
|
"""未显式配置 _TIMEOUT_SECONDS/_MAX_RETRIES 的 profile 取 SLA 默认值。"""
|
||||||
|
monkeypatch.setenv("LLM_PROVIDERS", "probe")
|
||||||
|
monkeypatch.setenv("LLM_PROBE_BASE_URL", "http://127.0.0.1:1/v1")
|
||||||
|
monkeypatch.setenv("LLM_PROBE_API_KEY", "probe-key")
|
||||||
|
monkeypatch.setenv("LLM_PROBE_MODEL", "probe-model")
|
||||||
|
|
||||||
|
from src.core.config import Config
|
||||||
|
|
||||||
|
# LLM_PROVIDERS 是 import 时固化的类属性,需直接替换
|
||||||
|
monkeypatch.setattr(Config, "LLM_PROVIDERS", "probe")
|
||||||
|
profile = Config.llm_profile("probe")
|
||||||
|
assert profile["timeout_seconds"] == FASTGPT_TIMEOUT_SECONDS
|
||||||
|
assert profile["max_retries"] == FASTGPT_MAX_RETRIES
|
||||||
Reference in New Issue
Block a user