Files
dealerhub/backend/apps/ai/llm.py
T

87 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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