diff --git a/.env.example b/.env.example index 616ffd5..54423c1 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,8 @@ VOICE_CONFIG=config/voice.json # 每组 LLM_<名>_BASE_URL / _API_KEY / _MODEL 必填; # 可选 _TIMEOUT_SECONDS / _MAX_RETRIES / _TEMPERATURE / _EXTRA_BODY(JSON 字符串, # 如思考开关 KIMI 网关为 {"reasoning_effort":"none"})。 +# 超时/重试缺省 60 秒 / 3 次,与 FastGPT 客户端 SLA 对齐 +# (契约测试 test/backends/test_sla_alignment.py)。 # LLM_PROVIDERS=deepseek,kimi,gpt6luna # LLM_PROVIDER=deepseek # LLM_DEEPSEEK_BASE_URL=https://api.openai.com/v1 diff --git a/src/agent/llm.py b/src/agent/llm.py index 10d6ff1..e73d5d8 100644 --- a/src/agent/llm.py +++ b/src/agent/llm.py @@ -100,8 +100,8 @@ class OpenAILLMService: model: str, api_key: str, base_url: str = "", - timeout_seconds: float = 30.0, - max_retries: int = 2, + timeout_seconds: float = 60.0, + max_retries: int = 3, temperature: float | None = None, extra_body: dict | None = None, ): diff --git a/src/core/config.py b/src/core/config.py index a37473f..130069a 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -91,8 +91,8 @@ class Config: "base_url": base_url, "api_key": api_key, "model": model, - "timeout_seconds": float(spec.get("timeout_seconds", 30)), - "max_retries": int(spec.get("max_retries", 2)), + "timeout_seconds": float(spec.get("timeout_seconds", 60)), + "max_retries": int(spec.get("max_retries", 3)), "temperature": float(temperature) if temperature is not None else None, "extra_body": str(extra_body), } @@ -113,8 +113,8 @@ class Config: "base_url": base_url, "api_key": api_key, "model": model, - "timeout_seconds": float(os.getenv(prefix + "TIMEOUT_SECONDS", "30")), - "max_retries": int(os.getenv(prefix + "MAX_RETRIES", "2")), + "timeout_seconds": float(os.getenv(prefix + "TIMEOUT_SECONDS", "60")), + "max_retries": int(os.getenv(prefix + "MAX_RETRIES", "3")), "temperature": float(temperature) if temperature else None, "extra_body": os.getenv(prefix + "EXTRA_BODY", ""), } diff --git a/src/core/fastgpt_client.py b/src/core/fastgpt_client.py index 9695256..9e2f7c7 100644 --- a/src/core/fastgpt_client.py +++ b/src/core/fastgpt_client.py @@ -16,6 +16,14 @@ _chat_backend: ChatBackend | None = None _langgraph_graph = 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 async def lifespan(app: FastAPI): @@ -39,8 +47,8 @@ async def lifespan(app: FastAPI): _fastgpt_client = AsyncChatClient( api_key=Config.FASTGPT_API_KEY, base_url=Config.FASTGPT_BASE_URL, - timeout=60.0, - max_retries=3, + timeout=FASTGPT_TIMEOUT_SECONDS, + max_retries=FASTGPT_MAX_RETRIES, retry_delay=1.0, enable_logging=Config.DEBUG, ) diff --git a/test/backends/test_sla_alignment.py b/test/backends/test_sla_alignment.py new file mode 100644 index 0000000..686b4f8 --- /dev/null +++ b/test/backends/test_sla_alignment.py @@ -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