baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
"""LLM 客户端(OpenAI 兼容),无 KEY 时全部调用降级为 None。
设计原则(批次 B 计划):**统计引擎是主交付,LLM 只做话术增强**。
未配置 AI_API_KEY 时不报错、不阻塞、不改变任何统计结果,只把 `available()` 置 False,
调用方据此走纯统计文案。
"""
from __future__ import annotations
import json
import logging
from django.conf import settings
logger = logging.getLogger("dealerhub.ai")
def available() -> bool:
"""是否可用 LLM 增强(需 provider + key 都配置)。"""
return bool(getattr(settings, "AI_PROVIDER", "") and getattr(settings, "AI_API_KEY", ""))
def chat(messages: list, *, max_tokens: int = 400, temperature: float = 0.3) -> str | None:
"""调用 OpenAI 兼容 chat/completions。失败或未配置一律返回 None(调用方降级)。"""
if not available():
return None
base = (settings.AI_BASE_URL or "").rstrip("/")
url = f"{base}/chat/completions"
payload = {
"model": settings.AI_MODEL,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {settings.AI_API_KEY}",
}
try:
import httpx
with httpx.Client(timeout=getattr(settings, "AI_TIMEOUT", 20)) as client:
resp = client.post(url, headers=headers, json=payload)
resp.raise_for_status()
data = resp.json()
return (data.get("choices") or [{}])[0].get("message", {}).get("content")
except Exception as exc: # 网络/鉴权/格式任何问题都降级
logger.warning("LLM call failed, falling back to statistics: %s", exc)
return None
def collect_advice(prompt: str) -> str | None:
"""催收/经营建议:单轮问答,返回一句话建议或 None。"""
return chat(
[
{"role": "system", "content": "你是经销商进销存系统的经营参谋,回答要短、要具体、可执行,不超过 60 字。"},
{"role": "user", "content": prompt},
],
max_tokens=160,
)
def extract_json(prompt: str) -> dict | None:
"""要求 LLM 返回 JSON 并解析;失败返回 None。"""
raw = chat(
[
{"role": "system", "content": "你只输出合法 JSON,不要任何解释文字、不要 markdown 代码块。"},
{"role": "user", "content": prompt},
],
max_tokens=600,
temperature=0.1,
)
if not raw:
return None
text = raw.strip()
if text.startswith("```"):
# 容错:剥掉可能的代码围栏
text = text.split("```")[1] if "```" in text[3:] else text[3:]
text = text.lstrip("json").lstrip()
try:
return json.loads(text)
except Exception as exc:
logger.warning("LLM JSON parse failed: %s | raw=%s", exc, raw[:200])
return None