refactor: unify LLM gateway policy

This commit is contained in:
leefer
2026-07-29 20:08:39 +08:00
parent 5f0b9735bd
commit a368174a75
10 changed files with 587 additions and 224 deletions
+112 -212
View File
@@ -19,6 +19,7 @@ from assistant_agent import ReviewAssistantError, stream_review_assistant
from api_access import ROUTES
from backend.bootstrap import build_application_container, load_runtime_settings
from backend.http import correlation_id, normalize_error_payload
from backend.llm import LLMGateway, LLMGatewayError
from chart_data_provider import ChartDataError
from app_config import (
DATA_DIR,
@@ -193,6 +194,13 @@ class DashboardService:
self.realtime_aggregator = self.container.realtime_aggregator
self.chart_data = self.container.chart_data
self.jobs = self.container.jobs
self.llm_gateway = LLMGateway(
database=self.database,
user_id_supplier=lambda: self.current_user_id,
membership_supplier=self.membership,
settings_supplier=lambda: self._system_credentials,
profile_supplier=self._resolved_llm_profile,
)
self.screener.ensure_builtin_strategies()
self._background_stop = threading.Event()
self._background_thread = self.jobs.start_scheduler(
@@ -482,7 +490,12 @@ class DashboardService:
raise ValueError("模型角色不支持。")
profile = self._validate_llm_profile(payload, current, required=True, label=label)
try:
return test_llm_connection(**profile)
return self.llm_gateway.probe(
profile,
lambda model: test_llm_connection(
model.api_key, model.base_url, model.model
),
)
except LLMCompilerError as exc:
raise ValueError(str(exc)) from exc
@@ -533,29 +546,6 @@ class DashboardService:
start.isoformat(timespec="seconds"),
)
def ensure_llm_access(self, feature: str) -> str:
profile = self._resolved_llm_profile()
source = str(profile.get("source") or "none")
if source == "none" or not self._profile_configured(profile.get("primary") or {}):
raise ValueError("智能功能尚未配置,请联系管理员。")
if source == "platform":
limit = max(1, int(self._system_credentials.get("member_daily_limit") or 50))
if self._platform_usage_today() >= limit:
raise ValueError(f"今日会员模型额度已用完({limit} 次)。")
return source
def record_llm_usage(
self,
feature: str,
source: str,
model: str,
status: str,
latency_ms: int = 0,
) -> None:
self.database.record_llm_usage(
self.current_user_id, feature, source, model, status, latency_ms
)
def system_status(self) -> dict[str, Any]:
platform = self._platform_llm_profile()
model_pool = []
@@ -707,7 +697,12 @@ class DashboardService:
payload, current, required=True, label=label
)
try:
return test_llm_connection(**profile)
return self.llm_gateway.probe(
profile,
lambda model: test_llm_connection(
model.api_key, model.base_url, model.model
),
)
except LLMCompilerError as exc:
raise ValueError(str(exc)) from exc
@@ -1617,55 +1612,33 @@ class DashboardService:
for item in self.assistant_messages()[-12:]
if item.get("role") in {"user", "assistant"}
]
source = self.ensure_llm_access("assistant")
profiles = [
(
self.llm_primary_api_key,
self.llm_primary_base_url,
self.llm_primary_model,
)
]
if self.llm_fallback_configured:
profiles.append(
(
self.llm_fallback_api_key,
self.llm_fallback_base_url,
self.llm_fallback_model,
)
)
def generate():
started = time.perf_counter()
last_error: Exception | None = None
for api_key, base_url, model in profiles:
try:
upstream = iter(
stream_review_assistant(
context, question, history, api_key, base_url, model
)
answer_parts: list[str] = []
events = self.llm_gateway.stream(
"assistant",
"review-assistant-v1",
lambda profile: stream_review_assistant(
context,
question,
history,
profile.api_key,
profile.base_url,
profile.model,
),
(ReviewAssistantError,),
)
for event in events:
if event.kind == "delta":
chunk = str(event.value or "")
answer_parts.append(chunk)
yield chunk
elif event.kind == "complete":
self.database.save_assistant_exchange(
self.current_user_id,
question,
"".join(answer_parts).strip(),
trade_date,
)
first = next(upstream)
except (ReviewAssistantError, StopIteration) as exc:
last_error = exc
continue
answer_parts = [first]
yield first
try:
for chunk in upstream:
answer_parts.append(chunk)
yield chunk
except ReviewAssistantError as exc:
self.record_llm_usage("assistant", source, model, "failed")
raise ValueError("智能解读连接中断,请稍后重试。") from exc
answer = "".join(answer_parts).strip()
latency_ms = round((time.perf_counter() - started) * 1000)
self.database.save_assistant_exchange(
self.current_user_id, question, answer, trade_date
)
self.record_llm_usage("assistant", source, model, "success", latency_ms)
return
self.record_llm_usage("assistant", source, self.llm_primary_model, "failed")
raise ValueError("智能解读服务暂不可用,请稍后重试。") from last_error
return generate()
@@ -1867,53 +1840,35 @@ class DashboardService:
if regime not in REGIMES:
raise ValueError("市场阶段不支持。")
notice = ""
compiled = None
primary_error = ""
source = self.llm_source
started = datetime.now(timezone.utc)
if source == "platform":
self.ensure_llm_access("screener")
if self.llm_configured:
try:
compiled = compile_strategy_with_llm(
prompt,
regime,
self.llm_primary_api_key,
self.llm_primary_base_url,
self.llm_primary_model,
gateway_result = self.llm_gateway.call(
"screener",
"strategy-compiler-v1",
lambda profile: compile_strategy_with_llm(
prompt,
regime,
profile.api_key,
profile.base_url,
profile.model,
),
(LLMCompilerError,),
)
except LLMCompilerError as exc:
primary_error = str(exc)
if compiled is None and self.llm_fallback_configured:
try:
compiled = compile_strategy_with_llm(
prompt,
regime,
self.llm_fallback_api_key,
self.llm_fallback_base_url,
self.llm_fallback_model,
)
compiled["compiler"] = "llm_fallback"
notice = "智能策略生成服务已自动切换。"
except LLMCompilerError as exc:
fallback_error = str(exc)
compiled = gateway_result.value
if gateway_result.role == "fallback":
compiled["compiler"] = "llm_fallback"
notice = "智能策略生成服务已自动切换。"
except LLMGatewayError as exc:
if exc.code != "unavailable":
raise
compiled = compile_local_strategy(prompt, regime)
notice = "智能策略生成暂不可用,已使用本地模板。"
if compiled is None:
else:
compiled = compile_local_strategy(prompt, regime)
notice = "智能策略生成暂不可用,已使用本地模板。"
compiled["formula"] = self.screener.validate_formula(compiled["formula"])
compiled["notice"] = notice
if source in {"personal", "platform"}:
elapsed = int((datetime.now(timezone.utc) - started).total_seconds() * 1000)
status = "success" if str(compiled.get("compiler") or "").startswith("llm") else "failed"
self.record_llm_usage(
"screener",
source,
str(compiled.get("model") or self.llm_primary_model),
status,
elapsed,
)
return compiled
def save_screener_strategy(self, payload: dict[str, Any]) -> dict[str, Any]:
@@ -2017,72 +1972,43 @@ class DashboardService:
)
context = self._build_mentor_context(trade_date, question, skill)
source = self.ensure_llm_access("mentor")
profiles = []
if self.llm_configured:
profiles.append(
(
"primary",
self.llm_primary_api_key,
self.llm_primary_base_url,
self.llm_primary_model,
)
)
if self.llm_fallback_configured:
profiles.append(
(
"fallback",
self.llm_fallback_api_key,
self.llm_fallback_base_url,
self.llm_fallback_model,
)
)
def generate():
started = time.perf_counter()
last_error: Exception | None = None
for compiler, api_key, base_url, model in profiles:
try:
upstream = iter(
stream_with_mentor(
skill, context, question, history, api_key, base_url, model
)
)
first = next(upstream)
except (MentorAgentError, StopIteration) as exc:
last_error = exc
continue
answer_parts = [first]
yield {"type": "delta", "content": first}
try:
for chunk in upstream:
answer_parts.append(chunk)
yield {"type": "delta", "content": chunk}
except MentorAgentError as exc:
self.record_llm_usage("mentor", source, model, "failed")
raise ValueError("智能解读连接中断,请稍后重试。") from exc
answer = "".join(answer_parts).strip()
latency_ms = round((time.perf_counter() - started) * 1000)
self.database.save_mentor_exchange(
self.current_user_id,
mentor_id,
trade_date,
answer_parts: list[str] = []
events = self.llm_gateway.stream(
"mentor",
f"mentor-skill-v1:{skill.skill_id}",
lambda profile: stream_with_mentor(
skill,
context,
question,
answer,
context["data_trade_date"],
)
self.record_llm_usage("mentor", source, model, "success", latency_ms)
yield {
"type": "meta",
"data_trade_date": context["data_trade_date"],
"notice": "智能解读已自动切换可用服务。"
if compiler == "fallback"
else "",
}
return
failed_model = profiles[-1][3] if profiles else self.llm_primary_model
self.record_llm_usage("mentor", source, failed_model, "failed")
raise ValueError("智能解读服务暂不可用,请稍后重试。") from last_error
history,
profile.api_key,
profile.base_url,
profile.model,
),
(MentorAgentError,),
)
for event in events:
if event.kind == "delta":
chunk = str(event.value or "")
answer_parts.append(chunk)
yield {"type": "delta", "content": chunk}
elif event.kind == "complete":
self.database.save_mentor_exchange(
self.current_user_id,
mentor_id,
trade_date,
question,
"".join(answer_parts).strip(),
context["data_trade_date"],
)
yield {
"type": "meta",
"data_trade_date": context["data_trade_date"],
"notice": "智能解读已自动切换可用服务。"
if event.role == "fallback"
else "",
}
return generate()
@@ -3155,45 +3081,19 @@ class DashboardService:
return bool(reading and str(reading.get("answer") or "").rstrip().endswith("……"))
def _call_heaven_agent(self, mode: str, context: dict[str, Any]) -> tuple[dict[str, Any], str]:
source = self.ensure_llm_access(f"heaven_{mode}")
primary_error = ""
if self.llm_configured:
try:
result = interpret_heaven(
mode,
context,
self.llm_primary_api_key,
self.llm_primary_base_url,
self.llm_primary_model,
)
self.record_llm_usage(
f"heaven_{mode}", source, str(result.get("model") or ""),
"success", int(result.get("latency_ms") or 0),
)
return result, "primary"
except HeavenAgentError as exc:
primary_error = str(exc)
if self.llm_fallback_configured:
try:
result = interpret_heaven(
mode,
context,
self.llm_fallback_api_key,
self.llm_fallback_base_url,
self.llm_fallback_model,
)
self.record_llm_usage(
f"heaven_{mode}", source, str(result.get("model") or ""),
"success", int(result.get("latency_ms") or 0),
)
return result, "fallback"
except HeavenAgentError as exc:
self.record_llm_usage(
f"heaven_{mode}", source, self.llm_fallback_model, "failed"
)
raise ValueError("智能解读服务暂不可用,请稍后重试。") from exc
self.record_llm_usage(f"heaven_{mode}", source, self.llm_primary_model, "failed")
raise ValueError("智能解读服务暂不可用,请稍后重试。")
result = self.llm_gateway.call(
f"heaven_{mode}",
f"heaven-{mode}-v1",
lambda profile: interpret_heaven(
mode,
context,
profile.api_key,
profile.base_url,
profile.model,
),
(HeavenAgentError,),
)
return result.value, result.role
def _heaven_index_context(
self,