feat: dual LLM mode (default via .env, custom per-request) for AI analysis
This commit is contained in:
+52
-39
@@ -56,36 +56,60 @@ def _extract_json(text: str) -> dict:
|
||||
return json.loads(text[start:end + 1])
|
||||
|
||||
|
||||
def analyze_requirements(content: str) -> dict:
|
||||
"""调用 LLM 分析作业要求,返回检测维度计划。未配置 LLM 时返回默认计划。"""
|
||||
key = settings.LLM_API_KEY
|
||||
if not key or key.startswith("sk-placeholder") or not settings.LLM_MODEL:
|
||||
logger.warning("LLM 未配置,使用默认检测计划")
|
||||
def resolve_llm(override: dict | None = None) -> tuple[str, str, str]:
|
||||
"""返回 (base_url, api_key, model)。default 模式读 settings;
|
||||
custom 模式用 override 中的 base_url/api_key/model。"""
|
||||
mode = (override or {}).get("mode") or settings.LLM_MODE
|
||||
if mode == "custom":
|
||||
base_url = (override.get("base_url") or "").strip()
|
||||
api_key = (override.get("api_key") or "").strip()
|
||||
model = (override.get("model") or "").strip()
|
||||
if not base_url or not model:
|
||||
raise ValueError("custom 模式必须提供 base_url 和 model")
|
||||
return base_url.rstrip("/"), api_key, model
|
||||
base_url = settings.LLM_BASE_URL.rstrip("/")
|
||||
api_key = settings.LLM_API_KEY
|
||||
model = settings.LLM_MODEL
|
||||
if not base_url or not model:
|
||||
raise ValueError("LLM 未配置: 请在 .env 设置 LLM_BASE_URL/LLM_MODEL, 或用 custom 模式")
|
||||
return base_url, api_key, model
|
||||
|
||||
|
||||
def _chat(base_url: str, api_key: str, model: str, messages: list[dict], timeout: int = 120) -> str:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
resp = requests.post(
|
||||
base_url + "/chat/completions",
|
||||
headers=headers,
|
||||
json={"model": model, "messages": messages, "temperature": 0.1},
|
||||
timeout=timeout,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()["choices"][0]["message"]["content"]
|
||||
|
||||
|
||||
def analyze_requirements(content: str, llm_override: dict | None = None) -> dict:
|
||||
"""调用 LLM 分析作业要求,返回检测维度计划。LLM 不可用时返回默认计划。"""
|
||||
try:
|
||||
base_url, api_key, model = resolve_llm(llm_override)
|
||||
except ValueError as exc:
|
||||
logger.warning("%s, 使用默认检测计划", exc)
|
||||
return _default_spec()
|
||||
url = settings.LLM_BASE_URL.rstrip("/") + "/chat/completions"
|
||||
prompt = ANALYZE_PROMPT.format(
|
||||
content=content[:8000],
|
||||
stability_concurrency=settings.STABILITY_CONCURRENCY,
|
||||
base_duration=settings.STABILITY_BASE_DURATION,
|
||||
rampup_duration=settings.STABILITY_RAMPUP_DURATION,
|
||||
)
|
||||
resp = requests.post(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
||||
json={
|
||||
"model": settings.LLM_MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是严格遵循 JSON 输出格式的 OJ 检测分析引擎。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"temperature": 0.1,
|
||||
},
|
||||
timeout=120,
|
||||
text = _chat(
|
||||
base_url, api_key, model,
|
||||
[
|
||||
{"role": "system", "content": "你是严格遵循 JSON 输出格式的 OJ 检测分析引擎。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
)
|
||||
resp.raise_for_status()
|
||||
text = resp.json()["choices"][0]["message"]["content"]
|
||||
spec = _extract_json(text)
|
||||
return _validate_spec(spec)
|
||||
return _validate_spec(_extract_json(text))
|
||||
|
||||
|
||||
def _default_spec() -> dict:
|
||||
@@ -98,12 +122,13 @@ def _default_spec() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def ai_summarize(project_dir: str, spec: dict) -> dict:
|
||||
def ai_summarize(project_dir: str, spec: dict, llm_override: dict | None = None) -> dict:
|
||||
"""AI 检测: 阅读项目关键文件,对照检测计划输出中文报告片段。"""
|
||||
key = settings.LLM_API_KEY
|
||||
if not key or key.startswith("sk-placeholder") or not settings.LLM_MODEL:
|
||||
return {"summary": "LLM 未配置,跳过 AI 检测。请在 .env 中配置 LLM_API_KEY。", "issues": []}
|
||||
from .code_reader import read_key_files
|
||||
try:
|
||||
base_url, api_key, model = resolve_llm(llm_override)
|
||||
except ValueError as exc:
|
||||
return {"summary": f"LLM 不可用({exc}),跳过 AI 检测", "issues": []}
|
||||
excerpt = read_key_files(project_dir, max_chars=12000)
|
||||
prompt = (
|
||||
"你是 OJ 平台的代码审查专家。下面是成员提交的项目关键代码摘录和检测计划(JSON):\n"
|
||||
@@ -113,19 +138,7 @@ def ai_summarize(project_dir: str, spec: dict) -> dict:
|
||||
"\"issues\": [{\"point\": \"检查点\", \"status\": \"pass|fail|warn\", \"detail\": \"中文说明\"}]}\n"
|
||||
"对照检测计划逐项核查,重点关注: 功能点是否完善、稳定度是否可能达标、潜在风险。只输出 JSON。"
|
||||
)
|
||||
url = settings.LLM_BASE_URL.rstrip("/") + "/chat/completions"
|
||||
resp = requests.post(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
||||
json={
|
||||
"model": settings.LLM_MODEL,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0.2,
|
||||
},
|
||||
timeout=180,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
text = resp.json()["choices"][0]["message"]["content"]
|
||||
text = _chat(base_url, api_key, model, [{"role": "user", "content": prompt}], timeout=180)
|
||||
data = _extract_json(text)
|
||||
return {
|
||||
"summary": str(data.get("summary", "")),
|
||||
|
||||
Reference in New Issue
Block a user