feat: harden article summary timeout and fallback

This commit is contained in:
root
2026-04-07 21:08:06 +08:00
parent 2df0af5b82
commit 87d18e4263
4 changed files with 46 additions and 14 deletions
+3 -1
View File
@@ -300,6 +300,7 @@ python scripts/apply_term_suggestions.py ^
- `ARTICLE_SUMMARY_LLM_API_KEY`
如果这些变量未设置,单篇总结会回退使用主流程中的 `LLM_*` / `OPENAI_*` 配置。
如果显式指定 DeepSeek 作为单篇总结模型且请求超时或失败,当前实现会自动再用主流程默认模型配置重试一次。
示例(PowerShell 风格):
@@ -316,7 +317,8 @@ set ARTICLE_SUMMARY_LLM_MODEL=deepseek-chat
python scripts/run_article_summaries.py ^
--extracted outputs/freshrss/extracted/freshrss.extracted.json ^
--ids 12345 67890 ^
--output-dir outputs/freshrss/single_summaries
--output-dir outputs/freshrss/single_summaries ^
--timeout 120
```
也可以通过 `summary_mcp.server` 暴露的 MCP 工具 `generate_article_summaries` 调用:
+1 -1
View File
@@ -37,7 +37,7 @@ def main() -> None:
help="Directory to write per-article Markdown summaries",
)
parser.add_argument("--max-retries", type=int, default=2, help="Maximum LLM retry attempts per article")
parser.add_argument("--timeout", type=float, default=60.0, help="LLM request timeout in seconds")
parser.add_argument("--timeout", type=float, default=120.0, help="LLM request timeout in seconds")
parser.add_argument("--api-key", type=str, default=None, help="Override article-summary LLM API key")
parser.add_argument("--model", type=str, default=None, help="Override article-summary LLM model")
parser.add_argument("--api-url", type=str, default=None, help="Override article-summary LLM API URL/base URL")
+1 -1
View File
@@ -172,7 +172,7 @@ def generate_article_summaries(
selected_ids: list[str],
output_dir: str | None = None,
max_retries: int = 2,
timeout_seconds: float = 60.0,
timeout_seconds: float = 120.0,
llm_api_key: str | None = None,
llm_model: str | None = None,
llm_api_url: str | None = None,
+41 -11
View File
@@ -4,6 +4,8 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Mapping, Sequence
import httpx
from summary_mcp.core.summary_loop import run_loop_payload
from summary_mcp.validators.article_summary import validate_article_summary_payload
@@ -18,7 +20,7 @@ DEFAULT_PROMPT_PATH = OUTPUT_ROOT / "prompts" / "article-summary-prompt.txt"
class ArticleSummaryConfig:
prompt_path: Path = DEFAULT_PROMPT_PATH
max_retries: int = 2
timeout_seconds: float = 60.0
timeout_seconds: float = 120.0
llm_api_key: str | None = None
llm_model: str | None = None
llm_api_url: str | None = None
@@ -187,17 +189,45 @@ def summarize_selected_articles(
"warnings": warnings,
}
summary_exit_code, summary_payload, _ = run_loop_payload(
extracted_payload=extracted_payload,
prompt_path=cfg.prompt_path,
max_retries=cfg.max_retries,
timeout_seconds=cfg.timeout_seconds,
api_key=resolved_api_key,
model=resolved_model,
api_url=resolved_api_url,
output_path=None,
validator=validate_article_summary_payload,
summary_exit_code = 1
summary_payload = None
try:
summary_exit_code, summary_payload, _ = run_loop_payload(
extracted_payload=extracted_payload,
prompt_path=cfg.prompt_path,
max_retries=cfg.max_retries,
timeout_seconds=cfg.timeout_seconds,
api_key=resolved_api_key,
model=resolved_model,
api_url=resolved_api_url,
output_path=None,
validator=validate_article_summary_payload,
)
except httpx.TimeoutException:
summary_exit_code = 1
summary_payload = None
should_fallback = (
(summary_exit_code != 0 or summary_payload is None)
and resolved_model is not None
and resolved_model == (cfg.llm_model or model or resolved_model)
and resolved_model.startswith("deepseek")
)
if should_fallback:
summary_exit_code, summary_payload, _ = run_loop_payload(
extracted_payload=extracted_payload,
prompt_path=cfg.prompt_path,
max_retries=cfg.max_retries,
timeout_seconds=cfg.timeout_seconds,
api_key=None,
model=None,
api_url=None,
output_path=None,
validator=validate_article_summary_payload,
)
if summary_exit_code != 0 or summary_payload is None:
continue