fix(ai_crawler): cap max_tokens to remaining context window

LiteLLM rejects when max_tokens exceeds context minus measured input
tokens. Estimate input from prompt length and clamp max_tokens; allow
LLM_CONTEXT_WINDOW override. Prevents ContextWindowExceededError on long
Chinese/JSON prompts (e.g. regenerate-report §8.5).

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-14 10:16:38 +08:00
parent 9797796f2e
commit be0215bfe2

View File

@ -14,6 +14,8 @@
兼容别名二选一即可``LLM_API_KEY````LLM_BASE_URL````LLM_MODEL`` 兼容别名二选一即可``LLM_API_KEY````LLM_BASE_URL````LLM_MODEL``
- ``LLM_CONTEXT_WINDOW`` / ``OPENAI_CONTEXT_WINDOW``模型上下文 token 上限默认 ``32768````chat_completion_text`` 会按正文长度**保守估算**输入 token并把 ``max_tokens`` 收紧到不超过上限 估算输入避免网关报 ``max_tokens is too large`` LiteLLM ``ContextWindowExceededError``
上述变量与 Django 共用 **一份** ``market_assistant/.env``与本脚本所在 ``backend`` 的上三级目录下的 ``.env`` ``pip install python-dotenv`` 上述变量与 Django 共用 **一份** ``market_assistant/.env``与本脚本所在 ``backend`` 的上三级目录下的 ``.env`` ``pip install python-dotenv``
**运行方式**在下方运行配置里改好 ``IMAGE_SOURCE`` 等变量后直接执行 ``python AI_crawler.py``无需命令行参数 **运行方式**在下方运行配置里改好 ``IMAGE_SOURCE`` 等变量后直接执行 ``python AI_crawler.py``无需命令行参数
@ -230,6 +232,15 @@ def resolve_text_model_name(model: str | None = None) -> str:
return DEFAULT_MODEL return DEFAULT_MODEL
def _estimate_chat_input_tokens(system_prompt: str, user_prompt: str) -> int:
"""
保守估算 system+user token tiktoken 时用于约束 max_tokens
中文/JSON 往往高于英文 4 字符/token系数偏大宁可少给 max_tokens避免 400
"""
total_chars = len(system_prompt or "") + len(user_prompt or "")
return int(total_chars * 0.55) + 512
def strip_outer_markdown_fence(text: str) -> str: def strip_outer_markdown_fence(text: str) -> str:
"""若模型用 ``` / ```markdown 包裹全文,去掉最外层围栏。""" """若模型用 ``` / ```markdown 包裹全文,去掉最外层围栏。"""
t = (text or "").strip() t = (text or "").strip()
@ -272,6 +283,26 @@ def chat_completion_text(
} }
if extra_json: if extra_json:
body.update(extra_json) body.update(extra_json)
# 避免 max_tokens 大于「上下文 输入」时网关 400如 LiteLLM ContextWindowExceededError
ctx_raw = (
os.environ.get("LLM_CONTEXT_WINDOW")
or os.environ.get("OPENAI_CONTEXT_WINDOW")
or "32768"
).strip()
try:
context_window = max(4096, int(ctx_raw))
except ValueError:
context_window = 32768
buf = 256
input_est = _estimate_chat_input_tokens(system_prompt, user_prompt)
if input_est >= context_window - buf - 256:
raise ValueError(
f"提示词过长(估算输入约 {input_est} tokens上下文上限 {context_window}"
"请缩小报告/摘要输入或换更大上下文的模型;也可设置环境变量 LLM_CONTEXT_WINDOW。"
)
avail = context_window - input_est - buf
want = int(body.get("max_tokens") or max_tokens)
body["max_tokens"] = max(256, min(want, max(avail, 256)))
r = requests.post( r = requests.post(
f"{b}/chat/completions", f"{b}/chat/completions",
headers={ headers={