baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user