From c729d718c4ad0aae240c52b695892957c2cca69a Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Thu, 16 Apr 2026 17:37:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(llm):=20=E7=AD=96=E7=95=A5=E4=B8=8E?= =?UTF-8?q?=E6=9C=BA=E4=BC=9A=E6=8C=89=20token=20=E4=B8=8A=E9=99=90?= =?UTF-8?q?=E6=94=B6=E6=95=9B=E8=BE=93=E5=85=A5=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=E8=BF=87=E9=95=BF=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- backend/pipeline/llm/generate.py | 65 +++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/backend/pipeline/llm/generate.py b/backend/pipeline/llm/generate.py index 70924ac..f46f09e 100644 --- a/backend/pipeline/llm/generate.py +++ b/backend/pipeline/llm/generate.py @@ -829,6 +829,29 @@ def _truncate_strategy_narrative(text: str, max_chars: int) -> str: ) +def _llm_context_window_limit() -> int: + """与 ``AI_crawler.chat_completion_text`` 使用的上下文上限一致。""" + raw = (os.environ.get("LLM_CONTEXT_WINDOW") or os.environ.get("OPENAI_CONTEXT_WINDOW") or "32768").strip() + try: + return max(4096, int(raw)) + except ValueError: + return 32768 + + +def _estimate_chat_input_tokens(system_prompt: str, user_prompt: str) -> int: + """与 ``AI_crawler._estimate_chat_input_tokens`` 一致(保守估输入 token)。""" + total_chars = len(system_prompt or "") + len(user_prompt or "") + return int(total_chars * 0.55) + 512 + + +def _strategy_prompt_fits_context(system: str, user: str) -> bool: + """若为 False,``chat_completion_text`` 会在发请求前因过长而抛错。""" + est = _estimate_chat_input_tokens(system, user) + ctx = _llm_context_window_limit() + buf = 256 + return est < ctx - buf - 256 + + def generate_strategy_opportunities_llm( brief: dict[str, Any], *, @@ -845,12 +868,20 @@ def generate_strategy_opportunities_llm( for k, v in (chapter_llm_narratives or {}).items() if isinstance(v, str) and v.strip() } - _TARGET = 118_000 + sys_prompt = STRATEGY_OPPORTUNITIES_SYSTEM + + def _user_from_payload(p: dict[str, Any]) -> str: + return STRATEGY_OPPORTUNITIES_USER_PREFIX + json.dumps(p, ensure_ascii=False) + + # 按「字符上限」递减尝试;最终以 token 估算为准(与 AI_crawler 一致),避免 JSON 仍超长导致整段失败。 for cap_brief, cap_narr in ( - (100_000, 7_000), - (72_000, 5_000), - (52_000, 3_500), - (40_000, 2_500), + (48_000, 2_800), + (42_000, 2_200), + (36_000, 1_700), + (30_000, 1_300), + (26_000, 950), + (22_000, 700), + (18_000, 500), ): compact = compact_brief_for_llm(brief, max_chars=cap_brief) narratives = { @@ -862,13 +893,19 @@ def generate_strategy_opportunities_llm( } if narratives: payload["prior_chapter_llm_narratives"] = narratives - raw = json.dumps(payload, ensure_ascii=False) - if len(raw) <= _TARGET: - user = STRATEGY_OPPORTUNITIES_USER_PREFIX + raw - return _call_llm(STRATEGY_OPPORTUNITIES_SYSTEM, user) - compact = compact_brief_for_llm(brief, max_chars=40_000) + user = _user_from_payload(payload) + if _strategy_prompt_fits_context(sys_prompt, user): + return _call_llm(sys_prompt, user) + + # 去掉各章节选,仅保留 brief + for cap_brief in (40_000, 32_000, 26_000, 20_000, 16_000): + compact = compact_brief_for_llm(brief, max_chars=cap_brief) + payload = {"keyword": keyword, "competitor_brief": compact} + user = _user_from_payload(payload) + if _strategy_prompt_fits_context(sys_prompt, user): + return _call_llm(sys_prompt, user) + + compact = compact_brief_for_llm(brief, max_chars=14_000) payload = {"keyword": keyword, "competitor_brief": compact} - user = STRATEGY_OPPORTUNITIES_USER_PREFIX + json.dumps( - payload, ensure_ascii=False - ) - return _call_llm(STRATEGY_OPPORTUNITIES_SYSTEM, user) + user = _user_from_payload(payload) + return _call_llm(sys_prompt, user)