feat: make chat model routing configurable

This commit is contained in:
2026-07-31 17:57:26 +08:00
parent 85a6da5949
commit ab2c945f0b
22 changed files with 567 additions and 88 deletions

View File

@@ -136,6 +136,7 @@ class AgentDebugService:
chat_route = ModelRoutingService.resolve_chat(
db,
[chunk.knowledge_type for chunk in rag_result.chunks],
rag_result.question,
)
automatic_route = default_model is not None and requested_model.id == default_model.id
if automatic_route:

View File

@@ -146,6 +146,7 @@ class ChatService:
failed_route = ModelRoutingService.resolve_chat(
db,
[chunk.knowledge_type for chunk in rag_result.chunks] if rag_result is not None else [],
rag_result.question if rag_result is not None else normalized_question,
)
AiRequestLogService.write_failed(
db,

View File

@@ -441,6 +441,7 @@ def _model_log_context(
route = ModelRoutingService.resolve_chat(
db,
[chunk.knowledge_type for chunk in rag_result.chunks] if rag_result is not None else [],
rag_result.question if rag_result is not None else "",
)
return (
route.reason,

View File

@@ -1,19 +1,21 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from typing import Literal, cast
from sqlalchemy import desc, select
from sqlalchemy.orm import Session
from app.models.ai_config import ModelConfig, SystemConfig
ModelScenario = Literal["report", "summary", "fixed_info", "deep_chat"]
ModelScenario = Literal["report", "summary", "fixed_info", "simple_knowledge", "deep_chat"]
ChatRoutingMode = Literal["off", "fixed_only", "conservative"]
SCENARIO_LABELS: dict[ModelScenario, str] = {
"report": "周期报告",
"summary": "摘要沉淀",
"fixed_info": "固定信息",
"simple_knowledge": "简单知识问答",
"deep_chat": "深度对话",
}
@@ -21,9 +23,59 @@ _SCENARIO_FIELDS = {
"report": ModelConfig.allow_report,
"summary": ModelConfig.allow_summary,
"fixed_info": ModelConfig.allow_fixed_info,
"simple_knowledge": ModelConfig.allow_simple_knowledge,
"deep_chat": ModelConfig.allow_deep_chat,
}
_SIMPLE_KNOWLEDGE_TYPES = {"course", "qa", "general"}
_SIMPLE_QUERY_MARKERS = (
"是什么",
"有哪些",
"有什么",
"包括什么",
"定义",
"含义",
"区别",
"时间",
"几点",
"在哪里",
"多少",
"多久",
"步骤",
"怎么操作",
"怎么练",
"注意事项",
"作业",
"功课",
)
_DEEP_QUERY_MARKERS = (
"",
"自己",
"感觉",
"感受",
"情绪",
"身体",
"害怕",
"担心",
"焦虑",
"难受",
"痛苦",
"卡住",
"怎么办",
"为什么",
"关系",
"孩子",
"父母",
"伴侣",
"创伤",
"建议",
"分析",
"帮我",
"适合我",
"对不对",
"确认一下",
)
@dataclass(frozen=True)
class ModelRoute:
@@ -90,14 +142,20 @@ class ModelRoutingService:
)
@classmethod
def resolve_chat(cls, db: Session, knowledge_types: list[str]) -> ModelRoute:
def resolve_chat(
cls,
db: Session,
knowledge_types: list[str],
question: str = "",
) -> ModelRoute:
normalized_types = {item.strip().lower() for item in knowledge_types if item and item.strip()}
mode = cls.chat_routing_mode(db)
if normalized_types == {"fixed"}:
if not cls._config_bool(db, "fixed_info_model_routing_enabled", True):
if mode == "off":
return ModelRoute(
model=cls.default_model(db),
scenario="deep_chat",
reason="正式聊天:仅召回固定信息类知识库,但固定信息模型分流开关已关闭;使用默认主模型",
reason="正式聊天:仅召回固定信息类知识库,但正式问答模型分流开关已关闭;使用默认主模型",
fallback_used=False,
question_type="fixed_info",
)
@@ -110,6 +168,16 @@ class ModelRoutingService:
question_type="fixed_info",
)
if mode == "conservative" and cls._is_simple_knowledge_query(question, normalized_types):
route = cls.resolve(db, "simple_knowledge")
return ModelRoute(
model=route.model,
scenario=route.scenario,
reason=f"正式聊天:命中知识库且符合保守简单知识规则;{route.reason}",
fallback_used=route.fallback_used,
question_type="simple_knowledge",
)
default = cls.default_model(db)
if not normalized_types:
reason = "正式聊天:未召回知识库;使用默认主模型"
@@ -128,9 +196,35 @@ class ModelRoutingService:
question_type=question_type,
)
@classmethod
def chat_routing_mode(cls, db: Session) -> ChatRoutingMode:
value = cls._config_value(db, "chat_model_routing_mode")
if value in {"off", "fixed_only", "conservative"}:
return cast(ChatRoutingMode, value)
# Compatibility with the previous emergency switch.
return "fixed_only" if cls._config_bool(db, "fixed_info_model_routing_enabled", True) else "off"
@staticmethod
def _is_simple_knowledge_query(question: str, knowledge_types: set[str]) -> bool:
normalized_question = "".join(question.split()).lower()
if not normalized_question or len(normalized_question) > 80:
return False
if not knowledge_types or not knowledge_types.issubset(_SIMPLE_KNOWLEDGE_TYPES):
return False
if any(marker in normalized_question for marker in _DEEP_QUERY_MARKERS):
return False
return any(marker in normalized_question for marker in _SIMPLE_QUERY_MARKERS)
@staticmethod
def _config_bool(db: Session, key: str, default: bool) -> bool:
value = ModelRoutingService._config_value(db, key)
if value is None:
return default
return value.lower() in {"1", "true", "yes", "on", "启用"}
@staticmethod
def _config_value(db: Session, key: str) -> str | None:
config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == key))
if config is None or not config.config_value.strip():
return default
return config.config_value.strip().lower() in {"1", "true", "yes", "on", "启用"}
return None
return config.config_value.strip()

View File

@@ -35,6 +35,7 @@ class ModelClientService:
route = ModelRoutingService.resolve_chat(
db,
[chunk.knowledge_type for chunk in rag_result.chunks],
rag_result.question,
)
model = route.model
mock_model_enabled = _system_config_bool(db, "mock_model_enabled", get_settings().mock_model_enabled)

View File

@@ -154,6 +154,7 @@ def _chat_route(db: Session, rag_result: RagResult) -> ModelRoute:
return ModelRoutingService.resolve_chat(
db,
[chunk.knowledge_type for chunk in rag_result.chunks],
rag_result.question,
)